Skip to content

从规范中探究隐式转换 #6

@moyui

Description

@moyui

前言,分享一道与隐式转换相关的面试题。

    if ([] == false) console.log(1);
    if ({} == false) console.log(2);
    if ([]) console.log(3);
    if ([1] == [1]) console.log(4);
    if ('1' == [1]) console.log(5);

    答案:
    1
    3
    5

头都大了,可能有些人会说平时写代码哪有这样写的,分析这玩意没有用。嘛,没用就没用,但是说实话隐式转换在平时写代码中不知不觉地就会用到。总还是想去研究研究的,难道没有点程序员的浪漫与理想精神嘛?(笑)。

同样,不想看中间一大堆翻译的直接可以调到最后了。

废话不多说,看一下规范吧。

从规范中探究隐式转换

7.2.14 抽象相等比较

这个比较:'x==y',当x和y是值,产生true与false。以下列形式进行比较:

  1. 如果Type(x)与Type(y)类型相同,然后返回运行String Equality Comparsion x===y
  2. 如果x是null,y是undefined,返回true
  3. 如果x是undefined,y是null,返回true
  4. 如果Type(x)是Number然而Type(y)是String,返回比较结果x == !ToNumber(y)
  5. 如果Type(x)是String然而Type(y)是Number,返回比较结果!ToNumber(x) == y
  6. 如果Type(x)是Boolean,返回比较结果!ToNumber(x) == y
  7. 如果Type(y)是Boolean,返回比较结果x == !ToNumber(y)
  8. 如果Type(x)是String,Number或者Symbol并且Type(y)是Object,返回x==ToPrimitive(y)
  9. 如果Type(x)是Object并且Type(y)是String,Number或者Symbol,返回ToPrimitive(x)==y
  10. 返回false

其中两个比较重要的操作ToPrimitive和ToNumber

7.1.1 ToPrimitive(input[, PreferredType]

抽象操作ToPrimitive获取参数input和一个可选参数PreferredType。抽象操作转换input到非Object类型的。如果一个对象可以转换到多个基本类型,就会用PreferredType转换到特定的type,转换通过以下算法实现:

  1. 断言:input是语言类型。
  2. 如果type(input)是Object,然后:
    • a. if PT(PreferredType)不存在,让hint为'default',
    • b. else if PT是String,让hint为'string',
    • c. else PT是Number,hint为'number',
    • d. 让exoticToPrim为GetMethod(input, @@toPrimitive)函数返回值(译注,后面这个@@toPrimitive不知道是什么)
    • e. 如果exoticToPrim不是undefined,然后
      • i. 让result为Call(exoticToPrim,input,<<hint>>)返回值。
      • ii. 如果Type(result)不是Object,返回result,
      • iii. 抛出TypeError
    • f. 如果hint是'default',将hint设为'number'(译注:这里我们就可以明白了,通常情况下ToPrimitive尽力转换为number类型)
    • g. 返回OrdinaryToPrimitive(input, hint)的返回值.
      3.返回input

注:调用ToPrimitive时没有hint,那么它通常表现为hint是Number。然而,对象可能重写(override)这种行为通过定义一个@@toPrimitive方法

(译注:的确翻译为method,但是根据GetMethod函数,传入的@@toPrimitive应该为String或者Symbol类型,在这里应该指的是方法名,不是function类型。)。

本规范中定义的对象只有Date对象(见20.3.4.45)和Symbol对象(参见19.4.3.4)重写默认ToPrimitive行为。如果hint是String,那么Date对象当作没有hint.

(译注:没明白,有人指教嘛)。

看一下GetMethod函数

7.3.9 GetMethod(V,P)

抽象操作GetMethod是用于获取一个特定属性的ES语言值当这个特定属性的值为function类型。V是ES语言类型,P是特定属性,这个操作以参数V,P被调用。这个操作以下几步进行。

  1. 断言:IsPropertyKey(P)为true,
  2. 让func为GetV(V,P)的返回值
  3. 如果func为undefined或者null,返回undefined,
  4. 如果IsCallable(func)是false,返回TypeError,
  5. 返回func

看一下IsPropertyKey、GetV、IsCallable

7.2.7 IsPropertyKey(argument)

抽象操作IsPropertyKey决定如果有argument,必须是一个ES语言值,这个值作为一个键。

  1. 如果Type(argument)为String,返回true,
  2. 如果Type(argument)为Symbol,返回true,
  3. 返回false

7.3.2 GetV(V,P)

抽象操作GetV用于检索一个特定属性的ES语言值。如果值不是一个对象,属性使用包装对象

(译注:应该是Number(),String()这类的)

执行查找适合的类型的值。以参数V和P操作被调用,V是值、P是键。

  1. 断言:IsPropertyKey(P)为true,
  2. 让O为ToObject返回值(V),
  3. 返回O.[[GET]](P,V)

7.1.13 ToObject

参数类型 结果
Undefined 抛出TypeError
Null 抛出TypeError
Boolean 返回一个新的Boolean对象,这个对象的[[BooleanData]]内置插槽被传入参数设置
Number 返回一个新的Number对象,这个对象的[[NumberData]]内置插槽被传入参数设置
String 返回一个新的String对象,这个对象的[[StringData]]内置插槽被传入参数设置
Symbol 返回一个新的Symbol对象,这个对象的[[SymbolData]]内置插槽被传入参数设置
Object 返回参数

7.2.3 IsCallable(argument)

抽象操作IsCallable确定参数,这个参数这必须是一个ES语言值,是一个可调用,有着[[Call]]内部方法的函数。

  1. 如果Type(argument)不是Object,返回false。
  2. 如果argument有[[Call]]内部方法,返回true。
  3. 返回false。

剩下Call与OrdinaryToPrimitive两个函数。

7.3.12 Call(F,V[,argumentsList])

抽象操作Call通常是调用函数对象内部的[[Call]]方法。操作以参数F,V被调用,并有选择的参数argumentsList当F是函数对象,V是一个ES语言值,并且这个(this)值是[[Call]]的值

(译注:我更觉得这个this是指的是函数的上下文)

,argumentsList是值,这个值传递给相应的参数的内部方法。如果argumentsList不存在,一个新的空列表作为它的值。这个抽象操作执行以下步骤:

  1. argumentsList不存在,argumentsList设置为一个新的空列表。
  2. 如果IsCallable(F)是false,抛出一个TypeError异常。
  3. 返回F.[[Call]](V, argumentsList)。(译注:这边明白了,V实际上就是this);

7.1.1.1 OrdinaryToPrimitive(O, hint)

当抽象操作OrdinaryToPrimitive以参数O和hint被调用,采取以下步骤:

  1. 断言:Type(O)是Object。
  2. 单元:Type(hint)是'string'或者'number',
  3. 如果hint是string,然后
    • a. 让methodNames为<<"toString", "valueOf">>,
  4. 否则
    • a. 让methodNames为<<"valueOf", "toString">>
  5. 对于每一个在methodNames中的name,按照列表顺序
    • a. 让method为Get(O,name)的返回值,
    • b. 如果IsCallable(method)为true,然后
      • i. 让result为Call(method,O)的返回值
      • ii. 如果Type(Object)不是Object,返回result
  6. 抛出TypeError

还要看一下ToNumber

7.1.3 ToNumber(argument)

抽象操作ToNumber转换参数为Number类型的值

参数类型 结果
Undefined 返回NaN
Null 返回+0
Boolean 如果参数是true,返回1。如果为false,返回+0
Number 返回参数
String 查看下方的算法
Symbol 抛出TypeError
Object 1.让primValue为ToPrimitive(argument, hint Number)返回值
2. 返回ToNumber(primValue)

最后是String中的算法部分

7.1.3.1 ToNumber Applied to the String Type

ToNumber算法中字符串转换为ToNumber是根据输入的字符串,这个字符串是被翻译成为一串序列的UTF-16编码的码点

(译注:不太明白的建议百度或者google字符串码点,或者看这篇文章

如果语法不能将字符串翻译成StringNumericLiteral的扩展,那么就返回NaN。

注:这个语法的终端符号都是由Unicode字符的基本多文种平面(BMP)所组成的。因此,如果字符串包含了任意的高位代理码元(leading surrogate)或者低位代理码元(trailing surrogate)的话,不论是不是匹配,返回的结果都是NaN。

(译注:简单介绍一下,在javascript中是以UTF-16作为编码字符。UTF-16编码是以两个字节也就是16bit保存的。码元是指使用某种给定的编码规则给抽象字符编码后得到的比特序列,比如大写字母A,unicode为U+0061,存入码元就表示为0x0061。一般我们常用的文字,只需要1个码元,而遇到比较复杂的文字的时候,UTF-16可能需要两个码元来编码,每个16bit,所以一个代码点(可以理解成一个文字)就被拆成了两个16进制数来表示,比如U+1F600拆成了0xD83D 0xDE00,0xD83D为高位代理码元,剩下一个就是低位代理码元)。

后面就不翻译了,没什么太大意义。

基本上大致翻译了一遍与隐式转换有关的一系列规范条目,其实发现主要为我们进行指导思想的还是一开始的抽象相等比较。

之后根据最开始的题目逐个分析一下吧,最开始肯定都先调用抽象相等比较。

    [] == false
    先到7 ToNumber(false) +0,变为[] == +0
    再到9 ToPrimitive([])  ""(依次调用valueOf和toString), 变为 "" == +0
    再到5 ToNumber("")  0, 所以返回true

    {} == false
    先到7 ToNumber(false) +0,变为{} == +0
    再到9 ToPrimitive({})  "[object Object]" 变为 "[object Object]" == +0
    再到5 ToNumber("[object Object]") 返回NaN
    最后return false

题外话,这也能解释为什么 {} == true 返回false,{} == false 返回 false了。如果不看规范真的觉得是魔法。

    第三个不属于隐式转换的内容
    
    [1] == [1]
    这两个类型相同,直接使用规则1,严格等于,明显返回false

    '1' == [1] 
    不多做解释了,留给读者的问题2333

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions