美文网首页
详解如何实现call/apply/bind

详解如何实现call/apply/bind

作者: 潼潼爱coding | 来源:发表于2019-03-20 15:25 被阅读0次

    call、apply 及 bind 函数内部实现是怎么样的?

    一、call

    • call改变了this指向
    • 函数执行
    • 支持传参

    ⚠️注意:this可以传入undefined或者null,此时this指向window,this参数可以传入基本类型,call会自动用Object转换,函数可以有返回值

    Function.prototype.call2 = function(context) {
        context = context ? Object(context) : window;
        context.fn = this;
        
        let args = [...arguments].slice(1);
        const result = context.fn(...args);
        
        // es3写法
        // var args = [];
        // for(var i = 1, len = arguments.length; i < len; i++) {
            // args 会自动调用 args.toString() 方法,因为'context.fn(' + args +')'本质上是字符串拼接,会自动调用toString()方法
        //    args.push('arguments[' + i + ']');
        // }
        // var result = eval('context.fn(' + args +')');
           
        delete context.fn;
        return result;
    }
    

    思考上面的写法会有什么问题?
    这里假设context本身没有fn属性,这样肯定不行,我们必须保证fn属性的唯一性

    • 实现方式一:es3模拟实现
      首先判断 context中是否存在属性 fn,如果存在那就随机生成一个属性fnxx,然后循环查询 context对象中是否存在属性 fnxx。如果不存在则返回最终值。
    function fnFactory (context) {
        let unique_fn = 'fn';
        while (context.hasOwnProperty) {
            unique_fn = 'fn' + Math.random();
        }
        reutrn unique_fn;
    }
    

    tips:有两种方式可以判断对象中是否存在某个属性
    1.in操作符,会检查属性名是否存在对象及其原型链中,注意数组的话是检查索引而不是具体值
    例如对于数组来说,4 in [2, 4, 6] 结果返回 false,因为 [2, 4, 6] 这个数组中包含的属性名是0,1,2 ,没有4。
    2.Object.hasOwnProperty(...)方法,只会检查属性是否存在对象中,不会向上检查其原型链。

    • 实现方式二:es6模拟实现
      利用Symbol,表示独一无二的值,不能使用 new 命令,因为这是基本类型的值,不然会报错。
    Function.prototype.call2 = function(context) {
        context = context ? Object(context) : window;
        let fn = Symbol();
        context[fn] = this;
        
        let args = [...arguments].slice(1);
        const result = context[fn](...args);
        
        delete context[fn];
        return result;
    }
    

    测试一下:

    let value = 2;
    let foo = {
        value: 1
    };
    
    function bar(name, age) {
        console.log(this);
        console.log(name, age);
    }
    bar.call2(foo, 'test', 18); // foo{value:1}   test 18
    bar.call2(null); // window  undefined undefined
    bar.call2(123, 'test', 18); // Number{123}   test 18
    

    二、apply

    apply与call的思路基本相同,区别在于传参为数组,实现如下

    Function.prototype.apply2 = function(context, arr) {
        context = context ? Object(context) : window;
        let fn = Symbol();
        context[fn] = this;
        
        let result;
        if (arr) {
            result = context[fn](...arr);
        } else {
            result = context[fn]();
        }
        
        delete context[fn];
        return result;
    }
    

    测试一下:

    let value = 2;
    let foo = {
        value: 1
    };
    
    function bar(name, age) {
        console.log(this);
        console.log(name, age);
    }
    bar.apply2(foo, ['test', '18']); // foo{value:1}   test 18
    bar.apply2(null); // window  undefined undefined
    bar.apply2(123, ['test', '18']); // Number{123}   test 18
    
    

    三、bind

    bind() 函数在 ES5 才被加入,所以并不是所有浏览器都支持,IE8及以下的版本中不被支持,如果需要兼容可以使用 Polyfill 来实现。

    bind方法与call/apply最大的区别就是bind返回一个绑定上下文的函数,而call/apply是直接执行了函数,特性如下:

    • 可以指定this
    • 返回一个绑定了this的函数
    • 可以传参
    • 柯里化
      -- 获取返回函数的参数,然后同第3点的参数合并成一个参数数组,并作为 self.apply() 的第二个参数。

    ⚠️特别:绑定函数也能使用new操作符创建对象
    这种行为就行把原函数当作构造器,提供的this被忽略,同时调用时的参数被提供给模拟函数,可以通过修改返回函数的原型来实现

    Function.prototype.bind2 = function (context) {
        // 保存this指向调用函数的对象
        const self = this;
        // 截取第一个参数后的参数
        const args = [...arguments].slice(1);
        //const args = Array.prototype.slice.call(arguments, 1); 
    
        // 返回函数
        return function () {
            const bindArgs = [...arguments];
            // const bindArgs = Array.prototype.slice.call(arguments);
            return self.apply(context, args.concat(bindArgs));
        }
    }
    

    测试一下:

    var value = 2;
    
    var foo = {
        value: 1
    };
    
    function bar(name, age) {
        return {
            value: this.value,
            name: name,
            age: age
        }
    };
    
    var bindFoo = bar.bind2(foo, "Jack");
    bindFoo(20);
    // {value: 1, name: "Jack", age: 20}
    

    bind还有一个在上文说过的特性需要实现:

    一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

    例如:

    const value = 2;
    const foo = {
        value: 1
    };
    function bar(name, age) {
        this.habit = 'shopping';
        console.log(this.value);
        console.log(name);
        console.log(age);
    };
    bar.prototype.friend = 'kevin';
    
    const bindFoo = bar.bind(foo, 'bob');
    const obj = new bindFoo(20);
    // undefined 
    // bob
    // 20
    
    obj.habit; // shopping
    obj.friend; // kevin
    

    👆上面的例子this.value 输出为 undefined,既不是全局value 也不是foo对象中的value,这说明绑定的 this 对象失效了,new 的实现中生成一个新的对象,这个时候的 this指向的是 obj。

    这一特性可以通过修改返回的函数的原型来实现

    说明:

    • 当作为构造函数时,this 指向实例,此时 this instanceof fBound 结果为 true,可以让实例获得来自绑定函数的值,即上例中实例会具有 habit 属性。
    • 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
    • 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值,即上例中 obj 可以获取到 bar 原型上的 friend。

    ⚠️ 注意:调用 bind 的不是函数,需要抛出异常。

    完整代码如下:

    Function.prototype.bind2 = function (context) {
        if (typeof this !== 'function') {
            throw new Errow('Function.prototype.bind - what is trying to be bound is not callable');
        }
        const self = this;
        const args = [...arguments].slice(1);
        
        const fBound = function () {
            const bindArgs = [...arguments];
            return self.apply(
                this instanceof fBound ? this : context,
                args.concat(bindArgs)
            );
        }
        // 直接使用ES5的 Object.create()方法生成一个新对象
        fBound.prototype = Object.create(this.prototype);
        // 或者:
        // const fNOP = function () {}; // 创建一个空对象
        // fNOP.prototype = this.prototype; // 空对象的原型指向绑定函数的原型
        // fBound.prototype = new fNOP(); // 空对象的实例赋值给 fBound.prototype
        
        return fBound;
    }
    

    本文参考链接

    相关文章

      网友评论

          本文标题:详解如何实现call/apply/bind

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