echo

echo 查看完整档案

北京编辑西南大学  |  软件工程 编辑字节跳动  |  搬砖的 编辑 github.com/HCThink/h-blog 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

echo 赞了文章 · 2019-01-22

JavaScriptCore全面解析

本文由云+社区发表

作者:殷源,专注移动客户端开发,微软Imagine Cup中国区特等奖获得者

JavaScript越来越多地出现在我们客户端开发的视野中,从ReactNative到JSpatch,JavaScript与客户端相结合的技术开始变得魅力无穷。本文主要讲解iOS中的JavaScriptCore框架,正是它为iOS提供了执行JavaScript代码的能力。未来的技术日新月异,JavaScript与iOS正在碰撞出新的激情。

JavaScriptCore是JavaScript的虚拟机,为JavaScript的执行提供底层资源。

一、JavaScript

在讨论JavaScriptCore之前,我们首先必须对JavaScript有所了解。

1. JavaScript干啥的?

  • 说的高大上一点:一门基于原型、函数先行的高级编程语言,通过解释执行,是动态类型的直译语言。是一门多范式的语言,它支持面向对象编程,命令式编程,以及函数式编程。
  • 说的通俗一点:主要用于网页,为其提供动态交互的能力。可嵌入动态文本于HTML页面,对浏览器事件作出响应,读写HTML元素,控制cookies等。
  • 再通俗一点:抢月饼,button.click()。(PS:请谨慎使用while循环)

img

2. JavaScript起源与历史

  • 1990年底,欧洲核能研究组织(CERN)科学家Tim Berners-Lee,在互联网的基础上,发明了万维网(World Wide Web),从此可以在网上浏览网页文件。
  • 1994年12月,Netscape 发布了一款面向普通用户的新一代的浏览器Navigator 1.0版,市场份额一举超过90%。
  • 1995年,Netscape公司雇佣了程序员Brendan Eich开发这种嵌入网页的脚本语言。最初名字叫做Mocha,1995年9月改为LiveScript。
  • 1995年12月,Netscape公司与Sun公司达成协议,后者允许将这种语言叫做JavaScript。

3. JavaScript与ECMAScript

  • “JavaScript”是Sun公司的注册商标,用来特制网景(现在的Mozilla)对于这门语言的实现。网景将这门语言作为标准提交给了ECMA——欧洲计算机制造协会。由于商标上的冲突,这门语言的标准版本改了一个丑陋的名字“ECMAScript”。同样由于商标的冲突,微软对这门语言的实现版本取了一个广为人知的名字“Jscript”。
  • ECMAScript作为JavaScript的标准,一般认为后者是前者的实现。

4. Java和JavaScript

img

《雷锋和雷峰塔》

Java 和 JavaScript 是两门不同的编程语言 一般认为,当时 Netscape 之所以将 LiveScript 命名为 JavaScript,是因为 Java 是当时最流行的编程语言,带有 “Java” 的名字有助于这门新生语言的传播。

二、 JavaScriptCore

1. 浏览器演进

  • 演进完整图

https://upload.wikimedia.org/...

  • WebKit分支

现在使用WebKit的主要两个浏览器Sfari和Chromium(Chorme的开源项目)。WebKit起源于KDE的开源项目Konqueror的分支,由苹果公司用于Sfari浏览器。其一条分支发展成为Chorme的内核,2013年Google在此基础上开发了新的Blink内核。

img

2. WebKit排版引擎

webkit是sfari、chrome等浏览器的排版引擎,各部分架构图如下

img

  • webkit Embedding API是browser UI与webpage进行交互的api接口;
  • platformAPI提供与底层驱动的交互, 如网络, 字体渲染, 影音文件解码, 渲染引擎等;
  • WebCore它实现了对文档的模型化,包括了CSS, DOM, Render等的实现;
  • JSCore是专门处理JavaScript脚本的引擎;

3. JavaScript引擎

  • JavaScript引擎是专门处理JavaScript脚本的虚拟机,一般会附带在网页浏览器之中。第一个JavaScript引擎由布兰登·艾克在网景公司开发,用于Netscape Navigator网页浏览器中。JavaScriptCore就是一个JavaScript引擎。
  • 下图是当前主要的还在开发中的JavaScript引擎

img

4. JavaScriptCore组成

JavaScriptCore主要由以下模块组成:

  • Lexer 词法分析器,将脚本源码分解成一系列的Token
  • Parser 语法分析器,处理Token并生成相应的语法树
  • LLInt 低级解释器,执行Parser生成的二进制代码
  • Baseline JIT 基线JIT(just in time 实施编译)
  • DFG 低延迟优化的JIT
  • FTL 高通量优化的JIT

关于更多JavaScriptCore的实现细节,参考 https://trac.webkit.org/wiki/...

5. JavaScriptCore

JavaScriptCore是一个C++实现的开源项目。使用Apple提供的JavaScriptCore框架,你可以在Objective-C或者基于C的程序中执行Javascript代码,也可以向JavaScript环境中插入一些自定义的对象。JavaScriptCore从iOS 7.0之后可以直接使用。

在JavaScriptCore.h中,我们可以看到这个

#ifndef JavaScriptCore_h
#define JavaScriptCore_h

#include <JavaScriptCore/JavaScript.h>
#include <JavaScriptCore/JSStringRefCF.h>

#if defined(__OBJC__) && JSC_OBJC_API_ENABLED

#import "JSContext.h"
#import "JSValue.h"
#import "JSManagedValue.h"
#import "JSVirtualMachine.h"
#import "JSExport.h"

#endif

#endif /* JavaScriptCore_h */

这里已经很清晰地列出了JavaScriptCore的主要几个类:

  • JSContext
  • JSValue
  • JSManagedValue
  • JSVirtualMachine
  • JSExport

接下来我们会依次讲解这几个类的用法。

6. Hello World!

这段代码展示了如何在Objective-C中执行一段JavaScript代码,并且获取返回值并转换成OC数据打印

//创建虚拟机
JSVirtualMachine *vm = [[JSVirtualMachine alloc] init];

//创建上下文
JSContext *context = [[JSContext alloc] initWithVirtualMachine:vm];

//执行JavaScript代码并获取返回值
JSValue *value = [context evaluateScript:@"1+2*3"];

//转换成OC数据并打印
NSLog(@"value = %d", [value toInt32]);

Output

value = 7

三、 JSVirtualMachine

一个JSVirtualMachine的实例就是一个完整独立的JavaScript的执行环境,为JavaScript的执行提供底层资源。

这个类主要用来做两件事情:

  1. 实现并发的JavaScript执行
  2. JavaScript和Objective-C桥接对象的内存管理

看下头文件SVirtualMachine.h里有什么:

NS_CLASS_AVAILABLE(10_9, 7_0)
@interface JSVirtualMachine : NSObject

/* 创建一个新的完全独立的虚拟机 */
(instancetype)init;

/* 对桥接对象进行内存管理 */
- (void)addManagedReference:(id)object withOwner:(id)owner;

/* 取消对桥接对象的内存管理 */
- (void)removeManagedReference:(id)object withOwner:(id)owner;

@end

每一个JavaScript上下文(JSContext对象)都归属于一个虚拟机(JSVirtualMachine)。每个虚拟机可以包含多个不同的上下文,并允许在这些不同的上下文之间传值(JSValue对象)。

然而,每个虚拟机都是完整且独立的,有其独立的堆空间和垃圾回收器(garbage collector ),GC无法处理别的虚拟机堆中的对象,因此你不能把一个虚拟机中创建的值传给另一个虚拟机。

img

线程和JavaScript的并发执行

JavaScriptCore API都是线程安全的。你可以在任意线程创建JSValue或者执行JS代码,然而,所有其他想要使用该虚拟机的线程都要等待。

  • 如果想并发执行JS,需要使用多个不同的虚拟机来实现。
  • 可以在子线程中执行JS代码。

通过下面这个demo来理解一下这个并发机制

JSContext *context = [[CustomJSContext alloc] init];
JSContext *context1 = [[CustomJSContext alloc] init];
JSContext *context2 = [[CustomJSContext alloc] initWithVirtualMachine:[context virtualMachine]];
NSLog(@"start");
dispatch_async(queue, ^{
    while (true) {
        sleep(1);
        [context evaluateScript:@"log('tick')"];
    }
});
dispatch_async(queue1, ^{
    while (true) {
        sleep(1);
        [context1 evaluateScript:@"log('tick_1')"];
    }
});
dispatch_async(queue2, ^{
    while (true) {
        sleep(1);
        [context2 evaluateScript:@"log('tick_2')"];
    }
});
[context evaluateScript:@"sleep(5)"];
NSLog(@"end");

context和context2属于同一个虚拟机。

context1属于另一个虚拟机。

三个线程分别异步执行每秒1次的js log,首先会休眠1秒。

在context上执行一个休眠5秒的JS函数。

首先执行的应该是休眠5秒的JS函数,在此期间,context所处的虚拟机上的其他调用都会处于等待状态,因此tick和tick_2在前5秒都不会有执行。

而context1所处的虚拟机仍然可以正常执行tick_1

休眠5秒结束后,tick和tick_2才会开始执行(不保证先后顺序)。

实际运行输出的log是:

start
tick_1
tick_1
tick_1
tick_1
end
tick
tick_2

四、 JSContext

一个JSContext对象代表一个JavaScript执行环境。在native代码中,使用JSContext去执行JS代码,访问JS中定义或者计算的值,并使JavaScript可以访问native的对象、方法、函数。

img

1. JSContext执行JS代码

  • 调用evaluateScript函数可以执行一段top-level 的JS代码,并可向global对象添加函数和对象定义
  • 其返回值是JavaScript代码中最后一个生成的值

API Reference

NS_CLASS_AVAILABLE(10_9, 7_0)
@interface JSContext : NSObject

/* 创建一个JSContext,同时会创建一个新的JSVirtualMachine */
(instancetype)init;

/* 在指定虚拟机上创建一个JSContext */
(instancetype)initWithVirtualMachine:
        (JSVirtualMachine*)virtualMachine;

/* 执行一段JS代码,返回最后生成的一个值 */
(JSValue *)evaluateScript:(NSString *)script;

/* 执行一段JS代码,并将sourceURL认作其源码URL(仅作标记用) */
- (JSValue *)evaluateScript:(NSString *)script withSourceURL:(NSURL*)sourceURL     NS_AVAILABLE(10_10, 8_0);

/* 获取当前执行的JavaScript代码的context */
+ (JSContext *)currentContext;

/* 获取当前执行的JavaScript function*/
+ (JSValue *)currentCallee NS_AVAILABLE(10_10, 8_0);

/* 获取当前执行的JavaScript代码的this */
+ (JSValue *)currentThis;

/* Returns the arguments to the current native callback from JavaScript code.*/
+ (NSArray *)currentArguments;

/* 获取当前context的全局对象。WebKit中的context返回的便是WindowProxy对象*/
@property (readonly, strong) JSValue *globalObject;

@property (strong) JSValue *exception;
@property (copy) void(^exceptionHandler)(JSContext *context, JSValue
    *exception);

@property (readonly, strong) JSVirtualMachine *virtualMachine;

@property (copy) NSString *name NS_AVAILABLE(10_10, 8_0);


@end

2. JSContext访问JS对象

一个JSContext对象对应了一个全局对象(global object)。例如web浏览器中中的JSContext,其全局对象就是window对象。在其他环境中,全局对象也承担了类似的角色,用来区分不同的JavaScript context的作用域。全局变量是全局对象的属性,可以通过JSValue对象或者context下标的方式来访问。

一言不合上代码:

JSValue *value = [context evaluateScript:@"var a = 1+2*3;"];

NSLog(@"a = %@", [context objectForKeyedSubscript:@"a"]);
NSLog(@"a = %@", [context.globalObject objectForKeyedSubscript:@"a"]);
NSLog(@"a = %@", context[@"a"]);

/
Output:

a = 7
a = 7
a = 7

这里列出了三种访问JavaScript对象的方法

  • 通过context的实例方法objectForKeyedSubscript
  • 通过context.globalObject的objectForKeyedSubscript实例方法
  • 通过下标方式

设置属性也是对应的。

API Reference

/* 为JSContext提供下标访问元素的方式 */
@interface JSContext (SubscriptSupport)

/* 首先将key转为JSValue对象,然后使用这个值在JavaScript context的全局对象中查找这个名字的属性并返回 */
(JSValue *)objectForKeyedSubscript:(id)key;

/* 首先将key转为JSValue对象,然后用这个值在JavaScript context的全局对象中设置这个属性。
可使用这个方法将native中的对象或者方法桥接给JavaScript调用 */
(void)setObject:(id)object forKeyedSubscript:(NSObject <NSCopying>*)key;

@end



/* 例如:以下代码在JavaScript中创建了一个实现是Objective-C block的function */
context[@"makeNSColor"] = ^(NSDictionary *rgb){
    float r = [rgb[@"red"] floatValue];
    float g = [rgb[@"green"] floatValue];
    float b = [rgb[@"blue"] floatValue];
    return [NSColor colorWithRed:(r / 255.f) green:(g / 255.f) blue:(b / 255.f)         alpha:1.0];
};
JSValue *value = [context evaluateScript:@"makeNSColor({red:12, green:23, blue:67})"];

五、 JSValue

一个JSValue实例就是一个JavaScript值的引用。使用JSValue类在JavaScript和native代码之间转换一些基本类型的数据(比如数值和字符串)。你也可以使用这个类去创建包装了自定义类的native对象的JavaScript对象,或者创建由native方法或者block实现的JavaScript函数。

每个JSValue实例都来源于一个代表JavaScript执行环境的JSContext对象,这个执行环境就包含了这个JSValue对应的值。每个JSValue对象都持有其JSContext对象的强引用,只要有任何一个与特定JSContext关联的JSValue被持有(retain),这个JSContext就会一直存活。通过调用JSValue的实例方法返回的其他的JSValue对象都属于与最始的JSValue相同的JSContext。

img

每个JSValue都通过其JSContext间接关联了一个特定的代表执行资源基础的JSVirtualMachine对象。你只能将一个JSValue对象传给由相同虚拟机管理(host)的JSValue或者JSContext的实例方法。如果尝试把一个虚拟机的JSValue传给另一个虚拟机,将会触发一个Objective-C异常。

img

1. JSValue类型转换

JSValue提供了一系列的方法将native与JavaScript的数据类型进行相互转换:

img

2. NSDictionary与JS对象

NSDictionary对象以及其包含的keys与JavaScript中的对应名称的属性相互转换。key所对应的值也会递归地进行拷贝和转换。

[context evaluateScript:@"var color = {red:230, green:90, blue:100}"];

//js->native 给你看我的颜色
JSValue *colorValue = context[@"color"];
NSLog(@"r=%@, g=%@, b=%@", colorValue[@"red"], colorValue[@"green"], colorValue[@"blue"]);
NSDictionary *colorDic = [colorValue toDictionary];
NSLog(@"r=%@, g=%@, b=%@", colorDic[@"red"], colorDic[@"green"], colorDic[@"blue"]);

//native->js 给你点颜色看看
context[@"color"] = @{@"red":@(0), @"green":@(0), @"blue":@(0)};
[context evaluateScript:@"log('r:'+color.red+'g:'+color.green+' b:'+color.blue)"];
Output:

r=230, g=90, b=100
r=230, g=90, b=100
r:0 g:0 b:0

可见,JS中的对象可以直接转换成Objective-C中的NSDictionary,NSDictionary传入JavaScript也可以直接当作对象被使用。

3. NSArray与JS数组

NSArray对象与JavaScript中的array相互转转。其子元素也会递归地进行拷贝和转换。

[context evaluateScript:@“var friends = ['Alice','Jenny','XiaoMing']"];

//js->native 你说哪个是真爱?
JSValue *friendsValue = context[@"friends"];
NSLog(@"%@, %@, %@", friendsValue[0], friendsValue[1], friendsValue[2]);
NSArray *friendsArray = [friendsValue toArray];
NSLog(@"%@, %@, %@", friendsArray[0], friendsArray[1], friendsArray[2]);

//native->js 我觉XiaoMing和不不错,给你再推荐个Jimmy
context[@"girlFriends"] = @[friendsArray[2], @"Jimmy"];
[context evaluateScript:@"log('girlFriends :'+girlFriends[0]+' '+girlFriends[1])"];

Output:

Alice, Jenny, XiaoMing
Alice, Jenny, XiaoMing
girlFriends : XiaoMing Jimmy

4. Block/函数和JS function

Objective-C中的block转换成JavaScript中的function对象。参数以及返回类型使用相同的规则转换。

将一个代表native的block或者方法的JavaScript function进行转换将会得到那个block或方法。

其他的JavaScript函数将会被转换为一个空的dictionary。因为JavaScript函数也是一个对象。

5. OC对象和JS对象

对于所有其他native的对象类型,JavaScriptCore都会创建一个拥有constructor原型链的wrapper对象,用来反映native类型的继承关系。默认情况下,native对象的属性和方法并不会导出给其对应的JavaScript wrapper对象。通过JSExport协议可选择性地导出属性和方法。

后面会详细讲解对象类型的转换。

此文已由腾讯云+社区在各渠道发布

获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号

查看原文

赞 50 收藏 40 评论 0

echo 收藏了文章 · 2019-01-22

JavaScriptCore全面解析

本文由云+社区发表

作者:殷源,专注移动客户端开发,微软Imagine Cup中国区特等奖获得者

JavaScript越来越多地出现在我们客户端开发的视野中,从ReactNative到JSpatch,JavaScript与客户端相结合的技术开始变得魅力无穷。本文主要讲解iOS中的JavaScriptCore框架,正是它为iOS提供了执行JavaScript代码的能力。未来的技术日新月异,JavaScript与iOS正在碰撞出新的激情。

JavaScriptCore是JavaScript的虚拟机,为JavaScript的执行提供底层资源。

一、JavaScript

在讨论JavaScriptCore之前,我们首先必须对JavaScript有所了解。

1. JavaScript干啥的?

  • 说的高大上一点:一门基于原型、函数先行的高级编程语言,通过解释执行,是动态类型的直译语言。是一门多范式的语言,它支持面向对象编程,命令式编程,以及函数式编程。
  • 说的通俗一点:主要用于网页,为其提供动态交互的能力。可嵌入动态文本于HTML页面,对浏览器事件作出响应,读写HTML元素,控制cookies等。
  • 再通俗一点:抢月饼,button.click()。(PS:请谨慎使用while循环)

img

2. JavaScript起源与历史

  • 1990年底,欧洲核能研究组织(CERN)科学家Tim Berners-Lee,在互联网的基础上,发明了万维网(World Wide Web),从此可以在网上浏览网页文件。
  • 1994年12月,Netscape 发布了一款面向普通用户的新一代的浏览器Navigator 1.0版,市场份额一举超过90%。
  • 1995年,Netscape公司雇佣了程序员Brendan Eich开发这种嵌入网页的脚本语言。最初名字叫做Mocha,1995年9月改为LiveScript。
  • 1995年12月,Netscape公司与Sun公司达成协议,后者允许将这种语言叫做JavaScript。

3. JavaScript与ECMAScript

  • “JavaScript”是Sun公司的注册商标,用来特制网景(现在的Mozilla)对于这门语言的实现。网景将这门语言作为标准提交给了ECMA——欧洲计算机制造协会。由于商标上的冲突,这门语言的标准版本改了一个丑陋的名字“ECMAScript”。同样由于商标的冲突,微软对这门语言的实现版本取了一个广为人知的名字“Jscript”。
  • ECMAScript作为JavaScript的标准,一般认为后者是前者的实现。

4. Java和JavaScript

img

《雷锋和雷峰塔》

Java 和 JavaScript 是两门不同的编程语言 一般认为,当时 Netscape 之所以将 LiveScript 命名为 JavaScript,是因为 Java 是当时最流行的编程语言,带有 “Java” 的名字有助于这门新生语言的传播。

二、 JavaScriptCore

1. 浏览器演进

  • 演进完整图

https://upload.wikimedia.org/...

  • WebKit分支

现在使用WebKit的主要两个浏览器Sfari和Chromium(Chorme的开源项目)。WebKit起源于KDE的开源项目Konqueror的分支,由苹果公司用于Sfari浏览器。其一条分支发展成为Chorme的内核,2013年Google在此基础上开发了新的Blink内核。

img

2. WebKit排版引擎

webkit是sfari、chrome等浏览器的排版引擎,各部分架构图如下

img

  • webkit Embedding API是browser UI与webpage进行交互的api接口;
  • platformAPI提供与底层驱动的交互, 如网络, 字体渲染, 影音文件解码, 渲染引擎等;
  • WebCore它实现了对文档的模型化,包括了CSS, DOM, Render等的实现;
  • JSCore是专门处理JavaScript脚本的引擎;

3. JavaScript引擎

  • JavaScript引擎是专门处理JavaScript脚本的虚拟机,一般会附带在网页浏览器之中。第一个JavaScript引擎由布兰登·艾克在网景公司开发,用于Netscape Navigator网页浏览器中。JavaScriptCore就是一个JavaScript引擎。
  • 下图是当前主要的还在开发中的JavaScript引擎

img

4. JavaScriptCore组成

JavaScriptCore主要由以下模块组成:

  • Lexer 词法分析器,将脚本源码分解成一系列的Token
  • Parser 语法分析器,处理Token并生成相应的语法树
  • LLInt 低级解释器,执行Parser生成的二进制代码
  • Baseline JIT 基线JIT(just in time 实施编译)
  • DFG 低延迟优化的JIT
  • FTL 高通量优化的JIT

关于更多JavaScriptCore的实现细节,参考 https://trac.webkit.org/wiki/...

5. JavaScriptCore

JavaScriptCore是一个C++实现的开源项目。使用Apple提供的JavaScriptCore框架,你可以在Objective-C或者基于C的程序中执行Javascript代码,也可以向JavaScript环境中插入一些自定义的对象。JavaScriptCore从iOS 7.0之后可以直接使用。

在JavaScriptCore.h中,我们可以看到这个

#ifndef JavaScriptCore_h
#define JavaScriptCore_h

#include <JavaScriptCore/JavaScript.h>
#include <JavaScriptCore/JSStringRefCF.h>

#if defined(__OBJC__) && JSC_OBJC_API_ENABLED

#import "JSContext.h"
#import "JSValue.h"
#import "JSManagedValue.h"
#import "JSVirtualMachine.h"
#import "JSExport.h"

#endif

#endif /* JavaScriptCore_h */

这里已经很清晰地列出了JavaScriptCore的主要几个类:

  • JSContext
  • JSValue
  • JSManagedValue
  • JSVirtualMachine
  • JSExport

接下来我们会依次讲解这几个类的用法。

6. Hello World!

这段代码展示了如何在Objective-C中执行一段JavaScript代码,并且获取返回值并转换成OC数据打印

//创建虚拟机
JSVirtualMachine *vm = [[JSVirtualMachine alloc] init];

//创建上下文
JSContext *context = [[JSContext alloc] initWithVirtualMachine:vm];

//执行JavaScript代码并获取返回值
JSValue *value = [context evaluateScript:@"1+2*3"];

//转换成OC数据并打印
NSLog(@"value = %d", [value toInt32]);

Output

value = 7

三、 JSVirtualMachine

一个JSVirtualMachine的实例就是一个完整独立的JavaScript的执行环境,为JavaScript的执行提供底层资源。

这个类主要用来做两件事情:

  1. 实现并发的JavaScript执行
  2. JavaScript和Objective-C桥接对象的内存管理

看下头文件SVirtualMachine.h里有什么:

NS_CLASS_AVAILABLE(10_9, 7_0)
@interface JSVirtualMachine : NSObject

/* 创建一个新的完全独立的虚拟机 */
(instancetype)init;

/* 对桥接对象进行内存管理 */
- (void)addManagedReference:(id)object withOwner:(id)owner;

/* 取消对桥接对象的内存管理 */
- (void)removeManagedReference:(id)object withOwner:(id)owner;

@end

每一个JavaScript上下文(JSContext对象)都归属于一个虚拟机(JSVirtualMachine)。每个虚拟机可以包含多个不同的上下文,并允许在这些不同的上下文之间传值(JSValue对象)。

然而,每个虚拟机都是完整且独立的,有其独立的堆空间和垃圾回收器(garbage collector ),GC无法处理别的虚拟机堆中的对象,因此你不能把一个虚拟机中创建的值传给另一个虚拟机。

img

线程和JavaScript的并发执行

JavaScriptCore API都是线程安全的。你可以在任意线程创建JSValue或者执行JS代码,然而,所有其他想要使用该虚拟机的线程都要等待。

  • 如果想并发执行JS,需要使用多个不同的虚拟机来实现。
  • 可以在子线程中执行JS代码。

通过下面这个demo来理解一下这个并发机制

JSContext *context = [[CustomJSContext alloc] init];
JSContext *context1 = [[CustomJSContext alloc] init];
JSContext *context2 = [[CustomJSContext alloc] initWithVirtualMachine:[context virtualMachine]];
NSLog(@"start");
dispatch_async(queue, ^{
    while (true) {
        sleep(1);
        [context evaluateScript:@"log('tick')"];
    }
});
dispatch_async(queue1, ^{
    while (true) {
        sleep(1);
        [context1 evaluateScript:@"log('tick_1')"];
    }
});
dispatch_async(queue2, ^{
    while (true) {
        sleep(1);
        [context2 evaluateScript:@"log('tick_2')"];
    }
});
[context evaluateScript:@"sleep(5)"];
NSLog(@"end");

context和context2属于同一个虚拟机。

context1属于另一个虚拟机。

三个线程分别异步执行每秒1次的js log,首先会休眠1秒。

在context上执行一个休眠5秒的JS函数。

首先执行的应该是休眠5秒的JS函数,在此期间,context所处的虚拟机上的其他调用都会处于等待状态,因此tick和tick_2在前5秒都不会有执行。

而context1所处的虚拟机仍然可以正常执行tick_1

休眠5秒结束后,tick和tick_2才会开始执行(不保证先后顺序)。

实际运行输出的log是:

start
tick_1
tick_1
tick_1
tick_1
end
tick
tick_2

四、 JSContext

一个JSContext对象代表一个JavaScript执行环境。在native代码中,使用JSContext去执行JS代码,访问JS中定义或者计算的值,并使JavaScript可以访问native的对象、方法、函数。

img

1. JSContext执行JS代码

  • 调用evaluateScript函数可以执行一段top-level 的JS代码,并可向global对象添加函数和对象定义
  • 其返回值是JavaScript代码中最后一个生成的值

API Reference

NS_CLASS_AVAILABLE(10_9, 7_0)
@interface JSContext : NSObject

/* 创建一个JSContext,同时会创建一个新的JSVirtualMachine */
(instancetype)init;

/* 在指定虚拟机上创建一个JSContext */
(instancetype)initWithVirtualMachine:
        (JSVirtualMachine*)virtualMachine;

/* 执行一段JS代码,返回最后生成的一个值 */
(JSValue *)evaluateScript:(NSString *)script;

/* 执行一段JS代码,并将sourceURL认作其源码URL(仅作标记用) */
- (JSValue *)evaluateScript:(NSString *)script withSourceURL:(NSURL*)sourceURL     NS_AVAILABLE(10_10, 8_0);

/* 获取当前执行的JavaScript代码的context */
+ (JSContext *)currentContext;

/* 获取当前执行的JavaScript function*/
+ (JSValue *)currentCallee NS_AVAILABLE(10_10, 8_0);

/* 获取当前执行的JavaScript代码的this */
+ (JSValue *)currentThis;

/* Returns the arguments to the current native callback from JavaScript code.*/
+ (NSArray *)currentArguments;

/* 获取当前context的全局对象。WebKit中的context返回的便是WindowProxy对象*/
@property (readonly, strong) JSValue *globalObject;

@property (strong) JSValue *exception;
@property (copy) void(^exceptionHandler)(JSContext *context, JSValue
    *exception);

@property (readonly, strong) JSVirtualMachine *virtualMachine;

@property (copy) NSString *name NS_AVAILABLE(10_10, 8_0);


@end

2. JSContext访问JS对象

一个JSContext对象对应了一个全局对象(global object)。例如web浏览器中中的JSContext,其全局对象就是window对象。在其他环境中,全局对象也承担了类似的角色,用来区分不同的JavaScript context的作用域。全局变量是全局对象的属性,可以通过JSValue对象或者context下标的方式来访问。

一言不合上代码:

JSValue *value = [context evaluateScript:@"var a = 1+2*3;"];

NSLog(@"a = %@", [context objectForKeyedSubscript:@"a"]);
NSLog(@"a = %@", [context.globalObject objectForKeyedSubscript:@"a"]);
NSLog(@"a = %@", context[@"a"]);

/
Output:

a = 7
a = 7
a = 7

这里列出了三种访问JavaScript对象的方法

  • 通过context的实例方法objectForKeyedSubscript
  • 通过context.globalObject的objectForKeyedSubscript实例方法
  • 通过下标方式

设置属性也是对应的。

API Reference

/* 为JSContext提供下标访问元素的方式 */
@interface JSContext (SubscriptSupport)

/* 首先将key转为JSValue对象,然后使用这个值在JavaScript context的全局对象中查找这个名字的属性并返回 */
(JSValue *)objectForKeyedSubscript:(id)key;

/* 首先将key转为JSValue对象,然后用这个值在JavaScript context的全局对象中设置这个属性。
可使用这个方法将native中的对象或者方法桥接给JavaScript调用 */
(void)setObject:(id)object forKeyedSubscript:(NSObject <NSCopying>*)key;

@end



/* 例如:以下代码在JavaScript中创建了一个实现是Objective-C block的function */
context[@"makeNSColor"] = ^(NSDictionary *rgb){
    float r = [rgb[@"red"] floatValue];
    float g = [rgb[@"green"] floatValue];
    float b = [rgb[@"blue"] floatValue];
    return [NSColor colorWithRed:(r / 255.f) green:(g / 255.f) blue:(b / 255.f)         alpha:1.0];
};
JSValue *value = [context evaluateScript:@"makeNSColor({red:12, green:23, blue:67})"];

五、 JSValue

一个JSValue实例就是一个JavaScript值的引用。使用JSValue类在JavaScript和native代码之间转换一些基本类型的数据(比如数值和字符串)。你也可以使用这个类去创建包装了自定义类的native对象的JavaScript对象,或者创建由native方法或者block实现的JavaScript函数。

每个JSValue实例都来源于一个代表JavaScript执行环境的JSContext对象,这个执行环境就包含了这个JSValue对应的值。每个JSValue对象都持有其JSContext对象的强引用,只要有任何一个与特定JSContext关联的JSValue被持有(retain),这个JSContext就会一直存活。通过调用JSValue的实例方法返回的其他的JSValue对象都属于与最始的JSValue相同的JSContext。

img

每个JSValue都通过其JSContext间接关联了一个特定的代表执行资源基础的JSVirtualMachine对象。你只能将一个JSValue对象传给由相同虚拟机管理(host)的JSValue或者JSContext的实例方法。如果尝试把一个虚拟机的JSValue传给另一个虚拟机,将会触发一个Objective-C异常。

img

1. JSValue类型转换

JSValue提供了一系列的方法将native与JavaScript的数据类型进行相互转换:

img

2. NSDictionary与JS对象

NSDictionary对象以及其包含的keys与JavaScript中的对应名称的属性相互转换。key所对应的值也会递归地进行拷贝和转换。

[context evaluateScript:@"var color = {red:230, green:90, blue:100}"];

//js->native 给你看我的颜色
JSValue *colorValue = context[@"color"];
NSLog(@"r=%@, g=%@, b=%@", colorValue[@"red"], colorValue[@"green"], colorValue[@"blue"]);
NSDictionary *colorDic = [colorValue toDictionary];
NSLog(@"r=%@, g=%@, b=%@", colorDic[@"red"], colorDic[@"green"], colorDic[@"blue"]);

//native->js 给你点颜色看看
context[@"color"] = @{@"red":@(0), @"green":@(0), @"blue":@(0)};
[context evaluateScript:@"log('r:'+color.red+'g:'+color.green+' b:'+color.blue)"];
Output:

r=230, g=90, b=100
r=230, g=90, b=100
r:0 g:0 b:0

可见,JS中的对象可以直接转换成Objective-C中的NSDictionary,NSDictionary传入JavaScript也可以直接当作对象被使用。

3. NSArray与JS数组

NSArray对象与JavaScript中的array相互转转。其子元素也会递归地进行拷贝和转换。

[context evaluateScript:@“var friends = ['Alice','Jenny','XiaoMing']"];

//js->native 你说哪个是真爱?
JSValue *friendsValue = context[@"friends"];
NSLog(@"%@, %@, %@", friendsValue[0], friendsValue[1], friendsValue[2]);
NSArray *friendsArray = [friendsValue toArray];
NSLog(@"%@, %@, %@", friendsArray[0], friendsArray[1], friendsArray[2]);

//native->js 我觉XiaoMing和不不错,给你再推荐个Jimmy
context[@"girlFriends"] = @[friendsArray[2], @"Jimmy"];
[context evaluateScript:@"log('girlFriends :'+girlFriends[0]+' '+girlFriends[1])"];

Output:

Alice, Jenny, XiaoMing
Alice, Jenny, XiaoMing
girlFriends : XiaoMing Jimmy

4. Block/函数和JS function

Objective-C中的block转换成JavaScript中的function对象。参数以及返回类型使用相同的规则转换。

将一个代表native的block或者方法的JavaScript function进行转换将会得到那个block或方法。

其他的JavaScript函数将会被转换为一个空的dictionary。因为JavaScript函数也是一个对象。

5. OC对象和JS对象

对于所有其他native的对象类型,JavaScriptCore都会创建一个拥有constructor原型链的wrapper对象,用来反映native类型的继承关系。默认情况下,native对象的属性和方法并不会导出给其对应的JavaScript wrapper对象。通过JSExport协议可选择性地导出属性和方法。

后面会详细讲解对象类型的转换。

此文已由腾讯云+社区在各渠道发布

获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号

查看原文

echo 回答了问题 · 2019-01-14

node(v10.15.0) npm不能用?

nvm 有沙箱机制, 不同版本的 node 之间应该是隔离的。没太看明白你的问题的意思。

关注 3 回答 2

echo 回答了问题 · 2019-01-06

解决没接触过nodejs,应该如何系统的学习呢

这里有个 node 最佳实践

https://github.com/i0natan/no...

关注 6 回答 4

echo 赞了文章 · 2019-01-04

技术年货:美团技术沙龙合辑大放送——85个演讲,70+小时视频

你好,2019

再见,2018

又到了一年辞旧迎新的时候,大家应该也和美美一样,在忙着总结回顾和展望规划吧。

按老惯例,我们该献上技术年货了。今年首先出场的,是我们的技术沙龙大套餐!

美团技术沙龙是由美团技术团队和美团科协主办的线下技术活动,每期沙龙邀请美团和同行公司的技术专家分享来自一线的实践经验,覆盖前端、后台、系统、数据、算法、测试、运维等技术领域。目前已在北京、上海、厦门、成都等城市成功举办了48期,吸引了超过2万名工程师报名参会。除了现场亲密交流之外,大家还在会后继续进微信群里切磋问道。

我们同时也定期举办线上沙龙,有更多的朋友已经在线收听或收看过我们的直播。

过去的一年里,我们一共举办了19场线下沙龙,16次线上沙龙,包括99位讲师,85个演讲,70+小时分享。除美团的几十位专家之外,还邀请了腾讯、蚂蚁金服、百度、京东、去哪儿、美亚柏科同行。主题既有最火的AI热点——深度学习、强化学习、知识图谱,新潮技术Go、Kotlin、Flutter的落地实践,也有大规模互联网平台不可或缺的稳定性保障、高可用架构、监控体系、持续集成,还有接地气的运营系统、数据平台、配送调度的经验,更少不了CAT、mpvue、EasyReact这些火爆的开源项目……

我们整理了今年所有线上线下沙龙的演讲稿和视频,分享给大家,希望能帮助大家在技术之路上继续追求卓越。

还没有关注美团技术团队公众号(meituantech)的同学,赶快关注哟。

现在,大家请闭眼,大餐来了~~~

美团线下沙龙

任意点击标题,就能直接查看视频及演讲稿

Tech Salon 048:AI在复杂业务场景中的应用(请在微信端打开)

出品人|何仁清 美团研究员 2018/12/22

《深度强化学习技术在智能调度中的应用》王超@美团高级技术专家

《深度学习在物流配送舆情监控中的应用》李超@京东物流NLP方向资深算法工程师

《美团搜索中的深度学习实践》周翔@美团技术专家


Tech Salon 047:运营效率系统架构演进之道(请在微信端打开)

出品人|阳纯飞 美团高级技术专家 2018/12/08

《面向O2O大促活动的高效运营系统实践》李力@美团高级技术专家

《京东物流实时数据平台性能优化实践》杨国斌@京东技术专家

《外卖商家菜品运营效率提升实践》罗震@美团高级技术专家

《外卖商户数据化运营体系建设》李觊@美团高级技术专家


Tech Salon 046:AI在外卖配送的应用(请在微信端打开)

出品人|王兴星&郝井华 美团研究员 2018/11/25

《预估模型在外卖广告的演进》谢乾龙@美团外卖广告算法专家

《即时配送中的运筹优化技术》郝井华@美团配送智能调度平台负责人

《即时配送中的机器学习技术》何仁清@美团配送AI方向负责人


Tech Salon 045:如何构建高性能、稳定的后端服务系统(请在微信端打开)

出品人 |方建平 美团技术总监 2018/11/24

《美团度假交易稳定性建设实践》韩建起@美团技术专家

《美团配送智能调度系统架构演进》郑伟@美团技术专家

《美团稳定性保障平台Rhino》占军@美团技术专家

《美团外卖LBS服务实践》易奎@美团高级技术专家


Tech Salon 044:千万级日活App的质量保证(请在微信端打开)

出品人|李永刚 美团研究员 2018/09/15

《AI语音产品测试》于海生@美团技术专家

《美团外卖客户端 UI 自动化测试实践》刘健@美团技术专家

《客户端QA效率工具》赵晔@美团测试开发架构师


Tech Salon 043:美团金融千万级交易系统质量保障之路(请在微信端打开)

出品人|赫大龙 美团高级技术专家 2018/09/01

《智能支付稳定性测试实战》范勋伟@美团高级测试开发工程师

《去哪儿网服务端自动化测试体系》郑希文@去哪儿

《线上支付SDK质量保证体系》张朋飞@美团高级测试开发工程师

《质量运营演变之道》符欣@美团高级开发工程师


Tech Salon 042:高效率前端与架构实战(请在微信端打开)

出品人|王鹤 美团高级技术专家 2018/08/26

《EH — 用黑科技打造全新的 Hybrid 体验》 陈禹霖@美团技术专家

《mpvue小程序开发框架和最佳实践》胡成全@美团技术专家

《美团金融前端工程化最佳实践》田泱@美团高级研发工程师

《前端架构师应该做的事——架构模型与技术实践》王鹤@美团高级技术专家


Tech Salon 041:企业级前端开发360°实践(请在微信端打开)

出品人|任远 美团高级技术专家 2018/08/18

《浅谈流量劫持与防治》刘洋河@美团高级工程师

《前端高可用性保障实践》田泱@美团高级研发工程师

《跨容器高性能的离线化方案在美团的实践和探索》龙佳文@美团高级工程师

《前端遇上Go: 静态资源增量更新的新实践》杨义天@美团高级工程师


Tech Salon 040:前端遇上黑科技,打造全新界面体验与效率(请在微信端打开)

出品人 | 冯湧 美团研发总监 2018/08/04

[《EH——用黑科技打造的全新的 Hybrid 体验》](http://dpurl.cn/dcUUsv7
)陈禹霖@美团技术专家

《Vix——如何打造一个高效的前端组件库》张胜@美团高级研发工程师

《委以重任的Node.js——百亿级访问实践》迪波威@百度资深软件工程师

《构建时预渲染——网页首帧优化实践》尚寒阳@美团资深研发工程师


Tech Salon 039:新思路打造移动端高效研发体系(请在微信端打开)

出品人|周辉 美团资深架构师 2018/07/21

《Picasso在大众点评首页的应用》虞惠文@美团Android开发工程师

《移动前后端开发解耦》林晨@美团后端技术专家

《打造稳定、灵活、高效的运营配置平台》蒋国宝@美团后端技术架构师

《移动持续集成实践》何智聪@美团iOS技术专家


Tech Salon 038:旅游电商架构攻略指南(请在微信端打开)

出品人|金孟哲 美团高级技术专家 2018/07/07

《旅游度假预订系统架构演进实践》 金孟哲@美团高级技术专家

《旅游度假多要素动态打包实践》 王超@美团高级技术专家

《旅游度假商品中心架构演进》 徐泼@美团高级技术专家

《直连低可信系统,实现高可用服务实践》 郑旭@美团高级技术专家


Tech Salon 037:美团外卖两千万日订单背后的客户端技术架构(请在微信端打开)

出品人|吴凯 美团高级技术专家 2018/06/23

《美团外卖客户端技术架构演进》朱瑞利@美团资深工程师

《客户端监控体系建设》冯天锡@美团资深工程师

《客户端多端复用架构》郑金光@美团高级工程师

《客户端容灾能力体系》李富强@美团资深工程师


Tech Salon 036:大前端的动态化与可用性建设(请在微信端打开)

出品人|洪磊 美团研究员 2018/06/02

《大前端高可用性建设》李富强@美团资深前端工程师

《React fiber漫谈》覃俊文@蚂蚁金服前端技术专家

《小程序开发的新姿势 -mpvue 的应用和实践》胡成全@美团前端技术专家

《ReactNative性能优化企业级应用》赵宏罡@腾讯资深前端开发工程师


Tech Salon 035:前端热点技术的企业级实践(请在微信端打开)

出品人|任远 美团高级技术专家 2018/04/21

《前端遇上 Go——静态资源增量更新的新实践》刘洋河@美团高级前端工程师

《金融扫码付 H5 迁移小程序拓荒之旅》陈瑶@美团前端工程师

《小程序开发的新姿势 - mpvue 开发框架和最佳实践》胡成全@美团技术专家


Tech Salon 034:聊聊酒店系统高可用架构实践(请在微信端打开)

出品人|许关飞 美团研究员 2018/04/14

《高可用之中间件技术》吴湘@美团技术专家

《分布式开源监控系统CAT在美团点评演进》尤勇@美团研究员

《美团旅行住宿产品中心系统高可用实战》槐国涛@美团酒店CRS商品负责人

《美团旅行酒店住宿订单系统高可用实战》李磊@美团资深工程师

《美团旅行直连系统高可用实战》秦浏杰@美团资深工程师


Tech Salon 033:企业信息化建设和安全管理(请在微信端打开)

出品人|邓昀泽 美团企业平台研发负责人 2018/02/03

《美团点评内部系统构建之路》邓昀泽@美团企业平台研发部负责人

《企业信息防护体系建设》陆平@美亚柏科企业电子数据取证事业部副总

《企业如何应对安全威胁》游兴旺@美团企业平台研发高级总监


Tech Salon 032:移动开发热点技术—动态日志、监控、Hybrid、插件化(请在微信端打开)

出品人|方锦涛 美团研究员 2018/02/03

《线上问题排查体系构建:动态日志》 陈潼@美团Android资深工程师

《混合应用工程化实践》 李罡@美团前端工程师

《Caesium iOS启动时间监控》 吴君阳@美团iOS工程师


Tech Salon 031:线下支付千万级订单服务—前后端架构实践(请在微信端打开)

出品人|任远 美团高级技术专家 2018/01/27

《扫码付前端可用性保障实践》 田泱@美团高级工程师

《业务系统的建设之路》刘运@美团技术专家

《Web离线化加载》 于秋@美团技术专家

《基于WTL框架的Windows开发最佳实践》 钱声鹏@美团高级工程师


Tech Salon 030:餐饮生态高可用系统实践(请在微信端打开)

出品人|温华剑 美团技术专家 2018/01/13

《从“0”开始-美团收银客户端》聂龙羽@美团技术专家

《美团餐饮生态大数据架构实践》牛江浩@美团技术专家

《餐饮开放平台的高可用实践》朱洋波@美团技术专家


美团线上沙龙

任意点击标题,就能在线观看视频或者文章

On-Line Salon 33:美团大脑 智享生活
分享人:王仲远 美团点评高级研究员

On-Line Salon 32:基于深度学习的计算机视觉技术在无人驾驶中的应用
分享人:刘宇达 美团点评资深算法工程师

On-Line Salon 31:利用响应式来解决客户端问题
分享人:臧成威 美团点评前端技术专家

On-Line Salon 30:美团针对 Redis Rehash 机制的探索和实践
分享人:葛春林 美团点评资深工程师

On-Line Salon 29:Flutter 原理与实践
分享人:李少杰 美团点评资深工程师

On-Line Salon 28:如何基于深度学习实现图像的智能审核
分享人:魏晓明 美团点评高级技术专家

On-Line Salon 27:美团客户端响应式框架 EasyReact 开源
分享人:臧成威 美团点评前端技术专家

On-Line Salon 26:移动持续集成在大众点评的实践
分享人:何智聪、邢轶 美团点评前端技术专家

On-Line Salon 25:Kotlin 代码检查探索与实践
分享人:周佳 美团点评前端开发工程师

On-Line Salon 24:静态资源增量更新的新实践
分享人:刘洋河 美团点评资深工程师

On-Line Salon 23:点评侧用户行为检索系统
分享人:朱凯 美团点评资深工程师

On-Line Salon 22:深度学习在美团搜索广告排序的应用实践
分享人:薛欢、梁玉林、王新 美团点评资深工程师

On-Line Salon 21:使用 Mpvue 开发微信小程序的最佳实践
分享人:胡成全 美团点评前端技术专家

On-Line Salon 20:亿级用户个性化品类推荐实战解析
分享人:张晓宇 美团高级工程师

On-Line Salon 19:MVVM 在美团点评酒旅业务中的实践
分享人:吴卓 美团点评技术专家

On-Line Salon 18:深度学习在美团点评推荐业务中实践
分享人:陈文石 美团点评算法专家

感恩!99位出品人和讲师,70+小时的分享,感谢他们给我们带来一线的技术实践分享,感谢他们给我们2万多小伙伴送上技术的“饕餮盛宴”。除了美团技术团队的讲师们,我们还要特别感谢来自腾讯、蚂蚁金服、百度、京东、去哪儿、美亚柏科等公司的讲师们,感谢你们的付出和努力,我们期待新的一年会有更多的合作!

(2018全年出品人及讲师)

---------- END ----------

查看原文

赞 15 收藏 9 评论 1

echo 评论了文章 · 2018-12-29

前端面试中遇到 [] == ![] ? 刨祖坟式博客解析,从 ECMAScript 规范说起,比脱下帽子更有说服力!

[] == ![] ?

应该是腾讯面试题, 原题更加复杂

面试遇到这种令人头皮发麻的题,该怎么办呢? 不要慌,我们科学的应对即可。

经验法,简称瞎蒙

对于简短而罕见的写法,最好的方法就是经验法,基本原则就是瞎蒙,虽然听着有点扯淡,实际上这不失为一个好办法,对于一个比较陌生的问题,我们通过经验瞎几把猜一个「大众」答案:

简单观察此题,我们发现题目想让一个 数组和他的 非 作比较, 从正常的思维来看,一个数和他的非,应该是不相等的。

所以我们 first An is : false

反向操作法

然而你看着面试官淫邪的笑容,突然意识到,问题并不简单,毕竟这家公司还可以,不会来这么小儿科的问题吧。再转念一想,这 tm 的是 js 啊,毕竟 js 经常不按套路出牌啊。

于是你又大胆做出了一个假设: [] == ![] 是 true!

大致结论有了, 那该怎么推导这个结论呢?我们逐步分解一下这个问题。分而治之

最终结论

后面分析很长,涉及到大篇幅的 ECMAScript 规范的解读,冗长而枯燥,不想看的同学,可以在这里直接拿到结论

[] == ![] -> [] == false -> [] == 0 -> [].valueOf() == 0 -> [].toString() == 0 -> '' == 0 -> 0 == 0 -> true

分析

如果你决定要看,千万坚持看完,三十分钟之后我一定会给你一个惊喜。

这是个奇怪的问题,乍一看形式上有些怪异, 如果面试中你遇到这么个题,应该会有些恼火:这 tm 什么玩意?! shift!(防和谐梗)。

虽然有点懵,不过还是理性的分析一下,既然这个表达式含有多个运算符, 那首先还是得看看运算符优先级。

运算符优先级

运算符优先级表

clipboard.png

而此题中出现了两个操作符: 「!」, 「==」, 查表可知, 逻辑非优先级是 16, 而等号优先级是 10, 可见先执行 ![] 操作。在此之前我们先看看 逻辑非

逻辑非 !

mozilla 逻辑非: !

逻辑运算符通常用于Boolean型(逻辑)值。这种情况,它们返回一个布尔型值。

语法描述: 逻辑非(!) !expr

  • 如果expr能转换为true,返回false;
  • 如果expr能转换为false,则返回true。

转 bool

js 中能够转换为false的字面量是可枚举的,包含

  • null;
  • NaN;
  • 0;
  • 空字符串("");
  • undefined。

所以 ![] => false

于是乎我们将问题转化为: [] == false

== 运算符

这是个劲爆的操作符,正经功能没有,自带隐式类型转换经常令人对 js 刮目相看, 实际上现在网上也没有对这个操作符转换规则描述比较好的,这个时候我们就需要去 ECMAscript 上去找找标准了。

ECMAScript® 2019 : 7.2.14 Abstract Equality Comparison

规范描述: The comparison x == y, where x and y are values, produces true or false. Such a comparison is performed as follows:

  1. If Type(x) is the same as Type(y), then

    1. Return the result of performing Strict Equality Comparison x === y.
  2. If x is null and y is undefined, return true.
  3. If x is undefined and y is null, return true.
  4. If Type(x) is Number and Type(y) is String, return the result of the comparison x == ! ToNumber(y).
  5. If Type(x) is String and Type(y) is Number, return the result of the comparison ! ToNumber(x) == y.
  6. If Type(x) is Boolean, return the result of the comparison ! ToNumber(x) == y.
  7. If Type(y) is Boolean, return the result of the comparison x == ! ToNumber(y).
  8. If Type(x) is either String, Number, or Symbol and Type(y) is Object, return the result of the comparison x == ToPrimitive(y).
  9. If Type(x) is Object and Type(y) is either String, Number, or Symbol, return the result of the comparison ToPrimitive(x) == y.
  10. Return false.

依据规范 6, 7 可知,存在 bool 则会将自身 ToNumber 转换 !ToNumber(x) 参考 花絮下的 !ToNumber, 主要是讲解 !的意思 ! 前缀在最新规范中表示某个过程会按照既定的规则和预期的执行【必定会返回一个 number 类型的值,不会是其他类型,甚至 throw error】

得到: [] == !ToNumber(false)

ToNumber

ECMAScript® 2019 : 7.1.3ToNumber

clipboard.png

If argument is true, return 1. If argument is false, return +0.

可知: !ToNumber(false) => 0; [] == 0

然后依据规范 8 9, 执行 ToPrimitive([])

ToPrimitive

ECMAScript® 2019 : 7.1.1ToPrimitive ( input [ , PreferredType ] )

The abstract operation ToPrimitive converts its input argument to a non-Object type. [尝试转换为原始对象]

If an object is capable of converting to more than one primitive type, it may use the optional hint PreferredType to favour that type. Conversion occurs according to the following algorithm. [如果一个对象可以被转换为多种原语类型, 则参考 PreferredType, 依据如下规则转换]

  1. Assert: input is an ECMAScript language value.
  2. If Type(input) is Object, then

    1. If PreferredType is not present, let hint be "default".
    2. Else if PreferredType is hint String, let hint be "string".
    3. Else PreferredType is hint Number, let hint be "number".
    4. Let exoticToPrim be ? GetMethod(input, @@toPrimitive).
    5. If exoticToPrim is not undefined, then

      1. Let result be ? Call(exoticToPrim, input, « hint »).
      2. If Type(result) is not Object, return result.
      3. Throw a TypeError exception.
    6. If hint is "default", set hint to "number".
    7. Return ? OrdinaryToPrimitive(input, hint).
  3. Return input.

大致步骤就是 确定 PreferredType 值[If hint is "default", set hint to "number".], 然后调用 GetMethod, 正常情况下 GetMethod 返回 GetV, GetV 将每个属性值 ToObject, 然后返回 O.[[Get]](P, V).

  1. Assert: IsPropertyKey(P) is true.
  2. Let O be ? ToObject(V).
  3. Return ? O.[[Get]](P, V).

[[Get]]

ECMAScript® 2019 : 9.1.8[[Get]] ( P, Receiver )

Return the value of the property whose key is propertyKey from this object[检索对象的 propertyKey 属性值]

然后 ToPrimitive step 7 返回 OrdinaryToPrimitive(input, hint)

OrdinaryToPrimitive( O, hint )

ECMAScript® 2019 : 7.1.1.1OrdinaryToPrimitive ( O, hint )

  1. Assert: Type(O) is Object.
  2. Assert: Type(hint) is String and its value is either "string" or "number".
  3. If hint is "string", then

    • Let methodNames be « "toString", "valueOf" ».
  4. Else,

    • Let methodNames be « "valueOf", "toString" ».
  5. For each name in methodNames in List order, do

    • 5.1 Let method be ? Get(O, name).
    • 5.2 If IsCallable(method) is true, then

      • 5.2.1 Let result be ? Call(method, O).
      • 5.2.2 If Type(result) is not Object, return result.
  6. Throw a TypeError exception.

上述过程说的很明白: 如果 hint is String,并且他的 value 是 string 或者 number【ToPrimitive 中给 hint 打的标签】,接下来的处理逻辑,3,4 步描述的已经很清楚了。

步骤 5,则是依次处理放入 methodNames 的操作[这也解答了我一直以来的一个疑问,网上也有说对象转 string 的时候,是调用 tostring 和 valueof, 但是总是含糊其辞,哪个先调用,哪个后调用,以及是不是两个方法都会调用等问题总是模棱两可,一句带过 /手动狗头]。

推论

该了解的基本上都梳理出来了, 说实话,非常累,压着没有每个名词都去发散。不过大致需要的环节都有了.

我们回过头来看这个问题: 在对 == 操作符描述的步骤 8 9中,调用 ToPrimitive(y) 可见没指定 PreferredType, 因此 hint 是 default,也就是 number【参考: 7.1.1ToPrimitive 的步骤2-f】

接着调用 OrdinaryToPrimitive(o, number) 则进入 7.1.1.1OrdinaryToPrimitive 的步骤 4 ,然后进入步骤 5 先调用 valueOf,步骤 5.2.2 描述中如果返回的不是 Object 则直接返回,否则才会调用 toString。

所以 [] == 0 => [].valueOf()[.toString()] == 0. 我们接着来看 数组的 valueOf 方法, 请注意区分一点,js 里内置对象都继承的到 valueOf 操作,但是部分对象做了覆写, 比如 String.prototype.valueOf,所以去看看 Array.prototype.valueOf 有没有覆写。

结果是没有,啪啪打脸啊,尼玛,于是乎我们看 Object.prototype.valueOf

Array.prototype.valueOf from Object.prototype.valueOf

ECMAScript® 2019 : 19.1.3.7Object.prototype.valueOf ( )

When the valueOf method is called, the following steps are taken:

  1. Return ? ToObject(this value).

This function is the %ObjProto_valueOf% intrinsic object.

我们接着看 ToObject【抓狂,但是要坚持】。

ToObject

ECMAScript® 2019 : 7.1.13ToObject ( argument )

clipboard.png

Object : Return argument?! 这步算是白走了。我们接着看 toString,同样的我们要考虑覆写的问题。

Array.prototype.toString()

ECMAScript® 2019 : 22.1.3.28Array.prototype.toString ( )

  1. Let array be ? ToObject(this value).
  2. Let func be ? Get(array, "join").
  3. If IsCallable(func) is false, set func to the intrinsic function %ObjProto_toString%.
  4. Return ? Call(func, array).

可见调用了 join 方法【ps: 这里面还有个小故事,我曾经去滴滴面试,二面和我聊到这个问题,我说数组的 toString 调用了 join ,面试官给我说,你不要看着调用结果就臆测内部实现,不是这样思考问题的...... 我就摇了摇头,结果止步二面,猎头反馈的拒绝三连: 方向不匹配,不适合我们,滚吧。😂 😂 😂 】

通过非常艰辛的努力我们走到了这一步

[].valueOf().toString() == 0 => [].join() == 0 => '' == 0

如果你也认真看到这一步,不妨在博客提个 issue 留下联系方式,交个朋友 ^_^。

接着我们看到两边还是不同类型,所以类型转换还得继续, 我们回到 7.2.14 Abstract Equality Comparison 的步骤 4 5 ,

    1. If Type(x) is Number and Type(y) is String, return the result of the comparison x == ! ToNumber(y).
    1. If Type(x) is String and Type(y) is Number, return the result of the comparison ! ToNumber(x) == y.

可见 '' 需要 ToNumber, 我们在上面讲述了 ToNumber 以及转换映射表, 表格里说的很清楚『 String See grammar and conversion algorithm below. 』....

ToNumber Applied to the String Type

ECMAScript® 2019 : 7.1.3.1ToNumber Applied to the String Type

可惜这一步描述的非常抽象

StringNumericLiteral:::
    StrWhiteSpaceopt
    StrWhiteSpaceoptStrNumericLiteralStrWhiteSpaceopt
StrWhiteSpace:::
    StrWhiteSpaceCharStrWhiteSpaceopt
StrWhiteSpaceChar:::
    WhiteSpace
    LineTerminator
StrNumericLiteral:::
    StrDecimalLiteral
    BinaryIntegerLiteral
    OctalIntegerLiteral
    HexIntegerLiteral
StrDecimalLiteral:::
    StrUnsignedDecimalLiteral
    +StrUnsignedDecimalLiteral
    -StrUnsignedDecimalLiteral
StrUnsignedDecimalLiteral:::
    Infinity
    DecimalDigits.DecimalDigitsoptExponentPartopt
    .DecimalDigitsExponentPartopt
    DecimalDigitsExponentPartopt

具体分解如下:

ECMAScript® 2019 : 11.8.3Numeric Literals

摘录一点我们需要用的:

...
DecimalIntegerLiteral::
0
NonZeroDigitDecimalDigitsopt

确认过眼神,是我搞不定的人!整个过程大致描述的是

  1. 如果字符串中只包含数字(包括前面带加号或负号的情况),则将其转换为十进制数值,即"1"会变成1,"123"会变成123,而"011"会变成11(注意:前导的零被忽略了);
  2. 如果字符串中包含有效的浮点格式,如"1.1",则将其转换为对应的浮点数值(同样,也会忽略前导零);
  3. 如果字符串中包含有效的十六进制格式,例如"0xf",则将其转换为相同大小的十进制整数值;
  4. 什么八进制二进制同上;
  5. 如果字符串是空的(不包含任何字符),则将其转换为0;
  6. 如果字符串中包含除上述格式之外的字符,则将其转换为NaN。

不过我们还有 Mozilla : Number("") // 0

所以最终答案就转化为:

'' == 0 => 0 == 0

哦,大哥,原来这 tm 就是惊喜啊!小弟我愿意... 愿意个鬼啊!

Final answer

true

胡说八道

  • Q: 这么做是矫枉过正么?
  • A: 这个问题写博客的时候确实追的非常全,其中涉及到的所有规范都做了详细解释,但其实面试时候只需要知道一些关键点就可以了:
  1. 左边空数组不是转 bool 而是转 number。
  2. 空数组转 number 怎么调用 valueOf 和 toString 的,调用顺序和调用规则是什么?
  3. 「==」大致的隐式转换规则。
  4. js 中那些字面量转 bool 是 false?

但是如果博客就写这四点的话, 那你看完还是知其然不知其所以然。 所以我就写的比较详细。同时也是因为网上很多博客在 valueOf 和 toString 的调用上(顺序,和为什么两个都调用)总是说不清楚,转载几次就开始乱写了, 还有==的转换规则上,都是含糊其辞,所以我就想吧这个问题搞得明明白白。

  • Q: 有人会认认真真看到这里么?
  • A: 有。
  • Q: 这么做有什么用啊?
  • A: 没用,下一个。【ps: 面试中也经常有人问我这个问题,我认为这本质上是你对自己定位的问题,你定位自己是前端,就学应用层,你定位自己是程序员,就看全栈,如果你定位自己是工程师,就看底层,看规范。工作五年以上的程序员,不应该问这个问题。【pss: 我定位自己就是爱好,于是我就瞎鸡儿看】】
  • Q: 工作中用的到么? 工作这么忙哪来的时间?
  • A: pass.
  • Q: 这么写博客,累么?
  • A: 很累,我要查很多很多资料,还要甄别,很多英文文档,对我这个持有「大不列颠负十级的英语认证」的人来说,简直就是美利坚版诗经。一篇博客,起码三四天起。而且大家看起来也需要基础和成本,我也不知道能坚持多久。
  • Q:...
  • A:...

如果你也有问题, 请点开 issue ,加上去吧,不想踩坑的技术可以提上去,问题也可以提上去。

花絮

!ToNumber: !前缀

ECMAScript® 2019 : 5.2.3.4 ReturnIfAbrupt Shorthands

Similarly, prefix ! is used to indicate that the following invocation of an abstract or syntax-directed operation will never return an abrupt completion[The term “abrupt completion” refers to any completion with a [[Type]] value other than normal.] and that the resulting Completion Record's [[Value]] field should be used in place of the return value of the operation. For example, the step:

  • Let val be ! OperationName().

is equivalent to the following steps:

  1. Let val be OperationName().
  2. Assert: val is never an abrupt completion.
  3. If val is a Completion Record, set val to val.[[Value]].

Syntax-directed operations for runtime semantics make use of this shorthand by placing ! or ? before the invocation of the operation:

  • Perform ! SyntaxDirectedOperation of NonTerminal.

大意是: !后面的语法操作的调用永远不会返回突然的完成,我理解是一定会执行一个预期的结果类型,执行步骤就是 上述 1, 2, 3步骤。 !ToNumber 描述的是 一定会讲操作数转换为 number 类型并返回 val.[[value]]

? ToNumber: ? 前缀

同理自己看规范, 就不一一展开了,太多「逃」。

ECMAScript® 2019 : 5.2.3.4 ReturnIfAbrupt Shorthands

Invocations of abstract operations and syntax-directed operations that are prefixed by ? indicate that ReturnIfAbrupt should be applied to the resulting Completion Record.

拓展

  • [] == ![]
  • [] == []
  • [] == false
  • [] == 0
  • [] == '' // [注意转换过程,并不会转 numbe, 看下一题]
  • [] == '0'
  • {} == '0'
查看原文

echo 赞了文章 · 2018-12-28

如何优雅的选择字体(font-family)

大家都知道,在不同操作系统、不同游览器里面默认显示的字体是不一样的,并且相同字体在不同操作系统里面渲染的效果也不尽相同,那么如何设置字体显示效果会比较好呢?下面我们逐步的分析一下:

一、首先我们看看各平台的默认字体情况

1、Window下:
  • 宋体(SimSun):Win下大部分游览器的默认字体,宋体在小字号下(如12px、14px)的显示效果还可以接受,但是字号一大就非常糟糕了,所以使用的时候要注意。

  • 微软雅黑("Microsoft Yahei"):从 Vista 开始,微软提供了这款新的字体,一款无衬线的黑体类字体,并且拥有 RegularBold 两种粗细的字重,显著提高了字体的显示效果。现在这款字体已经成为Windows游览器中最值得使用的中文字体。从Win8开始,微软雅黑又加入了 Light 这款更细的字重,对于喜欢细字体的设计、开发人员又多了一个新的选择。

  • Arial:Win平台上默认的无衬线西文字体(为什么要说英文字体后面会解释),有多种变体,显示效果一般。

  • Tahoma:十分常见的无衬线字体,被采用为Windows 2000、Windows XP、Windows Server 2003及Sega游戏主机Dreamcast等系统的预设字型,显示效果比Arial要好。

  • Verdana:无衬线字体,优点在于它在小字上仍结构清晰端整、阅读辨识容易。

  • 其他:Windows 下默认字体列表:微软官网维基百科Office字体

  • 结论:微软雅黑为Win平台上最值得选择的中文字体,但非游览器默认,需要设置;西文字体的选择以ArialTahoma等无衬线字体为主。

2、Mac OS下:
  • 华文黑体(STHeiti)、华文细黑(STXihei):属于同一字体家族系列,OS X 10.6 之前的简体中文系统界面默认字体,也是目前Chrome游览器下的默认字体,有 RegularBold 两个字重,显示效果可以接受,华文细黑也曾是我最喜欢的字体之一。

  • 黑体-简(Heiti SC):从 10.6 开始,黑体-简代替华文黑体用作简体中文系统界面默认字体,苹果生态最常用的字体之一,包括iPhone、iPad等设备用的也是这款字体,显示效果不错,但是喇叭口设计遭人诟病。

  • 冬青黑体( Hiragino Sans GB ):听说又叫苹果丽黑,日文字体Hiragino KakuGothic的简体中文版,简体中文有 常规体粗体 两种,冬青黑体是一款清新的专业印刷字体,小字号时足够清晰,拥有很多人的追捧。

  • Times New Roman:Mac平台Safari下默认的字体,是最常见且广为人知的西文衬线字体之一,众多网页浏览器和文字处理软件都是用它作为默认字体。

  • Helvetica、Helvetica Neue:一种被广泛使用的传奇般的西文字体(这货还有专门的记录片呢),在微软使用山寨货的Arial时,乔布斯却花费重金获得了Helvetica苹果系统上的使用权,因此该字体也一直伴随着苹果用户,是苹果生态中最常用的西文字体。Helvetica NeueHelvetica的改善版本,且增加了更多不同粗细与宽度的字形,共拥有51种字体版本,极大的满足了日常的使用。

  • 苹方(PingFang SC):在Mac OS X EL Capitan上,苹果为中国用户打造了一款全新中文字体--苹方,去掉了为人诟病的喇叭口,整体造型看上去更加简洁,字族共六枚字体:极细体、纤细体、细体、常规体、中黑体、中粗体

  • San Francisco:同样是Mac OS X EL Capitan上最新发布的西文字体,感觉和Helvetica看上去差别不大,目前已经应用在Mac OS 10.11+、iOS 9.0+、watch OS等最新系统上。

  • 其他:Mac下默认字体列表:苹果官网维基百科

  • 结论:目前苹方San Francisco为苹果推出的最新字体,显示效果也最为优雅,但只有最新系统才能支持,而黑体-简Helvetica可以获得更多系统版本支持,显示效果也相差无几,可以接受。

3、Android系统:
  • Droid Sans、Droid Sans FallbackDroid Sans为安卓系统中默认的西文字体,是一款人文主义无衬线字体,而Droid Sans Fallback则是包含汉字、日文假名、韩文的文字扩展支持。

  • 结论:Droid Sans为默认的字体,并结合了中英文,无需单独设置。

4、iOS系统:
  • iOS系统的字体和Mac OS系统的字体相同,保证了Mac上的字体效果,iOS设备就没有太大问题。

5、Linux:
  • 文泉驿点阵宋体:类似宋体的衬线字体,一般不推荐使用。

  • 文泉驿微米黑:几乎是 Linux 社区现有的最佳简体中文字体。

二、选择字体需要注意的问题

1、字体的中英文写法:

我们在操作系统中常常看到宋体微软雅黑这样的字体名称,但实际上这只是字体的显示名称,而不是字体文件的名称,一般字体文件都是用英文命名的,如SimSunMicrosoft Yahei。在大多数情况下直接使用显示名称也能正确的显示,但是有一些用户的特殊设置会导致中文声明无效。
因此,保守的做法是使用字体的字体名称(英文)或者两者兼写。如下示例:

font-family: STXihei, "Microsoft YaHei";
font-family: STXihei, "华文细黑", "Microsoft YaHei", "微软雅黑";
2、声明英文字体:

绝大部分中文字体里都包含英文字母和数字,不进行英文字体声明是没有问题的,但是大多数中文字体中的英文和数字的部分都不是特别漂亮,所以建议也对英文字体进行声明。
由于英文字体中大多不包含中文,我们可以先进行英文字体的声明,这样不会影响到中文字体的选择,因此优先使用最优秀的英文字体,中文字体声明则紧随其次。如下示例:

font-family: Arial, "Microsoft YaHei";
3、照顾不同的操作系统:
  • 英文、数字部分:在默认的操作系统中,Mac和Win都会带有Arial, Verdana, Tahoma等几个预装字体,从显示效果来看,Tahoma要比Arial更加清晰一些,因此字体设置Tahoma最好放到前面,当找不到Tahoma时再使用Arial;而在Mac中,还拥有一款更加漂亮的Helvetica字体,所以为了照顾Mac用户有更好的体验,应该更优先设置Helvetica字体;Android系统下默认的无衬线字体就可以接受,因此无需单独设置。最后,英文、数字字体的最佳写法如下:

font-family: Helvetica, Tahoma, Arial;
  • 中文部分:在Win下,微软雅黑为大部分人最常使用的中文字体,由于很多人安装Office的缘故,Mac电脑中也会出现微软雅黑字体,因此把显示效果不错的微软雅黑加入到字体列表是个不错的选择;同样,为了保证Mac中更为优雅字体苹方(PingFang SC)黑体-简(Heiti SC)冬青黑体( Hiragino Sans GB )的优先显示,需要把这些字体放到中文字体列表的最前面;同时为了照顾到Linux操作系统的体验,还需要添加文泉驿微米黑字体。最后,中文字体部分最佳写法如下:

font-family: "PingFang SC", "Hiragino Sans GB", "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei";

中英文整合写法:

font-family: Helvetica, Tahoma, Arial, "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei";
font-family: Helvetica, Tahoma, Arial, "PingFang SC", "Hiragino Sans GB", "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei";
4、注意向下兼容

如果还需要考虑旧版本操作系统用户的话,不得不加上一些旧版操作系统存在的字体:Mac中的华文黑体冬青黑体,Win中的黑体等。同样按照显示效果排列在列表后面,写法如下:

font-family: Helvetica, Tahoma, Arial, "PingFang SC", "Hiragino Sans GB", "Heiti SC", STXihei, "Microsoft YaHei", SimHei, "WenQuanYi Micro Hei";

加入了 STXihei(华文细黑) SimHei(黑体)

5、补充字体族名称

字体族大体上分为两类:sans-serif(无衬线体)serif(衬线体),当所有的字体都找不到时,我们可以使用字体族名称作为操作系统最后选择字体的方向。一般非衬线字体在显示器中的显示效果会比较好,因此我们需要在最后添加 sans-serif,写法如下:

font-family: Helvetica, Tahoma, Arial, "PingFang SC", "Hiragino Sans GB", "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;

三、我们看一下大公司的常见写法(2016.07查看)

1、小米
font: 14px/1.5 "Helvetica Neue",Helvetica,Arial,"Microsoft Yahei","Hiragino Sans GB","Heiti SC","WenQuanYi Micro Hei",sans-serif;

小米公司优先使用Helvetica Neue这款字体以保证最新版本Mac用户的最佳体验,选择了Arial作为Win下默认英文字体及Mac的替代英文字体;中文字体方面首选了微软雅黑,然后选择了冬青黑体黑体-简作为Mac上的替代方案;最后使用文泉驿微米黑兼顾了Linux系统。

2、淘宝

鉴于淘宝网改版频率较频繁,这里截图保存了一下,点此查看

font: 12px/1.5 tahoma,arial,'Hiragino Sans GB','\5b8b\4f53',sans-serif;

其实从图中明显看出淘宝网的导航及内容有着大量的衬线字体,鉴于淘宝网站大部分字号比较小,显示效果也还可以接受。代码中可以看出淘宝使用了TahomaArial作为首选英文字体,中文字体首选了冬青黑体,由于Win下没有预装该款字体,所以显示出了后面的宋体(5b8b4f53为汉字宋体用 unicode 表示的写法,不用SimSun是因为 Firefox 的某些版本和 Opera 不支持 SimSun的写法)

3、简书
font-family: "lucida grande", "lucida sans unicode", lucida, helvetica, "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;

自认为简书的阅读体验很棒,我们看看简书所用的字体,简书优先选择了lucida家族的系列字体作为英文字体,该系列字体在Mac和Win上都是预装的,并且有着不俗的表现;中文字体方面将冬青黑体作为最优先使用的字体,同样考虑了Linux系统。

各大公司的字体设置大同小异,这里不再一一举例查看,有兴趣的可以自己多多查看。

四、其他的一些注意点

1、字体何时需要添加引号

当字体具体某个取值中若有一种样式名称包含空格,则需要用双引号或单引号表示,如:

font-family: "Microsoft YaHei", "Arial Narrow", sans-serif;

如果书写中文字体名称为了保证兼容性也会添加引号,如:

font-family: "黑体-简", "微软雅黑", "文泉驿微米黑";
2、引用外部字体

大多数的中文字体由于版权原因不能随意使用,这里推荐一个免版权而且漂亮的中文字体思源黑体,该字体为Adobe与Google推出的一款开源字体, 有七种字体粗细(ExtraLight、Light、Normal、Regular、Medium、Bold 和 Heavy),完全支持日文、韩文、繁体中文和简体中文,字形优美,依稀记得小米公司好像有使用过。
鉴于中文字体的体积比较大(一般字库全一点的中文字体动辄几Mb),所以较少人会使用外部字体,如果真的需要引入,也可以考虑通过工具根据页面文字的使用多少单独生成中文字体,以减小文件大小。

五、最后,推荐写法

由于每个人的审美不一样,钟爱的字体也会有所有不同,这里给出我个人的常用写法:

font-family: "Helvetica Neue", Helvetica, Arial, "PingFang SC", "Hiragino Sans GB", "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;

另外推荐两个github上的关于中文字体和排版的项目:

--参考资料

查看原文

赞 283 收藏 496 评论 24

echo 收藏了文章 · 2018-12-27

前端面试中遇到 [] == ![] ? 刨祖坟式博客解析,从 ECMAScript 规范说起,比脱下帽子更有说服力!

[] == ![] ?

应该是腾讯面试题, 原题更加复杂

面试遇到这种令人头皮发麻的题,该怎么办呢? 不要慌,我们科学的应对即可。

经验法,简称瞎蒙

对于简短而罕见的写法,最好的方法就是经验法,基本原则就是瞎蒙,虽然听着有点扯淡,实际上这不失为一个好办法,对于一个比较陌生的问题,我们通过经验瞎几把猜一个「大众」答案:

简单观察此题,我们发现题目想让一个 数组和他的 非 作比较, 从正常的思维来看,一个数和他的非,应该是不相等的。

所以我们 first An is : false

反向操作法

然而你看着面试官淫邪的笑容,突然意识到,问题并不简单,毕竟这家公司还可以,不会来这么小儿科的问题吧。再转念一想,这 tm 的是 js 啊,毕竟 js 经常不按套路出牌啊。

于是你又大胆做出了一个假设: [] == ![] 是 true!

大致结论有了, 那该怎么推导这个结论呢?我们逐步分解一下这个问题。分而治之

最终结论

后面分析很长,涉及到大篇幅的 ECMAScript 规范的解读,冗长而枯燥,不想看的同学,可以在这里直接拿到结论

[] == ![] -> [] == false -> [] == 0 -> [].valueOf() == 0 -> [].toString() == 0 -> '' == 0 -> 0 == 0 -> true

分析

如果你决定要看,千万坚持看完,三十分钟之后我一定会给你一个惊喜。

这是个奇怪的问题,乍一看形式上有些怪异, 如果面试中你遇到这么个题,应该会有些恼火:这 tm 什么玩意?! shift!(防和谐梗)。

虽然有点懵,不过还是理性的分析一下,既然这个表达式含有多个运算符, 那首先还是得看看运算符优先级。

运算符优先级

运算符优先级表

clipboard.png

而此题中出现了两个操作符: 「!」, 「==」, 查表可知, 逻辑非优先级是 16, 而等号优先级是 10, 可见先执行 ![] 操作。在此之前我们先看看 逻辑非

逻辑非 !

mozilla 逻辑非: !

逻辑运算符通常用于Boolean型(逻辑)值。这种情况,它们返回一个布尔型值。

语法描述: 逻辑非(!) !expr

  • 如果expr能转换为true,返回false;
  • 如果expr能转换为false,则返回true。

转 bool

js 中能够转换为false的字面量是可枚举的,包含

  • null;
  • NaN;
  • 0;
  • 空字符串("");
  • undefined。

所以 ![] => false

于是乎我们将问题转化为: [] == false

== 运算符

这是个劲爆的操作符,正经功能没有,自带隐式类型转换经常令人对 js 刮目相看, 实际上现在网上也没有对这个操作符转换规则描述比较好的,这个时候我们就需要去 ECMAscript 上去找找标准了。

ECMAScript® 2019 : 7.2.14 Abstract Equality Comparison

规范描述: The comparison x == y, where x and y are values, produces true or false. Such a comparison is performed as follows:

  1. If Type(x) is the same as Type(y), then

    1. Return the result of performing Strict Equality Comparison x === y.
  2. If x is null and y is undefined, return true.
  3. If x is undefined and y is null, return true.
  4. If Type(x) is Number and Type(y) is String, return the result of the comparison x == ! ToNumber(y).
  5. If Type(x) is String and Type(y) is Number, return the result of the comparison ! ToNumber(x) == y.
  6. If Type(x) is Boolean, return the result of the comparison ! ToNumber(x) == y.
  7. If Type(y) is Boolean, return the result of the comparison x == ! ToNumber(y).
  8. If Type(x) is either String, Number, or Symbol and Type(y) is Object, return the result of the comparison x == ToPrimitive(y).
  9. If Type(x) is Object and Type(y) is either String, Number, or Symbol, return the result of the comparison ToPrimitive(x) == y.
  10. Return false.

依据规范 6, 7 可知,存在 bool 则会将自身 ToNumber 转换 !ToNumber(x) 参考 花絮下的 !ToNumber, 主要是讲解 !的意思 ! 前缀在最新规范中表示某个过程会按照既定的规则和预期的执行【必定会返回一个 number 类型的值,不会是其他类型,甚至 throw error】

得到: [] == !ToNumber(false)

ToNumber

ECMAScript® 2019 : 7.1.3ToNumber

clipboard.png

If argument is true, return 1. If argument is false, return +0.

可知: !ToNumber(false) => 0; [] == 0

然后依据规范 8 9, 执行 ToPrimitive([])

ToPrimitive

ECMAScript® 2019 : 7.1.1ToPrimitive ( input [ , PreferredType ] )

The abstract operation ToPrimitive converts its input argument to a non-Object type. [尝试转换为原始对象]

If an object is capable of converting to more than one primitive type, it may use the optional hint PreferredType to favour that type. Conversion occurs according to the following algorithm. [如果一个对象可以被转换为多种原语类型, 则参考 PreferredType, 依据如下规则转换]

  1. Assert: input is an ECMAScript language value.
  2. If Type(input) is Object, then

    1. If PreferredType is not present, let hint be "default".
    2. Else if PreferredType is hint String, let hint be "string".
    3. Else PreferredType is hint Number, let hint be "number".
    4. Let exoticToPrim be ? GetMethod(input, @@toPrimitive).
    5. If exoticToPrim is not undefined, then

      1. Let result be ? Call(exoticToPrim, input, « hint »).
      2. If Type(result) is not Object, return result.
      3. Throw a TypeError exception.
    6. If hint is "default", set hint to "number".
    7. Return ? OrdinaryToPrimitive(input, hint).
  3. Return input.

大致步骤就是 确定 PreferredType 值[If hint is "default", set hint to "number".], 然后调用 GetMethod, 正常情况下 GetMethod 返回 GetV, GetV 将每个属性值 ToObject, 然后返回 O.[[Get]](P, V).

  1. Assert: IsPropertyKey(P) is true.
  2. Let O be ? ToObject(V).
  3. Return ? O.[[Get]](P, V).

[[Get]]

ECMAScript® 2019 : 9.1.8[[Get]] ( P, Receiver )

Return the value of the property whose key is propertyKey from this object[检索对象的 propertyKey 属性值]

然后 ToPrimitive step 7 返回 OrdinaryToPrimitive(input, hint)

OrdinaryToPrimitive( O, hint )

ECMAScript® 2019 : 7.1.1.1OrdinaryToPrimitive ( O, hint )

  1. Assert: Type(O) is Object.
  2. Assert: Type(hint) is String and its value is either "string" or "number".
  3. If hint is "string", then

    • Let methodNames be « "toString", "valueOf" ».
  4. Else,

    • Let methodNames be « "valueOf", "toString" ».
  5. For each name in methodNames in List order, do

    • 5.1 Let method be ? Get(O, name).
    • 5.2 If IsCallable(method) is true, then

      • 5.2.1 Let result be ? Call(method, O).
      • 5.2.2 If Type(result) is not Object, return result.
  6. Throw a TypeError exception.

上述过程说的很明白: 如果 hint is String,并且他的 value 是 string 或者 number【ToPrimitive 中给 hint 打的标签】,接下来的处理逻辑,3,4 步描述的已经很清楚了。

步骤 5,则是依次处理放入 methodNames 的操作[这也解答了我一直以来的一个疑问,网上也有说对象转 string 的时候,是调用 tostring 和 valueof, 但是总是含糊其辞,哪个先调用,哪个后调用,以及是不是两个方法都会调用等问题总是模棱两可,一句带过 /手动狗头]。

推论

该了解的基本上都梳理出来了, 说实话,非常累,压着没有每个名词都去发散。不过大致需要的环节都有了.

我们回过头来看这个问题: 在对 == 操作符描述的步骤 8 9中,调用 ToPrimitive(y) 可见没指定 PreferredType, 因此 hint 是 default,也就是 number【参考: 7.1.1ToPrimitive 的步骤2-f】

接着调用 OrdinaryToPrimitive(o, number) 则进入 7.1.1.1OrdinaryToPrimitive 的步骤 4 ,然后进入步骤 5 先调用 valueOf,步骤 5.2.2 描述中如果返回的不是 Object 则直接返回,否则才会调用 toString。

所以 [] == 0 => [].valueOf()[.toString()] == 0. 我们接着来看 数组的 valueOf 方法, 请注意区分一点,js 里内置对象都继承的到 valueOf 操作,但是部分对象做了覆写, 比如 String.prototype.valueOf,所以去看看 Array.prototype.valueOf 有没有覆写。

结果是没有,啪啪打脸啊,尼玛,于是乎我们看 Object.prototype.valueOf

Array.prototype.valueOf from Object.prototype.valueOf

ECMAScript® 2019 : 19.1.3.7Object.prototype.valueOf ( )

When the valueOf method is called, the following steps are taken:

  1. Return ? ToObject(this value).

This function is the %ObjProto_valueOf% intrinsic object.

我们接着看 ToObject【抓狂,但是要坚持】。

ToObject

ECMAScript® 2019 : 7.1.13ToObject ( argument )

clipboard.png

Object : Return argument?! 这步算是白走了。我们接着看 toString,同样的我们要考虑覆写的问题。

Array.prototype.toString()

ECMAScript® 2019 : 22.1.3.28Array.prototype.toString ( )

  1. Let array be ? ToObject(this value).
  2. Let func be ? Get(array, "join").
  3. If IsCallable(func) is false, set func to the intrinsic function %ObjProto_toString%.
  4. Return ? Call(func, array).

可见调用了 join 方法【ps: 这里面还有个小故事,我曾经去滴滴面试,二面和我聊到这个问题,我说数组的 toString 调用了 join ,面试官给我说,你不要看着调用结果就臆测内部实现,不是这样思考问题的...... 我就摇了摇头,结果止步二面,猎头反馈的拒绝三连: 方向不匹配,不适合我们,滚吧。😂 😂 😂 】

通过非常艰辛的努力我们走到了这一步

[].valueOf().toString() == 0 => [].join() == 0 => '' == 0

如果你也认真看到这一步,不妨在博客提个 issue 留下联系方式,交个朋友 ^_^。

接着我们看到两边还是不同类型,所以类型转换还得继续, 我们回到 7.2.14 Abstract Equality Comparison 的步骤 4 5 ,

    1. If Type(x) is Number and Type(y) is String, return the result of the comparison x == ! ToNumber(y).
    1. If Type(x) is String and Type(y) is Number, return the result of the comparison ! ToNumber(x) == y.

可见 '' 需要 ToNumber, 我们在上面讲述了 ToNumber 以及转换映射表, 表格里说的很清楚『 String See grammar and conversion algorithm below. 』....

ToNumber Applied to the String Type

ECMAScript® 2019 : 7.1.3.1ToNumber Applied to the String Type

可惜这一步描述的非常抽象

StringNumericLiteral:::
    StrWhiteSpaceopt
    StrWhiteSpaceoptStrNumericLiteralStrWhiteSpaceopt
StrWhiteSpace:::
    StrWhiteSpaceCharStrWhiteSpaceopt
StrWhiteSpaceChar:::
    WhiteSpace
    LineTerminator
StrNumericLiteral:::
    StrDecimalLiteral
    BinaryIntegerLiteral
    OctalIntegerLiteral
    HexIntegerLiteral
StrDecimalLiteral:::
    StrUnsignedDecimalLiteral
    +StrUnsignedDecimalLiteral
    -StrUnsignedDecimalLiteral
StrUnsignedDecimalLiteral:::
    Infinity
    DecimalDigits.DecimalDigitsoptExponentPartopt
    .DecimalDigitsExponentPartopt
    DecimalDigitsExponentPartopt

具体分解如下:

ECMAScript® 2019 : 11.8.3Numeric Literals

摘录一点我们需要用的:

...
DecimalIntegerLiteral::
0
NonZeroDigitDecimalDigitsopt

确认过眼神,是我搞不定的人!整个过程大致描述的是

  1. 如果字符串中只包含数字(包括前面带加号或负号的情况),则将其转换为十进制数值,即"1"会变成1,"123"会变成123,而"011"会变成11(注意:前导的零被忽略了);
  2. 如果字符串中包含有效的浮点格式,如"1.1",则将其转换为对应的浮点数值(同样,也会忽略前导零);
  3. 如果字符串中包含有效的十六进制格式,例如"0xf",则将其转换为相同大小的十进制整数值;
  4. 什么八进制二进制同上;
  5. 如果字符串是空的(不包含任何字符),则将其转换为0;
  6. 如果字符串中包含除上述格式之外的字符,则将其转换为NaN。

不过我们还有 Mozilla : Number("") // 0

所以最终答案就转化为:

'' == 0 => 0 == 0

哦,大哥,原来这 tm 就是惊喜啊!小弟我愿意... 愿意个鬼啊!

Final answer

true

胡说八道

  • Q: 这么做是矫枉过正么?
  • A: 这个问题写博客的时候确实追的非常全,其中涉及到的所有规范都做了详细解释,但其实面试时候只需要知道一些关键点就可以了:
  1. 左边空数组不是转 bool 而是转 number。
  2. 空数组转 number 怎么调用 valueOf 和 toString 的,调用顺序和调用规则是什么?
  3. 「==」大致的隐式转换规则。
  4. js 中那些字面量转 bool 是 false?

但是如果博客就写这四点的话, 那你看完还是知其然不知其所以然。 所以我就写的比较详细。同时也是因为网上很多博客在 valueOf 和 toString 的调用上(顺序,和为什么两个都调用)总是说不清楚,转载几次就开始乱写了, 还有==的转换规则上,都是含糊其辞,所以我就想吧这个问题搞得明明白白。

  • Q: 有人会认认真真看到这里么?
  • A: 有。
  • Q: 这么做有什么用啊?
  • A: 没用,下一个。【ps: 面试中也经常有人问我这个问题,我认为这本质上是你对自己定位的问题,你定位自己是前端,就学应用层,你定位自己是程序员,就看全栈,如果你定位自己是工程师,就看底层,看规范。工作五年以上的程序员,不应该问这个问题。【pss: 我定位自己就是爱好,于是我就瞎鸡儿看】】
  • Q: 工作中用的到么? 工作这么忙哪来的时间?
  • A: pass.
  • Q: 这么写博客,累么?
  • A: 很累,我要查很多很多资料,还要甄别,很多英文文档,对我这个持有「大不列颠负十级的英语认证」的人来说,简直就是美利坚版诗经。一篇博客,起码三四天起。而且大家看起来也需要基础和成本,我也不知道能坚持多久。
  • Q:...
  • A:...

如果你也有问题, 请点开 issue ,加上去吧,不想踩坑的技术可以提上去,问题也可以提上去。

花絮

!ToNumber: !前缀

ECMAScript® 2019 : 5.2.3.4 ReturnIfAbrupt Shorthands

Similarly, prefix ! is used to indicate that the following invocation of an abstract or syntax-directed operation will never return an abrupt completion[The term “abrupt completion” refers to any completion with a [[Type]] value other than normal.] and that the resulting Completion Record's [[Value]] field should be used in place of the return value of the operation. For example, the step:

  • Let val be ! OperationName().

is equivalent to the following steps:

  1. Let val be OperationName().
  2. Assert: val is never an abrupt completion.
  3. If val is a Completion Record, set val to val.[[Value]].

Syntax-directed operations for runtime semantics make use of this shorthand by placing ! or ? before the invocation of the operation:

  • Perform ! SyntaxDirectedOperation of NonTerminal.

大意是: !后面的语法操作的调用永远不会返回突然的完成,我理解是一定会执行一个预期的结果类型,执行步骤就是 上述 1, 2, 3步骤。 !ToNumber 描述的是 一定会讲操作数转换为 number 类型并返回 val.[[value]]

? ToNumber: ? 前缀

同理自己看规范, 就不一一展开了,太多「逃」。

ECMAScript® 2019 : 5.2.3.4 ReturnIfAbrupt Shorthands

Invocations of abstract operations and syntax-directed operations that are prefixed by ? indicate that ReturnIfAbrupt should be applied to the resulting Completion Record.

拓展

  • [] == ![]
  • [] == []
  • [] == false
  • [] == 0
  • [] == '' // [注意转换过程,并不会转 numbe, 看下一题]
  • [] == '0'
  • {} == '0'
查看原文

echo 评论了文章 · 2018-12-27

前端面试中遇到 [] == ![] ? 刨祖坟式博客解析,从 ECMAScript 规范说起,比脱下帽子更有说服力!

[] == ![] ?

应该是腾讯面试题, 原题更加复杂

面试遇到这种令人头皮发麻的题,该怎么办呢? 不要慌,我们科学的应对即可。

经验法,简称瞎蒙

对于简短而罕见的写法,最好的方法就是经验法,基本原则就是瞎蒙,虽然听着有点扯淡,实际上这不失为一个好办法,对于一个比较陌生的问题,我们通过经验瞎几把猜一个「大众」答案:

简单观察此题,我们发现题目想让一个 数组和他的 非 作比较, 从正常的思维来看,一个数和他的非,应该是不相等的。

所以我们 first An is : false

反向操作法

然而你看着面试官淫邪的笑容,突然意识到,问题并不简单,毕竟这家公司还可以,不会来这么小儿科的问题吧。再转念一想,这 tm 的是 js 啊,毕竟 js 经常不按套路出牌啊。

于是你又大胆做出了一个假设: [] == ![] 是 true!

大致结论有了, 那该怎么推导这个结论呢?我们逐步分解一下这个问题。分而治之

最终结论

后面分析很长,涉及到大篇幅的 ECMAScript 规范的解读,冗长而枯燥,不想看的同学,可以在这里直接拿到结论

[] == ![] -> [] == false -> [] == 0 -> [].valueOf() == 0 -> [].toString() == 0 -> '' == 0 -> 0 == 0 -> true

分析

如果你决定要看,千万坚持看完,三十分钟之后我一定会给你一个惊喜。

这是个奇怪的问题,乍一看形式上有些怪异, 如果面试中你遇到这么个题,应该会有些恼火:这 tm 什么玩意?! shift!(防和谐梗)。

虽然有点懵,不过还是理性的分析一下,既然这个表达式含有多个运算符, 那首先还是得看看运算符优先级。

运算符优先级

运算符优先级表

clipboard.png

而此题中出现了两个操作符: 「!」, 「==」, 查表可知, 逻辑非优先级是 16, 而等号优先级是 10, 可见先执行 ![] 操作。在此之前我们先看看 逻辑非

逻辑非 !

mozilla 逻辑非: !

逻辑运算符通常用于Boolean型(逻辑)值。这种情况,它们返回一个布尔型值。

语法描述: 逻辑非(!) !expr

  • 如果expr能转换为true,返回false;
  • 如果expr能转换为false,则返回true。

转 bool

js 中能够转换为false的字面量是可枚举的,包含

  • null;
  • NaN;
  • 0;
  • 空字符串("");
  • undefined。

所以 ![] => false

于是乎我们将问题转化为: [] == false

== 运算符

这是个劲爆的操作符,正经功能没有,自带隐式类型转换经常令人对 js 刮目相看, 实际上现在网上也没有对这个操作符转换规则描述比较好的,这个时候我们就需要去 ECMAscript 上去找找标准了。

ECMAScript® 2019 : 7.2.14 Abstract Equality Comparison

规范描述: The comparison x == y, where x and y are values, produces true or false. Such a comparison is performed as follows:

  1. If Type(x) is the same as Type(y), then

    1. Return the result of performing Strict Equality Comparison x === y.
  2. If x is null and y is undefined, return true.
  3. If x is undefined and y is null, return true.
  4. If Type(x) is Number and Type(y) is String, return the result of the comparison x == ! ToNumber(y).
  5. If Type(x) is String and Type(y) is Number, return the result of the comparison ! ToNumber(x) == y.
  6. If Type(x) is Boolean, return the result of the comparison ! ToNumber(x) == y.
  7. If Type(y) is Boolean, return the result of the comparison x == ! ToNumber(y).
  8. If Type(x) is either String, Number, or Symbol and Type(y) is Object, return the result of the comparison x == ToPrimitive(y).
  9. If Type(x) is Object and Type(y) is either String, Number, or Symbol, return the result of the comparison ToPrimitive(x) == y.
  10. Return false.

依据规范 6, 7 可知,存在 bool 则会将自身 ToNumber 转换 !ToNumber(x) 参考 花絮下的 !ToNumber, 主要是讲解 !的意思 ! 前缀在最新规范中表示某个过程会按照既定的规则和预期的执行【必定会返回一个 number 类型的值,不会是其他类型,甚至 throw error】

得到: [] == !ToNumber(false)

ToNumber

ECMAScript® 2019 : 7.1.3ToNumber

clipboard.png

If argument is true, return 1. If argument is false, return +0.

可知: !ToNumber(false) => 0; [] == 0

然后依据规范 8 9, 执行 ToPrimitive([])

ToPrimitive

ECMAScript® 2019 : 7.1.1ToPrimitive ( input [ , PreferredType ] )

The abstract operation ToPrimitive converts its input argument to a non-Object type. [尝试转换为原始对象]

If an object is capable of converting to more than one primitive type, it may use the optional hint PreferredType to favour that type. Conversion occurs according to the following algorithm. [如果一个对象可以被转换为多种原语类型, 则参考 PreferredType, 依据如下规则转换]

  1. Assert: input is an ECMAScript language value.
  2. If Type(input) is Object, then

    1. If PreferredType is not present, let hint be "default".
    2. Else if PreferredType is hint String, let hint be "string".
    3. Else PreferredType is hint Number, let hint be "number".
    4. Let exoticToPrim be ? GetMethod(input, @@toPrimitive).
    5. If exoticToPrim is not undefined, then

      1. Let result be ? Call(exoticToPrim, input, « hint »).
      2. If Type(result) is not Object, return result.
      3. Throw a TypeError exception.
    6. If hint is "default", set hint to "number".
    7. Return ? OrdinaryToPrimitive(input, hint).
  3. Return input.

大致步骤就是 确定 PreferredType 值[If hint is "default", set hint to "number".], 然后调用 GetMethod, 正常情况下 GetMethod 返回 GetV, GetV 将每个属性值 ToObject, 然后返回 O.[[Get]](P, V).

  1. Assert: IsPropertyKey(P) is true.
  2. Let O be ? ToObject(V).
  3. Return ? O.[[Get]](P, V).

[[Get]]

ECMAScript® 2019 : 9.1.8[[Get]] ( P, Receiver )

Return the value of the property whose key is propertyKey from this object[检索对象的 propertyKey 属性值]

然后 ToPrimitive step 7 返回 OrdinaryToPrimitive(input, hint)

OrdinaryToPrimitive( O, hint )

ECMAScript® 2019 : 7.1.1.1OrdinaryToPrimitive ( O, hint )

  1. Assert: Type(O) is Object.
  2. Assert: Type(hint) is String and its value is either "string" or "number".
  3. If hint is "string", then

    • Let methodNames be « "toString", "valueOf" ».
  4. Else,

    • Let methodNames be « "valueOf", "toString" ».
  5. For each name in methodNames in List order, do

    • 5.1 Let method be ? Get(O, name).
    • 5.2 If IsCallable(method) is true, then

      • 5.2.1 Let result be ? Call(method, O).
      • 5.2.2 If Type(result) is not Object, return result.
  6. Throw a TypeError exception.

上述过程说的很明白: 如果 hint is String,并且他的 value 是 string 或者 number【ToPrimitive 中给 hint 打的标签】,接下来的处理逻辑,3,4 步描述的已经很清楚了。

步骤 5,则是依次处理放入 methodNames 的操作[这也解答了我一直以来的一个疑问,网上也有说对象转 string 的时候,是调用 tostring 和 valueof, 但是总是含糊其辞,哪个先调用,哪个后调用,以及是不是两个方法都会调用等问题总是模棱两可,一句带过 /手动狗头]。

推论

该了解的基本上都梳理出来了, 说实话,非常累,压着没有每个名词都去发散。不过大致需要的环节都有了.

我们回过头来看这个问题: 在对 == 操作符描述的步骤 8 9中,调用 ToPrimitive(y) 可见没指定 PreferredType, 因此 hint 是 default,也就是 number【参考: 7.1.1ToPrimitive 的步骤2-f】

接着调用 OrdinaryToPrimitive(o, number) 则进入 7.1.1.1OrdinaryToPrimitive 的步骤 4 ,然后进入步骤 5 先调用 valueOf,步骤 5.2.2 描述中如果返回的不是 Object 则直接返回,否则才会调用 toString。

所以 [] == 0 => [].valueOf()[.toString()] == 0. 我们接着来看 数组的 valueOf 方法, 请注意区分一点,js 里内置对象都继承的到 valueOf 操作,但是部分对象做了覆写, 比如 String.prototype.valueOf,所以去看看 Array.prototype.valueOf 有没有覆写。

结果是没有,啪啪打脸啊,尼玛,于是乎我们看 Object.prototype.valueOf

Array.prototype.valueOf from Object.prototype.valueOf

ECMAScript® 2019 : 19.1.3.7Object.prototype.valueOf ( )

When the valueOf method is called, the following steps are taken:

  1. Return ? ToObject(this value).

This function is the %ObjProto_valueOf% intrinsic object.

我们接着看 ToObject【抓狂,但是要坚持】。

ToObject

ECMAScript® 2019 : 7.1.13ToObject ( argument )

clipboard.png

Object : Return argument?! 这步算是白走了。我们接着看 toString,同样的我们要考虑覆写的问题。

Array.prototype.toString()

ECMAScript® 2019 : 22.1.3.28Array.prototype.toString ( )

  1. Let array be ? ToObject(this value).
  2. Let func be ? Get(array, "join").
  3. If IsCallable(func) is false, set func to the intrinsic function %ObjProto_toString%.
  4. Return ? Call(func, array).

可见调用了 join 方法【ps: 这里面还有个小故事,我曾经去滴滴面试,二面和我聊到这个问题,我说数组的 toString 调用了 join ,面试官给我说,你不要看着调用结果就臆测内部实现,不是这样思考问题的...... 我就摇了摇头,结果止步二面,猎头反馈的拒绝三连: 方向不匹配,不适合我们,滚吧。😂 😂 😂 】

通过非常艰辛的努力我们走到了这一步

[].valueOf().toString() == 0 => [].join() == 0 => '' == 0

如果你也认真看到这一步,不妨在博客提个 issue 留下联系方式,交个朋友 ^_^。

接着我们看到两边还是不同类型,所以类型转换还得继续, 我们回到 7.2.14 Abstract Equality Comparison 的步骤 4 5 ,

    1. If Type(x) is Number and Type(y) is String, return the result of the comparison x == ! ToNumber(y).
    1. If Type(x) is String and Type(y) is Number, return the result of the comparison ! ToNumber(x) == y.

可见 '' 需要 ToNumber, 我们在上面讲述了 ToNumber 以及转换映射表, 表格里说的很清楚『 String See grammar and conversion algorithm below. 』....

ToNumber Applied to the String Type

ECMAScript® 2019 : 7.1.3.1ToNumber Applied to the String Type

可惜这一步描述的非常抽象

StringNumericLiteral:::
    StrWhiteSpaceopt
    StrWhiteSpaceoptStrNumericLiteralStrWhiteSpaceopt
StrWhiteSpace:::
    StrWhiteSpaceCharStrWhiteSpaceopt
StrWhiteSpaceChar:::
    WhiteSpace
    LineTerminator
StrNumericLiteral:::
    StrDecimalLiteral
    BinaryIntegerLiteral
    OctalIntegerLiteral
    HexIntegerLiteral
StrDecimalLiteral:::
    StrUnsignedDecimalLiteral
    +StrUnsignedDecimalLiteral
    -StrUnsignedDecimalLiteral
StrUnsignedDecimalLiteral:::
    Infinity
    DecimalDigits.DecimalDigitsoptExponentPartopt
    .DecimalDigitsExponentPartopt
    DecimalDigitsExponentPartopt

具体分解如下:

ECMAScript® 2019 : 11.8.3Numeric Literals

摘录一点我们需要用的:

...
DecimalIntegerLiteral::
0
NonZeroDigitDecimalDigitsopt

确认过眼神,是我搞不定的人!整个过程大致描述的是

  1. 如果字符串中只包含数字(包括前面带加号或负号的情况),则将其转换为十进制数值,即"1"会变成1,"123"会变成123,而"011"会变成11(注意:前导的零被忽略了);
  2. 如果字符串中包含有效的浮点格式,如"1.1",则将其转换为对应的浮点数值(同样,也会忽略前导零);
  3. 如果字符串中包含有效的十六进制格式,例如"0xf",则将其转换为相同大小的十进制整数值;
  4. 什么八进制二进制同上;
  5. 如果字符串是空的(不包含任何字符),则将其转换为0;
  6. 如果字符串中包含除上述格式之外的字符,则将其转换为NaN。

不过我们还有 Mozilla : Number("") // 0

所以最终答案就转化为:

'' == 0 => 0 == 0

哦,大哥,原来这 tm 就是惊喜啊!小弟我愿意... 愿意个鬼啊!

Final answer

true

胡说八道

  • Q: 这么做是矫枉过正么?
  • A: 这个问题写博客的时候确实追的非常全,其中涉及到的所有规范都做了详细解释,但其实面试时候只需要知道一些关键点就可以了:
  1. 左边空数组不是转 bool 而是转 number。
  2. 空数组转 number 怎么调用 valueOf 和 toString 的,调用顺序和调用规则是什么?
  3. 「==」大致的隐式转换规则。
  4. js 中那些字面量转 bool 是 false?

但是如果博客就写这四点的话, 那你看完还是知其然不知其所以然。 所以我就写的比较详细。同时也是因为网上很多博客在 valueOf 和 toString 的调用上(顺序,和为什么两个都调用)总是说不清楚,转载几次就开始乱写了, 还有==的转换规则上,都是含糊其辞,所以我就想吧这个问题搞得明明白白。

  • Q: 有人会认认真真看到这里么?
  • A: 有。
  • Q: 这么做有什么用啊?
  • A: 没用,下一个。【ps: 面试中也经常有人问我这个问题,我认为这本质上是你对自己定位的问题,你定位自己是前端,就学应用层,你定位自己是程序员,就看全栈,如果你定位自己是工程师,就看底层,看规范。工作五年以上的程序员,不应该问这个问题。【pss: 我定位自己就是爱好,于是我就瞎鸡儿看】】
  • Q: 工作中用的到么? 工作这么忙哪来的时间?
  • A: pass.
  • Q: 这么写博客,累么?
  • A: 很累,我要查很多很多资料,还要甄别,很多英文文档,对我这个持有「大不列颠负十级的英语认证」的人来说,简直就是美利坚版诗经。一篇博客,起码三四天起。而且大家看起来也需要基础和成本,我也不知道能坚持多久。
  • Q:...
  • A:...

如果你也有问题, 请点开 issue ,加上去吧,不想踩坑的技术可以提上去,问题也可以提上去。

花絮

!ToNumber: !前缀

ECMAScript® 2019 : 5.2.3.4 ReturnIfAbrupt Shorthands

Similarly, prefix ! is used to indicate that the following invocation of an abstract or syntax-directed operation will never return an abrupt completion[The term “abrupt completion” refers to any completion with a [[Type]] value other than normal.] and that the resulting Completion Record's [[Value]] field should be used in place of the return value of the operation. For example, the step:

  • Let val be ! OperationName().

is equivalent to the following steps:

  1. Let val be OperationName().
  2. Assert: val is never an abrupt completion.
  3. If val is a Completion Record, set val to val.[[Value]].

Syntax-directed operations for runtime semantics make use of this shorthand by placing ! or ? before the invocation of the operation:

  • Perform ! SyntaxDirectedOperation of NonTerminal.

大意是: !后面的语法操作的调用永远不会返回突然的完成,我理解是一定会执行一个预期的结果类型,执行步骤就是 上述 1, 2, 3步骤。 !ToNumber 描述的是 一定会讲操作数转换为 number 类型并返回 val.[[value]]

? ToNumber: ? 前缀

同理自己看规范, 就不一一展开了,太多「逃」。

ECMAScript® 2019 : 5.2.3.4 ReturnIfAbrupt Shorthands

Invocations of abstract operations and syntax-directed operations that are prefixed by ? indicate that ReturnIfAbrupt should be applied to the resulting Completion Record.

拓展

  • [] == ![]
  • [] == []
  • [] == false
  • [] == 0
  • [] == '' // [注意转换过程,并不会转 numbe, 看下一题]
  • [] == '0'
  • {} == '0'
查看原文

echo 回答了问题 · 2018-12-27

解决ts数据类型检查报错 Type 'void' is not assignable to type 'never[]'.

请把代码贴全一些, this.list 是什么类型的? task.get 返回的类型怎么声明的?

一般来说async 返回一个 promise, 所以你需要声明一个 Promise 的泛型, 但是 resolve 和 reject 的返回可能不同, 你大概率是一个 联合泛型, 类似 Promise<Object | string> 之类的, 或者直接 any 。

你提供的信息太少了。

不过还可以给你一个 万能招数: ignore, 很不建议。

// @ts-ignore

关注 4 回答 3

认证与成就

  • 获得 187 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2018-11-28
个人主页被 702 人浏览