JavaScript進階之模擬call,apply和bind

原文: https://zhehuaxuan.github.io/...
做者:zhehuaxuan

目的

本文主要用於理解和掌握callapplybind的使用和原理,本文適用於對它們的用法不是很熟悉,或者想搞清楚它們原理的童鞋。
好,那咱們開始!
在JavaScript中有三種方式來改變this的做用域callapplybind。咱們先來看看它們是怎麼用的,只有知道怎麼用的,咱們才能來模擬它。javascript

Function.prototype.call()

首先是Function.prototype.call(),不熟的童鞋請猛戳MDN,它是這麼說的:call()容許爲不一樣的對象分配和調用屬於一個對象的函數/方法。也就是說:一個函數,只要調用call()方法,就能夠把它分配給不一樣的對象。java

若是仍是不明白,不急!跟我往下看,咱們先來寫一個call()函數最簡單的用法:git

function source(){
    console.log(this.name); //打印 xuan
}
let destination = {
    name:"xuan"
};
console.log(source.call(destination));

上述代碼會打印出destinationname屬性,也就是說source()函數經過調用call()source()函數中的this對象能夠分配到destination對象中。相似於實現destination.source()的效果,固然前提是destination要有一個source屬性github

好,如今你們應該明白call()的基本用法,咱們再來看下面的例子:數組

function source(age,gender){
    console.log(this.name);
    console.log(age);
    console.log(gender);
}
let destination = {
    name:"xuan"
};
console.log(source.call(destination,18,"male"));

打印效果以下:閉包

咱們能夠看到能夠call()也能夠傳參,並且是以參數,參數,...的形式傳入。app

上述咱們知道call()的兩個做用:ide

1.改變this的指向

2.支持對函數傳參函數

咱們看到最後還還輸出一個undefined,說明如今調用source.call(…args)沒有返回值。ui

咱們給source函數添加一個返回值試一下:

function source(age,gender){
    console.log(this.name);
    console.log(age);
    console.log(gender);
    //添加一個返回值對象
    return {
        age:age,
        gender:gender,
        name:this.name
    }
}
let destination = {
    name:"xuan"
};
console.log(source.call(destination,18,"male"));

打印結果:

果不其然!call()函數的返回值就是source函數的返回值,那麼call()函數的做用已經很明顯了。

這邊再總結一下:

  1. 改變this的指向
  2. 支持對函數傳參
  3. 函數返回什麼,call就返回什麼。

模擬Function.prototype.call()

根據call()函數的做用,咱們下面一步一步的進行模擬。咱們先把上面的部分代碼摘抄下來:

function source(age,gender){
    console.log(this.name);
    console.log(age);
    console.log(gender);
    //添加一個返回值對象
    return {
        age:age,
        gender:gender,
        name:this.name
    }
}
let destination = {
    name:"xuan"
};

上面的這部分代碼咱們先不變。如今只要實現一個函數call1()並使用下面方式

console.log(source.call1(destination));

若是得出的結果和call()函數同樣,那就沒問題了。

如今咱們來模擬第一步:改變this的指向

假設咱們destination的結構是這樣的:

let destination = {
    name:"xuan",
    source:function(age,gender){
        console.log(this.name);
        console.log(age);
        console.log(gender);
        //添加一個返回值對象
        return {
            age:age,
            gender:gender,
            name:this.name
        }
    }
}

咱們執行destination.source(18,"male");就能夠在source()函數中把正確的結果打印出來而且返回咱們想要的值。

如今咱們的目的更明確了:給destination對象添加一個source屬性,而後添加參數執行它

因此咱們定義以下:

Function.prototype.call1 = function(ctx){
    ctx.fn = this;   //ctx爲destination   this指向source   那麼就是destination.fn = source;
    ctx.fn(); // 執行函數
    delete ctx.fn;  //在刪除這個屬性
}
console.log(source.call1(destination,18,"male"));

打印效果以下:

咱們發現this的指向已經改變了,可是咱們傳入的參數尚未處理。

第二步:支持對函數傳參
咱們使用ES6語法修改以下:

Function.prototype.call1 =function(ctx,...args){
    ctx.fn = this;
    ctx.fn(...args);
    delete ctx.fn;
}
console.log(source.call1(destination,18,"male"));

打印效果以下:

參數出現了,如今就剩下返回值了,很簡單,咱們再修改一下:

Function.prototype.call1 =function(ctx,...args){
    ctx.fn = this || window; //防止ctx爲null的狀況
    let res = ctx.fn(...args);
    delete ctx.fn;
    return res;
}
console.log(source.call1(destination,18,"male"));

打印效果以下:

如今咱們實現了call的效果!

模擬Function.prototype.apply()

apply()函數的做用和call()函數同樣,只是傳參的方式不同。apply的用法能夠查看MDN,MDN這麼說的:apply() 方法調用一個具備給定this值的函數,以及做爲一個數組(或相似數組對象)提供的參數。

apply()函數的第二個參數是一個數組,數組是調用apply()的函數的參數。

function source(age,gender){
    console.log(this.name);
    console.log(age);
    console.log(gender);
    return {
        age:age,
        gender:gender,
        name:this.name
    }
}
let destination = {
    name:"xuan"
};
console.log(source.apply(destination,[18,"male"]));

效果和call()是同樣的。既然只是傳參不同,咱們把模擬call()函數的代碼稍微改改:

Function.prototype.apply1 =function(ctx,args=[]){
    ctx.fn = this || window;
    let res = ctx.fn(...args);
    delete ctx.fn;
    return res;
}
console.log(source.apply1(destination,[18,'male']));

執行效果以下:

apply()函數的模擬完成。

Function.prototype.bind()

對於bind()函數的做用,咱們引用MDNbind()方法會建立一個新函數。當這個新函數被調用時,bind() 的第一個參數將做爲它運行時的 this對象,以後的一序列參數將會在傳遞的實參前傳入做爲它的參數。咱們看一下代碼:

function source(age,gender){
    console.log(this.name);
    console.log(age);
    console.log(gender);
    return {
        age:age,
        gender:gender,
        name:this.name
    }
}
let destination = {
    name:"xuan"
};
var res = source.bind(destination,18,"male");
console.log(res());
console.log("==========================")
var res1 = source.bind(destination,18);
console.log(res1("male"));
console.log("==========================")
var res2 = source.bind(destination);
console.log(res2(18,"male"));

打印效果以下:

咱們發現bind函數跟applycall有兩個區別:

1.bind返回的是函數,雖然也有call和apply的做用,可是須要在調用bind()時生效

2.bind中也能夠添加參數

明白了區別,下面咱們來模擬bind函數。

模擬Function.prototype.bind()

和模擬call同樣,現摘抄下面的代碼:

function source(age,gender){
    console.log(this.name);
    console.log(age);
    console.log(gender);
    //添加一個返回值對象
    return {
        age:age,
        gender:gender,
        name:this.name
    }
}
let destination = {
    name:"xuan"
};

而後咱們定義一個函數bind1,若是執行下面的代碼可以返回和bind函數同樣的值,就達到咱們的目的。

var res = source.bind1(destination,18);
console.log(res("male"));

首先咱們定義一個bind1函數,由於返回值是一個函數,因此咱們能夠這麼寫:

Function.prototype.bind1 = function(ctx,...args){
    var that = this;//外層的this指向經過變量傳進去
    return function(){
        //將外層函數的參數和內層函數的參數合併
        var all_args = [...args].concat([...arguments]);
        //由於ctx是外層的this指針,在外層咱們使用一個變量that引用進來
        return that.apply(ctx,all_args);
    }
}

打印效果以下:

這裏咱們利用閉包,把外層函數的ctx和參數args傳到內層函數,再將內外傳遞的參數合併,而後使用apply()call()函數,將其返回。

當咱們調用res("male")時,由於外層ctxargs仍是會存在內存當中,因此調用時,前面的ctx也就是sourceargs也就是18,再將傳入的"male"跟18合併[18,'male'],執行source.apply(destination,[18,'male']);返回函數結果便可。bind()的模擬完成!

可是bind除了上述用法,還能夠有以下用法:

function source(age,gender){
    console.log(this.name);
    console.log(age);
    console.log(gender);
    //添加一個返回值對象
    return {
        age:age,
        gender:gender,
        name:this.name
    }
}
let destination = {
    name:"xuan"
};
var res = source.bind1(destination,18);
var person = new res("male");
console.log(person);

打印效果以下:


咱們發現bind函數支持new關鍵字,調用的時候this的綁定失效了,那麼new以後,this指向哪裏呢?咱們來試一下,代碼以下:

function source(age,gender){
  console.log(this);
}
let destination = {
    name:"xuan"
};
var res = source.bind(destination,18);
console.log(new res("male"));
console.log(res("male"));

執行new的時候,咱們發現雖然bind的第一個參數是destination,可是this是指向source的。

不用new的話,this指向destination

好,如今再來回顧一下咱們的bind1實現:

Function.prototype.bind1 = function(ctx,...args){
    var that = this;
    return function(){
        //將外層函數的參數和內層函數的參數合併
        var all_args = [...args].concat([...arguments]);
        //由於ctx是外層的this指針,在外層咱們使用一個變量that引用進來
        return that.apply(ctx,all_args);
    }
}

若是咱們使用:

var res = source.bind(destination,18);
console.log(new res("male"));

若是執行上述代碼,咱們的ctx仍是destination,也就是說這個時候下面的source函數中的ctx仍是指向destination。而根據Function.prototype.bind的用法,這時this應該是指向source自身。

咱們先把部分代碼抄下來:

function source(age,gender){
    console.log(this.name);
    console.log(age);
    console.log(gender);
    //添加一個返回值對象
    return {
        age:age,
        gender:gender,
        name:this.name
    }
}
let destination = {
    name:"xuan"
};

咱們改一下bind1函數:

Function.prototype.bind1 = function (ctx, ...args) {
    var that = this;//that確定是source
    //定義了一個函數
    let f = function () {
        //將外層函數的參數和內層函數的參數合併
        var all_args = [...args].concat([...arguments]);
        //由於ctx是外層的this指針,在外層咱們使用一個變量that引用進來
        var real_ctx = this instanceof f ? this : ctx;
        return that.apply(real_ctx, all_args);
    }
    //函數的原型指向source的原型,這樣執行new f()的時候this就會經過原型鏈指向source
    f.prototype = this.prototype;
    //返回函數
    return f;
}

咱們執行

var res = source.bind1(destination,18);
console.log(new res("male"));

效果以下:

已經達到咱們的效果!

如今分析一下上述實現的代碼:

//調用var res = source.bind1(destination,18)時的代碼分析
Function.prototype.bind1 = function (ctx, ...args) {
    var that = this;//that確定是source
    //定義了一個函數
    let f = function () {
       ... //內部先無論
    }
    //函數的原型指向source的原型,這樣執行new f()的時候this就會指向一個新家的對象,這個對象經過原型鏈指向source,這正是咱們上面執行apply的時候須要傳入的參數
     //f.prototype==>source.prototype
    f.prototype = this.prototype;
    //返回函數
    return f;
}

f()函數的內部實現分析:

//new res("male")至關於運行new f("male");下面進行函數的運行態分析
let f = function () {
     console.log(this);//這個時候打印this就是一個_proto_指向f.prototype的對象,由於f.prototype==>source.prototype,因此this._proto_==>source.prototype
     //將外層函數的參數和內層函數的參數合併
     var all_args = [...args].concat([...arguments]);
     //正常不用new的時候this指向當前調用處的this指針(在全局環境中執行,this就是window對象);使用new的話這個this對象的原型鏈上有一個類型是f的原型對象。
    //那麼判斷一下,若是this instanceof f,那麼real_ctx=this,不然real_ctx=ctx;
     var real_ctx = this instanceof f ? this : ctx;
    //如今把真正分配給source函數的對象傳入
     return that.apply(real_ctx, all_args);
}

至此bind()函數的模擬實現完畢!若有不對之處,歡迎拍磚!您的寶貴意見是我寫做的動力,謝謝你們。

相關文章
相關標籤/搜索