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

注:本文案例環境爲非嚴格模式,嚴格模式下禁止關鍵字this指向全局對象javascript

1、方法是怎麼執行的?

首先說一下js中方法的執行,在window全局下聲明一個方法a:java

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

全局中執行這個方法廣泛的方法是直接a(),這個方法的執行環境是window,控制檯會打印出window對象。node

那麼爲何會打印出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。app

2、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)。ui

3、call和apply改變了什麼?

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

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。

4、call和apply的區別

call方法除了第一個obj參數外,還接受一串參數做爲被執行的方法的參數,apply用法和call相似,只不過除第一個obj參數外,接收的第二個參數是一個數組來做爲被執行的方法的參數。

5、延伸拓展

咱們來執行下面的代碼:

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的理解。

6、補充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
複製代碼
相關文章
相關標籤/搜索