簡單快速理解js中的this、call和apply

就那發表於2018-03-16

注:本文案例環境為非嚴格模式,嚴格模式下禁止關鍵字this指向全域性物件

一、方法是怎麼執行的?

首先說一下js中方法的執行,在window全域性下宣告一個方法a:

function a () {
  console.log(this);
}
a();//window
複製程式碼

全域性中執行這個方法普遍的方法是直接a(),這個方法的執行環境是window,控制檯會列印出window物件。

那麼為什麼會列印出window物件呢?我們可以這樣理解,方法的執行必須要有個直接呼叫者,剛才那個方法a是定義在window全域性下的,window下的變數和方法有個特點就是訪問和呼叫的時候可以省略window!所以剛才執行a() === window.a(),也就是說,執行a方法時的直接呼叫者是window。!

上面有提到直接呼叫者,怎麼看待這個直接呼叫者呢?舉個例子,宣告一個全域性物件obj:

var name = "window-name";
var obj = {
    name:"obj-name",
    a:function(){
        console.log(this.name);
    },
    b:{
        name:"b-name",
        a:function(){
            console.log(this.name);
        }
    }
}
obj.a();//obj-name
obj.b.a();//b-name
複製程式碼

分別執行obj.a();和obj.b.a();控制檯會分別列印出obj-name和b-name(這裡obj.a() === window.obj.a(),obj.b.a() === window.obj.b.a()),方法執行時的直接呼叫者就是離這個被呼叫方法最近的那個物件,兩個分別是obj和obj.b,列印出的name分別是obj的name和obj.b的name。

二、this指向了誰?

那麼函式裡面的this到底是誰呢?this就是這個方法被呼叫時的直接呼叫者。可以再來個特殊的例子,理解這個例子了就能很好理解this指向了誰。在剛才的基礎上定義一個全域性變數:

var ax = obj.b.a;
ax();//window-name
複製程式碼

此時執行ax();控制檯則會列印出window-name;為什麼會列印出window-name?這是因為 ax 是定義在window全域性下的變數,執行ax()時的直接呼叫者是window(ax() === window.ax()),所以執行ax()時內部的this就是它的直接呼叫者window,因此列印出的值就是定義在window下的name的值,所以本文最開始時的a(),執行後會列印window,因為內部的this指向的是a的呼叫者window。

實際上在非嚴格模式下,如果方法有直接呼叫者,那麼this指向的是這個直接呼叫者,在沒有直接呼叫者(比如回撥函式)的情況下this指向的是全域性物件(瀏覽器中是window,node中是global)。

三、call和apply改變了什麼?

理解了函式的直接呼叫者this,再說call和apply就比較容易理解了。 在此對call和apply不做過多的定義性解釋,先來看下呼叫了call後誰是那個被執行的方法,直接程式碼示例:

function fn1 () {
    console.log(1);
};
function fn2 () {
    console.log(2);
};
fn1.call(fn2);//1
複製程式碼

執行fn1.call(fn2);控制檯會列印1,這裡可以說明fn1呼叫call後被執行的方法還是fn1。一定要弄清楚誰是這個被執行的方法,就是呼叫call的函式,而fn2現在的身份是替代window作為fn1的直接呼叫者,這是理解call和apply的關鍵,也可以執行下fn2.call(fn1);//2來驗證被執行的方法是誰。那麼call的作用是什麼呢? 再來個程式碼示例:

var obj1 = {
    num : 20,
    fn : function(n){
        console.log(this.num+n);
    }
};
var obj2 = {
    num : 15,
    fn : function(n){
        console.log(this.num-n);
    }
};
obj1.fn.call(obj2,10);//25
複製程式碼

執行obj1.fn.call(obj2,10);控制檯會列印25,call在此的作用其實很簡單,就是在執行obj1.fn的時候把這個fn的直接呼叫者由obj1變為obj2,obj1.fn(n)內部的this經過call的作用指向了obj2,所以this.num就是obj2.num,10作為執行obj1.fn時傳入的引數,obj2.num是15,因此列印出的值是15+10=25。

所以我們可以這樣理解:call的作用是改變了那個被執行的方法(也就是呼叫call的那個方法)的直接呼叫者!而這個被執行的方法內部的this也會重新指向那個新的呼叫者,就是call方法所接收的第一個obj引數。還有兩個特殊情況就是當這個obj引數為null或者undefined的時候,this會指向window。

四、call和apply的區別

call方法除了第一個obj引數外,還接受一串引數作為被執行的方法的引數,apply用法和call類似,只不過除第一個obj引數外,接收的第二個引數是一個陣列來作為被執行的方法的引數。

五、延伸擴充

我們來執行下面的程式碼:

fn1.call.call(fn2);//2
複製程式碼

執行fn1.call.call(fn2);控制檯會列印出2,先不說為什麼會列印出2,先來理解下fn1.call.call是什麼,call()方法是Function物件原型鏈上的方法,所以fn1這個函式可以通過原型鏈繼承使用這個方法,也就是說fn1.call === Function.prototype.call === Function.call。所以fn1.call.call(fn2) === Function.call.call(fn2),可以把Function.call先看做一個整體,用FunCall來表示如下:

FunCall.call(fn2);
複製程式碼

這樣就比較好理解,相當於是fn2作為FunCall的直接呼叫者來執行FunCall,而FunCall === Function.call,所以就相當於是fn2.call()。

此時call沒有傳入物件,那麼全域性物件window就會作為預設物件,也就是相當於fn2.call(window),再繼續解釋就是window.fn2.call(window),把fn2的直接呼叫物件變成window,所以就相當於直接執行了fn2();控制檯會列印出2。

此外還有Function.call.apply和Function.apply.call等多種組合,原理都類似,只不過接收的引數型別不太一樣,可以嘗試一下。加深對call和apply的理解。

六、補充bind

bind用法和call類似,只不過呼叫bind後方法不能立即執行需要再次呼叫,其實就是柯里化的一個語法糖。我們來實現一個簡易版的bind方法,命名為bindFn,大致就能瞭解bind了:

Function.prototype.bindFn = function() {
    var args = Array.prototype.slice.call(arguments);//得到傳入的引數
    var obj = args.shift();//得到第一個傳入的物件
    var self = this; // 呼叫bindFn的函式
    
    return function() { // return一個函式 實現柯里化
        //拼接新引數
        var newArgs = args.concat(Array.prototype.slice.call(arguments));
        //下面這裡使用了apply,用來改變self的直接呼叫者
        return self.apply(obj,newArgs);
    }
}
//測試一下,doSum方法實現對傳入的引數的累加,並把累加結果返回
function doSum(){
    var arg = Array.prototype.slice.call(arguments);
    return arg.length ? arg.reduce((a,b) => a + b) : "";
}
var newDoSum = doSum.bindFn(null,1,2,3);
console.log(newDoSum());//6
console.log(newDoSum(4));//10
console.log(newDoSum(4,5));//15
複製程式碼

相關文章