前言
你好,我是若川。这是面试官问系列的第三篇,旨在帮助读者提升
JS基础知识,包含new、call、apply、this、继承相关知识。
面试官问系列文章如下:感兴趣的读者可以点击阅读。
之前写过两篇《面试官问:能否模拟实现JS的new操作符》和《面试官问:能否模拟实现JS的bind方法》
其中模拟bind方法时是使用的call和apply修改this指向。但面试官可能问:能否不用call和apply来实现呢。意思也就是需要模拟实现call和apply的了。
附上之前写文章写过的一段话:已经有很多模拟实现
call和apply的文章,为什么自己还要写一遍呢。学习就好比是座大山,人们沿着不同的路登山,分享着自己看到的风景。你不一定能看到别人看到的风景,体会到别人的心情。只有自己去登山,才能看到不一样的风景,体会才更加深刻。
先通过MDN认识下call和apply
MDN 文档:Function.prototype.call()
语法
1 | 复制代码fun.call(thisArg, arg1, arg2, ...) |
thisArg
在fun函数运行时指定的this值。需要注意的是,指定的this值并不一定是该函数执行时真正的this值,如果这个函数处于非严格模式下,则指定为null和undefined的this值会自动指向全局对象(浏览器中就是window对象),同时值为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象。
arg1, arg2, …
指定的参数列表
返回值
返回值是你调用的方法的返回值,若该方法没有返回值,则返回undefined。
MDN 文档:Function.prototype.apply()
1 | 复制代码func.apply(thisArg, [argsArray]) |
thisArg
可选的。在 func 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。
argsArray
可选的。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。如果该参数的值为 null 或 undefined,则表示不需要传入任何参数。从ECMAScript 5 开始可以使用类数组对象。
返回值
调用有指定this值和参数的函数的结果。
直接先看例子1
call 和 apply 的异同
相同点:
1、call和apply的第一个参数thisArg,都是func运行时指定的this。而且,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。
2、都可以只传递一个参数。
不同点:apply只接收两个参数,第二个参数可以是数组也可以是类数组,其实也可以是对象,后续的参数忽略不计。call接收第二个及以后一系列的参数。
看两个简单例子1和2**:
1 | 复制代码// 例子1:浏览器环境 非严格模式下 |
1 | 复制代码// 例子2:浏览器环境 严格模式下 |
typeof 有7种类型(undefined number string boolean symbol object function),笔者都验证了一遍:更加验证了相同点第一点,严格模式下,函数的this值就是call和apply的第一个参数thisArg,非严格模式下,thisArg值被指定为 null 或 undefined 时this值会自动替换为指向全局对象,原始值则会被自动包装,也就是new Object()。
重新认识了call和apply会发现:它们作用都是一样的,改变函数里的this指向为第一个参数thisArg,如果明确有多少参数,那可以用call,不明确则可以使用apply。也就是说完全可以不使用call,而使用apply代替。
也就是说,我们只需要模拟实现apply,call可以根据参数个数都放在一个数组中,给到apply即可。
模拟实现 apply
既然准备模拟实现apply,那先得看看ES5规范。ES5规范 英文版,ES5规范 中文版。apply的规范下一个就是call的规范,可以点击打开新标签页去查看,这里摘抄一部分。
Function.prototype.apply (thisArg, argArray)
当以
thisArg和argArray为参数在一个func对象上调用apply方法,采用如下步骤:
1.如果
IsCallable(func)是false, 则抛出一个TypeError异常。2.如果
argArray是null或undefined, 则返回提供thisArg作为this值并以空参数列表调用func的[[Call]]内部方法的结果。3.返回提供
thisArg作为this值并以空参数列表调用func的[[Call]]内部方法的结果。4.如果
Type(argArray)不是Object, 则抛出一个TypeError异常。5~8 略
9.提供
thisArg作为this值并以argList作为参数列表,调用func的[[Call]]内部方法,返回结果。
apply方法的length属性是2。
在外面传入的
thisArg值会修改并成为this值。thisArg是undefined或null时它会被替换成全局对象,所有其他值会被应用ToObject并将结果作为this值,这是第三版引入的更改。
结合上文和规范,如何将函数里的this指向第一个参数thisArg呢,这是一个问题。
这时候请出例子3:
1 | 复制代码// 浏览器环境 非严格模式下 |
可以得出结论1:在对象student上加一个函数doSth,再执行这个函数,这个函数里的this就指向了这个对象。那也就是可以在thisArg上新增调用函数,执行后删除这个函数即可。
知道这些后,我们试着容易实现第一版本:
1 | 复制代码// 浏览器环境 非严格模式 |
实现第一版后,很容易找出两个问题:
- 1.
__fn同名覆盖问题,thisArg对象上有__fn,那就被覆盖了然后被删除了。
针对问题1
解决方案一:采用ES6 Sybmol() 独一无二的。可以本来就是模拟ES3的方法。如果面试官不允许用呢。
解决方案二:自己用Math.random()模拟实现独一无二的key。面试时可以直接用生成时间戳即可。
1 | 复制代码// 生成UUID 通用唯一识别码 |
如果这个key万一这对象中还是有,为了保险起见,可以做一次缓存操作。比如如下代码:
1 | 复制代码var student = { |
- 2.使用了
ES6扩展符...
解决方案一:采用eval来执行函数。
eval把字符串解析成代码执行。语法
1 | 复制代码eval(string) |
参数
string
表示JavaScript表达式,语句或一系列语句的字符串。表达式可以包含变量以及已存在对象的属性。
返回值
执行指定代码之后的返回值。如果返回值为空,返回undefined
解决方案二:但万一面试官不允许用eval呢,毕竟eval是魔鬼。可以采用new Function()来生成执行函数。
MDN 文档:Function
语法
1 | 复制代码new Function ([arg1[, arg2[, ...argN]],] functionBody) |
参数
arg1, arg2, … argN
被函数使用的参数的名称必须是合法命名的。参数名称是一个有效的JavaScript标识符的字符串,或者一个用逗号分隔的有效字符串的列表;例如“×”,“theValue”,或“A,B”。
functionBody
一个含有包括函数定义的JavaScript语句的字符串。
接下来看两个例子:
1 | 复制代码简单例子: |
1 | 复制代码// 稍微复杂点的例子: |
你可能不知道在ES3、ES5中 undefined 是能修改的
可能大部分人不知道。ES5中虽然在全局作用域下不能修改,但在局部作用域中也是能修改的,不信可以复制以下测试代码在控制台执行下。虽然一般情况下是不会的去修改它。
1 | 复制代码function test(){ |
所以判断一个变量a是不是undefined,更严谨的方案是typeof a === 'undefined'或者a === void 0;
这里面用的是void,void的作用是计算表达式,始终返回undefined,也可以这样写void(0)。
更多可以查看韩子迟的这篇文章:为什么用「void 0」代替「undefined」
解决了这几个问题,比较容易实现如下代码。
使用 new Function() 模拟实现的apply
1 | 复制代码// 浏览器环境 非严格模式 |
利用模拟实现的apply模拟实现call
1 | 复制代码Function.prototype.callFn = function call(thisArg){ |
细心的你会发现注释了这一句argsArray.push(arguments[i + 1]);,事实上push方法,内部也有一层循环。所以理论上不使用push性能会更好些。面试官也可能根据这点来问时间复杂度和空间复杂度的问题。
1 | 复制代码// 看看V8引擎中的具体实现: |
行文至此,就基本结束了,你可能还发现就是写的非严格模式下,thisArg原始值会包装成对象,添加函数并执行,再删除。而严格模式下还是原始值这个没有实现,而且万一这个对象是冻结对象呢,Object.freeze({}),是无法在这个对象上添加属性的。所以这个方法只能算是非严格模式下的简版实现。最后来总结一下。
总结
通过MDN认识call和apply,阅读ES5规范,到模拟实现apply,再实现call。
就是使用在对象上添加调用apply的函数执行,这时的调用函数的this就指向了这个thisArg,再返回结果。引出了ES6 Symbol,ES6的扩展符...、eval、new Function(),严格模式等。
事实上,现实业务场景不需要去模拟实现call和apply,毕竟是ES3就提供的方法。但面试官可以通过这个面试题考察候选人很多基础知识。如:call、apply的使用。ES6 Symbol,ES6的扩展符...,eval,new Function(),严格模式,甚至时间复杂度和空间复杂度等。
读者发现有不妥或可改善之处,欢迎指出。另外觉得写得不错,可以点个赞,也是对笔者的一种支持。
1 | 复制代码// 最终版版 删除注释版,详细注释看文章 |
扩展阅读
《JavaScript设计模式与开发实践》- 第二章 第 2 章 this、call和apply
JS魔法堂:再次认识Function.prototype.call
笔者精选文章
学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK
学习 lodash 源码整体架构,打造属于自己的函数式编程类库
学习 underscore 源码整体架构,打造属于自己的函数式编程类库
学习 jQuery 源码整体架构,打造属于自己的 js 类库
前端使用puppeteer 爬虫生成《React.js 小书》PDF并合并
关于
作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。
segmentfault前端视野专栏,开通了前端视野专栏,欢迎关注~
掘金专栏,欢迎关注~
知乎前端视野专栏,开通了前端视野专栏,欢迎关注~
github blog,求个star^_^~
微信公众号 若川视野
可能比较有趣的微信公众号,长按扫码关注。也可以加微信 ruochuan12,注明来源,拉您进【前端视野交流群】。
本文转载自: 掘金