[执行上下文与闭包01] 开胃小菜:闭包声明与函数赋值

3 阅读3分钟

很多同学对 “原型链(找属性/方法)”“作用域链(找变量/this)” 这两套 JS 最核心的查找机制存在混淆。

  1. 原型链是对象之间通过 __proto__ 建立的静态关系。
  2. 作用域链和闭包是函数在执行时,由于词法环境(Lexical Environment)产生的动态内存驻留。

"很多前端写了几年代码,依然分不清这两者的边界。如果你能在高压下理清执行上下文(Execution Context)变量提升(Hoisting)闭包内存泄露以及 this 的四种绑定规则,你的 JS 底层就算真正打通了。"

第一轮提问

var a = 1;
function outer() {
    var a = 2;
    return function inner() {
        console.log(a);
    }
}
var fn = outer();
var obj = { a: 3, fn: fn };
obj.fn();

现在要求直接回答以下三个问题。请注意,不要长篇大论背诵概念,必须严格结合这段代码的执行过程来解释

  1. obj.fn() 的精确输出是什么?
  2. 请用“执行上下文(Execution Context)”和“词法作用域(Lexical Scope)”的专业术语,向我还原 V8 引擎在执行 console.log(a) 时,查找变量 a 的完整生命周期。 (我是怎么找到它的?经过了哪些步骤?)
  3. 既然函数是通过 obj.fn() 调用的,请问此时 inner 函数内部的 this 指向谁?为什么这个 this 的指向,完全没有影响到最终打印的 a 的值?

参考答案

面试官:精确输出是什么?

你:  输出是 2。

面试官:用底层术语还原 V8 引擎查找变量 a 的过程。

你:
变量的查找依赖于词法作用域(Lexical Scope) ,这意味着作用域链是在函数定义(书写代码)时就已经静态决定了,而不是在调用时决定的。
具体过程如下:

  1. 当执行 obj.fn() 时,V8 引擎为 inner 函数创建了一个新的执行上下文(Execution Context) ,推入调用栈。
  2. 引擎执行到 console.log(a) 时,首先在 inner 上下文的本地变量环境中寻找 a,没找到。
  3. 于是引擎沿着作用域链(Scope Chain) 向外层查找。因为 inner 函数是定义在 outer 函数内部的,所以 inner 的外部引用(Outer Reference)指向了 outer 函数的执行上下文。
  4. 在 outer 产生的闭包环境中,引擎找到了 var a = 2,于是直接返回 2。哪怕外面全局有个 var a = 1,也会被就近原则屏蔽掉。

面试官:this 指向谁?为什么 this 没有影响 a 的值?

你:
this 指向 obj。因为 fn 是作为 obj 的方法被点语法调用的,触发了 this 的隐式绑定规则
但这完全不影响打印结果。因为在 JS 中,变量的解析规则(作用域链)和对象的属性访问规则(原型链)是完全隔离的。
代码中打印的是变量 a,引擎只会顺着词法作用域链去闭包里找;如果代码改写成 console.log(this.a),引擎才会根据 this 的指向,去 obj 对象里找属性 a,这时候输出的才会是 3。


复盘时间:帮你理清这两个“模糊的能指”

如果你容易混淆这些术语,给你一个极其简单的记忆口诀:

  • 词法作用域(Lexical Scope)  = 娘胎里带出来的。  
    • 函数写在哪里,它能访问外部的哪些变量,在敲下代码的那一刻就定死了,执行的时候就算被带到天涯海角去调用,它认的还是娘家人。
  • 执行上下文(Execution Context)  = 案发现场。  
    • 函数每次被调用时,V8 引擎临时搭的草台班子(分配的内存),里面装了函数的局部变量、this、以及指向娘家人的线索(作用域链)。函数执行完,如果没形成闭包,这个草台班子就拆了。