52

原创禁止私自转载

广告

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

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

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


[] == ![] ?

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

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

经验法,简称瞎蒙

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

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

所以我们 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'

你可能感兴趣的

30 条评论
whiteplayer · 2018-12-14

我竟然硬着头皮瞎鸡儿的看完了...

+1 回复

0

你是好样的

hooper 作者 · 2018-12-14
squallyan · 2018-12-18

看完了,我还是去卖早餐吧

+1 回复

0

这个问题写博客的时候确实追的比较全,其中涉及到的几个点能知道为什么就可以了:

  1. 左边空数组不是转 bool 而是转 number。
  2. 空数组转 number 怎么调用 valueof 和 toString 的, 为什么这么做?
  3. ==号 大致的隐式转换规则。

但是如果博客就写这三点的话, 那你看完还是知其然不知其所以然。 所以我就写的比较详细。

也是因为往上很多博客在 valueof 和 toString 的调用上(顺序,和为什么两个都调用)总是说不清楚,转载几次就开始乱写了, 还有==的转换规则上,都是含糊其辞,所以我就想吧这个问题搞得明明白白

hooper 作者 · 2018-12-18
0

@hooper 点赞,哈哈

squallyan · 2018-12-18
万俟逍 · 2018-12-19

文风颇为讨喜,如果文档都是这种骚气的写法,应该少很多枯燥感~

+1 回复

0

瞎写的,喜欢就行, 可以 star 一波 github, 长期更新

hooper 作者 · 2018-12-19
xianshenglu · 2018-12-13

我觉得这个没什么好考的,能现场查文档讲清楚就行,隐式转换那么多,不可能都记着吧,也没理由去记

回复

0

bat,tmd 中两家公司面试题有这道题的变种。 是一道拉大区分度的题,答不出来没事,回答对了就很加分。其他的我没呆过不知道。

然后 这题本身是腾讯的面试题。

至于有什么用, 原问里有 Q-A 环节里有回答您的问题,链接: https://segmentfault.com/a/11...

hooper 作者 · 2018-12-13
0

@hooper 我并没有去疑问有什么用,我只是觉得,这属于,查文档就可以弄清楚的知识,为什么要记?跟考我一个罕见的api没什么区别,时间都是有成本的。ps,我不看重的点,就算bat考,我也仍然觉得这并不是什么好点,觉得他很重要,拿出理由来,而不是来一句,我乐意。

xianshenglu · 2018-12-13
0

我没说我乐意啊,不打嘴炮。喜不喜欢都是个人看法,对需要的人有益就行。

hooper 作者 · 2018-12-13
laoLiueizo · 2018-12-14

鬼鬼 考不考 有没有意义是另外一回事 楼主查原因的能力还是很牛逼啊! 赞

回复

0

我就是硬着头皮瞎鸡儿看的, 比起在座的各位差的远了

hooper 作者 · 2018-12-14
daixujie666 · 2018-12-14

文章的意义就是我们需要有探索精神,当然这个题目本身真的很无聊。设计到es6的语言规范,可以说就是语言本身的规定它就是这样的。这种题既不能考核到面试者的逻辑能力,也没能考核到面试者的技能,更没有考核到面试者的智商。

回复

0

这是个区分度的题, 知道结果就行, 如果知道转换过程就能拉开区分度。整个过程是考核对转换过程的熟悉度。 一般只要答出来会转换数字就可以了。 总不能知道结果是 true,却不知道为什么吧

hooper 作者 · 2018-12-16
1

对的,现在很多人的毛病就是伸手式的知识获取方式。自己完全没有了解本质的激情,这是最可怕的。

daixujie666 · 2018-12-17
xunmengxingyu · 2018-12-21

很厉害,就是中间的那段关于ToPrimitive([])看不下去了。。。(苦笑)没耐心了

回复

0

已经蛮不错了, 本文说的东西,其实我也记不住, 但是我知道大致的流程, 我认为这样就足够了。

很多东西需要反复看去巩固, 不要老想着一篇博客,看一次就把所有的东西搞得清清楚楚。

不断累计,慢慢就会量变达到质变,其实还有跟进问题,分析问题的思路等等。

个人看法😃😃

hooper 作者 · 2018-12-21
无力吐槽 · 2018-12-21

看见题目我就进来了,很厉害,我之前一直有个疑问 ,==的判断规则是靠什么判断的?如第一步的是如何区分x.y类型是否相同的呢?

回复

0

有空可以看看 规范, 虽然有点枯燥, 但是能看到很多语法现象的 原因。

hooper 作者 · 2018-12-21
0

额 谢谢 我上面问的问题您能回答一下吗?

无力吐槽 · 2018-12-21
0
  1. ==的判断规则是靠什么判断的

== 转换规则就是这 https://segmentfault.com/a/11... 10步骤的描述

  1. 如何区分x.y类型 这个问题

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

Return the result of performing Strict Equality Comparison x === y.

这里使用的 Type(x) 去判断 类型,

对于 type 的描述: https://tc39.github.io/ecma26..., 非常抽象,大段英文。 我就没跟下去了。 你就认为这个 Type 获得到一个变量的类型。 不然这个问题会无限拓展下去

hooper 作者 · 2018-12-21
shayyee · 2018-12-27

我是谁 我在哪里 执行8 9条规范的时候就懵了

回复

0

步骤89,是一个描述: If Type(x) is either String, Number, or Symbol and Type(y) is Object, return the result of the comparison x == ToPrimitive(y).

大致是说: 如果一个操作数x是 string | number , 另一个操作数y是 object, 则 x 不变, 对 y 求取ToPrimitive。

接着跟进到 ToPrimitive 的规范里。

简单来说就是顺藤摸瓜, 耐心看下去,就和调试一个 复杂的库一样,你得单步进到每一个步骤中,看看能不能找到蛛丝马迹

hooper 作者 · 2018-12-27
渭水空藏月 · 2018-12-29

滴滴公司面试官见识浅薄。
// This works because == invokes toString which calls .join for Arrays.
a = [1,2,3];
a.join = a.shift;
console.log(a == 1 && a == 2 && a == 3);

回复

0

滴滴那个是亲身经历, 但滴滴是个改变生活方式的公司,本身有比较满意的 offer 了, 就没太在意。

下面那个是看了我另一篇博客么 https://github.com/HCThink/h-...? 这个注释写的比较粗略。不过转换的部分都在本文提到过。

hooper 作者 · 2018-12-29
载入中...