JavaScript不同于其他语言,存在变量提升,如下面代码例子:
console.log(x)
var x = 'hello world';
这段代码不会报错,会输出 undefined
。这就是所谓的变量提升,但具体细节JS引擎是怎么处理的,还需要理解JS的Execution Context执行上下文。
1. Execution Context
Execution Context 是JS执行代码时候的一个上下文环境。如执行到一个调用函数,就会进入这个函数的执行上下文,执行上下文中会确定这个函数执行期间用到的诸如this,变量,对象以及定义的方法等。
当浏览器加载script的时候,默认直接进入Global Execution Context(全局上下文),将全局上下文入栈。如果在代码中调用了函数,则会创建Function Execution Context(函数上下文)并压入调用栈内,变成当前的执行环境上下文。当执行完该函数,该函数的执行上下文便从调用栈弹出返回到上一个执行上下文。
2. 执行上下文分类
-
Global execution context。当js文件加载进浏览器运行的时候,进入的就是全局执行上下文。全局变量都是在这个执行上下文中。代码在任何位置都能访问。
-
Functional execution context。定义在具体某个方法中的上下文。只有在该方法和该方法中的内部方法中访问。
-
Eval。定义在Eval方法中的上下文。该方法不建议使用对此就不进一步研究。
3. Execution Stack
Js是单线程执行,每次注定只能访问一个execution context。因此调用栈最上方的执行上下文将最先被执行,执行完后返回到上层的执行上下文继续执行。引用一篇博文的动态图示如下:
4. 执行上下文运行详情
execution context期间js引擎主要分两个阶段:
创建阶段(函数调用时,但在函数执行前)
-
JS解析器扫描一遍代码,创建execution context内对应的variables, functions和arguments。这三个称之为Variable Object。
-
创建作用域链scope chain
-
决定this的指向
executionContextObj = {
'scopeChain': { /* variableObject + all parent execution context's variableObject */ },
'variableObject': { /* function arguments / parameters, inner variable and function declarations */ },
'this': {}
}
executionContextObj由函数调用时运行前创建,创建阶段arguments的参数会直接传入,函数内部定义的变量会初始化为undefined。
执行阶段
- 重新扫描一次代码,给变量赋值,然后执行代码。
下面是执行上下文期间JS引擎执行伪代码
- 找到调用函数
- 执行函数代码前,创建execution context
- 进行创建阶段:
- 初始化调用链 Scope Chain
- 创建 variable object:
- 创建arguments对象,初始化该入参变量名和值
- 扫描该执行上下文中声明的函数:
- 对于声明的函数,variable object中创建对应的变量名,其值指向该函数(函数是存在heap中的)
- 如果函数名已经存在,用新的引用值覆盖已有的
- 扫描上下文中声明的变量:
- 对于变量的声明,同样在variable object中创建对应的变量名,其值初始化为undefined
- 如果变量的名字已经存在,则直接略过继续扫描
- 决定上下文this的指向
- 代码执行阶段:
- 执行函数内的代码并给对应变量进行赋值(创建阶段为undefined的变量)
一个简单例子如下:
console.log(foo(22))
console.log(x);
var x = 'hello world';
function foo(i) {
var a = 'hello';
var b = function privateB() {
};
function c() {
}
console.log(i)
}
(a):代码首先进入到全局上下文的创建阶段。
ExecutionContextGlobal = {
scopeChain: {...},
variableObject: {
x: undefined,
foo: pointer to function foo()
},
this: {...}
}
然后进入全局执行上下文的执行阶段。这一阶段从上至下逐条执行代码,运行到console.log(foo(22))
该行时,创建阶段已经为variableObject中的foo赋值了,因此执行时会执行foo(22)
函数。
当执行foo(22)
函数时,又将进入foo()
的执行上下文,详见(b)。
当执行到console.log(x)
时,此时x
在variableObject中赋值为undefined
,因此打印出undefined
,这也正是变量提升产生的结果。
当执行到var x = 'hello world';
,variableObject中的x被赋值为hello world
。
继续往下是foo
函数的声明,因此什么也不做,执行阶段结束。下面是执行阶段完成后的ExecutionContextGlobal。
ExecutionContextGlobal = {
scopeChain: {...},
variableObject: {
x: 'hello world',
foo: pointer to function foo()
},
this: {...}
}
(b):当js调用foo(22)时,进入到foo()函数的执行上下文,首先进行该上下文的创建阶段。
ExecutionContextFoo = {
scopeChain: {...},
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: undefined,
b: undefined
},
this: {...}
}
当执行阶段运行完后,ExecutionContextFoo如下。
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: 'hello',
b: pointer to function privateB()
},
this: { ... }
}
理清了JS中的执行上下文,就很容易明白变量提升具体是怎么回事了。在代码执行前,执行上下文已经给对应的声明赋值,只不过变量是赋值为undefined
,函数赋值为对应的引用,而后在执行阶段再将对应值赋值给变量。
5. 区分函数声明和函数表达式
首先看下面几个代码片段,分别输出是什么?
Question 1:
function foo(){
function bar() {
return 3;
}
return bar();
function bar() {
return 8;
}
}
alert(foo());
Question 2:
function foo(){
var bar = function() {
return 3;
};
return bar();
var bar = function() {
return 8;
};
}
alert(foo());
Question 3:
alert(foo());
function foo(){
var bar = function() {
return 3;
};
return bar();
var bar = function() {
return 8;
};
}
Question 4:
function foo(){
return bar();
var bar = function() {
return 3;
};
var bar = function() {
return 8;
};
}
alert(foo());
上面4个代码片段分别输出 8
,3
,3
,[Type Error: bar is not a function]
。
function name([param,[, param,[..., param]]]) { [statements] }
函数声明以关键字function
开头定义函数,同时有确定的函数名。如最简单的栗子:
function bar() {
return 3;
}
通过函数执行上下文,函数声明会产生hoisted,即函数声明会提升到代码最上面。
所以在Question 1中,foo.VO中 bar:pointer to the function bar()
,因为有声明了两次bar()
函数,所以后面的定义覆盖前面的定义。
var myFunction = function [name]([param1[, param2[, ..., paramN]]]) { statements };
函数表达式中,函数名字可以省略,简单栗子如下:
//anonymous function expression
var a = function() {
return 3;
}
//named function expression
var a = function bar() {
return 3;
}
//self invoking function expression
(function sayHello() {
alert("hello!");
})();
以上三种都是函数表达式,最后一种是立即执行函数。函数表达式不会提升到代码最上面,如Question 2中,在函数执行上下文的创建阶段中,foo.VO 中 bar : undefined
,在执行阶段才进行赋值。
在回头看看Question 4:
function foo(){
return bar(); // 执行阶段返回调用bar(),但创建阶段bar被赋值为 undefined,所以报Type Error。
var bar = function() {
return 3;
};
var bar = function() {
return 8;
};
}
alert(foo());