前端架构系列

bind的实现和原理

2020-06-11  本文已影响0人  羽晞yose

照旧,先分析bind的特点,再进行重现。只有清晰的了解一个方法的特点,才能完整重现并理解

bind的特点:

  1. bind方法可以绑定 this 指向
  2. bind方法会返回一个绑定了this的函数(返回一个函数,所以这是个高阶函数)
  3. bind支持柯里化(也就是调用时可以传入一部分参数,返回的函数可以继续传入其他参数)
  4. this一经绑定无法再被更改(call,apply也无效)
  5. 如果绑定的函数被new了,当前函数的this就是当前的实例
  6. 原函数的原型链上的属性,new出来的结果也可以找到

由于特点较多,所以需要一步步实现,先实现前四个特点

Function.prototype.bindModel = function (context) {
    // 保留原函数,返回的函数被调用的时候才执行
    let originFn = this;

    // 保存的传入参数(柯里化实现)
    let args = [].slice.call(arguments, 1);

    // 返回绑定了this的函数
    return function (parames) {
        originFn.apply(context, args.concat(parames));
    }
}

let obj = {
    name: '二哈',
    age: '1'
}

let obj2 = {
    name: '折耳',
    age: '2'
}

function fn1 (country, city) {
    console.log('我养了一只' + this.name + ', 它今年' + this.age + '岁了,我们一起生活在' + country + '的' + city);
}

const boundFn1 = fn1.bind(obj, '中国');
boundFn1('深圳');
boundFn1.call(obj2, '深圳'); // 我养了一只二哈, 它今年1岁了,我们一起生活在中国的深圳

const boundFn2 = fn1.bindModel(obj, '俄罗斯');
boundFn2.call('莫斯科'); // 我养了一只二哈, 它今年1岁了,我们一起生活在中国的深圳

上面代码originFn.apply(context, args.concat(parames));也可以看出,因为我们传入的context,其实一开始就被保存了,后面即使改变了返回函数的this指向,但是执行的时候apply的是context而不是this,所以再怎么call或apply也无法改变this指向
当然这个特点其实严谨的说也是不对的,其实返回函数的this指向是被更改了,只是它运行的时候会重新apply(context),所以更改无效。
因此正确的说法应该是:虽然返回函数的this被更改了,但是执行的时候this依然会重新指向回最初绑定的对象上


实现第5个特点,如果返回的函数被new,那么this将不再指向传入的context,而是指向该实例。
因为我们返回的函数现在是个匿名函数,那么去new这个匿名函数将无法判断,所以需要将其变成具名函数,再使用instanceof来判断this的指向

Function.prototype.bindModel = function (context) {
    // 保留原函数,返回的函数被调用的时候才执行,如果这里不保存,调用的时候this会变成window
    let originFn = this;

    // 去掉第一个参数,这样arguments则剩下应该被保存的参数(柯里化实现)
    let bindArgs = [].slice.call(arguments, 1);

    function fBound (parames) {
        // this instanceof fBound 如果是new出来的,那么this将会是fBound,否则将会是window
        // 参考上一节new的实现,在Animal函数里打印this,打印出来是Animal {}
        originFn.apply(this instanceof fBound ? this : context, bindArgs.concat(parames));
    }

    return fBound
}

const boundFn1 = fn1.bind(obj, '中国');
boundFn1('深圳');
let instance = new boundFn1('深圳');

const boundFn2 = fn1.bindModel(obj, '俄罗斯');
boundFn2('莫斯科');
let instance2 = new boundFn2('莫斯科'); // 如果是之前的实现,这里依然会指向obj,所以this.name和this。age还是能正常取到

最后实现第6个特点,原函数原型链上的属性会被继承,因此将fbound.prototype = this.prototype即可。当然这么写会导致原型链共享,所以还需要使用一个中间件来实现继承

Function.prototype.bindModel = function (context) {
    // 保留原函数,返回的函数被调用的时候才执行,如果这里不保存,调用的时候this会变成window
    let originFn = this;

    // 去掉第一个参数,这样arguments则剩下应该被保存的参数(柯里化实现)
    let bindArgs = [].slice.call(arguments, 1);

    // Object.create()原理,创建一个空白函数来做中间件
    function Fn() {};

    function fBound (parames) {
        // 改变this指向传入的上下文环境
        // 或者直接使用arguments,这里的arguments已经是返回函数的参数了
        // this instanceof fBound 如果是new出来的,那么this将会是fBound,否则将会是window
        // 参考上一节new的实现,在Animal函数里打印this,打印出来是Animal {}
        originFn.apply(this instanceof fBound ? this : context, bindArgs.concat(parames));
    }

    Fn.prototype = this.prototype;
    // 通过原型链找到原函数中的属性
    fBound.prototype = new Fn();

    // 返回一个函数,第三步
    return fBound
}

let obj = {
    name: '二哈',
    age: '1'
}

let obj2 = {
    name: '二哈哈',
    age: '2'
}

function fn1 (country, city) {
    console.log('我养了一只' + this.name + ', 它今年' + this.age + '岁了,我们一起生活在' + country + '的' + city);
}

fn1.prototype = {
     name: '折耳',
     age: '0.5'
}

const boundFn1 = fn1.bind(obj, '中国');
boundFn1('深圳');
let instance = new boundFn1('深圳');


const boundFn2 = fn1.bindModel(obj, '俄罗斯');
boundFn2( '莫斯科');
// 会通过原型链去寻找(fn1.prototype),所以name和age不再是undefined
let instance2 = new boundFn2('莫斯科'); // 我养了一只折耳, 它今年0.5岁了,我们一起生活在俄罗斯的莫斯科

更加详细的文章:js 手动实现bind方法,超详细思路分析!
这篇文章分得比我细很多,但我不是一个喜欢啰里啰嗦的人,点到即可,看不懂我的可以看看这位作者的

上一篇下一篇

猜你喜欢

热点阅读