很多同学对 “原型链(找属性/方法)” 和 “作用域链(找变量/this)” 这两套 JS 最核心的查找机制存在混淆。
- 原型链是对象之间通过
__proto__建立的静态关系。 - 作用域链和闭包是函数在执行时,由于词法环境(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();
现在要求直接回答以下三个问题。请注意,不要长篇大论背诵概念,必须严格结合这段代码的执行过程来解释:
- obj.fn() 的精确输出是什么?
- 请用“执行上下文(Execution Context)”和“词法作用域(Lexical Scope)”的专业术语,向我还原 V8 引擎在执行 console.log(a) 时,查找变量 a 的完整生命周期。 (我是怎么找到它的?经过了哪些步骤?)
- 既然函数是通过 obj.fn() 调用的,请问此时 inner 函数内部的 this 指向谁?为什么这个 this 的指向,完全没有影响到最终打印的 a 的值?
参考答案
面试官:精确输出是什么?
你: 输出是 2。
面试官:用底层术语还原 V8 引擎查找变量 a 的过程。
你:
变量的查找依赖于词法作用域(Lexical Scope) ,这意味着作用域链是在函数定义(书写代码)时就已经静态决定了,而不是在调用时决定的。
具体过程如下:
- 当执行 obj.fn() 时,V8 引擎为 inner 函数创建了一个新的执行上下文(Execution Context) ,推入调用栈。
- 引擎执行到 console.log(a) 时,首先在 inner 上下文的本地变量环境中寻找 a,没找到。
- 于是引擎沿着作用域链(Scope Chain) 向外层查找。因为 inner 函数是定义在 outer 函数内部的,所以 inner 的外部引用(Outer Reference)指向了 outer 函数的执行上下文。
- 在 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、以及指向娘家人的线索(作用域链)。函数执行完,如果没形成闭包,这个草台班子就拆了。