完全搞懂call、apply、bind區別及實現

call

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

舉個例子:git

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函數的實參。github

特色:面試

  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方法的第一個參數,其他參數爲這個新函數的實參。

仍是以上述代碼爲例:app

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的綁定

不一樣點: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的實現只支持大部分場景,好比要綁定的對象爲凍結對象,則會拋出錯誤。

能夠查看中文版的 call ES規範 15.3.4.4

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;
}

能夠查看中文版的 apply ES規範 15.3.4.3

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 ES規範 15.3.4.5

實現軟綁定

什麼是軟綁定?咱們知道經過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

相關文章
相關標籤/搜索