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的模拟实现

相关文章
相关标签/搜索