0 前情提要
最近面试遇到了如下一个问题:
// 问:下面的console.log输出什么?
var arrowFunction = () => {};
console.log(new arrowFunction());
复制代码
当时以为箭头函数就是this指向声明作用域的特殊函数,所以根据函数实体没返回具体值就返回空对象的想法答了个{},结果并不对,所以本着求知求索的想法这两天来深入研究下这个问题。
那么这就引申出很多问题:
-
为什么new箭头函数会报错?
-
new的实际执行逻辑是什么?
-
箭头函数和普通函数是什么关系?
-
如何解释箭头函数的call方法?
-
polyfill中是怎么实现箭头函数的?
1 旧认知与实践
这个问题看起来是个简单地箭头函数+new关键字实例化,当时答题的时候没深究过箭头函数和new的内部实现,只是了解箭头函数没有默认绑定this和arguments等变量,其他的跟普通函数差不多,所以套用了以前学习的常规函数实例化步骤:
以下面代码为例
var functionConstructor = function() {
this.a = 'normal word';
};
new functionConstructor('prop word);
复制代码
1.创建空对象tar = {};
2.tar.__proty__ = functionConstructor.prototype
3.创建res = functionConstructor.call(tar, arguments)
4.如果res是对象且不是null,则返回res,否则返回tar
那带到箭头函数理应得到的是tar,即{},然而实际执行一下会发现并不是这样:
复制代码
居然报错了(怪不得面试官之后问了一堆函数相关的问题)。
复制代码
2 追本溯源
既然认知跟不上了,与其像以前一样伸手学习,不如我们追本溯源,从ES规范入手解决这个问题吧。
ps: ECMAScript@2020 Language Specification
ps2: 规范规定的内容很多,本文只阐述与问题相关的部分。
2.1 ES规范 —— 箭头函数与普通函数
我们先来看下箭头函数
的运行时求值(14.2.17):
下边是普通函数
的运行时求值(14.1.24):
对比下发现主要差异在于:
-
两个方法调用·OrinaryFunctionCreate·的第四个参数
ThisMode
不同,lexical-this
最终会让运行环境不绑定this(及其内部检测等)或创建arguments指向。 -
普通函数在第3步会调用·MakeConstructor·方法,而箭头函数不会。该方法会向函数注入
Consturct
属性(重要)。
箭头函数的官方提示:
简译:箭头函数不会默认绑定arguments, super, this 或者 new.target(在new时指向函数自身,箭头函数会在声明时提示new.target在该语境内不合法),但super可能会存在绑定。
ps:这里的只能先挖个坑了,我要去了解环境声明的那套规范才能读明白这块。
2.2 Bonus —— 箭头函数与call
不过看规范的过程中我也解决了箭头函数.call方法到底干了啥,上文提到箭头函数会修改ThisMode
= lexical
,那么按照call的规范来看,其实call调用链中并不会修改原本的this,因此调用call
和直接调用最终是一样的。
e.g:
function someClass () {
this.variable = 'inside variable';
return () => {console.log(this.variable)};
}
someClass.call({})(); // inside variable
someClass.call({}).call({variable: 'outside variable'}); // inside variable
复制代码
2.3 ES规范 —— new关键字
其实看完函数那边,new关键字的逻辑就显得极其简单了。
执行new逻辑(12.3.5.1.1):
在校验和参数准备结束后,来到第7步时,会用IsConstructor(7.2.4)检查这个拿到的constructor是不是构造器,不是就抛出TypeError异常。
其检测方法就是这个值是不是对象,且包不包含Construct内部方法:
我们在之前对比时知道了箭头函数在执行过程中并没有运行MakeConstructor
,也就没有Construct内部方法,因此按照标准IsConstructor直接返回false,再由new关键字的执行时抛出TypeError异常。
不过在Chrome运行时的运行结果会稍显不同:
ps:猜测可能是Chrome在IsConstructor方法中就抛出了错误。
2.4 Bonus —— 其他new会抛出异常的方法
[
async function() {},
Function.prototype,
// 没有Construct的全局对象:Atomics、JSON、Math、Reflect
Math,
JSON,
Reflect,
Atomics,
// 代理了没有Construct属性的对象的Proxy
].map(e => {
try{
new e();
} catch(err) {
console.error('can`t use new with:', e, 'error msg:', err);
}
})
复制代码
3 Polyfill实现
babel => ie 10
4 小结
可能这就是温故而知新吧(误),但能学到新的东西就是好的,越挖掘越觉得自己只是略懂皮毛;同时也给自己开了个关于执行环境的新坑,这块尝试了一段时间,发现要综合很多块的内容一起看(前车之鉴),慢慢钻研,总之干就完了。
注:本文是我的第一篇文章,可能会有bug,如需对内容进行补充或勘误,可留言或私信,我会定期check并修补。