call()
和apply()
call()
方法调用一个函数,其具有一个指定的this
值和分别地提供的参数(参数的列表)
call()
和apply()
的区别在于,call()
方法接受的是若干参数的列表,而apply()
方法接受的是一个包含多个参数的数组
举个例子:
1 | var func = function(arg1, arg2) { |
call
的模拟实现
先看下面一个简单的例子
1 | var value = 1; |
当bar
这个函数调用call
这个方法时,将要绑定this
的对象foo
传递了进去,这样一来,bar
这个函数中的this
将指向foo
这个对象,而且最终代码执行结果为1
通过上面的介绍以及代码的执行结果我们可以知道,call()
主要有以下两点:
call()
改变了this
的指向- 函数
bar
执行了
模拟实现第一步
如果把bar
函数定义到foo
对象(函数也是对象)中,例如:
1 | var value = 1; |
由foo
这个对象来调用其属性bar()
,这样的话,执行效果是一样的。
这个改动就可以实现:改变了this
的指向并且执行了函数bar
但是这样写是有副作用的,即给foo
额外添加了一个属性,怎么解决呢?
解决方法很简单,用delete
删除掉就好了。
所以只要实现下面3步就可以模拟实现了。
- 将函数设置为对象的属性:
foo.fn = bar
- 执行函数:
foo.fn()
- 删除函数:
delete foo.fn
接下来还有一个问题,我们要模拟的call
方法是每个函数都可以进行调用的,那么如何做到每个函数中都可以调用这个方法呢?
将call
方法定义到函数原型中
- 首先,我们待实现的
mycall
这个方法需要定义在函数原型中,因此我们可以先写出这样的代码:
1 | Function.prototype.mycall = function() { |
- 然后,在函数内部进行具体的实现:
1 | Function.prototype.mycall = function() { |
目前的代码只能bar
函数作为属性添加到foo
函数中,直接写死了。因此,我们需要进行完善。
foo
是谁?- 待绑定
this
的对象(上下文对象),应该作为mycall()
函数的参数传入
- 待绑定
要给
foo
绑定哪个函数?- 看哪个函数调用
mycall
方法了,哪个函数调用mycall
方法,就给foo
绑定哪个函数
- 看哪个函数调用
如何获取待绑定的函数呢?
- 可以使用
this
。我们知道函数中的this
指向函数的调用者。mycall()
方法是被其他函数调用的,因此mycall
方法中的this
就指向这个函数。
- 可以使用
完善后的代码如下:
1 | // 第一版 |
模拟实现第二步
第一版有一个问题,那就是函数bar
不能接收参数,所以我们可以从arguments
中获取参数,取出第二个参数到最后一个参数放到数组中。为什么要抛弃第一个参数呢?因为第一个参数是context
。
这里回顾一个问题:arguments
是一个类数组对象,它保存着一个函数接收到的参数,即使函数声明中没有定义形参,在调用函数时,也一样可以直接传参,传递的参数可以通过arguments
对象通过下标来访问
1 | function func() { |
参数已经可以进行获取了,现在要考虑的是如何将参数提取出来,并传入context.fn()
中
现在我们来将代码添加进去参数部分:
- 首先,我们来打印出
arguments
对象接收到的参数
1 | // 第一版 |
输出结果为:
1 | 1 |
可以看到,我们获取到了需要的参数CoderLeiShuo
和23
,现在我们需要让这两个参数传入到context.fn()
中,即这样:context.fn(CoderLeiShuo,23)
。
我们可以这样写:
1 | // 第一版 |
输出结果为:
1 | 1 |
但是,我们向函数中传入的参数是不定长的,因此必须动态获取,不能直接通过下标来访问。我们可以将arguments
中的需要的相关参数提取出来,保存到一个数组中。
使用ES3
的方案可以这样做:
1 | var args = []; |
完整代码如下:
1 | // 第一版 |
输出结果:
1 | [ 'CoderLeiShuo', 23 ] |
显然这样直接将提取后的参数数组传入context.fn()
是不行的。因为args
是一个数组,传入时会被当作一个完整的参数传入,输出结果就变成了“我是CoderLeiShuo,23今年undefined岁”
这样的话,我们必须想办法,让context.fn([CoderLeiShuo, 23])
变成context.fn(CoderLeiShuo, 23)
。自然可以想到,先将参数数组拆分,然后使用,
进行拼接,然后再传入context.fn
中
于是:
1 | context.fn(args.join(',')); |
但是这样仍旧不行,我们可以打印看一下args
数组元素的拼接结果:
1 | 'CoderLeiShuo,23' |
拼接后的结果变成了一个字符串,如果把这个字符串传入,这个字符串仍然会被当作一个参数去解析
我们不妨这样做:
1 | 'context.fn(' + args + ')' // 'context.fn(CoderLeiShuo,23)' |
这样的话,整行代码都将变成字符串,通过eval()
函数我们就可以执行代码字符串了。
1 | eval('context.fn(' + args + ')') // context.fn(CoderLeiShuo, 23) |
看似这里已经成功了,但是这里其实是一个严重问题的。我们可以先运行一下代码,看看结果:
直接报错了:CoderLeiShuo is not defined
原因在于,原本的字符串'CoderLeiShuo'
在拼接以后变成了CoderLeiShuo
,被当成了一个变量(一个没有定义的变量)传入到context.fn()
中
args
是一个数组,在和字符串进行拼接时,会自动调用数组的toString()
方法。如下代码:
1 | var args = ["a1", "b2", "c3"]; |
字符串的引号会丢失掉
只需要修改一下代码
1 | Function.prototype.mycall = function (context) { |
修改完代码之后:
args
数组中保存的元素为['arguments[1]', 'arguments[2]']
'context.fn('+ args + ')'
变为context.fn(arguments[1], arguments[2])
eval
语句就会正常执行了。
最终输出结果为:
1 | [ 'arguments[1]', 'arguments[2]' ] |
所以说,第二个版本就实现了,代码如下:
1 | // 第二版 |
模拟第三步
目前的代码还有几个细节需要注意:
this
参数可以传null
或undefined
,此时this
应该指向window
this
参数可以传基本数据类型,原生的call
会自动调用Object()
转换- 函数是可以有返回值的
实现上面的三点很简单,代码如下:
1 | // 第三版 |
执行结果为:
本文作者: CoderLeiShuo
本文链接:https://coderleishuo.github.io/lele/23004.html
版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!