從[]==![]爲true來剖析JavaScript各類蛋疼的類型轉換

你是否在面試中遇到過各類奇葩和比較細節的問題?html

[]==[]
//false
[]==![]
//true
{}==!{}
//false
{}==![]
//VM1896:1 Uncaught SyntaxError: Unexpected token ==
![]=={}
//false
[]==!{}
//true
undefined==null
//true

看了這種題目,是否是想抽面試官幾耳光呢?哈哈,是否是看了以後一臉懵逼,兩臉茫然呢?心想這什麼玩意前端

其實這些都是紙老虎,知道原理和轉換規則,理解明白這些很容易的,炒雞容易的,真的一點都不難,咱們要打到一切紙老虎,不信?git


咱們就從[] == []和[] == ![]例子切入分析一下爲何輸出的結果是true而不是其它的呢?github

[]==[]爲何是false?

有點js基礎應該知道對象是引用類型,就會一眼看出來[] == []會輸出false,由於左邊的[]和右邊的[]看起來長的同樣,可是他們引用的地址並不一樣,這個是同一類型的比較,因此相對沒那麼麻煩,暫時不理解[] == []爲false的童鞋這裏就不細說,想要弄清楚能夠經過這篇文章來了解JavaScript的內存空間詳解.面試

前端基礎進階(一):內存空間詳細圖解算法

變量對象與堆內存數組

簡單類型都放在棧(stack)裏
對象類型都放在堆(heap)裏
var a = 20;
var b = 'abc';
var c = true;
var d = { m: 20 }//地址假設爲0x0012ff7c
var e = { m: 20 }//從新開闢一段內存空間假設爲0x0012ff8f
console.log(e==d);//false
那爲何引用值要放在堆中,而原始值要放在棧中,不都是在內存中嗎,爲何不放在一塊兒呢?那接下來,讓咱們來探索問題的答案!(扯遠了)

首先,咱們來看一下代碼:函數

function Person(id,name,age){
    this.id = id;
    this.name = name;
    this.age = age;
}
var num = 10;
var bol = true;
var str = "abc";
var obj = new Object();
var arr = ['a','b','c'];
var person = new Person(100,"笨蛋的座右銘",25);

而後咱們來看一下內存分析圖:ui

`變量num,bol,str爲基本數據類型,它們的值,直接存放在棧中,obj,person,arr爲複合數據類型,他們的引用變量存儲在棧中,指向於存儲在堆中的實際對象。
由上圖可知,咱們沒法直接操縱堆中的數據,也就是說咱們沒法直接操縱對象,但咱們能夠經過棧中對對象的引用來操做對象,就像咱們經過遙控機操做電視機同樣,區別在於這個電視機自己並無控制按鈕。
`this

如今讓咱們來回答爲何引用值要放在堆中,而原始值要放在棧中的問題:

`記住一句話:能量是守衡的,無非是時間換空間,空間換時間的問題
堆比棧大,棧比堆的運算速度快,對象是一個複雜的結構,而且能夠自由擴展,如:數組能夠無限擴充,對象能夠自由添加屬性。將他們放在堆中是爲了避免影響棧的效率。而是經過引用的方式查找到堆中的實際對象再進行操做。相對於簡單數據類型而言,簡單數據類型就比較穩定,而且它只佔據很小的內存。不將簡單數據類型放在堆是由於經過引用到堆中查找實際對象是要花費時間的,而這個綜合成本遠大於直接從棧中取得實際值的成本。因此簡單數據類型的值直接存放在棧中。`

搬運文章:理解js內存分配

知道的大神固然能夠飄過,這裏主要給計算機基礎薄弱的童鞋補一下課.

[]==![]爲何是true?

首先第一步:你要明白ECMAScript規範裏面==的真正含義

GetValue 會獲取一個子表達式的值(消除掉左值引用),在表達式 [] == ![] 中,[] 的結果就是一個空數組的引用(上文已經介紹到數組是引用類型),而 ![] 就有意思了,它會按照 11.4.9 和 9.2 節的要求獲得 false。

首先咱們瞭解一下運算符的優先級:

剛看到這裏是否是就咬牙切齒了,好戲還在後頭呢,哈哈

!取反運算符的優先級會高於==,因此咱們先看看!在ECAMScript是怎麼定義的?

因此![]最後會是一個Boolean類型的值(這點很關鍵,涉及到下面的匹配選擇).

二者比較的行爲在 11.9.3 節裏,因此翻到 11.9.3:

在這段算法裏,[]是Object,![]是Boolean,二者的類型不一樣,y是Boolean類型,由此可知[] == ![]匹配的是條件 8,因此會遞歸地調用[] == ToNumber(Boolean)進行比較。

[]空數組轉化成Boolean,那麼這個結果究竟是true仍是false呢,這個固然不是你說了算,也不是我說了算,ECMAScript定義的規範說了算:咱們來看看規範:

[]是一個對象,因此對應轉換成Boolean對象的值爲true;那麼![]對應的Boolean值就是false
進而就成了比較[] == ToNumber(false)

再來看看ECMAScipt規範中對於Number的轉換

由此能夠得出此時的比較成了[]==0;

在此處由於 [] 是對象,0是數字Number,比較過程走分支 10(若Type(x)爲Object且Type(y)爲String或Number, 返回比較ToPrimitive(x) == y的結果。),能夠對比上面那張圖.

ToPrimitive 默認是調用 toString 方法的(依 8.2.8),因而 ToPrimitice([]) 等於空字符串

再來看看ECMAScript標準怎麼定義ToPrimitice方法的:

是否是看了這個定義,仍是一臉懵逼,ToPrimitive這尼瑪什麼玩意啊?這不是等於沒說嗎?

再來看看火狐MDN上面文檔的介紹:
JS::ToPrimitive

查了一下資料,上面要說的能夠歸納成:

ToPrimitive(obj,preferredType)

JS引擎內部轉換爲原始值ToPrimitive(obj,preferredType)函數接受兩個參數,第一個obj爲被轉換的對象,第二個
preferredType爲但願轉換成的類型(默認爲空,接受的值爲Number或String)

在執行ToPrimitive(obj,preferredType)時若是第二個參數爲空而且obj爲Date的事例時,此時preferredType會
被設置爲String,其餘狀況下preferredType都會被設置爲Number若是preferredType爲Number,ToPrimitive執
行過程如
下:
1. 若是obj爲原始值,直接返回;
2. 不然調用 obj.valueOf(),若是執行結果是原始值,返回之;
3. 不然調用 obj.toString(),若是執行結果是原始值,返回之;
4. 不然拋異常。

若是preferredType爲String,將上面的第2步和第3步調換,即:
1. 若是obj爲原始值,直接返回;
2. 不然調用 obj.toString(),若是執行結果是原始值,返回之;
3. 不然調用 obj.valueOf(),若是執行結果是原始值,返回之;
4. 不然拋異常。

首先咱們要明白obj.valueOf()obj.toString()還有原始值分別是什麼意思,這是弄懂上面描述的前提之一:

toString用來返回對象的字符串表示。

var obj = {};
console.log(obj.toString());//[object Object]

var arr2 = [];
console.log(arr2.toString());//""空字符串
  
var date = new Date();
console.log(date.toString());//Sun Feb 28 2016 13:40:36 GMT+0800 (中國標準時間)

valueOf方法返回對象的原始值,多是字符串、數值或bool值等,看具體的對象。

var obj = {
  name: "obj"
};
console.log(obj.valueOf());//Object {name: "obj"}

var arr1 = [1];
console.log(arr1.valueOf());//[1]



var date = new Date();
console.log(date.valueOf());//1456638436303
如代碼所示,三個不一樣的對象實例調用valueOf返回不一樣的數據

**原始值指的是['Null','Undefined','String','Boolean','Number']五種基本數據類型之一(我猜的,查了一下
確實是這樣的)**

弄清楚這些之後,舉個簡單的例子:

var a={};
ToPrimitive(a)

分析:a是對象類型但不是Date實例對象,因此preferredType默認是Number,先調用a.valueOf()不是原始值,繼續來調
用a.toString()獲得string字符串,此時爲原始值,返回之.因此最後ToPrimitive(a)獲得就是"[object Object]".

若是以爲描述還很差明白,一大堆描述晦澀又難懂,咱們用代碼說話:

const toPrimitive = (obj, preferredType='Number') => {
    let Utils = {
        typeOf: function(obj) {
            return Object.prototype.toString.call(obj).slice(8, -1);
        },
        isPrimitive: function(obj) {
            let types = ['Null', 'String', 'Boolean', 'Undefined', 'Number'];
            return types.indexOf(this.typeOf(obj)) !== -1;
        }
    };
   
    if (Utils.isPrimitive(obj)) {
        return obj;
    }
    
    preferredType = (preferredType === 'String' || Utils.typeOf(obj) === 'Date') ?
     'String' : 'Number';

    if (preferredType === 'Number') {
        if (Utils.isPrimitive(obj.valueOf())) {
            return obj.valueOf()
        };
        if (Utils.isPrimitive(obj.toString())) {
            return obj.toString()
        };
    } else {
        if (Utils.isPrimitive(obj.toString())) {
            return obj.toString()
        };
        if (Utils.isPrimitive(obj.valueOf())) {
            return obj.valueOf()
        };
    }
}

var a={};
ToPrimitive(a);//"[object Object]",與上面文字分析的一致

分析了這麼多,剛纔分析到哪裏了,好像到了比較ToPrimitive([]) == 0如今咱們知道ToPrimitive([])="",也就是空字符串;

那麼最後就變成了""==0這種狀態,繼續看和比較這張圖

發現typeof("")爲string,0爲number,發現第5條知足規則,最後就成了toNumber("")==0的比較了,根據toNumber的轉換規則:

因此toNumber("")=0,最後也就成了0 == 0的問題,因而[]==![]最後成了0 == 0的問題,答案顯而易見爲true,一波三折

最後總結一下

==運算規則的圖形化表示

前面說得很亂,根據咱們獲得的最終的圖3,咱們總結一下==運算的規則:

1. undefined == null,結果是true。且它倆與全部其餘值比較的結果都是false。

2. String == Boolean,須要兩個操做數同時轉爲Number。

3. String/Boolean == Number,須要String/Boolean轉爲Number。

4. Object == Primitive,須要Object轉爲Primitive(具體經過valueOf和toString方法)。

瞧見沒有,一共只有4條規則!是否是很清晰、很簡單。

主要參考文章和文獻

ECMAScript5.1規範中文版

經過一張簡單的圖,讓你完全地、永久地搞懂JS的==運算

JavaScript中加號運算符的類型轉換優先級是什麼?

喜歡我總結的文章對你有幫助有收穫的話麻煩點個star
個人github博客地址,總結的第一篇,很差之處和借鑑不獲得之處還望見諒,您的支持就是個人動力!

相關文章
相關標籤/搜索