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());
参考
What is the Execution Context & Stack in JavaScript?
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。