天天看點

apply call bind的用法與實作

概念

apply call 和bind

允許為不同的對象配置設定和調用屬于一個對象的函數/方法。同時它們可以改變函數内 this 的指向。

差別

  • apply 和 call 接收的參數形式不同
  • apply 和 call 都是直接調用函數并得到函數執行結果,而 bind 會傳回待執行函數,需要再次調用

用法示範

我們先建立一個對象 parent

const parent = {
    name: 'parent',
    sayPerson (age, addr) {
        return {
            name: this.name,
            age,
            addr
        }
    }
}
           

顯然它具有 name 屬性,及方法 sayPerson,我們現在可以通過

parent.sayPerson()

來輸出該對象資訊。

const person = parent.sayPerson(60, 'shenzhen');
// {name: "parent", age: 60, addr: "shenzhen"}
           

現在我們再建立一個對象 son

const son = {
    name: 'son'
}
           

我們現在也想得到 son 的資訊,但是 son 對象沒有 sayPerson 函數怎麼辦?借助已有的 parent 對象和 call 方法,我們可以這樣寫

const person = parent.sayPerson.call(son, 26, 'shenzhen');
// {name: "son", age: 26, addr: "shenzhen"}
           

可以看出,通過調用 call 函數,我們為 son 對象配置設定了 sayPerson 方法并進行調用。實作了一個對象可以調用不屬于它自己的方法,并且函數内的 this 指向該對象。apply 方法的用法其實一樣,隻是傳參有些差別

const person = parent.sayPerson.call(son, [26, 'shenzhen']);
// {name: "son", age: 26, addr: "shenzhen"}
           

bind 函數則不直接調用函數,而是傳回待調用函數

const sayPersonFn = parent.sayPerson.bind(son, 26, 'shenzhen');

const person = sayPersonFn();
// {name: "son", age: 26, addr: "shenzhen"}
           

以上就是三者的使用方法和差別,下面我們來看看它們是如何實作的

實作

call的實作

實作原理就是為對象 obj 添加需要調用的方法,接着調用該方法(此時 this 指向 obj),調用過後再删除該方法

簡單版

Object.prototype.callFn = function (...args) {
    // 第一個參數為目标對象
    const context = args[0];

    args.shift();

    // 為對象指派需要調用的方法
    context.fn = this;

    // 調用該方法
    context.fn(...args);

    // 删除方法
    delete context.fn;
}
           

加上傳回值

Object.prototype.callFn = function (...args) {
    // 第一個參數為目标對象
    const context = args[0];

    args.shift();

    // 為對象指派需要調用的方法
    context.fn = this;

    // 調用該方法
    const result = context.fn(...args);

    // 删除方法
    delete context.fn;

    return result;
}
           

在測試中發現,我們調用 call,如果第一個參數是 null 或者 undefined,那麼 call 将以全局 window 來調用方法,此時 this 也指向 window。如果第一個參數不是對象類型,則以空對象 {} 來調用方法。

Object.prototype.callFn = function (...args) {
    // 第一個參數為目标對象
    let context = args[0];

    // undefined 和 null 指向 window
    if (context === null || context === undefined) {
        context = window;
    }

    // 不是對象類型則建立空對象
    if (typeof context !== 'object') {
        context = {};
    }

    args.shift();

    // 為對象指派需要調用的方法
    context.fn = this;

    // 調用該方法
    const result = context.fn(...args);

    // 删除方法
    delete context.fn;
    
    return result;
}
           

至此,我們實作了一個完整的 call 方法。

apply的實作

既然和 call 隻存在傳參的差別,那我們隻需要簡單修改下已實作的 call 方法即可。

Object.prototype.applyFn = function (...args) {
    let context = args[0];

    if (context === null || context === undefined) {
        context = window;
    }

    if (typeof context !== 'object') {
        context = {};
    }

    args.shift();

    context.fn = this;

    // 和 call 存在差異的地方
    const result = context.fn(...args[0]);

    delete context.fn;

    return result;
}
           

bind的實作

在實作了 apply 和 call 的前提下,bind 的實作也比較簡單。

Object.prototype.bindFn = function (...args) {
    // 實際就是多包了層待執行函數
    return () => {
        return this.applyFn(args[0], (args || []).slice(1));
    }
}
           

至于以 bind 方法傳回的函數作為構造函數來建立對象會存在的問題請參考JavaScript深入之bind的模拟實作。

總結

call apply bind

在工作中實際上是比較常見的函數,特别是在一些架構或庫的源碼中,但是經常有人會混淆它們的用法。希望大家通過此篇文章可以徹底弄清它們的作用與差別,并且知道其實作原理,知其然知其是以然。

參考

  • JavaScript深入之call和apply的模拟實作
  • JavaScript深入之bind的模拟實作

歡迎到前端學習打卡群一起學習~516913974

js