從Ecma規範深刻理解this

this是面向對象編程中的一個概念,它通常指向當前方法調用所在的對象,這一點在java、c++這類比較嚴格的面向對象編程語言裏是很是明確的。可是在javascript中,this的定義要靈活許多,若是未準確掌握,很是容易混淆。本人在面試過程當中也發現,面試者不多有由可以回答得很是全面的。本文總結了this的各類狀況,並從Ecma規範的角度探討了this的具體實現,但願對你們理解this有所幫助。javascript

this指向的四中狀況

在javascript裏面,this的指向能夠概括爲如下四種狀況。只要能牢記這四種狀況,大部分狀況下就已經夠用了。java

1.在全局代碼或者普通的函數調用中,this指向全局對象,在瀏覽器裏面既爲window對象。c++

console.log(this);//輸出window
    
    function foo(){
        console.log(this);
    }
    foo();//輸出window

在瀏覽器環境裏運行上述代碼,兩處輸出結果均爲window對象。程序員

2.經過call或apply方法調用函數,this指向方法調用的第一個參數。es6

var obj = {name:'test'};
    
    function foo(){
        console.log(this);
    }
    
    foo.call(obj);//輸出obj
    foo.apply(obj);//輸出obj

在瀏覽器環境裏執行以上代碼,輸出結果均爲對象obj。call和apply除了參數形式不同外其餘都同樣,call採用逗號分割,apply採用數組。說到這裏,順便介紹一個小技巧。如何在不生成新數組的狀況下實現兩個數組的鏈接?請看下面的代碼。面試

var arr1 = [1, 2 , 3],
        arr2 = [4, 5, 6];
    Array.prototype.push.apply(arr1, arr2);
    
    console.log(arr1);//輸出[1, 2, 3, 4, 5, 6]

執行上述代碼後,輸出結果爲[1, 2, 3, 4, 5, 6]。這是一個很是實用的小技巧,因爲apply第二個參數爲數組形式,因此咱們能夠把push方法「借」過來,從而實現兩個數組的鏈接。編程

3.調用對象的方法,this指向該對象。數組

var obj = {name:'test'};
    
    function foo(){
        console.log(this);
    }
    obj.foo = foo;
    
    obj.foo();//輸出obj

執行以上代碼後,控制檯輸出爲obj對象。這就是咱們常說的「誰調用,指向誰」。瀏覽器

4.構造方法中的this,指向新構造的對象。閉包

function C(){
        this.name = 'test';
        this.age = 18;
        console.log(this);
    }
    var c = new C();//輸出 c
    console.log(c);//輸出 c

執行以上代碼後,控制檯輸出均爲c所指向的對象。當new操做符用於函數時,會建立一個新對象,並用this指向它。

Ecma規範

Ecma規範裏面詳細介紹了this的實現細節,經過閱讀規範,咱們能夠更準確的理解上述四種狀況究竟是怎麼回事。
函數對象有一個叫[[Call]]內部方法,函數的執行實際上是經過[[Call]]方法來執行的。[[Call]]方法接收兩個參數thisArg和argumentList,thisArg和this的指向有直接關係,argumentList爲函數的實參列表。thisArg又是怎麼來的呢?咱們能夠和前面討論的四種狀況對應起來:

  1. 普通方法調用thisArg爲undefined。

  2. 經過call或apply調用,thisArg既爲第一個參數。

  3. 經過對象調用,thisArg指向該對象。

  4. 在構造方法中,thisArg爲新構造的對象。

thisArg和this是什麼關係?規範裏的描述是這樣的:

  1. If the function code is strict code, set the ThisBinding to thisArg.

  2. Else if thisArg is null or undefined, set the ThisBinding to the global object.

  3. Else if Type(thisArg) is not Object, set the ThisBinding to ToObject(thisArg).

  4. Else set the ThisBinding to thisArg.

在嚴格模式下,thisArg和this是一一對應的。

function foo(){
    'use strict';
    console.log(this);
}
foo();//輸出undefined

該示例輸出的結果爲undefined。

第二點是說若是thisArg爲null或者undefined則this指向全局對象。

function foo(){
    console.log(this);
}
foo.call(null);//輸出window

該示例的輸出結果爲window。

第三點說若是thisArg爲非對象類型,則會強制轉型成對象類型。

function foo(){
    console.log(this);
}
var aa = 2;
console.log(aa);//輸出2
foo.call(aa);//輸出 Number

這裏的輸出結果分別爲2和Number,它將基本類型轉型成了對象包裝類型。

第四點說明剩下的狀況thisArg和this爲一一對應的關係。

規範裏面對this指向的描述仍是比較明確的。只要你搞清楚thisArg怎麼肯定,thisArg和this的對應關係,那麼你就能搞定全部this的狀況了。

確保this的指向

在實際使用this的過程當中,遇到得最多得一個問題可能就是上下文丟失的問題了。由於javascript中的函數是能夠做爲參數傳遞的,那麼其餘對象在執行回調函數時就可能形成回調函數原來的上下文丟失,也就是this的指向改變了。

var C = function(){
    this.name = 'test';
    this.greet = function(){
        console.log('Hello,I am '+this.name+'!');
    };
}
var obj = new C();

obj.greet();//輸出 Hello,I am test!

setTimeout(obj.greet, 1000);//輸出 Hello,I am !

可見第二條輸出中this的值改變了,其實咱們是但願this可以指向obj的。解決該問題的方法有兩種。

1.bind方法。
bind方法經過閉包巧妙地實現了上下文的綁定,它其實是將原方法包裝成了一個新方法。通常的實現以下:

Function.prototype.bind = function(){
    var args = arguments,
        thisArg = arguments[0],
        func = this;
    return function(){
        var arg = Array.prototype.slice.call(args, 1);
        Array.prototype.push.apply(args, arguments);
        return func.apply(thisArg, arg);
    }
}

前面的示例代碼咱們只須要加上bind,就可以獲得咱們但願的結果了。

...
setTimeout(obj.greet.bind(obj), 1000);//輸出 Hello,I am test!
...

2.es6箭頭函數。
es6裏面提供了一個新的語法糖,箭頭函數。箭頭函數的this再也不變幻莫測,它永遠指向函數定義時的this值。

var C = function(){
    this.name = 'test';
    this.greet = ()=>{
        console.log('Hello,I am '+this.name+'!');
    };
}
var obj = new C();

obj.greet();//輸出 Hello,I am test!

setTimeout(obj.greet, 1000);//輸出 Hello,I am test!

咱們將前面的示例該成箭頭函數後,兩處的輸出結果同樣了。this的值再也不改變了,這是咱們想要的。

小結

this看起來是個很是小的知識點,其實挖起來仍是有不少細節的,特別是規範裏面的一些定義,本人以爲對於一個js程序員來講是很是重要的。本人後面也準備找些ecma規範裏面的知識點和你們分享,但願能對你們有所幫助。因爲本人能力有限,若有理解錯誤的地方還望指出。

相關文章
相關標籤/搜索