美文网首页
JavaScript基础专题之手动实现call、apply、bi

JavaScript基础专题之手动实现call、apply、bi

作者: Chris__Liu | 来源:发表于2019-07-06 18:31 被阅读0次

    实现自己的call

    MDN 定义:

    call() 提供新的 this 值给当前调用的函数/方法。你可以使用 call 来实现继承:写一个方法,然后让另外一个新的对象来继承它(而不是在新对象中再写一次这个方法)。

    简答的概括就是:

    call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。

    举个例子:

    var foo = {
        value: 1
    };
    
    function bar() {
        console.log(this.value);
    }
    
    bar.call(foo); // 1
    

    简单的解析一下call都做了什么:

    第一步:call 改变了 this 的指向,指向到 foo

    第二步:bar 函数执行

    函数通过 call 调用后,结构就如下面代码:

    var foo = {
        value: 1,
        bar: function() {
            console.log(this.value)
        }
    };
    
    foo.bar(); // 1
    

    这样this 就指向了 foo,但是我们给foo添加了一个属性,这并不可取。所以我们还要执行一步删除的动作。

    所以我们模拟的步骤可以分为:

    第一步:将函数设为传入对象的属性

    第二步:执行该函数

    第三部:删除该函数

    以上个例子为例,就是:

    // 第一步
    foo.fn = bar
    // 第二步
    foo.fn()
    // 第三步
    delete foo.fn
    

    注意:fn 是对象的临时属性,因为执行过后要删除滴。

    根据这个思路,我们可以尝试着去写一个call

    Function.prototype._call = function(context) {
        // 首先要获取调用call的函数,用this可以获取
        context.fn = this;
        context.fn();
        delete context.fn;
    }
    
    // 测试一下
    var foo = {
        value: 1
    };
    
    function bar() {
        console.log(this.value);
    }
    
    bar._call(foo); // 1
    

    OK,我们可以在控制台看到结果了,和预想的一样。

    这样只是将第一个参数作为上下文进行执行,但是并没用传入参数,下面我们尝试传入参数执行。

    举个例子:

    var foo = {
        value: 1
    };
    
    function bar(name, age) {
        console.log(name)
        console.log(age)
        console.log(this.value);
    }
    
    bar.call(foo, 'chris', 10);
    // chris
    // 10
    // 1
    

    我们会发现参数并不固定,所以要在 Arguments 对象的第二个参数截取,传入到数组中。

    比如这样:

    // 以上个例子为例,此时的arguments为:
    // arguments = {
    //      0: foo,
    //      1: 'kevin',
    //      2: 18,
    //      length: 3
    // }
    // 因为arguments是类数组对象,所以可以用for循环
    var args = [];
    vae len = arguments.length
    for(var i = 1,  i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    
    // 执行后 args为 ["arguments[1]", "arguments[2]", "arguments[3]"]
    

    OK,看到这样操作第一反应会想到 ES6 的方法,不过 call 是 ES3 的方法,所以就麻烦一点吧。所以我们这次用 eval 方法拼成一个函数,类似于这样:

    eval('context.fn(' + args +')')
    

    这里 args 会自动调用 Array.toString() 这个方法。

    代码如下:

    Function.prototype._call = function(context) {
        context.fn = this;
        var args = [];
        for(var i = 1, len = arguments.length; i < len; i++) {
            args.push('arguments[' + i + ']');
        }
        eval('context.fn(' + args +')');
        delete context.fn;
    }
    
    // 测试一下
    var foo = {
        value: 1
    };
    
    function bar(name, age) {
        console.log(name)
        console.log(age)
        console.log(this.value);
    }
    
    bar._call(foo, 'chris', 10); 
    // chris
    // 10
    // 1
    

    OK,这样我们实现了 80% call的功能。

    再看看定义:

    根据 MDN 对 call 语法的定义:

    第一个参数:

    fun 函数运行时指定的 this需要注意的是,指定的 this 值并不一定是该函数执行时真正的 this 值,如果这个函数在非严格模式下运行,则指定为 nullundefinedthis 值会自动指向全局对象(浏览器中就是 window 对象),同时值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象。

    执行参数:

    使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined

    所以我们还需要注意两个点

    1.this 参数可以传 null,当为 null 的时候,视为指向 window

    举个例子:

    var value = 1;
    
    function bar() {
        console.log(this.value);
    }
    
    bar.call(null); // 1
    

    虽然这个例子本身不使用 call,结果依然一样。

    2.函数是可以有返回值

    举个例子:

    var obj = {
        value: 1
    }
    
    function bar(name, age) {
        return {
            value: this.value,
            name: name,
            age: age
        }
    }
    
    bar.call(obj, 'chris', 10)
    // Object {
    //    value: 1,
    //    name: 'chris',
    //    age: 10
    // }
    

    不过都很好解决,让我们直接看第三版也就是最后一版的代码:

    Function.prototype._call = function (context = window) {
        var context = context;
        context.fn = this;
    
        var args = [];
        for(var i = 1, len = arguments.length; i < len; i++) {
            args.push('arguments[' + i + ']');
        }
    
        var result = eval('context.fn(' + args +')');
    
        delete context.fn
        return result;
    }
    
    // 测试一下
    var value = 2;
    
    var obj = {
        value: 1
    }
    
    function bar(name, age) {
        console.log(this.value);
        return {
            value: this.value,
            name: name,
            age: age
        }
    }
    
    bar._call(null); // 2
    
    console.log(bar._call(obj, 'kevin', 18));
    // 1
    // Object {
    //    value: 1,
    //    name: 'kevin',
    //    age: 18
    // }
    

    这样我们就成功的完成了一个call函数。

    实现自己的apply

    apply 的实现跟 call 类似,只是后面传的参数是一个数组或者类数组对象。

    Function.prototype.apply = function (context = window, arr) {
        var context = context;
        context.fn = this;
    
        var result;
        if (!arr) {
            result = context.fn();
        }
        else {
            var args = [];
            for (var i = 0, len = arr.length; i < len; i++) {
                args.push('arr[' + i + ']');
            }
            result = eval('context.fn(' + args + ')')
        }
    
        delete context.fn
        return result;
    }
    

    实现自己的bind

    根据 MDN 定义:

    bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。

    由此我们可以首先得出 bind 函数的三个特点:

    1. 改变this指向
    2. 返回一个函数
    3. 可以传入参数
    var foo = {
        value: 1
    };
    
    function bar() {
        console.log(this.value);
    }
    
    var bindFoo = bar.bind(foo); // 返回了一个函数
    
    bindFoo(); // 1
    

    关于指定 this 的指向,我们可以使用 call 或者 apply 实现。

    Function.prototype._bind = function (context) {
        var self = this;
        return function () {
            return self.apply(context);
        }
    }
    

    之所以是 return self.apply(context) ,是考虑到绑定函数可能是有返回值的,依然是这个例子:

    var foo = {
        value: 1
    };
    
    function bar() {
        return this.value;
    }
    
    var bindFoo = bar.bind(foo);
    
    console.log(bindFoo()); // 1
    

    第三点,可以传入参数。这个很困惑是在 bind 时传参还是在 bind 之后传参。

    var foo = {
        value: 1
    };
    
    function bar(name, age) {
        console.log(this.value);
        console.log(name);
        console.log(age);
    }
    
    var bindFoo = bar.bind(foo, 'chris');
    bindFoo('18');
    // 1
    // chris
    // 18
    

    通过实例,我们发现两者参数是可以累加的,就是第一次 bind 时传的参数和可以在调用的时候传入。

    所以我们还是用 arguments 进行处理:

    Function.prototype._bind = function (context) {
        var self = this;
        // 获取_bind函数从第二个参数到最后一个参数
        var args = Array.prototype.slice.call(arguments, 1);
    
        return function () {
            // 这个时候的arguments是指bind返回的函数传入的参数
            var bindArgs = Array.prototype.slice.call(arguments);
            return self.apply(context, args.concat(bindArgs));
        }
    }
    

    完成了上面三步,其实我们还有一个问题没有解决。

    根据 MDN 定义:

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

    举个例子:

    var value = 2;
    
    var foo = {
        value: 1
    };
    
    function bar(name, age) {
        this.habit = 'shopping';
        console.log(this.value);
        console.log(name);
        console.log(age);
    }
    
    bar.prototype.friend = 'james';
    
    var bindFoo = bar.bind(foo, 'chris');
    
    var obj = new bindFoo('18');
    // undefined
    // chris
    // 18
    console.log(obj.habit);
    console.log(obj.friend);
    // shopping
    // james
    

    尽管在全局和 foo 中都声明了 value 值,还是返回了 undefind,说明this已经失效了,如果大家了解 new 的实现,就会知道this是指向 obj 的。

    所以我们可以通过修改返回的函数的原型来实现,让我们写一下:

    Function.prototype.bind2 = function (context) {
        var self = this;
        var args = Array.prototype.slice.call(arguments, 1);
    
        var fBound = function () {
            var bindArgs = Array.prototype.slice.call(arguments);
            // 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
            // 以上面的是 demo 为例,如果改成 `this instanceof fBound ? null : context`,实例只是一个空对象,将 null 改成 this ,实例会具有 habit 属性
            // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
            return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
        }
        // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
        fBound.prototype = this.prototype;
        return fBound;
    }
    

    但是在这个写法中,我们直接将 fBound.prototype = this.prototype,我们直接修改 fBound.prototype 的时候,也会直接修改绑定函数的 prototype。这个时候,我们可以需要一个空函数来进行中转:

    Function.prototype._bind = function (context) {
    
        var self = this;
        var args = Array.prototype.slice.call(arguments, 1);
    
        var fNOP = function () {};
    
        var fBound = function () {
            var bindArgs = Array.prototype.slice.call(arguments);
            return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
        }
    
        fNOP.prototype = this.prototype;
        fBound.prototype = new fNOP();
        return fBound;
    }
    

    还存在一些问题:

    1.调用 bind 的不是函数咋办?

    做一个类型判断呗

    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }
    

    2.我要在线上用

    做一下兼容性测试

    Function.prototype.bind = Function.prototype.bind || function () {
        ……
    };
    

    好了,这样就我们就完成了一个 bind

    Function.prototype._bind = function (context) {
    
        if (typeof this !== "function") {
          throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
        }
    
        var self = this;
        var args = Array.prototype.slice.call(arguments, 1);
    
        var fNOP = function () {};
    
        var fBound = function () {
            var bindArgs = Array.prototype.slice.call(arguments);
            return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
        }
    
        fNOP.prototype = this.prototype;
        fBound.prototype = new fNOP();
        return fBound;
    }
    

    补充

    eval根据 MDN 定义:表示JavaScript表达式,语句或一系列语句的字符串。表达式可以包含变量以及已存在对象的属性。

    一个简单的例子:

    var x = 2;
    var y = 39;
    function add(x,y){
        return x + y
    }
    eval('add('+ ['x','y'] + ')')//等于add(x,y)
    

    也就说eavl调用函数后,字符串会被解析出变量,达到去掉字符串调用变量的目的。

    JavaScript基础系列目录

    JavaScript基础专题之原型与原型链(一)

    JavaScript基础专题之执行上下文和执行栈(二)

    JavaScript基础专题之深入执行上下文(三)

    JavaScript基础专题之闭包(四)

    JavaScript基础专题之参数传递(五)

    新手写作,如果有错误或者不严谨的地方,请大伙给予指正。如果这片文章对你有所帮助或者有所启发,还请给一个赞,鼓励一下作者,在此谢过。

    相关文章

      网友评论

          本文标题:JavaScript基础专题之手动实现call、apply、bi

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