js 手動實現bind方法,超詳細思路分析!

壹 ❀ 引

js 實現call和apply方法 一文中,咱們詳細分析並模擬實現了call/apply方法,因爲篇幅問題,關於bind方法實現只能另起一篇。javascript

在模擬bind以前,咱們先了解bind的概念,這裏引入MDN解釋:html

bind() 方法建立一個新的函數,在 bind() 被調用時,這個新函數的 this 被指定爲 bind() 的第一個參數,而其他參數將做爲新函數的參數,供調用時使用。java

說的通俗一點,bindapply/call同樣都能改變函數this指向,但bind並不會當即執行函數,而是返回一個綁定了this的新函數,你須要再次調用此函數才能達到最終執行。git

咱們來看一個簡單的例子:github

var obj = {
    z: 1
};
var obj1 = {
    z: 2
};

function fn(x, y) {
    console.log(x + y + this.z);
};
// call與apply
fn.call(obj, 2, 3); //6
fn.apply(obj, [2, 3]); //6

var bound = fn.bind(obj, 2);
bound(3); //6
//嘗試修改bind返回函數的this
bound.call(obj1, 3); //6

能夠到bind並非當即執行,而是返回一個新函數,且新函數的this沒法再次被修改,咱們總結bind的特色:閉包

  • 能夠修改函數this指向。
  • bind返回一個綁定了this的新函數`boundFcuntion,例子中咱們用bound表示。
  • 支持函數柯里化,咱們在返回bound函數時已傳遞了部分參數2,在調用時bound補全了剩餘參數。
  • boundFunction的this沒法再被修改,使用call、apply也不行。

考慮到有的同窗對於柯里化的陌生,所謂函數柯里化其實就是在函數調用時只傳遞一部分參數進行調用,函數會返回一個新函數去處理剩下的參數,一個經典簡單的例子:app

//函數柯里化
function fn(x, y) {
    return function (y) {
        console.log(x + y);
    };
};
var fn_ = fn(1);
fn_(1); //2

fn(1)(1) //2

不難發現函數柯里化使用了閉包,在執行內層函數時,它使用了外層函數的局部形參x,從而構成了閉包,扯遠了點。函數

咱們來嘗試實現bind方法,先從簡單的改變this和返回函數開始。this

貳 ❀ 實現bind

以前已經有了模擬call/apply的經驗,這裏直接給出版本一:prototype

Function.prototype.bind_ = function (obj) {
    var fn = this;
    return function () {
        fn.apply(obj);
    };
};

var obj = {
    z: 1
};

function fn() {
    console.log(this.z);
};

var bound = fn.bind_(obj);
bound(); //1

惟一須要留意的就是var fn = this這一行,若是不提早保存,在執行bound時內部this會指向window。

版本一以知足了this修改與函數返回,立刻有同窗就想到了,版本一不支持函數傳參,那麼咱們進行簡單修改讓其支持傳參:

Function.prototype.bind_ = function (obj) {
    //第0位是this,因此得從第一位開始裁剪
    var args = Array.prototype.slice.call(arguments, 1);
    var fn = this;
    return function () {
        fn.apply(obj, args);
    };
};

完美了嗎?並不完美,別忘了咱們前面說bind支持函數柯里化,在調用bind時能夠先傳遞部分參數,在調用返回的bound時能夠補全剩餘參數,因此還得進一步處理,來看看bind_第二版:

Function.prototype.bind_ = function (obj) {
    //第0位是this,因此得從第一位開始裁剪
    var args = Array.prototype.slice.call(arguments, 1);
    var fn = this;
    return function () {
        //二次調用咱們也抓取arguments對象
        var params = Array.prototype.slice.call(arguments);
        //注意concat的順序
        fn.apply(obj, args.concat(params));
    };
};

var obj = {
    z: 1
};

function fn(x, y) {
    console.log(x + y + this.z);
};

var bound = fn.bind_(obj, 1);
bound(2); //4

看,改變this,返回函數,函數柯里化均已實現。這段代碼須要注意的是args.concat(params)的順序,args在前,由於只有這樣才能讓先傳遞的參數和fn的形參按順序對應。

至少走到這一步都挺順序,須要注意的是,bind方法還有一個少見的特性,這裏引用MDN的描述

綁定函數也可使用 new 運算符構造,它會表現爲目標函數已經被構建完畢了似的。提供的 this 值會被忽略,但前置參數仍會提供給模擬函數。

說通俗點,經過bind返回的boundFunction函數也能經過new運算符構造,只是在構造過程當中,boundFunction已經肯定的this會被忽略,且返回的實例仍是會繼承構造函數的構造器屬性與原型屬性,而且能正常接收參數。

有點繞口,咱們來看個簡單的例子:

var z = 0;
var obj = {
    z: 1
};

function fn(x, y) {
    this.name = '聽風是風';
    console.log(this.z);
    console.log(x);
    console.log(y);
};
fn.prototype.age = 26;

var bound = fn.bind(obj, 2);
var person = new bound(3);//undefined 2 3

console.log(person.name);//聽風是風
console.log(person.age);//26

在此例子中,咱們先是將函數fnthis指向了對象obj,從而獲得了bound函數。緊接着使用new操做符構造了bound函數,獲得了實例person。不難發現,除了先前綁定好的this丟失了(後面會解釋緣由),構造器屬性this.name,以及原型屬性fn.prototype.age都有順利繼承,除此以外,兩個形參也成功傳遞進了函數。

難點來了,至少在ES6以前,JavaScript並無class類的概念,所謂構造函數其實只是對於類的模擬;而這就形成了一個問題,全部的構造函數除了可使用new構造調用之外,它還能被普通調用,好比上面例子中的bound咱們也能夠普通調用:

bound(3); //1 2 3

有同窗在這可能就有疑惑,bound()等同於window.bound(),此時this不是應該指向window從而輸出0嗎?咱們在前面說bind屬於硬綁定,一次綁定終生受益,上面的調用本質上等同於:

window.fn.bind(obj, 2);

函數fn存在this默認綁定window與顯示綁定bind,而顯示綁定優先級高於默認綁定,因此this仍是指向obj

當構造函數被new構造調用時,本質上構造函數中會建立一個實例對象,函數內部的this指向此實例,當執行到console.log(this.z)這一行時,this上並未被賦予屬性z,因此輸出undefined,這也解釋了爲何bound函數被new構造時會丟失本來綁定的this。

是否是以爲ES5構造函數特別混亂,不一樣調用方式函數內部this指向還不一樣,也正因如此在ES6中隆重推出了class類,凡是經過class建立的類均只能使用new調用,普通調用一概報錯處理:

class Fn {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    };
    sayName() {
        console.log(this.name);
    };
};
//只能new構造調用
const person = new Fn('聽風是風', 26);
person.sayName(); //聽風是風
const person1 = Fn(); //Class constructor Fn cannot be invoked without 'new'

扯遠了,讓咱們回到上面的例子,說了這麼多無非是爲了強調一點,咱們在模擬bind方法時,返回的bound函數在調用時得考慮new調用與普通調用,畢竟二者this指向不一樣。

再說直白一點,若是是new調用,bound函數中的this指向實例自身,而若是是普通調用this指向obj,怎麼區分呢?

不難,咱們知道(強行讓大家知道)構造函數實例的constructor屬性永遠指向構造函數自己,好比:

function Fn(){};
var o = new Fn();
console.log(o.constructor === Fn);//true

而構造函數在運行時,函數內部this指向實例,因此this的constructor也指向構造函數:

function Fn() {
    console.log(this.constructor === Fn); //true
};
var o = new Fn();
console.log(o.constructor === Fn); //true

因此我就用constructor屬性來判斷當前bound方法調用方式,畢竟只要是new調用,this.this.constructor === Fn必定爲true。

讓咱們簡單改寫bind_方法,爲bound方法新增this判斷以及原型繼承:

Function.prototype.bind_ = function (obj) {
    var args = Array.prototype.slice.call(arguments, 1);
    var fn = this;
    var bound = function () {
        var params = Array.prototype.slice.call(arguments);
        //經過constructor判斷調用方式,爲true this指向實例,不然爲obj
        fn.apply(this.constructor === fn ? this : obj, args.concat(params));
    };
    //原型鏈繼承
    bound.prototype = fn.prototype;
    return bound;
};

有同窗就問了,難道不該該是this.constructor===bound嗎?並非,雖然new的是bound方法,本質上執行的仍是fn,畢竟bound自身並無構造器屬性,這點關係仍是須要理清。

其次還有個缺陷。雖然構造函數產生的實例都是獨立的存在,實例繼承而來的構造器屬性隨便你怎麼修改都不會影響構造函數自己:

function Fn() {
    this.name = '聽風是風';
    this.sayAge = function () {
        console.log(this.age);
    };
};
Fn.prototype.age = 26;

var o = new Fn();
o.sayAge(); //26
//咱們改變實例繼承的構造器屬性,並不會影響構造函數自己
o.name = 'echo';
var o1 = new Fn();
console.log(o1.name) //聽風是風

可是若是咱們直接修改實例原型,這就會對構造函數Fn產生影響,來看個例子:

function Fn() {
    this.name = '聽風是風';
    this.sayAge = function () {
        console.log(this.age);
    };
};
Fn.prototype.age = 26;

var o = new Fn();
o.sayAge(); //26
//修改實例的原型屬性,這會影響構造函數自己
o.__proto__.age = 18;
var o1 = new Fn();
console.log(o1.age) //18

不難理解,構造器屬性(this.name,this.sayAge)在建立實例時,咱們能夠抽象的理解成實例深拷貝了一份,這是屬於實例自身的屬性,後面再改都與構造函數不相關。而實例要用prototype屬性時都是順着原型鏈往上找,構造函數有便借給實例用了,一共就這一份,誰要是改了那就都得變。

咱們能夠輸出實例o,觀察它的屬性,能夠看到age屬性確實是綁原型__proto__上(注意,prototype是函數特有,普通對象只有__proto__,都是原型,本質相同)。

怎麼作才保險呢,這裏就能夠藉助一個空白函數做爲中介,直接看個例子:

function Fn() {
    this.name = '聽風是風';
    this.sayAge = function () {
        console.log(this.age);
    };
};
Fn.prototype.age = 26;
// 建立一個空白函數Fn1,單純的拷貝Fn的prototype
var Fn1 = function () {};
Fn1.prototype = Fn.prototype;
// 這裏的Fn2對應咱們的bound方法,將其原型指向Fn1建立的實例
var Fn2 = function () {};
Fn2.prototype = new Fn1();
var o = new Fn2();
console.log(o.age); //26
//嘗試修改
o.__proto__.age = 18;
var o1 = new Fn();
console.log(o1.age);//26

說到底,咱們就是借用空白函數,讓Fn2的實例多了一層__proto__,達到修改原型不會影響Fn原型的目的,固然你若是經過沒法直接經過__proto__.__proto__仍是同樣能修改,差很少就是這個意思:

o.__proto__.__proto__.age = 18;
var o1 = new Fn();
console.log(o1.age);//18

因此綜上,咱們再次修改bind_方法,拿出第四版:

Function.prototype.bind_ = function (obj) {
    var args = Array.prototype.slice.call(arguments, 1);
    var fn = this;
    //建立中介函數
    var fn_ = function () {};
    var bound = function () {
        var params = Array.prototype.slice.call(arguments);
        //經過constructor判斷調用方式,爲true this指向實例,不然爲obj
        fn.apply(this.constructor === fn ? this : obj, args.concat(params));
        console.log(this);
    };
    fn_.prototype = fn.prototype;
    bound.prototype = new fn_();
    return bound;
};

最後,bind方法若是被非函數調用時會拋出錯誤,因此咱們要在第一次執行bind_時作一次調用判斷,加個條件判斷,咱們來一個完整的最終版:

Function.prototype.bind_ = function (obj) {
    if (typeof this !== "function") {
        throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    };
    var args = Array.prototype.slice.call(arguments, 1);
    var fn = this;
    //建立中介函數
    var fn_ = function () {};
    var bound = function () {
        var params = Array.prototype.slice.call(arguments);
        //經過constructor判斷調用方式,爲true this指向實例,不然爲obj
        fn.apply(this.constructor === fn ? this : obj, args.concat(params));
        console.log(this);
    };
    fn_.prototype = fn.prototype;
    bound.prototype = new fn_();
    return bound;
};

var z = 0;
var obj = {
    z: 1
};

function fn(x, y) {
    this.name = '聽風是風';
    console.log(this.z);
    console.log(x);
    console.log(y);
};
fn.prototype.age = 26;

var bound = fn.bind_(obj, 2);
var person = new bound(3); //undefined 2 3

console.log(person.name); //聽風是風
console.log(person.age); //26
person.__proto__.age = 18;
var person = new fn();
console.log(person.age); //26

看着有些長,不過咱們順着思路一步步走過來其實不難理解。

好啦,關於bind方法的模擬實現就說到這裏了,萬萬沒想到這篇實現竟然用了我五個小時時間...

另外,若是你們對於new一個構造函數發生了什麼存在疑惑,能夠閱讀博主這篇文章:

new一個對象的過程,實現一個簡單的new方法

若對於上文中修改實例原型會影響原構造函數存在疑慮,能夠閱讀博主這篇文章:

精讀JavaScript模式(八),JS類式繼承

那麼就寫到這裏了,巨累,準備睡覺。

叄 ❀ 參考

深度解析bind原理、使用場景及模擬實現

MDN Function.prototype.bind()

最詳盡的 JS 原型與原型鏈終極詳解,沒有「多是」。(一)

JavaScript深刻之bind的模擬實現

相關文章
相關標籤/搜索