JavaScript 一面:作用域、闭包与 this 指向
很多面试一上来就会从“闭包/this”切一刀,这一篇试着把这几个高频问题盘清楚,避免只停留在背定义的层面。
作用域与作用域链
在大多数 JS 引擎里,可以按照“变量在哪一块代码中可见”来理解作用域:
- 全局作用域:在任何地方都能访问到的绑定;
- 函数作用域:只在函数体内可见的绑定;
- 块级作用域:
let/const/class/try...catch块产生的作用域。
当访问一个标识符时,引擎会沿着作用域链向上查找:
- 先看当前作用域是否声明过;
- 没有就去上层(外层函数、全局)找;
- 一直找到全局为止。
在一面里,常见的考法是:
- 给出几层嵌套函数,问某个变量实际读到的是哪一层的值;
- 混合使用
var/let/const,考察变量提升和暂时性死区(TDZ)。
关键点不是记所有细节,而是脑中有一个“从内到外、一层层找”的模型。
闭包:从定义到常见考点
简单说,闭包就是“函数 + 它能访问到的外部变量的组合”。
一个典型的例子:
1function createCounter() {
2 let count = 0;
3 return function () {
4 count++;
5 return count;
6 };
7}
8
9const counter = createCounter();
10counter(); // 1
11counter(); // 2
这里返回的匿名函数就形成了闭包:createCounter 已经执行完,但 count 仍然活着,因为外层还有引用指向那个函数环境。
在一面里,闭包常见考点有:
- 循环里的闭包:
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i)); }打印什么?为什么? - 私有变量:如何用闭包实现“外部无法直接访问的内部状态”?
- 内存泄漏风险:长生命周期闭包引用了大对象/DOM,忘记释放。
一个比较好的回答方式是:
- 先用一句话定义闭包;
- 再用 1~2 个简单例子说明“它能做什么好事”;
- 最后顺带提一下“用不好也会有代价(内存/调试复杂度)”。
this 指向:和作用域不是一回事
容易混淆的一点是:this 和作用域、闭包没有直接关系。this 的值只取决于“函数是怎么被调用的”,而不是“写在哪里”。
常见的几种调用方式:
- 直接调用:
fn()→ 在非严格模式下,this指向全局对象(浏览器里是window),严格模式下是undefined; - 作为对象方法:
obj.method()→this指向obj; - 构造函数调用:
new Fn()→this指向新创建的实例; - 显式绑定:
fn.call(obj)/fn.apply(obj)/fn.bind(obj)。
在一面题里,经常会把这几种用法混在一起,让你判断每种情况下 this 指向哪里。
箭头函数的 this:从外层捕获
箭头函数有一个关键特性:它的 this 来自定义时所在的外层词法环境,不会因为调用方式变化而改变。
例如:
1const obj = {
2 value: 42,
3 method() {
4 setTimeout(function () {
5 console.log(this.value); // 取决于谁调用 setTimeout 里的函数
6 }, 0);
7
8 setTimeout(() => {
9 console.log(this.value); // 始终是 42
10 }, 0);
11 }
12};
13
14obj.method();
这里第一个 setTimeout 里的 function 调用时 this 不是 obj,
而箭头函数里的 this 会从 method 的执行上下文里捕获,也就是 obj。
一面经常会问:
- “能不能用箭头函数当构造函数?”(不行)
- “在对象方法里全用箭头函数有什么坑?”(如
this不再指向对象本身)
常见 this 题:用几条规则快速判断
面对一面常见的 this 题,可以用几条规则简化判断:
- 先看是不是箭头函数:
- 是 →
this来自定义时的外层; - 否 → 继续往下看。
- 是 →
- 再看调用方式:
obj.method()→this === obj;fn()→ 非严格模式下是全局对象,严格模式下是undefined;new Fn()→this是新实例;fn.call/apply/bind(obj)→this === obj。
- 如果是回调(如
setTimeout(fn)),要看真正调用它的环境是谁(浏览器里多半是全局)。
面试时不需要把所有细节都背下来,关键是展示出你有一套稳定的分析顺序,而不是靠“死记输出结果”。
常见面试题与参考答案
这里补几道一面里非常典型的题,以及可以参考的答题方式。
题 1:循环里的闭包打印什么?怎么改?
1for (var i = 0; i < 3; i++) {
2 setTimeout(() => {
3 console.log(i);
4 }, 0);
5}
参考答案思路:
- 这段代码会输出
3, 3, 3。 - 因为
var没有块级作用域,循环结束时i已经变成3,三个定时器回调里的闭包都共享同一个i。 - 到真正执行回调时(宏任务阶段),读到的都是同一个最终值
3。
怎么改?
- 使用
let引入块级作用域:
1for (let i = 0; i < 3; i++) {
2 setTimeout(() => console.log(i), 0); // 0, 1, 2
3}
- 或者用立即执行函数创建额外作用域:
1for (var i = 0; i < 3; i++) {
2 ((j) => {
3 setTimeout(() => console.log(j), 0);
4 })(i);
5}
讲清“为什么错 + 怎么修”比只背输出更能展示你的理解。
题 2:this 在不同调用方式下分别指向哪里?
1var name = "global";
2
3const obj = {
4 name: "obj",
5 getName() {
6 console.log(this.name);
7 }
8};
9
10const fn = obj.getName;
11
12obj.getName(); // ?
13fn(); // ?
14fn.call({ name: "call" }); // ?
参考答案:
obj.getName()中,this === obj,输出"obj";fn()是普通函数调用,在非严格模式下this指向全局对象,输出"global";fn.call({ name: "call" })显式绑定了this,输出"call"。
可以顺带强调一句:“this 只和调用方式有关,不和函数定义位置、是否是对象属性直接相关”。
题 3:箭头函数能不能当构造函数?this 指向哪里?
1const Foo = () => {
2 this.value = 42;
3};
4
5const obj = new Foo(); // 会发生什么?
参考答案:
- 箭头函数不能作为构造函数使用,
new Foo()会抛出 TypeError:Foo is not a constructor; - 箭头函数没有自己的
this,它的this来自定义时的外层词法作用域。
顺带可以提一句:
“在对象方法里一股脑用箭头函数,容易让 this 都变成外部作用域,而不是对象本身,这是实际开发中常见的坑。”
小结:一面回答时可以怎么组织?
把这篇内容压缩到一面几分钟的回答,大致可以这样组织:
- 先用一两句话解释作用域和作用域链的直觉;
- 接着用一个简洁的例子说明什么是闭包、能做什么;
- 再强调一句:
this和作用域无关,只和调用方式有关; - 用几条规则总结如何快速判断
this指向; - 如果有时间,可以提一句闭包的内存/调试成本,表现出对工程实践的意识。
面试官通常不只是看你“记不记得概念”,更在意你能不能把这些东西串成一套自己真正理解过的判断逻辑。