一、JavaScript 作用域的核心概念
在JavaScript面试中,js面试题 作用域几乎是必问的基础知识点。理解作用域不仅是写出正确代码的前提,更是理解JavaScript执行机制的基石。作用域(Scope)定义了变量和函数的可访问范围。简单来说,它决定了“在哪里可以找到变量”。
1.1 词法作用域(Lexical Scoping)
JavaScript使用的是词法作用域,这意味着函数的作用域在函数定义的时候就决定了,而不是在函数调用的时候。这与动态作用域(如Bash脚本)截然不同。
⚡ 静态绑定
变量的查找路径在代码编写阶段(词法分析阶段)就已经确定。无论函数在哪里被调用,它都只能访问定义它时的环境。
⚡ 嵌套结构
函数可以嵌套定义,内层函数可以访问外层函数的变量,形成作用域链。这种嵌套关系在代码书写时就是固定的。
// 示例:词法作用域
var a = 10;
function foo() {
var b = 20;
function bar() {
console.log(a); // 10,访问全局变量
console.log(b); // 20,访问外层函数变量
}
bar();
}
function baz() {
var a = 30;
bar(); // 报错 ReferenceError: bar is not defined
// 因为bar定义在foo内部,词法上只能被foo访问
}
二、作用域链与执行上下文
理解js面试题 作用域必须结合执行上下文(Execution Context)。当代码执行时,会创建一个执行上下文栈(ECS),每个上下文都关联着一个变量对象(VO)或活动对象(AO)。
2.1 变量提升(Hoisting)
在ES5及之前,var声明的变量会发生提升。这意味着变量声明会被移动到当前作用域的顶部,但赋值不会。
| 关键字 | 作用域类型 | 是否提升 | 暂时性死区(TDZ) |
|---|---|---|---|
| var | 函数作用域 | 是 | 否 |
| let | 块级作用域 | 是(不初始化) | 是 |
| const | 块级作用域 | 是(不初始化) | 是 |
// 变量提升示例
console.log(a); // undefined,而不是报错
var a = 10;
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 20;
三、闭包:高级面试题的核心
闭包(Closure)是js面试题 作用域相关最常被问到的难点。闭包是指有权访问另一个函数作用域中变量的函数。通常,当一个函数返回另一个函数时,就形成了闭包。
3.1 闭包的本质
闭包的存在使得外部函数执行完毕后,其内部的局部变量依然保存在内存中,不会被垃圾回收机制回收。这既带来了优势,也带来了潜在的性能问题。
1. 数据私有化
模拟私有变量,防止全局污染。例如在模块模式中,通过闭包暴露公共API,隐藏内部实现。
2. 函数柯里化
利用闭包保存已传入的参数,实现函数的部分应用。
3. 异步编程
在循环中绑定事件处理函数时,闭包可以捕获当前的循环变量值。
1. 内存泄漏
如果闭包长期持有对大对象的引用,且未被释放,可能导致内存占用过高。
2. 意外修改
外部代码可能通过闭包访问并修改内部变量,破坏封装性。
1. 及时释放
在不再需要闭包时,将引用设置为 null,帮助垃圾回收。
2. 严格模式
使用 'use strict' 减少意外的全局变量创建。
// 经典闭包面试题
function createCounter() {
let count = 0;
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
四、ES6 块级作用域与 let/const
ES6 引入了 let 和 const,引入了块级作用域(Block Scope),即花括号 {} 内的作用域。这是现代JavaScript开发的标准实践。
4.1 块级作用域的优势
- 防止变量污染: 变量仅在定义的块内有效,退出块后即被销毁。
- 避免循环变量泄漏: 在
for循环中使用let,每次迭代都会创建一个新的词法环境。 - 暂时性死区(TDZ): 在变量声明之前访问变量会抛出错误,有助于发现代码逻辑错误。
// for循环中的经典陷阱
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1);
}
// 输出: 3, 3, 3 (var 是函数作用域,共享同一个 i)
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 1);
}
// 输出: 0, 1, 2 (let 是块级作用域,每次迭代 j 都是新的)
五、高频 JS 面试题 作用域 真题解析
以下是基于真实面试场景整理的js面试题 作用域相关题目,附带深度解析。
for (var i = 1; i <= 3; i++) {
setTimeout(() => {
console.log(i);
}, 0);
console.log(i);
}
解析: 首先输出同步代码 1, 2, 3。由于 var 的函数作用域特性,i 是全局的,当定时器执行时,i 已经变成了 4。因此异步输出 4, 4, 4。
function createIncrementer() {
let count = 0;
return function() {
return ++count;
};
}
const inc = createIncrementer();
console.log(inc()); // 1
console.log(inc()); // 2
解析: 利用闭包保存状态 count。每次调用 inc 函数,都会访问同一个 count 变量并使其递增。
解析: 箭头函数没有自己的 this,也没有自己的 arguments 对象。它捕获其定义所在位置的词法作用域中的 this 值。这意味着箭头函数的作用域绑定是静态的,无法通过 call, apply 或 bind 改变。
🔥 网友们还关心
除了基础的js面试题 作用域,以下周边知识也是面试官常问的重点: