下面我们来看一段简单代码,试着理解其执行流程。
var height = 1.88
function outer() {
var age = 18
function inner() {
var name = 'xiaoPan'
console.log(name,age,height);
}
return inner
}
var fn = outer()
fn()
这段 JavaScript 代码涉及函数的定义、调用和闭包的概念。我们从代码的执行流程、执行上下文的创建、作用域链的生成、变量的赋值以及闭包的形成等方面进行详细解释。
1. 全局执行上下文的创建
当 JavaScript 开始执行时,首先会创建全局执行上下文。它主要包含三个部分:
- 变量对象 (VO):用于存储全局范围内的变量和函数声明。
- 作用域链 (Scope Chain):用于解析变量,它包含当前执行上下文的变量对象以及父级的变量对象,最终指向全局对象。
- this:在全局上下文中,
this指向全局对象(浏览器中为window,Node 中为global)。
1.1 全局执行上下文的创建阶段(预编译阶段)
在这个阶段,JavaScript 引擎会对变量和函数进行提升(Hoisting),即提前注册到内存中,但并不赋值。以下过程发生:
var height被提升,值为undefined。function outer被提升,整个函数体作为值赋给outer。
此时,全局上下文中的变量和函数是这样的:
var height = undefined;
function outer() { ... } // 函数声明提升
1.2 全局执行上下文的执行阶段
从上到下执行代码:
height = 1.88;:将height赋值为1.88。fn = outer();:调用outer()函数,并将其返回值赋给fn。这一点我们在后面详细分析。
2. outer() 执行:创建 outer 的执行上下文
当 outer() 被调用时,会创建 outer 函数的执行上下文。该上下文主要包含:
- 变量对象 (VO):存储
outer函数中声明的局部变量。 - 作用域链 (Scope Chain):包括当前
outer执行上下文的变量对象以及其父级(全局)上下文的变量对象。 - this:指向调用上下文。
2.1 outer() 的创建阶段
在创建 outer 函数的执行上下文时,outer 内部的变量 age 被提升,值为 undefined:
var age = undefined;
2.2 outer() 的执行阶段
接着,执行到 var age = 18;,为 age 赋值:
var age = 18;
在 outer() 中,inner 函数被声明并且携带了词法作用域(lexical scope)。也就是说,inner 函数在创建时已经记住了它所在的作用域——outer 函数的上下文。
最后,outer() 函数返回 inner 函数,赋值给全局变量 fn:
var fn = inner; // 此时 fn 是指向 inner 函数的引用
3. 闭包的形成
当 outer() 函数执行结束时,outer 的执行上下文被销毁,但由于 inner 函数形成了闭包,inner 仍然保留着对 outer 执行上下文中变量(如 age)的引用。这就是闭包的关键——即使 outer 执行完毕,inner 依然可以访问 outer 中的变量 age。
4. fn() 执行:创建 inner 的执行上下文
接下来,执行 fn(),即调用 inner 函数。此时会创建 inner 函数的执行上下文:
- 变量对象 (VO):存储
inner函数中的局部变量。 - 作用域链 (Scope Chain):包括当前
inner执行上下文的变量对象、outer执行上下文的变量对象,以及全局上下文的变量对象。 - this:指向调用上下文。
4.1 inner() 的创建阶段
在 inner 执行上下文中,局部变量 name 被提升,值为 undefined:
var name = undefined;
4.2 inner() 的执行阶段
接着执行 var name = 'xiaoPan';,将 name 赋值为 'xiaoPan'。随后执行 console.log(name, age, height)。
4.3 作用域链的解析
在 inner 执行时,JavaScript 引擎会根据作用域链依次查找变量:
name:在inner函数的变量对象中找到,值为'xiaoPan'。age:在inner的作用域链中找不到,于是沿着作用域链去outer函数的执行上下文中查找,找到age,值为18。height:在inner函数和outer函数的变量对象中都找不到,于是沿作用域链去全局上下文中查找,找到height,值为1.88。
最终,console.log(name, age, height) 打印出:
xiaoPan 18 1.88
总结
- 全局执行上下文创建时,
fn变量被声明但未赋值,outer函数被提升,height被赋值为1.88。 - 调用
outer()时,outer的执行上下文创建,age被定义并赋值为18,inner函数被定义并携带了对outer作用域的引用。outer返回inner函数,赋值给全局变量fn。 - 闭包:
fn(即inner函数)持有对outer执行上下文的引用,即使outer已经执行完毕,inner仍然可以访问age。 - 调用
fn()时,inner函数的执行上下文创建,name被赋值为'xiaoPan',通过作用域链可以访问到age和height,最终打印出'xiaoPan 18 1.88'。