【深入JavaScript日记七】call和apply的模拟实现

前言

这一次,我们继续学习javaScript中另外两个东西,call和apply
每个函数都包含两个非继承而来的方法:call()方法和apply()方法。


正文内容

Call

首先搬出来它的定义

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

举个例子!

    window.color = 'red';
    document.color = 'yellow';

    var s1 = {color: 'blue'};

    function changeColor() {
        console.log(this.color);
    }

    changeColor.call();         //red (默认传递参数)
    changeColor.call(this);     //red
    changeColor.call(window);   //red
    changeColor.call(document); //yellow
    changeColor.call(s1);       //blue

效果显而易见,这个方法可以通过传入参数的不同,改变this的指向。
在这段代码中,不传参,不改变this 指向的时候,结果都是输出默认的 red
在我们传入 document 和 s1 对象时 ,this指针发生了改变,获取到了这些对象里的 color的值。

我们再举一个传入参数的例子

    var Cat = {
        words : '喵喵喵',
        speak : function (say) {
            console.log(say + ''+ this.words)
        }
    }
    Cat.speak('Speak'); // 结果:Speak喵喵喵

    var Dog = {
        words:'汪汪汪'
    }
    // 将this的指向改变成了Dog
    Cat.speak.call(Dog, 'Speak'); // 结果: Speak汪汪汪

这段代码首先通过传参方式 Cat.speak ( ); 其中 say 来自传入的参数,words来自 Cat中的赋值。
而后一句打印,通过 call 方式来改变了 Cat 中 this 的指向,传递的第一个参数代表 改变之后的 this 指向。所以输出打印的 say 来自传入的第二个参数,words来自改变之后的this指向的Dog

Apply

效果和 call 类似,直接上个例子

    window.number = 'one';
    document.number = 'two';

    var s1 = {number: 'three' };
    function changeNumber(){
        console.log(this.number);
    }

    changeNumber.apply();         // one (默认传参)
    changeNumber.apply(window);   // one
    changeNumber.apply(this);     // one
    changeNumber.apply(document); // two
    changeNumber.apply(s1);       // three

在这段代码中,不传参,不改变this 指向的时候,结果都是输出默认的 one
在我们传入 document 和 s1 对象时 ,this指针发生了改变,获取到了这些对象里的 number 的值。

再来一个例子

    function Pet(words){
        this.words = words;
        this.speak = function () {
            console.log( this.words)
        }
    }

    function Dog(words){
        //Pet.call(this, words); //使用call方式     结果: 汪汪汪
        Pet.apply(this, arguments);  //使用apply方式    结果: 汪汪汪
    }

    var dog = new Dog('汪汪汪');
    dog.speak();

通过apply ,在执行 Pet()时, 使得 this 指针指向 Dog的参数列表 arguments

说到这,call 和 apply 的差别我们也能猜得到了

Call 和 Apply 的差别

apply()方法 接收两个参数,一个是函数运行的作用域(this),另一个是参数数组。
语法:apply([thisObj [,argArray] ]);

call()方法 第一个参数和apply()方法的一样,但是传递给函数的参数必须列举出来。
语法:call([thisObject[,arg1 [,arg2 [,...,argn]]]]);

再来两个例子巩固一下

function add(c,d){
 	return this.a + this.b + c + d;
 }
 
 var s = {a:1, b:2 };
 console.log(add.call(s,3,4)); // 1+2+3+4 = 10
 console.log(add.apply(s,[5,6])); // 1+2+5+6 = 14 

需要注意的是 apply传入的参数一定得是数组,call传入的必须列举

手动模拟实现

看到这里,终于大致弄明白了这两个方法,其实也就是改变了 this的指向,应该不是很难,动手实操一下加深印象

Call的模拟实现

尝试一
var num= 1;

var obj = {
    num: 2
};

function fn() {
    console.log(this.num);
};

fn(); //1
fn.call(obj); //2

首先分析一下流程,call 方法一共做了两件事

  • 修改了 this 指向
  • 执行了某个函数

那我们根据思路写一下试试

    var num= 1;

    var obj = {
        num: 2
    };

    function fn() {
        console.log(this.num);
    };
    
	----------------------上面的部分直接拿过来-------------------------------
	
    Function.prototype.my_cell= function (obj) {
        obj.fn = this; // 此时this就是函数fn
        obj.fn(); // 执行fn
        delete obj.fn; //删除fn
    };
    
    fn();					//1
    fn.my_cell(obj);		//2				调用我们自己写的 call 方法

写这段代码学到了一部分新知识

  • 我们通过Function.prototype.xxx 的形式绑定了my_cell 方法,使所有函数都可以直接访问被绑定的方法
  • fn.my_cell ( obj ) 属于隐式绑定,所以在执行时 my_cell 时内部 this 指向 fn,后续的 obj.fn = this; 其实就是把 this 指向的 fn() 当作 obj 的一个属性,属性名叫 fn
  • 此时对象 obj 已经有了 fn 方法,执行 obj.fn,因为隐式绑定的问题,此时的 fn 内部的 this 指向 obj,所以 num 的值从 obj 中获得,输出结果 2
  • 最后通过delete删除了 obj 上的 fn 方法,执行完不删除会导致obj上的属性越来越多,造成冗余

我们到目前为止,成功的改变了 this 的指向,但是目前还不可以接收参数,还得改改…

尝试二

思考过后,因为函数传递的参数我们也不能很好的确定,有多少我们也无法预测,突然想到函数有一个 arguments 属性,代指函数接收的所有参数,它是一个类数组,比如刚刚的代码我们拿来改改。

    var num = 1;

    var obj = {
        num: 2
    };

    function fn() {
        console.log(this.num);
    };
	----------------------上面的部分直接拿过来-------------------------------
	
    Function.prototype.my_cell = function (obj) {
        console.log(arguments);
    };

    fn();		//1
    fn.my_cell(obj, "参数一", "参数二", "参数三");

输出结果正是我们所要的
图片
可以很清楚的看到,arguments 里第一个参数,是我们需要让 this 指向的对象,从第二个参数开始,一直到最后一个参数为止,都是真正的参数。通过 for 循环把后面的东西拿出来

    var num = 1;

    var obj = {
        num: 2
    };

    function fn() {
        console.log(this.num);
    };
    
	----------------------上面的部分直接拿过来-------------------------------
	
    Function.prototype.my_cell = function (obj) {
        var args = [];
        // 注意i从1开始
        for (var i = 1, len = arguments.length; i < len; i++) {
            args.push(arguments[i]);
        };
        console.log(args);
    };

    fn();
    fn.my_cell(obj, "参数一", "参数二", "参数三");

输出一下
结果
嗯符合要求,但是 call 方法不接受整个数组为参数,还必须切开才行…
又是一阵查资料,发现了之前没有怎么学习的 eval 方法,没这方法还真不好折腾,举个例子认识一下

var fn = function (a, b, c) {
    console.log(a + b + c);
};
var arr = [1, 2, 3];

fn(1, 2, 3);//6
eval("fn(" + arr + ")");//6

两种输出结果居然一样!?
细究之下,发现 eval 在执行时会将变量转为字符串,这里隐性执行了 arr.toString()

console.log([1, 2, 3].toString()); //"1,2,3"
console.log([1, 2, 3].join(',')); //"1,2,3"

这俩也是一样的
补充一下join方法是将数组元素通过指定符号分割,化成字符串
可以看出 eval 帮我们做了数组处理,这里就不需要再使用 join 方法了
因此eval("fn(" + arr + ")")可以看成eval("fn(1,2,3)")
好家伙,这可太方便了。

到现在为止,我们代码长这样

    var num = 1;

    var obj = {
        num: 2
    };

    function fn(value1, value2, value3) {
        console.log(this.num + "   " + value1 + "   " + value2 + "   " + value3);
    }

    Function.prototype.my_cell = function (obj) {
        var args = [];
        // 注意i从1开始
        for (var i = 1, len = arguments.length; i < len; i++) {
            args.push(arguments[i]);
        }
        obj.fn = this; // 此时this就是函数fn
        eval("obj.fn(" + args + ")"); // 执行fn
        delete obj.fn; //删除fn
    };

    // fn();
    fn.my_cell(obj, "参数一", "参数二", "参数三");

运行之后
结果
嗯???这报错很显然是把 “参数一”看成了一个变量,我传递的不是个字符串吗,怎么变成变量了…
往上细看,是这里出了问题,这里已经把传进去的字符串解析了,下面再拿来用解析的时候自然就当作变量了…

args.push(arguments[i]);

那我们把这一句改一改

args.push("arguments[" + i + "]");

改成这样之后,就算他在 push 时先解析也无所谓了,eval 解析时遇到的是 arguments[1] ,arguments[2] 之类的东西,依然可以找得到具体的值。

于是代码就这样写了

    var num = 1;

    var obj = {
        num: 2
    };

    function fn(value1, value2, value3) {
        console.log(this.num + "   " + value1 + "   " + value2 + "   " + value3);
    }

    Function.prototype.my_cell = function (obj) {
        var args = [];
        // 注意i从1开始
        for (var i = 1, len = arguments.length; i < len; i++) {
            args.push("arguments[" + i + "]");
        }
        obj.fn = this; // 此时this就是函数fn
        eval("obj.fn(" + args + ")"); // 执行fn
        delete obj.fn; //删除fn
    }

    // fn();
    fn.my_cell(obj, "参数一", "参数二", "参数三");

结果也是我们想要的的
结果
到这里,基本功能已经完成了,只需要考虑一些合法性问题,当 call 第一个参数为 undefined 或者 null 时,this 默认指向 window,我们再来稍加改进。

尝试三

我们传递一个 null 看看

    fn.my_cell(null, "参数一", "参数二", "参数三");

报错
报错了,解决方法很简单,通过一个三目判断实现

obj = obj ? Object(obj) : window;

这样就可以了,打印的结果是 1
成功
所以模拟 call 函数的代码完成

    var num = 1;

    var obj = {
        num: 2
    };

    function fn(value1, value2, value3) {
        console.log(this.num + "   " + value1 + "   " + value2 + "   " + value3);
    }

    Function.prototype.my_cell = function (obj) {
        obj = obj ? Object(obj) : window;

        var args = [];

        for (var i = 1, len = arguments.length; i < len; i++) {
            args.push("arguments[" + i + "]");
        }
        obj.fn = this; 
        eval("obj.fn(" + args + ")"); 
        
        delete obj.fn; 
    }

    // fn();
    fn.my_cell(obj, "参数一", "参数二", "参数三");

Apply 的模拟实现

apply方法因为接受的参数是一个数组,所以模拟起来就更简单了,理解了call实现,也就可以少走一些坑。

    var num = 1;

    var obj = {
        num: 2
    };

    function fn(value1, value2, value3) {
        console.log(this.num + "   " + value1 + "   " + value2 + "   " + value3);
    }

    Function.prototype.my_apply = function (obj, arr) {

        obj = obj ? Object(obj) : window;

        obj.fn = this;
        
        if (!arr) {				//判断数组是否为空
            obj.fn();
        } else {
            var args = [];

            for (var i = 0, len = arr.length; i < len; i++) {	//遍历传入的数组
                args.push("arr[" + i + "]");
            }

            eval("obj.fn(" + args + ")");
        }

        delete obj.fn; 
    }

    fn.my_apply(obj, ["参数一", "参数二", "参数三"]);

结果,也符合我们的要求。
结果


总结

到这里,我们也算是完成了对于 call 和 apply 的模拟,收获还是非常大的,撒花庆祝!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AntyRia

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值