Javascript 中神奇的 this

Javascript 當中的 this 與其餘語言是徹底不一樣的機制,頗有可能會讓一些編寫其餘語言的工程師迷惑。javascript

1. 誤覺得 this 指向函數自身

根據 this 的英語語法,很容易將函數中出現的 this 理解爲函數自身。在 javascript 當中函數做爲一等公民,確實能夠在調用的時候將屬性值存儲起來。可是若是使用方法不對,就會發生與實際預期不一致的狀況。具體狀況,請看下面代碼java

function fn(num){
        this.count++;
    }
    
    fn.count = 0;
    
    for(var i=0;i<3;i++){
        fn(i);
    }
    console.log(fn.count); // 0

若是 fn 函數裏面的 this 指向自身函數,那麼 count 屬性的屬性值就應該產生變化,但實際上倒是紋絲不動。對於這個問題,有些人會利用做用域來解決,好比這麼寫數組

var data = {
        count:0
    };
    
    function fn(num){
        data.count++;
    }
    
    for(var i=0;i<3;i++){
        fn(i);
    }
    
    console.log(data.count);    //3

又或者更直接的這麼寫瀏覽器

function fn(num){
        fn.count++;
    }
    
    fn.count = 0;
    
    for(var i=0;i<3;i++){
        fn(i);
    }
    
    console.log(fn.count);//3

雖然這兩種方式都輸出了正確的結果,可是卻避開了 this 到底綁定在哪裏的問題。若是對一個事物的工做原理不清晰,就每每會產生頭痛治頭,腳痛治腳的問題,從而致使代碼變得的醜陋,並且維護性也會變得不好。安全

2. this神奇的綁定規則

2.1 默認綁定規則

第一種是最多見的 this 的綁定,看一下下面的代碼app

function fn(){
        console.log(window === this); //瀏覽器環境
    }
    fn(); //true

函數 fn 是直接在全局做用域下調用的,沒有帶其餘任何修飾,這種狀況下,函數調用的時候使用了 this 的默認綁定,指向了全局對象。函數

這樣就清楚了第一個例子中的 this 指向, fn 函數中的 this 指向了全局變量,因此 this.count++ 至關於 window.count++(瀏覽器環境下),固然不會對 fn 函數的count屬性產生影響。this

有一點要說明的是,上面種狀況只能在非嚴格模式(strict mode)下才能發生,在嚴格模式下,會將 this 默認綁定爲 undefined。以免全局變量的污染。spa

2.2 隱式綁定規則

若是函數在以對象爲上下文進行調用,那麼 this 的綁定就會產生變化。this 會綁定到調用這個函數的對象,查看下面代碼:prototype

var obj = {
        a:1,
        fn:function(){
            console.log(this.a);
        }
    }
    
    obj.fn(); //1

即便函數聲明不在對象當中,this 指向仍會產生變化

function fn(){
        console.log(this.a);
    }
    var obj = {
        a:1,
        fn:fn
    }
    obj.fn(); //1

因而可知,this 的綁定,不與函數定義的位置有關,而是與調用者和調用方式有關。

在隱式的綁定規則下,有一些特殊的地方,須要注意。

2.2.1 多層對象調用 this 的指向

function fn(){
        console.log(this.a);
    }

    var obj3 = {
        a:3,
        fn:fn
    }
    
    var obj2 = {
        a:2,
        obj3:obj3
    }

    var obj = {
        a:1,
        obj2:obj2
    }

    obj.obj2.obj3.fn(); //3

在多層對象引用下,this 指向的是調用的函數的那個對象。

2.2.2 隱式賦值可能存在丟失現象

查看下面代碼

function fn(){
        console.log(this);
    }
    var    obj = {
        fn:fn
    }
    
    var fun = obj.fn;
    fun(); //window

雖然 fn 引用了 obj.fun ,可是函數的調用方式,還是不帶任何修飾的,因此 this 仍是綁定在了 window 上。
還有一種狀況,容易讓你們忽略,那就是傳參的時候,其實會進行隱式賦值。

function fn(){
        console.log(this);
    }
    
    function doFn(fn){
        fn();
    }
    
    var obj = {
        fn:fn
    }
    
    doFn(obj.fn); //window

隱式綁定 this 不是一種很推薦的方式,由於頗有可能就發生丟失的狀況,若是業務當中對 this 的綁定有要求,建議仍是使用顯示綁定的方式。

2.3 顯式綁定規則

顯示綁定就是利用函數原型上的 apply 與 call 方法來對 this 進行綁定。用法就是把想要綁定的對象做爲第一個參數傳進去。

function fn(){
        console.log(this);
    }
    
    var obj = {};
    
    fn.call(obj); //{}

有些時候會想將函數的 this 綁定在某個對象上,可是不須要當即調用,這樣的話,直接利用 call 或者 apply 是沒法作的。

function fn(){
        console.log(this);
    }
    
    function bind(fn){
        fn();
    }
    
    var obj = {
        fn:fn
    }
    
    bind.call(obj,fn); //window

上面這個例子,看似好像能夠,但其實是 bind 函數的 this 綁定到了 obj 這個對象,可是 fn 仍然是沒有任何修飾的調用,因此 fn 仍然是默認的綁定方式。

function fn(){
        console.log(this);
    }
    
    function bind(fn,obj){
        return function(){
            fn.apply(obj,arguments);
        }
    }
    
    var obj = {
        fn:fn
    }
    
    var fun = bind(fn,obj);
    fun(); //obj

這樣調用,就能夠將靈活多變的 this ,緊緊的控制住了,由於 fn 的調用方式爲 apply 調用。因此,this 就被綁定在傳入的 obj 對象上,在 ES5 當中,函數的原型方法上多了一個 bind。效果與上面的函數基本一致,具體用法限於篇幅就很少說了。

2.4 new 綁定

new 是一個被不少人誤解的一個關鍵字,但實際上 javascript 的 new 與傳統面向對象的語言徹底不一樣。
我的把 new 理解爲一種特殊的函數調用,當使用 new 關鍵字來調用函數的時候,會執行下面操做,

  1. 建立一個全新的對象

  2. 將空對象的 __proto__ 指向構造函數的 prototype

  3. 將新對象的 this 綁定到調用的函數上

  4. 若是函數返回值爲基本類型或者爲 this又或者不返回任何值,那麼將會返回這個建立的新對象,若是返回了一個對象,那麼則會返回這個對象,而不會返回建立的新對象。

function fn(a){
        this.a = a;
    }
    fn.prototype.hi = function(){
        console.log('hi')
    }
    
    var obj = new fn(2);
    
    console.log(obj);

function fn(a){
        this.a = a;
        return {};
    }
    
    var obj = new fn(2);
    
    console.log(obj); //{}

2.5 特殊的傳參

null 和 undefined 也是能夠做爲 this 的綁定對象的,可是實際上應用的是默認的綁定。
可是這種傳參的實際效用是什麼呢?
常見的用法是將一個數組展開,做爲參數傳入參數。好比

function fn(a,b){
        console.log('a:',a,'b:',b);
    }
    
    fn.apply(null,[1,2]); // a: 1 b: 2

可是這種用法會有一個坑,那就是若是函數存在了 this ,那麼就會應用默認的綁定規則,將 this 綁定在全局對象上,發生於預期不一致的狀況。爲了代碼更加穩健,可使建立一個比空對象更空的對象。

var obj = Object.create(null);
console.log(obj.__proto__); //undefined

var obj2 = {}
console.log(obj2.__proto__); //Object {}

Object原型上有一個 create 方法,這個方法會建立一個對象,而後將對象的原型指向傳入的參數,因此傳入 null 的話,產生一個沒有 prototype 的對象,因此會比空對象更加"空"。

因此傳入這個對象,會比傳入 null 更加安全。

var obj = Object.create(null);

fn.apply(obj,[1,2]);

2.6 根據做用域來決定 this 的綁定

在 ES6 當中,出現了一個新的函數類型,箭頭函數。

若是使用箭頭函數,那麼就不會使用上面提到的四種 this 綁定方式,而是根據做用域來決定

比較常見的是用於事件函數和定時器的狀況。

下面是比較常見的傳統 this 寫法

function fn(){
        var _this = this;
        setTimeout(function(){
            console.log(_this.a);
        },100)
    }

    var obj = {
        a:2
    }
    
    fn.call(obj); //2

若是使用箭頭函數則能夠這麼寫

function fn(){
        setTimeout(()=>{
            //this 來源於 fn 函數的做用域
            console.log(this.a);
        },100)
    }

    var obj = {
        a:2
    }
    
    fn.call(obj); //2

2.7 事件函數當中 this 的綁定機制

若是是在事件函數當中,this 的綁定是指向觸發事件的 DOM 元素的,

$('body')[0].addEventListener('click',function(){
    console.log(this);
},false);

點擊 body 元素以後,控制檯則會顯示 body 元素

3. 小結

若是想判斷一個函數的 this 綁定在哪裏,首先是找到函數的調用位置,以後是按照規則來判斷。

  • 若是函數調用時沒有任何修飾條件,那麼在嚴格模式下則會綁定到 undefined ,非嚴格模式下會綁定到全局。

  • 若是是用對象作上下文,來對函數進行調用,那麼則會綁定到調用的這個對象上。

  • 若是是用 call 或者 apply 方法來進行調用的,則會綁定到第一個傳入參數上。

  • 若是是使用 new 關鍵字來調用函數的,則會綁定到新建立的那個對象上.

  • 若是是在事件函數內,則會綁定到觸發事件的那個DOM元素上。

相關文章
相關標籤/搜索