阅读 2628

由变量提升谈谈 JavaScript Execution Context

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。因此调用栈最上方的执行上下文将最先被执行,执行完后返回到上层的执行上下文继续执行。引用一篇博文的动态图示如下:

execution stack

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引擎执行伪代码

  1. 找到调用函数
  2. 执行函数代码前,创建execution context
  3. 进行创建阶段:
    • 初始化调用链 Scope Chain
    • 创建 variable object:
      • 创建arguments对象,初始化该入参变量名和值
      • 扫描该执行上下文中声明的函数:
        • 对于声明的函数,variable object中创建对应的变量名,其值指向该函数(函数是存在heap中的)
        • 如果函数名已经存在,用新的引用值覆盖已有的
      • 扫描上下文中声明的变量:
        • 对于变量的声明,同样在variable object中创建对应的变量名,其值初始化为undefined
        • 如果变量的名字已经存在,则直接略过继续扫描
    • 决定上下文this的指向
  4. 代码执行阶段:
    • 执行函数内的代码并给对应变量进行赋值(创建阶段为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 Declaration)

function name([param,[, param,[..., param]]]) { [statements] }

函数声明以关键字function开头定义函数,同时有确定的函数名。如最简单的栗子:

function bar() {
    return 3;
}
复制代码

通过函数执行上下文,函数声明会产生hoisted,即函数声明会提升到代码最上面。

所以在Question 1中,foo.VO中 bar:pointer to the function bar(),因为有声明了两次bar()函数,所以后面的定义覆盖前面的定义。

函数表达式(Function expression)

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?

Execution context, Scope chain and JavaScript internals

JavaScript. The core.

文章分类
前端
文章标签