我正在参与掘金创作者训练营第4期,点击了解活动详情,一起学习吧!
这道题,你能做对吗?
最近重新看执行上下文栈时,发现下面这道题非常有意思:
题目:
var a = 1;
function foo(a){
var a = 2;
console.log("inner a", a);
function a(){
console.log("It's function declaration")
}
console.log("inner a1, and typeof a1 is", a, typeof(a))
var a = function(){
console.log("It's a function expression")
}
console.log("inner a2, and typeof a2", typeof(a))
}
console.log("outer a1", a)
foo(1);
console.log("outer a2", a)
复制代码
答案揭晓:
JavaScript 变量对象
我们先来看看什么是变量对象:
变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。JavaScript 深入变量对象
根据打印结果,我们来回顾一下代码的执行顺序:
1. 创建执行上下文栈
JavaScript 引擎创建执行上下文栈来管理执行上下文,执行上下文分为全局执行上下文和函数执行上下文。它们都涉及到三个要素:活动对象、作用域链、this, 它们的值根据代码的初始化和执行阶段变化。
执行全局代码,用数组模拟为:
```js
ECStack = [
globalContext
]
```
复制代码
2. 初始化
全局上下文初始化,同时,foo
函数被创建,保存作用域链到函数的的内部属性 [[scope]]
。
```js
globalContext: {
VO: [global],
Scope: [globalContext.VO],
this: globalContext.VO
}
```
```js
foo.[[scope]] = [
globalContext.VO
]
```
复制代码
3. 创建foo
函数上下文
执行foo
函数,创建 foo
函数执行上下文,并把 foo
执行上下文压入执行上下栈。
ECStack = [
globalContext,
fooContext
]
复制代码
4. foo
函数初始化
(1)复制 foo
函数的 [[scope]]
属性创建作用域链
(2)用 arguments
创建活动对象
(3)初始化活动对象,即加入形参、函数声明、变量声明
AO = {
arguments: {
0: 1,
length: 1
}
},
a: 1, // 函数的形参
复制代码
(4)将活动对象压入foo
作用域链顶端
5. foo
函数执行
沿着作用域链查找 scope 值,返回 scope,执行完毕后, foo
上下文从执行上下文栈弹出。执行时,变量 a
多个赋值语句修改,同名变量还会覆盖前一步的值。
但是,函数声明的变量 a
明明在变量 a
的后面呀,为什么最后打印结果不是该函数呢?
变量和函数重名, 函数优先提升!
同名的变量声明或者函数声明,都是后者覆盖前者。
同名的变量声明和函数声明,函数声明优先提升,因此同名的变量声明会覆盖函数声明。
把函数表达式赋值给同名变量,等于是对该同名变量进行了赋值操作,因此最后变量的值是被修改后的值。
又是 var
!
这段代码还留了一个var
的坑,即全局作用域下声明了变量 a
,但是 foo
函数中也用 var
声明了变量 a
,这里的打印结果可以参照下面两段段代码思考一下:
第一段代码:
var a = 1;
function foo(a){
a = 2;
console.log("inner a", a);
}
console.log("outer a1", a)
foo(1);
console.log("outer a2", a)
复制代码
第二段代码:
var a = 1;
function foo(a){
var a = b = 2;
console.log("inner a", a);
}
console.log("outer a1", a)
foo(1);
console.log("outer a2", a, b)
复制代码
具体就不分析了,这和作用域有关,可以参照上面的执行步骤分析一下。但是我们自己写代码的时候,为了不坑自己,还是尽量使用 let
替代吧。
总结
将代码一步一步调试之后,可以发现: