你能讲清楚这段代码的执行流程吗?

102 阅读4分钟

下面我们来看一段简单代码,试着理解其执行流程。

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

总结

  1. 全局执行上下文创建时,fn 变量被声明但未赋值,outer 函数被提升,height 被赋值为 1.88
  2. 调用 outer() 时,outer 的执行上下文创建,age 被定义并赋值为 18inner 函数被定义并携带了对 outer 作用域的引用。outer 返回 inner 函数,赋值给全局变量 fn
  3. 闭包fn(即 inner 函数)持有对 outer 执行上下文的引用,即使 outer 已经执行完毕,inner 仍然可以访问 age
  4. 调用 fn() 时,inner 函数的执行上下文创建,name 被赋值为 'xiaoPan',通过作用域链可以访问到 ageheight,最终打印出 'xiaoPan 18 1.88'