call和apply的模拟实现

2019-06-14  本文已影响0人  小泡_08f5

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

如:

var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar.call(foo); // 1

注意两点:

  1. call 改变了this的指向, 指向到foo
  2. bar函数执行了
模拟实现第一步

试想当调用call的时候, 把foo对象改造如下:

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

foo.bar(); // 1

这个时候this就指向了 foo,
这样给foo对象本身添加了一个属性bar, 所以最后执行完函数,我们得把属性删除
模拟步骤:
1.将函数设为对象的属性
2.执行该函数
3.删除该函数

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

fn 是属性名, 可随意取,因为最后会删除掉

      // 第一版
      var foo = {
            value: 1,
            fn: bar  // 新增属性指向 函数bar
        };
        
        function bar() {
            console.log(this.value);
        }
        console.log(foo);
        foo.fn(); // 执行函数
        delete foo.fn;  // 删除函数
        console.log(foo);
image.png

根据这个思路, 试着写一个call2方法

// 第二版
        Function.prototype.call2 = function(obj){
            console.log(this); // this指向调用该方法的函数bar
            console.log(obj); // 传入的foo对象
            obj.fn = this; // 往foo对象新增属性fn, 指向函数bar
            obj.fn(); // 执行函数fn(bar), 成功输出1
            delete obj.fn; // 删除属性fn
            console.log(obj); // 最后没有改变foo对象本身(里面的属性)
        }

        var foo = {
            value: 1
        };
        
        function bar() {
            console.log(this.value);
        }
        bar.call2(foo);
image.png
模拟实现第二步

call 方法还能给定参数执行函数

var foo = {
    value: 1
};

function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value);
}

bar.call(foo, 'kevin', 18);
// kevin
// 18
// 1

注意:传入的参数并不确定。怎么办? 我们可以从 Arguments 对象中取值, 取出第二个到最后一个参数, 放在一个数组里。

// 第三版
        Function.prototype.call2 = function(obj){
            console.log(arguments);
            var args = [];
            for(var i=1; i<arguments.length; i++){
                args.push(arguments[i]); 
            }
            console.log(args); // ["Ailse", "18"]
            console.log(args.join(',')); // "Ailse,18"
            obj.fn = this;
            obj.fn(args.join(','));
            delete obj.fn;
        }

        var foo = {
            value: 1
        };
        
        function bar(name,age) {
            console.log(arguments);
            console.log(name,age);
        }
        bar.call2(foo, "Ailse","18");

但是这样是不行的,看控制台输出,call2函数里的3个console:


image.png

这样执行fn传入的参数是一个字符串“Ailse,18”, 函数bar接收的参数永远只能接收到一个字符串参数了,如下打印:


image.png

怎么正确传参呢,
通过eval方法拼成一个函数,类似这样:
这里简单说一下eval()方法:

eval 是全局对象上的一个函数,会把传入的字符串当做 JavaScript 代码执行。如果传入的参数不是字符串,它会原封不动地将其返回。eval 分为直接调用和间接调用两种,通常间接调用的性能会好于直接调用。
进一步了解eval方法:https://juejin.im/post/5bead276e51d452ceb51e027

 eval('obj.fn('+args+')');

args 还得改一下,不然会报错

args.push('arguments['+i+']');
image.png

代码改一下

// 第三版
        Function.prototype.call2 = function(obj){
            var args = [];
            for(var i=1; i<arguments.length; i++){
                args.push('arguments['+i+']'); 
            }
            obj.fn = this;
            eval('obj.fn('+args+')');
            delete obj.fn;
        }

        var foo = {
            value: 1
        };
        
        function bar(name,age) {
            console.log(arguments);
            console.log(name,age);
        }
        bar.call2(foo, "Ailse","18");

这样就成功输出了


image.png
模拟实现第三步

模拟代码已经完成80%, 还有两个小点要注意:

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

var value = 1;

function bar() {
    console.log(this.value);
}

bar.call(null); // 1

2.函数是可以有返回值的

var obj = {
    value: 1
}

function bar(name, age) {
    return {
        value: this.value,
        name: name,
        age: age
    }
}

console.log(bar.call(obj, 'kevin', 18));
// Object {
//    value: 1,
//    name: 'kevin',
//    age: 18
// }

都好解决, 最后代码:

// 第四版
        Function.prototype.call2 = function(obj){
            var obj = obj || window;

            var args = [];
            for(var i=1; i<arguments.length; i++){
                args.push('arguments[' + i + ']');
            }

            obj.fn = this;
            var result = eval('obj.fn('+args+')'); // eval方法拼成一个函数
            delete obj.fn;
            return result;
        }

        var foo = {
            value: 1
        };
        
        function bar(name,age) {
            console.log(name,age);
            return {
                name: name
            }
        }
        console.log(bar.call2(null, "Ailse","18"));
apply的模拟实现

apply的实现跟call类似

Function.prototype.apply = function (context, arr) {
    var context = Object(context) || window;
    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;
}

参考:https://juejin.im/post/5907eb99570c3500582ca23c

面试题:call与apply的模拟思路
关键点:

1.将函数设为对象的属性
2.执行该函数
3.删除该函数

传入的参数并不确定怎么办?(通过从Arguments对象取值)
如何正确传参?(通过eval方法拼成一个函数)

上一篇下一篇

猜你喜欢

热点阅读