可否模擬實現JS的bind方法

用過React的同窗都知道,常常會使用bind來綁定thisjavascript

import React, { Component } from 'react';
class TodoItem extends Component{
    constructor(props){
        super(props);
        this.handleClick = this.handleClick.bind(this);
    }
    handleClick(){
        console.log('handleClick');
    }
    render(){
        return  (
            <div onClick={this.handleClick}>點擊</div>
        );
    };
}
export default TodoItem;

那麼面試官可能會問是否想過bind到底作了什麼,怎麼模擬實現呢。html

附上以前寫文章寫過的一段話:已經有不少模擬實現bind的文章,爲何本身還要寫一遍呢。學習就比如是座大山,人們沿着不一樣的路爬山,分享着本身看到的風景。你不必定能看到別人看到的風景,體會到別人的心情。只有本身去爬山,才能看到不同的風景,體會才更加深入。html5

先看一下bind是什麼。從上面的React代碼中,能夠看出bind執行後是函數,而且每一個函數均可以執行調用它。 眼見爲實,耳聽爲虛。讀者能夠在控制檯一步步點開例子1中的obj:java

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

 

`Function.prototype.bind`

 

所以能夠得出結論1:

一、bindFunctoin原型鏈中Function.prototype的一個屬性,每一個函數均可以調用它。
二、bind自己是一個函數名爲bind的函數,返回值也是函數,函數名是bound。(打出來就是bound加上一個空格)。 知道了bind是函數,就能夠傳參,並且返回值'bound '也是函數,也能夠傳參,就很容易寫出例子2
後文統一 bound指原函數originalbind以後返回的函數,便於說明。react

var obj = {
    name: '軒轅Rowboat',
};
function original(a, b){
    console.log(this.name);
    console.log([a, b]);
    return false;
}
var bound = original.bind(obj, 1);
var boundResult = bound(2); // '軒轅Rowboat', [1, 2]
console.log(boundResult); // 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

由此能夠得出結論2:

一、調用bind的函數中的this指向bind()函數的第一個參數。git

二、傳給bind()的其餘參數接收處理了,bind()以後返回的函數的參數也接收處理了,也就是說合並處理了。es6

三、而且bind()後的namebound + 空格 + 調用bind的函數名。若是是匿名函數則是bound + 空格github

四、bind後的返回值函數,執行後返回值是原函數(original)的返回值。面試

五、bind函數形參(即函數的length)是1bind後返回的bound函數形參不定,根據綁定的函數原函數(original)形參個數肯定。數組

根據結論2:咱們就能夠簡單模擬實現一個簡版bindFn

// 初版 修改this指向,合併參數
Function.prototype.bindFn = function bind(thisArg){
    if(typeof this !== 'function'){
        throw new TypeError(this + 'must be a function');
    }
    // 存儲函數自己
    var self = this;
    // 去除thisArg的其餘參數 轉成數組
    var args = [].slice.call(arguments, 1);
    var bound = function(){
        // bind返回的函數 的參數轉成數組
        var boundArgs = [].slice.call(arguments);
        // apply修改this指向,把兩個函數的參數合併傳給self函數,並執行self函數,返回執行結果
        return self.apply(thisArg, args.concat(boundArgs));
    }
    return bound;
}
// 測試
var obj = {
    name: '軒轅Rowboat',
};
function original(a, b){
    console.log(this.name);
    console.log([a, b]);
}
var bound = original.bindFn(obj, 1);
bound(2); // '軒轅Rowboat', [1, 2]

若是面試官看到你答到這裏,估計對你的印象60、70分應該是會有的。 但咱們知道函數是能夠用new來實例化的。那麼bind()返回值函數會是什麼表現呢。
接下來看例子3

var obj = {
    name: '軒轅Rowboat',
};
function original(a, b){
    console.log('this', this); // original {}
    console.log('typeof this', typeof this); // object
    this.name = b;
    console.log('name', this.name); // 2
    console.log('this', this);  // original {name: 2}
    console.log([a, b]); // 1, 2
}
var bound = original.bind(obj, 1);
var newBoundResult = new bound(2);
console.log(newBoundResult, 'newBoundResult'); // original {name: 2}

例子3種能夠看出this指向了new bound()生成的新對象。

能夠分析得出結論3:

一、bind原先指向obj的失效了,其餘參數有效。

二、new bound的返回值是以original原函數構造器生成的新對象。original原函數的this指向的就是這個新對象。 另外前不久寫過一篇文章:面試官問:可否模擬實現JS的new操做符。簡單摘要:new作了什麼:

1.建立了一個全新的對象。
2.這個對象會被執行[[Prototype]](也就是__proto__)連接。
3.生成的新對象會綁定到函數調用的this。
4.經過new建立的每一個對象將最終被[[Prototype]]連接到這個函數的prototype對象上。
5.若是函數沒有返回對象類型Object(包含FunctoinArrayDateRegExgError),那麼new表達式中的函數調用會自動返回這個新的對象。

因此至關於new調用時,bind的返回值函數bound內部要模擬實現new實現的操做。 話很少說,直接上代碼。

// 第三版 實現new調用
Function.prototype.bindFn = function bind(thisArg){
    if(typeof this !== 'function'){
        throw new TypeError(this + ' must be a function');
    }
    // 存儲調用bind的函數自己
    var self = this;
    // 去除thisArg的其餘參數 轉成數組
    var args = [].slice.call(arguments, 1);
    var bound = function(){
        // bind返回的函數 的參數轉成數組
        var boundArgs = [].slice.call(arguments);
        var 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){
                // ES5 提供的方案 Object.create()
                // bound.prototype = Object.create(self.prototype);
                // 但 既然是模擬ES5的bind,那瀏覽器也基本沒有實現Object.create()
                // 因此採用 MDN ployfill方案 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create
                function Empty(){}
                Empty.prototype = self.prototype;
                bound.prototype = new Empty();
            }
            // 這裏是實現上文描述的 new 的第 3 步
            // 3.生成的新對象會綁定到函數調用的`this`。
            var result = self.apply(this, finalArgs);
            // 這裏是實現上文描述的 new 的第 5 步
            // 5.若是函數沒有返回對象類型`Object`(包含`Functoin`, `Array`, `Date`, `RegExg`, `Error`),
            // 那麼`new`表達式中的函數調用會自動返回這個新的對象。
            var isObject = typeof result === 'object' && result !== null;
            var isFunction = typeof result === 'function';
            if(isObject || isFunction){
                return result;
            }
            return this;
        }
        else{
            // apply修改this指向,把兩個函數的參數合併傳給self函數,並執行self函數,返回執行結果
            return self.apply(thisArg, finalArgs);
        }
    };
    return bound;
}

面試官看到這樣的實現代碼,基本就是滿分了,內心獨白:這小夥子/小姑娘不錯啊。不過可能還會問this instanceof bound不許確問題。 上文註釋中提到this instanceof bound也不是很準確,ES6 new.target很好的解決這一問題,咱們舉個例子4:

instanceof不許確,ES6 new.target很好的解決這一問題

function Student(name){
    if(this instanceof Student){
        this.name = name;
        console.log('name', name);
    }
    else{
        throw new Error('必須經過new關鍵字來調用Student。');
    }
}
var student = new Student('軒轅');
var notAStudent = Student.call(student, 'Rowboat'); // 不拋出錯誤,且執行了。
console.log(student, 'student', notAStudent, 'notAStudent');

function Student2(name){
    if(typeof new.target !== 'undefined'){
        this.name = name;
        console.log('name', name);
    }
    else{
        throw new Error('必須經過new關鍵字來調用Student2。');
    }
}
var student2 = new Student2('軒轅');
var notAStudent2 = Student2.call(student2, 'Rowboat');
console.log(student2, 'student2', notAStudent2, 'notAStudent2'); // 拋出錯誤

細心的同窗可能會發現了這版本的代碼沒有實現bind後的bound函數的nameMDN Function.namelengthMDN Function.length。面試官可能也發現了這一點繼續追問,如何實現,或者問是否看過es5-shim的源碼實現L201-L335。若是不限ES版本。其實能夠用ES5Object.defineProperties來實現。

Object.defineProperties(bound, {
    'length': {
        value: self.length,
    },
    'name': {
        value: 'bound ' + self.name,
    }
});

es5-shim的源碼實現bind

直接附上源碼(有刪減註釋和部分修改等)

var $Array = Array;
var ArrayPrototype = $Array.prototype;
var $Object = Object;
var array_push = ArrayPrototype.push;
var array_slice = ArrayPrototype.slice;
var array_join = ArrayPrototype.join;
var array_concat = ArrayPrototype.concat;
var $Function = Function;
var FunctionPrototype = $Function.prototype;
var apply = FunctionPrototype.apply;
var max = Math.max;
// 簡版 源碼更復雜些。
var isCallable = function isCallable(value){
    if(typeof value !== 'function'){
        return false;
    }
    return true;
};
var Empty = function Empty() {};
// 源碼是 defineProperties
// 源碼是bind筆者改爲bindFn便於測試
FunctionPrototype.bindFn = function bind(that) {
    var target = this;
    if (!isCallable(target)) {
        throw new TypeError('Function.prototype.bind called on incompatible ' + target);
    }
    var args = array_slice.call(arguments, 1);
    var bound;
    var binder = function () {
        if (this instanceof bound) {
            var result = apply.call(
                target,
                this,
                array_concat.call(args, array_slice.call(arguments))
            );
            if ($Object(result) === result) {
                return result;
            }
            return this;
        } else {
            return apply.call(
                target,
                that,
                array_concat.call(args, array_slice.call(arguments))
            );
        }
    };
    var boundLength = max(0, target.length - args.length);
    var boundArgs = [];
    for (var i = 0; i < boundLength; i++) {
        array_push.call(boundArgs, '$' + i);
    }
    // 這裏是Function構造方式生成形參length $1, $2, $3...
    bound = $Function('binder', 'return function (' + array_join.call(boundArgs, ',') + '){ return binder.apply(this, arguments); }')(binder);

    if (target.prototype) {
        Empty.prototype = target.prototype;
        bound.prototype = new Empty();
        Empty.prototype = null;
    }
    return bound;
};

你說出es5-shim源碼bind實現,感慨這代碼真是高效、嚴謹。面試官內心獨白多是:你就是我要找的人,薪酬福利你能夠和HR去談下。

最後總結一下

一、bindFunction原型鏈中的Function.prototype的一個屬性,它是一個函數,修改this指向,合併參數傳遞給原函數,返回值是一個新的函數。
二、bind返回的函數能夠經過new調用,這時提供的this的參數被忽略,指向了new生成的全新對象。內部模擬實現了new操做符。
三、es5-shim源碼模擬實現bind時用Function實現了length
事實上,平時其實不多須要使用本身實現的投入到生成環境中。但面試官經過這個面試題能考察不少知識。好比this指向,原型鏈,閉包,函數等知識,能夠擴展不少。
讀者發現有不妥或可改善之處,歡迎指出。另外以爲寫得不錯,能夠點個贊,也是對筆者的一種支持。

文章中的例子和測試代碼放在githubbind模擬實現 githubbind模擬實現 預覽地址F12看控制檯輸出,結合source面板查看效果更佳。

// 最終版 刪除註釋 詳細註釋版請看上文
Function.prototype.bind = Function.prototype.bind || function bind(thisArg){
    if(typeof this !== 'function'){
        throw new TypeError(this + ' must be a function');
    }
    var self = this;
    var args = [].slice.call(arguments, 1);
    var bound = function(){
        var boundArgs = [].slice.call(arguments);
        var finalArgs = args.concat(boundArgs);
        if(this instanceof bound){
            if(self.prototype){
                function Empty(){}
                Empty.prototype = self.prototype;
                bound.prototype = new Empty();
            }
            var result = self.apply(this, finalArgs);
            var isObject = typeof result === 'object' && result !== null;
            var isFunction = typeof result === 'function';
            if(isObject || isFunction){
                return result;
            }
            return this;
        }
        else{
            return self.apply(thisArg, finalArgs);
        }
    };
    return bound;
}
相關文章
相關標籤/搜索