对于下面这段代码,您觉得会输出什么?
var x = 10
function fn() {
console.log(x)
}
function show(f) {
var x = 20;
(function () {
f() // 10,而不是 20
})()
}
show(fn)
由于第8行的f()执行的就是第2行定于你的fn()函数,而第二行的函数fn()的创建是在全局作用域下的,所以无论在哪里调用函数fn()里面输出的x都是读取的是全局的x,在这里就会输出10
运行 JavaScript 代码时,当代码执行进入一个环境时,就会为该环境创建一个执行上下文,它会在你运行代码前做一些准备工作。接下来我们就来看一下具体会做哪些准备工作。
具体要做的事,和执行上下文的生命周期有关。
执行上下文的生命周期有两个阶段:
创建阶段
创建阶段要做的事情主要如下:
创建变量对象(VO:variable object)
确定函数的形参(并赋值)
函数环境会初始化创建 Arguments对象(并赋值)
确定普通字面量形式的函数声明(并赋值)
变量声明,函数表达式声明(未赋值)
确定 this 指向(this 由调用者确定)
确定作用域(词法环境决定,哪里声明定义,就在哪里确定)
这里有必要说一下变量对象。
当处于执行上下文的建立阶段时,我们可以将整个上下文环境看作是一个对象。该对象拥有 3 个属性,如下:
executionContextObj = {
variableObject : {}, // 变量对象,里面包含 Arguments 对象,形式参数,函数和局部变量
scopeChain : {},// 作用域链,包含内部上下文所有变量对象的列表
this : {}// 上下文中 this 的指向对象
}
可以看到,这里执行上下文抽象成为了一个对象,拥有 3 个属性,分别是变量对象,作用域链以及 this 指向,这里我们重点来看一下变量对象里面所拥有的东西。
在函数的建立阶段,首先会建立 Arguments 对象。然后确定形式参数,检查当前上下文中的函数声明,每找到一个函数声明,就在 variableObject 下面用函数名建立一个属性,属性值就指向该函数在内存中的地址的一个引用。
如果上述函数名已经存在于 variableObject(简称 VO) 下面,那么对应的属性值会被新的引用给覆盖。
最后,是确定当前上下文中的局部变量,如果遇到和函数名同名的变量,则会忽略该变量。
执行阶段
两个阶段要做的事情介绍完毕,接下来我们来通过代码来演示一下这两个阶段做的每一件事以及变量对象是如何变化的。
const foo = function(i){
var a = "Hello";
var b = function privateB(){};
function c(){}
}
foo(10);
首先在建立阶段的变量对象如下:
fooExecutionContext = {
variavleObject : {
arguments : {0 : 10,length : 1}, // 确定 Arguments 对象
i : 10, // 确定形式参数
c : pointer to function c(), // 确定函数引用
a : undefined, // 局部变量 初始值为 undefined
b : undefined // 局部变量 初始值为 undefined
},
scopeChain : {},
this : {}
}
由此可见,在建立阶段,除了 Arguments,函数的声明,以及形式参数被赋予了具体的属性值外,其它的变量属性默认的都是 undefined。并且普通形式声明的函数的提升是在变量的上面的。
一旦上述建立阶段结束,引擎就会进入代码执行阶段,这个阶段完成后,上述执行上下文对象如下,变量会被赋上具体的值。
fooExecutionContext = {
variavleObject : {
arguments : {0 : 10,length : 1},
i : 10,
c : pointer to function c(),
a : "Hello",// a 变量被赋值为 Hello
b : pointer to function privateB() // b 变量被赋值为 privateB() 函数
},
scopeChain : {},
this : {}
}
我们看到,只有在代码执行阶段,局部变量才会被赋予具体的值。在建立阶段局部变量的值都是 undefined。
这其实也就解释了变量提升的原理。