沐鳴代理:_徹底搞懂call、apply、bind區別及實現

call

call 方法使用一個指定的 this 值和單獨給出的一個或多個參數來調用一個函數

舉個例子:

var value=1;
function foo(x,y) {
    console.log(this.value)
}
var obj={
    value: 2
}
foo(); // 1 
foo.call(obj,3,4);  // 2

上述例子中,當foo函數單獨調用時內部this綁定為全局對象window。當通過call方法調用時this被綁定為call方法中的第一個參數。call方法中的除了第一個參數外的剩餘參數為foo函數的實參。

特點:

  1. 改變this執行。
  2. 執行調用call方法的函數。

apply

上述例子也可以用apply來改寫:

var value=1;
function foo(x,y) {
    console.log(this.value)
}
var obj={
    value: 2
}
foo(); // 1 
foo.apply(obj,[3,4]);  // 2

apply與call的唯一區別就是:調用apply方法時的參數,實參應該是以數組的形式來書寫。

bind

bind 方法創建一個新的函數,也可以說是當前調用bind方法的函數的一個引用,這個函數的this被綁定為bind方法的第一個參數,其餘參數為這個新函數的實參。

還是以上述代碼為例:

var value=1;
function foo(x,y) {
    console.log(this.value)
}
var obj={
    value: 2
}
var bar=foo.bind(obj,3,4);
bar(); // 2

bind與call,apply的區別就是:bind方法不會立即調用函數,它只是改變了新函數的this綁定。

當我們使用bind方法創建一個新函數,這個新函數再使用call或者apply來更改this綁定時,還是以bing綁定的this為準。

var value=1;
function foo(x,y) {
    console.log(this.value)
}
var obj={
    value: 2
}
var o={
    value: 3
}
var bar=foo.bind(obj,3,4);
bar.call(o); // 2

區別

相同點:

  1. 都會更改this的綁定

不同點:

  1. call和apply會立即執行函數,bind不會。
  2. apply方法的傳參格式為數組,call和bind不是。

call的實現

怎樣來實現call呢?先想想call的特點:

第一個參數為要綁定的this,剩餘參數為函數的實參。

那我們怎樣改更改this的綁定呢?

我們直到當我們以 對象 . 方法 調用一個普通函數時,this始終指向當前調用的對象。

var value=1;
function foo(x,y) {
    console.log(this.value)
}
var obj={
    value: 2
}
foo.call(obj,3,4);  // 2

// 相當於
obj.foo(3,4);

思路:

  1. 將函數作為要更改this綁定的對象的一個屬性。也就是把函數作為call方法中第一個參數中的一個屬性。
  2. 通過 對象 . 方法 執行這個函數。
  3. 返回當前函數執行后的結果。
  4. 刪除該對象上的屬性。

call的第一個參數還有幾個特點:

  1. 當第一個參數(要更改的this綁定的對象)為null或者undefined時,this綁定為window(非嚴格模式)。如果為嚴格模式,均為第一個參數的值。
  2. 當call方法中第一個參數為除null和undefined外的基本類型(String,Number,Boolean)時,先對該基本類型進行”裝箱”操作。
/**
 * @description: 實現call方法
 * @param : context this要綁定的值
 * @param : args 除第一個參數外的參數集合
 * @return: 函數返回值
 */
Function.prototype.myCall=function(context,...args) {
    let handler=Symbol();// 生成一個唯一的值,用來作為要綁定對象的屬性key,儲存當前調用call方法的函數
    if(typeof this!=='function') {
        //調用者不是函數

        throw this+'.myCall is not a function'
    }
    // 如果第一個參數為引用類型或者null
    if(typeof context==='object'||typeof context==='function') {
        // 如果為null 則this為window
        context=context||window;
    } else {
        // 如果為undefined 則this綁定為window
        if(typeof context==='undefined') {
            context=window;
        } else {
            // 基本類型包裝  1 => Number{1}
            context=Object(context);
        }
    }

    // this 為當前調用call方法的函數。
    context[handler]=this;
    // 執行這個函數。這時這個函數內部this綁定為cxt,儲存函數執行后的返回值。
    let result=context[handler](...args);
    // 刪除對象上的函數
    delete context[handler];
    // 返回返回值
    return result;
}

上述call的實現只支持大部分場景,比如要綁定的對象為凍結對象,則會拋出錯誤。

apply的實現

由於apply跟call的唯一區別只是除了第一個參數外其餘參數的傳遞形式不一樣。在實現call的基礎上略作修改就可以了。

call參數的特點:

  1. 除第一個參數外,其餘參數必須為數組的形式。
  2. 如果第二個參數存在

    2.1 如果第二個參數為null或者undefined,則無效。
    2.2 如果第二個參數類型不是Object,則拋出一個異常。如果不是數組,則無效。

/**
 * @description: 實現apply方法
 * @param : context this要綁定的值
 * @param : argsArr 要傳遞給調用apply方法的函數的實參集合。數組形式。
 * @return: 函數返回值
 */
Function.prototype.myApply=function(context,argsArr) {
    let handler=Symbol();// 生成一個唯一的值,用來作為要綁定對象的屬性key,儲存當前調用call方法的函數
    if(typeof this!=='function') {
        //調用者不是函數

        throw this+'.myBind is not a function'
    }
    let args=[];
    // 如果傳入的參數是不是數組,則無效
    if(typeof argsArr==='object'||typeof context==='function'||typeof argsArr==='undefined') {
        args=Array.isArray(argsArr)? argsArr:[];
    } else {
        // 如果為基本類型,如果是undefined,則無效,其它類型則拋出錯誤。
        throw 'TypeError: CreateListFromArrayLike called on non-object'
    }
    // 如果第一個參數為引用類型或者null
    if(typeof context==='object') {
        // 如果為null 則this為window
        context=context||window;
    } else {
        // 如果為undefined 則this綁定為window
        if(typeof context==='undefined') {
            context=window;
        } else {
            // 基本類型包裝  1 => Number{1}
            context=Object(context);
        }
    }

    // this 為當前調用call方法的函數。
    context[handler]=this;
    // 執行這個函數。這時這個函數內部this綁定為cxt,儲存函數執行后的返回值。
    let result=context[handler](...args);
    // 刪除對象上的函數
    delete context[handler];
    // 返回返回值
    return result;
}

bind的實現

bind與call和apply區別還是很大的。
先看一個例子:

var obj={
    name: 'erdong'
}

function foo(name,age) {
    this.age=age;
    console.log(this.name+':'+age+'歲');
}

var bar=foo.bind(obj,'chen');
bar(18); // erdong:18歲


var b=new bar(27); // undefined:27歲
console.log(b.age); // 27

綜合上述例子,我們總結一下bind方法特點:

1.調用bind方法會創建一個新函數,我們成它為綁定函數(boundF)。

2.當我們直接調用boundF函數時,內部this被綁定為bind方法的第一個參數。

3.當我們把這個boundF函數當做構造函數通過new關鍵詞調用時,函數內部的this綁定為新創建的對象。(相當於bind提供的this值被忽略)。

4.調用bind方法時,除第一個參數外的其餘參數,將作為boundF的預置參數,在調用boundF函數時默認填充進boundF函數實參列表中。

<!–bind方法中第一個參數的特點:

  1. 當第一個參數(要更改的this綁定的對象)為null或者undefined時,this綁定為window(非嚴格模式)。
  2. 當call方法中第一個參數為除null和undefined外的基本類型(String,Number,Boolean)時,先對該基本類型進行”裝箱”操作。–>

我們根據上述的bind方法的特點,一步一步實現bind方法。

// 第一步  返回一個函數
/**
 * @description: 實現bind方法
 * @param : context this要綁定的值
 * @param : args 調用bind方法時,除第一個參數外的參數集合,這些參數會被預置在綁定函數的參數列表中
 * @return: 返回一個函數
 */
Function.prototype.myBind=function(context,...args) {
    // 這裏的this為調用bind方法的函數。
    let thisFunc=this;

    let boundF =  function() {
    }
    return boundF;
}

第一步我們實現了myBind方法返回一個函數。沒錯就是這就是利用了閉包。

// 第二步 
/**
 * @description: 實現bind方法
 * @param : context this要綁定的值
 * @param : args 調用bind方法時,除第一個參數外的參數集合,這些參數會被預置在綁定函數的參數列表中
 * @return: 返回一個函數
 */
Function.prototype.myBind=function(context,...args) {
    // 這裏的this為調用bind方法的函數。
    let thisFunc=this;

    let boundF=function() {
        thisFunc.call(context,...args);
    }
    return boundF;
}

第二步:當調用boundF方法時,原函數內部this綁定為bind方法的第一個參數,這裏我們利用了call來實現。

// 第三步
/**
 * @description: 實現bind方法
 * @param : context this要綁定的值
 * @param : args 調用bind方法時,除第一個參數外的參數集合,這些參數會被預置在綁定函數的參數列表中
 * @return: 返回一個函數
 */
Function.prototype.myBind=function(context,...args) {
    // 這裏的this為調用bind方法的函數。
    let thisFunc=this;
    let boundF=function() {
        let isUseNew=this instanceof boundF;
        thisFunc.call(isUseNew? this:context,...args);
    }
    return boundF;
}

第三部:先判斷boundF是否通過new調用,也就是判斷boundF內部的this是否為boundF的一個實例。如果是通過new調用,boundF函數的內部this綁定為當前新創建的對象,因此調用call方法時把當前新創建的對象當做第一個參數傳遞。

// 第四步
/**
 * @description: 實現bind方法
 * @param : context this要綁定的值
 * @param : args 調用bind方法時,除第一個參數外的參數集合,這些參數會被預置在綁定函數的參數列表中
 * @return: 返回一個函數
 */
Function.prototype.myBind=function(context,...args) {
    // 這裏的this為調用bind方法的函數。
    let thisFunc=this;
    let boundF=function() {
        let boundFAgrs=arguments;
        let totalAgrs=[...args,...arguments];
        let isUseNew=this instanceof boundF;
        thisFunc.call(isUseNew? this:context,...totalAgrs);
    }
    return boundF;
}

第四部:通過閉包的特性我們知道,boundF函數可以訪問到外部的args變量,將它與boundF函數中的參數合併。然後當做調用原函數的參數。

到此我們簡易版的bind已經显示完畢,下面測試:

Function.prototype.myBind=function(context,...args) {
    // 這裏的this為調用bind方法的函數。
    let thisFunc=this;
    let boundF=function() {
        let boundFAgrs=arguments;
        let totalAgrs=[...args,...arguments];
        let isUseNew=this instanceof boundF;
        thisFunc.call(isUseNew? this:context,...totalAgrs);
    }
    return boundF;
}
var obj={
    name: 'erdong'
}

function foo(name,age) {
    this.age=age;
    console.log(this.name+':'+age+'歲');
}

var bar=foo.myBind(obj,'chen');
bar(18); // erdong:18歲


var b=new bar(27); // undefined:27歲
console.log(b)
console.log(b.age); // 27

我們發現上述代碼中調用myBind跟bind方法輸出的結果一致。

其實bind方法還有一個特點。

看例子:

var obj={
    name: 'erdong'
}
    
function foo(name,age) {
    this.age=age;
}
foo.prototype.say=function() {
    console.log(this.age);
}
var bar=foo.bind(obj,'chen');

var b=new bar(27);
b.say();

通過上述例子我們發現,通過new(新函數)創建的對象 b 。它可以獲取原函數原型上的方法。因為我們實現的myBind,b是通過新函數創建的,它跟原函數理論上來說並沒有什麼關係。

再來看:


var obj={
    name: 'erdong'
}

function foo(name,age) {
    this.age=age;
}

var bar=foo.bind(obj,'chen');

var b=new bar(27);

console.log(b instanceof foo); // true
console.log(b instanceof bar); // true

它的原型鏈上出現了foo.prototype和bar.prototype。按照我們的常規理解 b 的原型鏈為:

b.__proto__ => bar.prototype => bar.prototype.__proto__ => Object.prototype

但是跟foo.prototype有什麼關係呢?

我個人的理解:

foo函數調用bind方法產生的新函數bar,這個函數不是一個真正的函數,mdn解釋它為怪異函數對象。我們通過console.log(bar.prototype)發現
輸出的值為undefined。我們暫且把它理解成一個foo函數的一個簡化版。可以形象的理解成foo == bar。

通過我們上面實現的myBind並不能達到讓新對象b跟原函數和新函數的原型都產生關係。

var obj={
    name: 'erdong'
}

function foo(name,age) {
    this.age=age;
}

var bar=foo.myBbind(obj,'chen');

var b=new bar(27);

console.log(b instanceof foo); // fasle
console.log(b instanceof bar); // true

這是我們就需要對我們的myBind進行迭代升級:

// 迭代一
Function.prototype.myBind=function(context,...args) {
    // 這裏的this為調用bind方法的函數。
    let thisFunc=this;

    let boundF=function() {
        let boundFAgrs=arguments;
        let totalAgrs=[...args,...arguments];
        let isUseNew=this instanceof boundF;
        thisFunc.call(isUseNew? this:context,...totalAgrs);
    }
    // 調用myBind方法的函數的prototype賦值給 boundF 的prototype。
    boundF.prototype=thisFunc.prototype;
    return boundF;
}

在我們myBind實現中bar函數其實就是boundF函數,因此把原函數的原型賦值給新函數的原型,這時創建的對象就會跟原函數的原型有關係。

這時b的原型鏈就會變成:

b.__proto__ => bar.prototype => foo.prototype => foo.prototype.__proto__ => Object.prototype

這時b的原型鏈上就會出現 bar.prototype 和 foo.prototype。

var obj={
    name: 'erdong'
}

function foo(name,age) {
    this.age=age;
}

var bar=foo.myBbind(obj,'chen');

var b=new bar(27);

console.log(b instanceof foo); // true
console.log(b instanceof bar); // true

我們在實現里把foo的原型直接賦值給bar的原型。由於引用地址相同,所以改變bar原型的時候foo的原型也會改變。

var obj={
    name: 'erdong'
}

function foo(name,age) {
    this.age=age;
}

var bar=foo.myBbind(obj,'chen');
bar.prototype.aaa = 1;
console.log(bar.prototype.aaa); // 1
var b=new bar(27);

console.log(b instanceof foo); // true
console.log(b instanceof bar); // true

這樣是不合理的,我們繼續迭代:

Function.prototype.myBind=function(context,...args) {
    // 這裏的this為調用bind方法的函數。
    let thisFunc=this;

    let boundF=function() {
        let boundFAgrs=arguments;
        let totalAgrs=[...args,...arguments];
        let isUseNew=this instanceof boundF;
        thisFunc.call(isUseNew? this:context,...totalAgrs);
    }
    var F=function() {};
    F.prototype=thisFunc.prototype;
    boundF.prototype=new F();
    return boundF;
}

這裏我們聲明了一個函數F,讓它的prototype的值為foo的prototype。再讓boundF的prototype的值賦值為F的實例。利用原型鏈繼承,來讓原函數與新函數的原型之間沒有直接關係。 這個時候b的原型鏈為:

b.__proto__ => bar.prototype => new F() => new F().__proto__ => F.prototype => thisFunc.prototype => thisFunc.prototype.__proto__ => Object.prototype

綜上最終版:

/**
 * @description: 實現bind方法
 * @param : context this要綁定的值
 * @param : args 調用bind方法時,除第一個參數外的參數集合,這些參數會被預置在綁定函數的參數列表中
 * @return: 返回一個新函數
 */
Function.prototype.myBind=function(context,...args) {
    // 這裏的this為調用bind方法的函數。
    let thisFunc=this;
    // 如果調用bind的變量不是Function類型,拋出異常。
    if(typeof thisFunc!=='function') {
        throw new TypeError('Function.prototype.bind - '+
            'what is trying to be bound is not callable');
    }
    // 定義一個函數boundF
    // 下面的”新函數“ 均為函數調用bind方法之後創建的函數。
    let boundF=function() {
        // 這裏的 arguments 為函數經過bind方法調用之後生成的函數再調用時的實參列表
        let boundFAgrs=arguments;
        // 把調用bind方法時除第一個參數外的參數集合與新函數調用時的參數集合合併。當做參數傳遞給call方法
        let totalAgrs=[...args,...arguments];
        // 判斷當前新函數是否是通過new關鍵詞調用
        let isUseNew=this instanceof boundF;
        // 如果是->把call方法第一個參數值為當前的this(這裏的this也就是通過new調用新函數生成的新對象)
        // 如果否->把調用bind方法時的傳遞的第一個參數當做call的第一個參數傳遞

        thisFunc.call(isUseNew? this:context,...totalAgrs);
    }
    //通過原型鏈繼承的方式讓原函數的原型和新函數的原型,都在通過new關鍵詞構造的新對象的原型鏈上
    // b instanceof 原函數  -> true
    // b instanceof 新函數  -> true
    var F=function() {};
    F.prototype=thisFunc.prototype;
    boundF.prototype=new F();

    return boundF;
}

實現軟綁定

什麼是軟綁定?我們知道通過bind可以更改this綁定為bind方法的第一個參數(除了new)。綁定之後就無法改變了。我們稱bind綁定this為硬綁定。

// bind
var o={
    name: 'erdong'
}
var o1={
    name: "chen"
}
var foo=function() {
    console.log(this);
}
var bar=foo.bind(o);

var obj={
    foo: bar
}
bar(); //  this => o
bar.call(o1); // this => o
obj.foo(); // this => o

上述例子中,當foo函數通過bind綁定this為o,再通過call或者對象.方法的形式調用時,this始終被綁定為o。無法被改變。當然這裏我們不考慮new(通過new調用的話,this不綁定為o)。那麼我們怎樣再調用bar函數時,還能動態的修改this的綁定呢?

// softBind
var o={
    name: 'erdong'
}
var o1={
    name: "chen"
}
var foo=function() {
    console.log(this);
}
var bar=foo.softBind(o);

var obj={
    foo: bar
}
bar(); //  this => o
bar.call(o1); // this => o1
obj.foo(); // this => obj

其實這裏的實現softBind的原理跟實現myBind的原理類似。

這裏我們在myBind源代碼中更改:

Function.prototype.softBind=function(context,...args) {
    // 這裏的this為調用bind方法的函數。
    let thisFunc=this;
    // 如果調用bind的變量不是Function類型,拋出異常。
    if(typeof thisFunc!=='function') {
        throw new TypeError('Function.prototype.bind - '+
            'what is trying to be bound is not callable');
    }
    // 定義一個函數boundF
    // 下面的”新函數“ 均為函數調用bind方法之後創建的函數。
    let boundF=function() {
        // 這裏的 arguments 為函數經過bind方法調用之後生成的函數再調用時的實參列表
        let boundFAgrs=arguments;
        // 把調用bind方法時除第一個參數外的參數集合與新函數調用時的參數集合合併。當做參數傳遞給call方法
        let totalAgrs=[...args,...arguments];
        
        // 如果調用新函數時存在新的this,並且新的this不是全局對象,那麼我們認為這裏想要更改新函數this的綁定。因此讓新函數的內部this綁定為當前新的this。
        
        thisFunc.call(this && this !== window ? this : context,...totalAgrs);
    }
    //通過原型鏈繼承的方式讓原函數的原型和新函數的原型,都在通過new關鍵詞構造的新對象的原型鏈上
    // b instanceof 原函數  -> true
    // b instanceof 新函數  -> true
    var F=function() {};
    F.prototype=thisFunc.prototype;
    boundF.prototype=new F();

    return boundF;
}

這時我們用softBind再輸出一下上面的例子:

var o={
    name: 'erdong'
}
var o1={
    name: "chen"
}
var foo=function() {
    console.log(this);
}
var bar=foo.softBind(o);

var obj={
    foo: bar
}
bar(); //  this => o
bar.call(o1); // this => o1  這裏如果上面使用bind  這裏的this還是被綁定為o  
bar.call(); // this => o1   這裏如果上面使用bind  這裏的this還是被綁定為o  

obj.foo(); // this => obj   這裏如果上面使用bind  這裏的this還是被綁定為o  

這時達到了我們期望的輸出。

重點就在這一句:

thisFunc.call(this && this !== window ? this : context,…totalAgrs);

面試題

看下述代碼:

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

看到這裏我們肯定對 func.call(func); 輸出什麼很清楚了。

但是 func.call.call(func); 這樣有輸出什麼呢?

我們一步一步拆解來看

func.call.call(func);

// 此時 func.call 內部的this為 func。
// 這裡是在上一步代碼的基礎上執行的
// 此時func.call的內部this被綁定為func
// 但是此時又執行了func.call();

func.call(); 
// 由於call中沒有參數,因此func的內部this被綁定為window

如果此時把 func.call.call(func)結合我們的源碼實現來看,會很容易理解。

最後

如果文中有錯誤,請務必留言指正,萬分感謝。

點個贊哦,讓我們共同學習,共同進步。GitHub

站長推薦

1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲

2.廣告聯盟: 整理了目前主流的廣告聯盟平台,如果你有流量,可以作為參考選擇適合你的平台點擊進入

鏈接: http://www.fly63.com/article/detial/9267