JavaScript深入之call和apply的模拟实现 #11
Comments
受益匪浅!学到了很多,谢谢前辈! |
哈哈,确实可以,没有注意到这点,感谢指出,(๑•̀ㅂ•́)و✧ |
arr.push('arguments['+i+']'); |
eval函数接收参数是个字符串定义和用法
语法:
简单来说吧,就是用JavaScript的解析引擎来解析这一堆字符串里面的内容,这么说吧,你可以这么理解,你把
|
@jawil 感谢回答哈~ var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
} 最终的数组为: var args = [arguments[1], arguments[2], ...] 然后 var result = eval('context.fn(' + args +')'); 在eval中,args 自动调用 args.toString()方法,eval的效果如 jawil所说,最终的效果相当于: var result = context.fn(arguments[1], arguments[2], ...); 这样就做到了把传给call的参数传递给了context.fn函数 |
apply郑航的实现,循环是不是应该从 i = 1 开始? |
@lz-lee call 的实现中,是通过 arguments 取各个参数,所以从 1 开始,省略掉为 0 的 context,而apply的实现中,arr 直接就表示参数的数组,循环这个参数数组,直接就从 0 开始。 |
粗心大意了。感谢提醒。@mqyqingfeng |
分析的很透彻,点个赞! |
赞赞赞 |
@lynn1824 @qianlongo 感谢夸奖,写的时候我就觉得模拟实现一遍 call 和 apply 最能让大家明白 call 和 apply 的原理 (~ ̄▽ ̄)~ |
var context = Object(context) || window; 这里有问题吗?context为null时Object(null)返回空对象,不会被赋值为window |
@lzr900515 没有什么问题哈,非严格模式下,指定为 null 或 undefined 时会自动指向全局对象,郑航写的是严格模式下的,我写的是非严格模式下的,实际上现在的模拟代码有一点没有覆盖,就是当值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象。 |
不过这个方法似乎有点傻 |
@hujiulong 哈哈,有道理哈~ 确实会覆盖之前对象的方法,还好模拟实现 call 和 apply 的目的在于让大家通过模拟实现了解 call 和 apply 的原理,实际开发的时候还是要直接使用 call 和 apply 的~ |
实在是佩服! |
这一句可能造成误导。结果为:["arguments[1]", "arguments[2]"] |
第三版Function.prototype.call2 = function (context) {
// 增加一条判断,否则在Node环境下会报错,用原生的call就不会报错,所以这边对windows要判断一下
var window = window || global;
var context = context || window;
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;
} |
这一行确定是可以的吗.....
是不是应该用 |
我参考 MDN 对 call 方法的介绍实现了一个
但是我发现不管怎么实现,这种实现方法依然会存在一个潜在问题,就是当函数调用 $call 方法时,在函数执行期间,context上始终存在 fn,只有当函数执行完成之后 fn 才会从 context 上删除。这样就会导致我们在函数内部访问 context 时和传进函数时的context 永远不会相等。 |
为此,我想到了一个新的实现方法 ,但存在一些性能上的问题
|
这样写好像还存在问题,如果我们在函数内部操作了context(例如修改),不会映射到原context上(深拷贝了context),所以应该再加一层代理,实现两个对象之间的映射(这里可以用Object.definedProperty 或 Proxy) |
我有个疑问,在郑航的apply这个方法实现上,不需要对arr类别做判断么? Function.prototype.apply2 = function (context, arr) {
} 那么我执行下面的代码: |
有个小问题想问下apply中可以直接result = context.fn(args);这样吗? |
可以的 |
context如果是字符串咋办,应该是context?Object(context) || window |
Function.prototype.call2 = function () {
var context = arguments[0] || window
context.fn = this
var args = arguments.slice(1)
var result = eval('context.fn('+args.join(',')+')')
delete context.fn
return result
} 一开始想用这样的办法,后来发现 |
[... arguments].slice(1) |
你这里第二点参数值为原始类型值时,会包装成原始类型的对象。比如fn.call(2) == fn.call(new Number(2)) |
怎么提问呀 |
这样实现怎么样 Function.prototype.myCall = function() {
const args = []
for(let i = 0; i< arguments.length; i++) {
args.push(arguments[i])
}
/**
* 没有传入上下文 或者传入的为 null 就认为上下文是 window
*/
const context = args.shift() || window
/**
* 修改原形了链 不把新增的属性添加到 需要绑定的 上下文环境中 、也避免不小心删除了 绑定上下文中的 方法
* 即 再 context 中 打印 this 不会出现 _fn 这个函数
* 但是这个操作比较耗时间
*/
const prototype = Object.getPrototypeOf(context)
prototype._fn = this
Object.setPrototypeOf(context, prototype)
const result = eval('context._fn('+ args +')')
delete prototype._fn
return result
} |
@1415333545 试了下你的写法,发现两个问题
|
谢谢 |
hello @wenwen1995 很高兴回答你的问题,这是作用域不一样,作者所描述的作用域是执行时的作用域,而你打印的是bar的作用域 |
嗷嗷。明白啦~谢谢你 |
call 函数里面执行时还多了一个 绑定的对象 (this) 参数 |
针对node中不存在window全局对象、context为null或undefined时context指向全局对象、context为基本类型值时context为基本值的对象类型等情况,可以添加一个对context的判断逻辑。具体如下
|
我丢 真有意思 |
call
一句话介绍 call:
举个例子:
注意两点:
模拟实现第一步
那么我们该怎么模拟实现这两个效果呢?
试想当调用 call 的时候,把 foo 对象改造成如下:
这个时候 this 就指向了 foo,是不是很简单呢?
但是这样却给 foo 对象本身添加了一个属性,这可不行呐!
不过也不用担心,我们用 delete 再删除它不就好了~
所以我们模拟的步骤可以分为:
以上个例子为例,就是:
fn 是对象的属性名,反正最后也要删除它,所以起成什么都无所谓。
根据这个思路,我们可以尝试着去写第一版的 call2 函数:
正好可以打印 1 哎!是不是很开心!(~ ̄▽ ̄)~
模拟实现第二步
最一开始也讲了,call 函数还能给定参数执行函数。举个例子:
注意:传入的参数并不确定,这可咋办?
不急,我们可以从 Arguments 对象中取值,取出第二个到最后一个参数,然后放到一个数组里。
比如这样:
不定长的参数问题解决了,我们接着要把这个参数数组放到要执行的函数的参数里面去。
也许有人想到用 ES6 的方法,不过 call 是 ES3 的方法,我们为了模拟实现一个 ES3 的方法,要用到ES6的方法,好像……,嗯,也可以啦。但是我们这次用 eval 方法拼成一个函数,类似于这样:
这里 args 会自动调用 Array.toString() 这个方法。
所以我们的第二版克服了两个大问题,代码如下:
(๑•̀ㅂ•́)و✧
模拟实现第三步
模拟代码已经完成 80%,还有两个小点要注意:
1.this 参数可以传 null,当为 null 的时候,视为指向 window
举个例子:
虽然这个例子本身不使用 call,结果依然一样。
2.函数是可以有返回值的!
举个例子:
不过都很好解决,让我们直接看第三版也就是最后一版的代码:
到此,我们完成了 call 的模拟实现,给自己一个赞 b( ̄▽ ̄)d
apply的模拟实现
apply 的实现跟 call 类似,在这里直接给代码,代码来自于知乎 @郑航的实现:
下一篇文章
JavaScript深入之bind的模拟实现
重要参考
知乎问题 不能使用call、apply、bind,如何用 js 实现 call 或者 apply 的功能?
深入系列
JavaScript深入系列目录地址:https://github.com/mqyqingfeng/Blog。
JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。
如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。
The text was updated successfully, but these errors were encountered: