前端面試中遇到 [] == ![] ? 刨祖墳式博客解析,從 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'
相關文章
相關標籤/搜索