1. 程式人生 > 實用技巧 >模擬實現js的bind方法

模擬實現js的bind方法

目錄

var obj = {};
console.log(obj);
console.log(typeof Function.prototype.bind); // function
console.log(typeof Function.prototype.bind());  // function
console.log(Function.prototype.bind.name);  // bind
console.log(Function.prototype.bind().name);  // bound

bind是什麼

  • a. bindFunction原型鏈中Function.prototype的一個函式,每個函式都可以呼叫它。

  • b. bind本身是一個函式名為bind的函式,返回值也是函式,函式名是bound。(console出來為

    bound 加上一個空格)。

let obj = {
    name: "yato"
}

function original(a,b){
    console.log(this.name)
    console.log([a,b])
    return false
}

let bound = original.bind(obj, 1)
let boundInvoke = bound(2)                    // 'yato', Array(2)[1,2]

console.log(boundInvoke)                       // false
console.log(original.bind.name)                // bind
console.log(original.bind.length)              // 1
console.log(original.bind().length)            // 2 返回original函式形參個數
console.log(bound.name)                        // 'bound original'
console.log((function(){}).bind().name)        // 'bound '
console.log((function(){}).bind().length)      // 0

進一步理解bind

  • a. 呼叫bind的函式中的this指向bind()函式的第一個引數。

  • b. 函式bind()時傳遞的引數被bind接受處理,bind()完畢之後,程式呼叫返回的函式(bound)時,傳遞的引數也接收處理了,也就是在bind()內部合併處理了。

  • c. 並且bind()後的函式的name為bound+空格+呼叫bind的函式名。如果呼叫函式為匿名函式,則名字為bound+空格

  • d. bind後的返回值函式,執行後返回值時原函式(original)的返回值(上例中的false)

  • e. bind函式的形參(即函式的length)是1bind後返回的bound函式形參不定

    ,根據繫結的函式原函式(original)形參個數決定。

根據上面的兩個例子,模擬實現一個簡單版的bindFn

Function.prototype.bindFn = function bindFake(thisArg){
    if(typeof this !== 'function'){
        throw new TypeError(this + 'must be a function')
    }

    // 儲存函式本身
    let self  = this
    
    // 去除thisArg的其他引數,轉成陣列
    let args = [].slice.call(arguments, 1)
    let bound = function(){
        // bind 返回的函式,也就是bound,在程式中被呼叫時傳遞的引數轉成陣列
        let boundArg = [].slice.call(arguments);

        // apply修改this指向,把兩個函式的引數合併傳給self函式,返回執行結果
        return self.apply(thisArg, args.concat(boundArg))
    }

    return bound
}

// Test
let obj = {
    name: 'yato'
}

function original(a, b){
    console.log(this.name)
    console.log([a,b])
}

let bound = original.bindFn(obj, 1)
bound(2);  // 'yato', [1,2]

但是函式是可以使用new來例項化的。

 let obj = {name : 'yato'}

 function original(a, b){
     console.log('this : ', this)
     console.log('typeof this : ', typeof this)
     this.name = b
     console.log('name: ', this.name)
     console.log('this: ', this)
     console.log([a,b])
 }

 let bound = original.bind(obj, 1)
 let newBoundInvoke = new bound(2)
 console.log('newBoundInvoke: ', newBoundInvoke)
// this :  original {}
// typeof this :  object
// name:  2
// this:  original { name: 2 }
// [ 1, 2 ]
// newBoundInvoke:  original { name: 2 }

分析例子可以得出結論

  • a. 從例子中可以看出this指向了new bound()生成的物件

  • b. new bound() 的返回值是以original原函式構造器生成的新物件。original原函式的this指向的就是這個新物件。

  • c.簡要剖析下new做了什麼

    1. 建立一個全新的空物件
    2. 對這個物件指向原型連結(instance.__proto__ = Class.prototype ),其實Class.prototype就是constructor
    3. 生成的新物件會繫結到函式呼叫的this
    4. 通過new建立的每個物件最終被[[prototype]]連結這個函式的prototype上(參考2)
    5. 如果函式沒有返回物件型別Object(包含Function, Array, Date, RegExg, Error),那麼new表示式中的函式呼叫會自動返回這個新的物件
所以相當於在new呼叫時,bind的返回值函式bound內部要實現new的操作
// 第二版 實現new呼叫
Function.prototype.bindFn = function bindFake(thisArg){
    if(typeof this !== 'function'){
        throw new TypeError(this + ' must be a function')
    }

    // 儲存呼叫bind的函式本身的引用
    let self = this

    // 去除thisArg引數,其他轉成陣列
    let args = [].slice.call(arguments, 1)
    let bound = function(){
        let boundArgs = [].slice.call(arguments)
        let finalArgs = args.concat(boundArgs)

        // new 呼叫時,其實this instanceof bound 判斷不是很準確。es6
        // new.target就是解決這一問題的
        if(this instanceof bound){
            // 這裡是實現上文描述的 new 的第 1, 2, 4 步
            // 1.建立一個全新的物件
            // 2.並且執行[[Prototype]]連結
            // 4.通過`new`建立的每個物件將最終被`[[Prototype]]`連結到這個函式的`prototype`物件上。
            // self可能是ES6的箭頭函式,沒有prototype,所以就沒必要再指向做prototype操作。
            if(self.prototype){
                function Empty(){}
                Empty.prototype = self.prototype
                bound.prototype = new Empty()
            }

            // 這裡實現的時上文描述的第三步
            // 3.生成的新物件會繫結到函式呼叫的this
            let result = self.apply(this, finalArgs);

            // 這裡是實現上文描述的 new 的第 5 步
            // 5.如果函式沒有返回物件型別`Object`(包含`Functoin`, `Array`, `Date`, `RegExg`,               //   `Error`),
            // 那麼`new`表示式中的函式呼叫會自動返回這個新的物件。
            let isObject = typeof result === 'object' && result !== null
            let isFunction = typeof result === 'function'

            if(isObject || isFunction)
                return result

            return this
        }else{
            // apply修改this指向,把兩個函式的引數合併傳給self函式,並執行self函式,返回執行結果
            return self.apply(thisArg, finalArgs)
        }
    }
    return bound
}

// Test
let obj = {name : 'yato'}

function original(a, b){
    console.log('this : ', this)
    console.log('typeof this : ', typeof this)
    this.name = b
    console.log('name: ', this.name)
    console.log('this: ', this)
    console.log([a,b])
}

let bound = original.bindFn(obj, 1)
let newBoundInvoke = new bound(2)
console.log('newBoundInvoke: ', newBoundInvoke)
// this :  bound {}
// typeof this :  object
// name:  2
// this:  bound { name: 2 }
// [ 1, 2 ]
// newBoundInvoke:  bound { name: 2 }

總結

    1. bindFunction原型鏈中Function.prototype的一個屬性,它是一個函式,修改this指向,合併引數傳遞給原函式,返回值是一個新的函式
    1. bind返回的函式可以通過new呼叫,這是提供的this引數被忽略,指向了new生成的全新物件。bind()內部模擬實現了new操作符