最近在看 ES6 函数形参默认值语法的时候发现一段代码:
function bar(x = y, y = 2) {
return [x, y];
}
bar(); // 报错
上面代码中,调用bar函数之所以报错,是因为参数x默认值等于另一个参数y,而此时y还没有声明,属于”死区“。如果y的默认值是x,就不会报错,因为此时x已经声明了。
我当时特别不能理解, 我觉得这段函数应该解析成下面这样:
function bar() {
var x; // 变量声明的提升
var y;
x = y;
y = 2;
return [x, y];
}
但是很显然如果这样解析就不会报错,后来我在网上搜了一下,有人解释说是用 let
声明的.
虽然用let
声明确实会报同样的错,但是我不服啊,这怎么就能看出来是用let
声明的了? 难道 ES6 默认这个语法就是用 let
来声明的?
后来请教了一个大牛,给指了个方向,说让我看看变量对象
和活动对象
.
相关概念
1. 执行环境( 执行上下文 )的生命周期
1.1 创建阶段
- 生成变量对象 ( Variable object, VO )
- 建立作用域链 ( Scope chain )
- 确定 this 指向
1.2 执行阶段
- 变量赋值
- 函数引用
- 执行其他代码
2. 变量对象
在写程序的时候会定义很多变量和函数, 那 js 解析器是如何找到这些变量和函数的呢 ?
变量对象是与上下文对应的概念, 在执行上下文的创建阶段, 它依次存储着上下文中定义的以下内容:2.1 函数的所有形参 ( 如果是函数上下文中 )
建立 arguments 对象. 检查当前上下文中的参数, 建立该对象下的属性与属性值. 没有实参的话, 属性值为 undefined.
2.2 所有函数声明: ( FunctionDeclaration, FD )
检查当前上下文的函数声明, 也就是使用 function 关键字声明的函数. 在变量对象中以函数名建立一个属性, 属性值为指向该
函数所在内存地址的引用.如果变量对象已经存在相同名称的属性, 则完全替换这个属性.
2.3 所有变量声明: ( var, VariableDeclaration )
检查当前上下文中的变量声明, 每找到一个变量声明, 就在变量对象中以变量名建立一个属性, 属性值为 undefined.
如果变量名称跟已经声明的形式参数或者函数相同, 则变量声明不会干扰已经存在的这类属性.
3. 活动对象
未进入执行阶段前, 变量对象中的属性都不能访问! 但是进入到执行阶段之后, 变量对象转变成了活动对象, 里面的属性都能被访问了, 然后开始进行执行阶段的操作.
- 只有全局上下文的变量对象允许通过 VO 的 属性名称来间接访问, 在其他上下文中是不能直接访问 VO 对象的.
- 在函数上下文中, VO 是不能直接访问的, 此时由活动对象 AO 继续扮演 VO 的角色.
因此, 对于函数上下文来说, 活动对象与变量对象其实都是同一个对象, 只是出于执行上下文的不同声明周期. 不过只有处于执行上下文栈栈顶的函数执行上下文中的变量对象, 才会变成活动对象.
那么我们现在再来看看文章开头的这个函数
// 创建阶段
// 第一步, 遇到全局代码, 首先, 从执行上下文的创建阶段来分析变量对象:
ESCStack = [
globalContext: {
VO: {
bar: < reference to function >
}
}
]
// 第二步, 发现 bar 函数被调用, 就又创建了一个函数上下文
ESCStack = [
globalContext: {
VO: {
bar:<reference to function bar() {}>
}
},
<bar>functionContext: {
VO: {
// 优先分析形参
arguments: {
0: undefined,
1: undefined,
length: 2,
callee: bar
},
x: y, // y还没有被声明
// y: 2
}
}
]
函数的形参是直接初始化的, 也就是说声明和赋值是一起走的,并不被分开操作, 所以走到 x=y
这里就会报错.
// 执行阶段:
// 第三步: 首先, 执行了 bar() , 紧接着进入函数,直接报错
// 此时全局上下文中依然是 VO, 但是函数上下文中 VO 就变成了 AO.
ESCStack = [
globalContext: {
VO: {
bar:<reference to function bar() {}>
}
},
<bar>functionContext: {
AO: {
// 优先分析形参
arguments: {
0: undefined,
1: undefined,
length: 2,
callee: bar
},
x: y, // 报错,因为y还没有被声明
// y: 2
}
}
]
函数上下文中, 进入执行阶段才可以访问到活动对象里的属性,所以报错是在执行阶段,我没有多写一坨东西. 逻辑上是这样的,具体嘛~,本人才疏学浅,不敢保证是绝对对的,这也是基于一个大神关于变量对象和活动对象的文章强行解释一通,中间的细枝末节可能会有锁雾,欢迎指教.