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

原创禁止私自转载前端

广告

部门长期招收大量研发岗位【前端,后端,算法】,欢迎各位大神投递,踊跃尝试。git

坐标: 头条,大量招人,难度有下降,大多能拿到很不错的涨幅,未上市,offer 给力!欢迎骚扰邮箱!程序员

戳我: 戳我: hooper.echo@gmail.comgithub


[] == ![] ?

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

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

经验法,简称瞎蒙

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

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

因此咱们 first An is : false

反向操做法

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

因而你又大胆作出了一个假设: [] == ![] 是 true!

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

最终结论

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

[] == ![] -> [] == 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'
相关文章
相关标签/搜索