關於call, apply, bind方法的區別與內部實現

weixin_34128411發表於2019-01-03

關於這三個方法,很多大神已經總結的很到位了,寫這篇文章只是想加深自己的理解。
一開始,我是在看設計模式這本書的時候,看到如下程式碼(new運算的過程):

// new運算的過程
/**
 * 1、建立一個空物件;
 * 2、該空物件的原型指向建構函式(連結原型):將建構函式的 prototype 賦值給物件的 __proto__屬性;
 * 3、繫結 this:將物件作為建構函式的 this 傳進去,並執行該建構函式;
 * 4、返回新物件:如果建構函式返回的是一個物件,則返回該物件;否則(若沒有返回值或者返回基本型別),返回第一步中新建立的物件;
 */
var Person = function(name) {
    this.name = name
    console.log('name is ', this.name)
}
Person.prototype.getName = function() {
    return this.name
}
var objectFactory = function() {
    // 1、建立一個空物件
    var obj = new Object()
    console.log('before shift arguments = ',arguments)
    獲取建構函式
    Constructor = [].shift.call(arguments)
    console.log('after shift arguments = ', arguments)
    console.log(`Constructor = ${Constructor}`)
    // 2、該空物件的原型指向建構函式: 將建構函式的prototype 賦值給空物件的 __proto__屬性;
    obj.__proto__ = Constructor.prototype
    // 3、將空物件作為建構函式的this傳進去,並執行該建構函式
    var ret = Constructor.apply(obj, arguments)
    // 4、返回新物件:如果建構函式返回的是一個物件,則返回該物件;否則(若沒有返回值或者返回基本型別),返回第一步中新建立的物件;
    return typeof ret == 'object' ? ret : obj
}
var a = objectFactory(Person, 'yandong')
console.log('執行後的name = ', a.name)

查詢一番資料之後,才明白過來。
原來,arguments是類陣列,並不是真正的陣列,所以不能直接呼叫陣列的shiftf方法,但是可以通過call呼叫。
call方法,表示傳入的物件引數呼叫call前面物件的方法,並且被呼叫的函式會被執行,call方法的引數是當前上下文的物件以及引數列表
apply也是如此,只不過它傳入的引數是物件引數陣列
而bind,用法與apply, call一樣,但是它被物件繫結的函式不會被執行,而是返回這個函式,需要你手動去呼叫返回的函式,才會返回結果。

那我們來看看這句程式碼:
Constructor = [].shift.call(arguments)
意思就是:arguments物件呼叫陣列的shift()方法。
而shift()方法會刪除並返回陣列的第一個元素
當我們執行objectFactory(Person, 'anne')的時候
跳轉到objectFactory函式內部,arguments這個類陣列會全部接收Person引數以及‘anne’引數

3077057-73d8caa9941fac57.png
image.png

我們可以看到,arguments類陣列物件的值。
所以Constructor = [].shift.call(arguments)這句程式碼刪除並且返回的就是傳入call方法或者apply方法的第一個物件引數,也就是Person。
執行了shift之後,arguments就只剩下執行函式所需的引數列表或者引數陣列了。如下圖:
3077057-3973e4707a2a2bc6.png
image.png

以上就是通過new返回一個物件例項的過程。

call, apply, bind呼叫的區別

// call, apply, bind的區別
var a = {value: 1}
function getValue(name, age) {
    console.log('arguments in fn = ', arguments)
    console.log(name, age)
    console.log(this.value)
}
getValue.call(a,'yandong1', 17)
let bindFoo = getValue.bind(a, 'testBind', 45)
console.log('bindFoo = ',bindFoo)
bindFoo()
getValue.apply(a,['yandong2', 18])
var returnedFunc = getValue.bind(a,'yandong3', 19)
console.log(returnedFunc)
returnedFunc()

執行結果:


3077057-81cbc777f103207a.png
image.png

我們可以看到,call, apply都是直接返回函式執行後的結果,而bind是返回一個函式,之後手動執行之後才會將結果返回。

手動實現call方法

// 手寫模擬call方法的思想
/**
 * call方法思想:改變this指向,讓新的物件可以執行這個方法
 * 實現思路:
 * 1、給新的物件新增一個函式(方法),並讓this(也就是當前繫結的函式)指向這個函式
 * 2、執行這個函式
 * 3、執行完以後刪除這個方法
 * 4、可以將執行結果返回
 */
Function.prototype.myCall = function(funcCtx) {
    // funcCtx是當前要呼叫函式的物件
    console.log('funcCtx = ',funcCtx)
    // this指被呼叫的函式
    console.log('this = ',this)
    if(typeof this != 'function') {
        throw new TypeError('Erorr')
    }
    let ctx = funcCtx || global
    console.log('arguemnets = ', arguments)
    let args = [...arguments].slice(1)
    console.log(`args = ${args}`)

    ctx.fn = this // 為當前物件新增一個函式fn, 值為要已經定義的要呼叫的函式
    console.log('ctx.fn = ', ctx.fn)
    // 執行新增的函式fn
    var result = ctx.fn(...args)
    // 執行完以後刪除
    delete ctx.fn
    return result
}
getValue.myCall(a,'test', 20)

執行結果:


3077057-034fe8cd09eebccd.png
image.png

手動實現apply方法

與call方法的思想類似,只不過它需要判斷一下引數陣列是否存在

// apply
Function.prototype.myApply = function(funcCtx) {
    console.log(this)
    if(typeof this != 'function') {
        throw new TypeError('Erorr')
    }
    let ctx = funcCtx || global

    ctx.fn = this
    console.log('arguemnets = ', arguments)
    let result
    if(arguments[1]) {
        result = ctx.fn(...arguments[1])
    } else {
        result = ctx.fn()
    }
    delete ctx.fn
    return result
}
getValue.myApply(a, ['eo', 50])
3077057-2a78f5d8a7a9bcc4.png
image.png

手動實現bind方法

//bind實現
/**
 * 實現思想:
 * 1、返回一個函式,其他與call, apply類似
 * 2、如果返回的函式作為建構函式,bind時指定的 this 值會失效,但傳入的引數依然生效。
 */
Function.prototype.myBind = function(funcCtx) {
    let ctx = funcCtx || global
    console.log(this)
    let _this = this
    let args = [...arguments].slice(1)
    // 作為建構函式使用
    let Fbind = function() {
        let self = this instanceof Fbind ? this : ctx
        return _this.apply(self,args.concat(...arguments))
    }
    let f = function() {}
    f.prototype = this.prototype
    Fbind.prototype = new f()
    return Fbind
}
var value = 2
var foo = {
    value: 1
}
function bar(name, age) {
    this.habbit = 'shopping'
    console.log('bar this.value = ', this.value)
    console.log(name, age)
}
bar.prototype.friend = 'shuaige'
var bindFoo = bar.myBind(foo, 'testbind',111)
// 返回的函式直接呼叫
bindFoo()

執行結果


3077057-a60564a7999b1941.png
image.png
// 當 bind 返回的函式作為建構函式的時候,bind 時指定的 this 值會失效,但傳入的引數依然生效。
var obj = new bindFoo('18')
console.log('obj = ', obj)
console.log(obj.friend)
console.log(obj.habbit)

執行結果


3077057-e2c229e34ebe16ee.png
image.png

相關文章