模拟实现 bind

作者: 请叫我大苏 | 来源:发表于2019-10-24 22:08 被阅读0次

    title: 模拟实现 bind
    date: 2019/10/24 22:30:25
    categories:

    • 面试题
    • 前端

    模拟实现 bind

    本文参考:深度解析bind原理、使用场景及模拟实现

    基础

    老样子,得先知道 bind 的用途、用法,才能来考虑如何去模拟实现它。

    bind 的用途跟 call 和 apply 可以说是基本一样的,都是用来修改函数内部的上下文 this 的指向,但有一个很大的区别,call 和 apply 在修改了函数内部 this 指向的同时,还会触发函数的调用执行。

    而对于 bind 来说,它只修改了函数内部的 this,并不会触发函数的调用执行,既然不触发函数执行,又不能影响原函数的使用,那也就只能返回一个修改了 this 的新函数了。

    function a() {
        console.log(this);
    }
    
    var b = a.bind({a: 2});  // 只是返回了新函数
    b();  // 输出: {a: 2}, 调用新函数会去触发原函数的执行,执行的时候,this 修改成绑定时传入的对象
    
    a();  // 输出 window, bind 不影响原函数
    a.call({a:1});  // 输出 {a: 1},改变 this 的同时也调用执行了函数
    

    可以发现,通过 bind 返回的新函数 b,当它执行的时候,逻辑跟原函数 a 是一样的,也就是会去触发 a 函数的执行,但内部 this 值却已经发生了改变。而且,之后对原函数 a 的操作仍旧保持原先行为,也就是不会对原函数 a 造成副作用影响。

    还有一些点需要注意下的是,原函数 a 可以是普通函数、对象的方法、箭头函数、经过 bind 后新生成的函数等等。只要是函数,那它就可以调用 bind 方法。

    但是,对于不同类型函数,bind 并不是都可以修改函数内部 this 值的:

    // 比如说箭头函数
    var a = () => {console.log(this)}
    
    var b = a.bind({a: 1});
    b();  // 输出: window,   因为箭头函数的 this 本质上是一个在作用域链寻值的变量
    

    另外,还有一点:因为 bind 执行后是返回一个新的普通函数,既然是普通函数,也就可以当做构造函数和 new 使用。当它作为构造函数使用时,构造的过程跟直接对原函数结合 new 使用的过程没有什么大区别:

    function a() {
        this.a = 1;
    }
    a.prototype.b = 2;
    var b = a.bind({a: 2});
    
    var c = new b(); // {a: 1}
    var d = new a(); // {a: 1}
    c.b; // 2
    d.b; // 2
    

    上面代码中,经过 bind 之后的新函数 b,当作为构造函数使用时,构造出的新对象,新对象的原型继承等都跟原函数 a 作为构造函数时是一致的。

    以上,就是 bind 的基本用法和概念,MDN 上有句解释蛮通俗易懂的:

    bind 就是返回一个原函数的拷贝,并拥有指定的 this 值和初始参数

    Function.prototype.bind()

    所以,bind 的应用场景:可以用来设定初始参数;可以用来绑定 this,在一些异步回调的场景中等等;

    模拟实现

    接下去讲讲模拟实现:

    bind 接收不定长的参数列表,第一个参数跟 call 和 apply 的第一个参数一样,都是用来指定 this 的指向,第二个参数开始的剩余参数,会依次传给原函数的参数,作为初始参数,并返回一个新函数;

    新函数调用的时候,参数列表还会继续传递给原函数,同时触发原函数的执行,执行过程中,函数内的 this 以 bind 时为主,如果能够生效的话。

    那么,模拟实现 bind,我们主要就要关注这几点:

    • 如何修改函数的 this 指向(可直接用 call/apply,或者模拟实现 call/apply 时用到的挂载到对象上的方式)
    • 如何区分返回的新函数是否被用作构造函数使用(ES6 中的 new.target 即可,或者对 this 进行原型检测)
    • 如何实现构造出的新对象保持原函数构造对象时的原型继承(拷贝原函数的 prototype 到返回的新函数上)
    • 对参数的处理工作

    主要的工作清楚了,各个工作的模拟实现方案也有了,那么就看看代码:

    Function.prototype.bind2 = function(thisArg, ...args) {
        // 1. 对 thisArg 参数的特殊处理,因为下面不用 call 来实现 this 的修改,那么就需要模拟实现 call,具体可看之前模拟实现 call 的文章
        let context = thisArg != null ? Object(thisArg) : window; 
        let fnSymbol = Symbol();  // 避免属性冲突或被外部修改
        
        // 2. 保存当前函数,并声明返回的新函数,新函数内部会根据是否作为构造函数使用的场景来调用原函数
        let self = this;
        let newFn = function(...newArgs) {
            let curContext;
            if (!new.target) {
                curContext = context;
            } else {
                curContext = this;
            }
            curContext[fnSymbol] = self;  
            let result = curContext[fnSymbol](...[...args, ...newArgs]);
            delete curContext[fnSymbol];
            return result;
        };
        
        // 3. 拷贝原函数的 prototype,用于实现实例对象的原型继承,多创建一层是可以避免外部直接对新函数 newFn.prototype 的修改影响到原函数
        if (this.prototype) {
            newFn.prototype = Object.create(this.prototype);
        }
        return newFn;
    }
    

    注意:我这里的模拟实现,借助了 ES6 里的扩展运算符 ... 和 Symbol 类型数据和 new.target,以及 ES5 中的 Object.create,那么自然就不能兼容一些老版本浏览器。

    解决方案有两种,参考其他文章给出的模拟实现,把上面用到的那几种新特性都用最基本的 ES3 的特性实现,比如 Object.create 就老老实实手动去对 prototype 赋值,扩展运算符就用 arguments 和 Array.prototype.slice 来处理,Symbol 这个就用 call 或 apply 来实现 this 的修改即可,函数是否作为构造函数和 new 使用,在 newFn 内部通过对 this 的判定即可,这样就可以替换掉上面用到的那些新特性。

    再或者,把上面代码借助 babel 这种工具,进行转换处理一下。

    思考

    上面的模拟是否有问题?能否100%模拟?

    很难 100% 模拟,我们顶多只能挑一些重要的功能来模拟实现,上面的模拟实现当然也有很多问题,用到 ES6 新特性这点先不讲。其他的问题,比如:

    • bind 返回的函数,name 属性,length 属性都不符合规范了
    • 无法处理箭头函数 bind 返回的新函数和 new 使用需要抛异常的场景
    • 未发现的坑

    这些也都是可以解决的,但处理起来就麻烦一些,可以参考文末的文章。反正,大概清楚 bind 的工作职责,能把主要的工作模拟实现出来,也就差不多了。不过追求 100% 也是好事,望你加油!

    推荐阅读

    相关文章

      网友评论

        本文标题:模拟实现 bind

        本文链接:https://www.haomeiwen.com/subject/huhjvctx.html