JavaScript 作用域核心知识点详解
1. 作用域的基本概念
什么是作用域?
作用域是变量和函数的可访问范围,决定了代码中变量和函数的可见性。
作用域的类型
- 词法作用域(静态作用域) :JavaScript 采用的作用域类型,在代码编写时确定
- 动态作用域:在代码运行时确定(JavaScript 不使用)
2. 作用域的种类
2.1 全局作用域(Global Scope)
javascript
var globalVar = 'I am global';
function test() {
console.log(globalVar); // 可以访问
}
// 以下也是全局作用域
window.globalVar2 = 'I am also global';
特点:
- 在任何地方都可以访问
- 在浏览器中是
window对象 - 生命周期与应用程序相同
2.2 函数作用域(Function Scope)
javascript
function outer() {
var functionScoped = 'I am function scoped';
function inner() {
console.log(functionScoped); // 可以访问
}
inner();
}
console.log(functionScoped); // ReferenceError
特点:
- 使用
var、function声明 - 只在函数内部可访问
- 每次函数调用都会创建新的作用域
2.3 块级作用域(Block Scope) - ES6+
javascript
{
let blockScoped = 'I am block scoped';
const alsoBlockScoped = 'Me too';
console.log(blockScoped); // 可以访问
}
console.log(blockScoped); // ReferenceError
// 常见块级作用域
if (true) {
let conditionScoped = 'only in if block';
}
for (let i = 0; i < 3; i++) {
// 每次循环都有独立的 i
}
特点:
- 使用
let、const声明 - 在
{}内部可访问 - 避免变量污染和意外覆盖
3. 作用域链(Scope Chain)
作用域链的形成
javascript
var global = 'global';
function outer() {
var outerVar = 'outer';
function inner() {
var innerVar = 'inner';
console.log(innerVar); // 当前作用域
console.log(outerVar); // 外层作用域
console.log(global); // 全局作用域
}
inner();
}
outer();
作用域链查找规则:
- 在当前作用域查找变量
- 如果找不到,向上一层作用域查找
- 重复步骤2直到全局作用域
- 如果全局作用域也找不到,抛出
ReferenceError
作用域链的创建时机
javascript
function createScopeChain() {
var parentVar = 'parent';
// 函数定义时作用域链就已确定
return function child() {
console.log(parentVar); // 可以访问父级变量
};
}
const childFunc = createScopeChain();
childFunc(); // "parent"
4. 变量提升(Hoisting)
var 的变量提升
javascript
console.log(a); // undefined
var a = 10;
console.log(a); // 10
// 实际执行顺序:
// var a; // 声明提升
// console.log(a); // undefined
// a = 10; // 赋值
// console.log(a); // 10
函数声明提升
javascript
foo(); // "Hello" - 可以调用
function foo() {
console.log("Hello");
}
// 函数表达式不会整体提升
bar(); // TypeError: bar is not a function
var bar = function() {
console.log("World");
};
let/const 的暂时性死区(TDZ)
javascript
console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 10;
// 暂时性死区:从块开始到变量声明之间的区域
{
console.log(b); // ReferenceError
let b = 20;
}
5. 闭包(Closure)
闭包的定义
函数能够记住并访问其词法作用域,即使函数在其它地方执行。
闭包的形成
javascript
function createCounter() {
let count = 0; // 私有变量
// 返回的函数形成了闭包
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
闭包的实际应用
javascript
// 1. 模块模式
const calculator = (function() {
let memory = 0;
return {
add: function(x) { memory += x; },
subtract: function(x) { memory -= x; },
getMemory: function() { return memory; }
};
})();
// 2. 事件处理
function setupButtons() {
for (let i = 0; i < 3; i++) {
document.getElementById(`btn-${i}`).addEventListener('click', function() {
console.log(`Button ${i} clicked`);
});
}
}
// 3. 函数柯里化
function multiply(a) {
return function(b) {
return a * b;
};
}
const double = multiply(2);
console.log(double(5)); // 10
6. 执行上下文与作用域的关系
执行上下文的创建阶段
javascript
function example(a) {
var b = 2;
let c = 3;
function inner() {
console.log(a, b, c);
}
return inner;
}
// 创建阶段:
// 1. 创建变量对象(VO)
// 2. 建立作用域链
// 3. 确定this指向
7. ES6+ 作用域新特性
块级作用域的实际应用
javascript
// 避免循环中的闭包问题
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 0, 1, 2
}, 100);
}
// 替代IIFE
{
let temp = 'temporary value';
// 使用temp...
}
// temp 在这里不可访问
// 在switch语句中
switch (condition) {
case 1: {
let message = 'case 1';
break;
}
case 2: {
let message = 'case 2'; // 不会冲突
break;
}
}
const 与不可变性
javascript
const PI = 3.14159;
// PI = 3.14; // TypeError: Assignment to constant variable
const obj = { name: 'John' };
obj.name = 'Jane'; // 允许 - 修改属性
// obj = {}; // 不允许 - 重新赋值
const arr = [1, 2, 3];
arr.push(4); // 允许
// arr = []; // 不允许
8. 作用域的最佳实践
1. 避免全局变量污染
javascript
// 不好
var globalData = 'data';
// 好
(function() {
var localData = 'data';
// 使用模块模式或ES6模块
})();
// 更好 - 使用模块
export const data = 'data';
2. 合理使用块级作用域
javascript
// 使用 let/const 替代 var
for (let i = 0; i < 10; i++) {
// i 只在循环内有效
}
// 使用块限制变量作用域
{
const temp = calculateSomething();
useTemp(temp);
}
// temp 自动回收
3. 管理闭包内存
javascript
function createHeavyClosure() {
const heavyData = new Array(1000000);
return function() {
// 使用 heavyData...
};
}
const closure = createHeavyClosure();
// 使用完后手动释放
closure = null;
4. 使用严格模式
javascript
'use strict';
function strictFunction() {
undeclaredVar = 10; // ReferenceError
delete variable; // SyntaxError
}
9. 常见作用域陷阱
陷阱1:循环中的闭包
javascript
// 问题
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 3, 3, 3
}
// 解决方案
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 0, 1, 2
}
陷阱2:变量提升混淆
javascript
var name = 'global';
function test() {
console.log(name); // undefined,不是 'global'
var name = 'local';
}
test();
陷阱3:意外的全局变量
javascript
function createGlobal() {
undeclared = 'I become global!'; // 没有 var/let/const
}
createGlobal();
console.log(undeclared); // 'I become global!'
10. 总结
| 概念 | 关键点 | 示例 |
|---|---|---|
| 全局作用域 | 整个程序可见 | var global = 'value' |
| 函数作用域 | var, function | function foo() { var local = 1 } |
| 块级作用域 | let, const | { let block = 1 } |
| 作用域链 | 从内向外查找 | inner → outer → global |
| 变量提升 | var 和函数声明提升 | console.log(a); var a = 1 |
| 暂时性死区 | let/const 声明前不可访问 | console.log(b); let b = 2 |
| 闭包 | 函数记住词法作用域 | function outer() { return function inner() {} } |
理解作用域是掌握 JavaScript 的核心,它影响着变量生命周期、内存管理和代码结构设计。
基础作用域题目
题目 1:变量提升
javascript
console.log(a);
var a = 10;
console.log(a);
答案:
undefined
10
知识点解析:
- 变量提升:
var声明的变量会被提升到当前作用域的顶部 - 创建阶段:
a被声明并初始化为undefined - 执行阶段:第一个
console.log时a还未赋值,第二个console.log时已赋值为10
题目 2:函数提升
foo();
function foo() {
console.log('foo called');
}
bar();
var bar = function() {
console.log('bar called');
};
答案:
foo called
TypeError: bar is not a function
知识点解析:
- 函数声明整体提升:
function foo(){}整个函数被提升 - 函数表达式只提升变量声明:
var bar被提升,但赋值不提升 bar()调用时bar是undefined,不是函数
题目 3:作用域链查找
var x = 1;
function outer() {
var x = 2;
function inner() {
console.log(x);
}
inner();
}
outer();
答案: 2
知识点解析:
- 作用域链:
inner→outer→ 全局 - 变量查找从内向外,找到最近的
x就停止 inner找到outer中的x = 2
题目 4:块级作用域
function test() {
if (true) {
var a = 1;
let b = 2;
const c = 3;
}
console.log(a);
console.log(b);
console.log(c);
}
test();
答案:
1
ReferenceError: b is not defined
知识点解析:
var没有块级作用域,只有函数作用域let和const有块级作用域,只在{}内有效b和c在if块外不可访问
闭包相关题目
题目 5:经典闭包
function createCounter() {
let count = 0;
return function() {
count++;
console.log(count);
};
}
const counter1 = createCounter();
const counter2 = createCounter();
counter1();
counter1();
counter2();
counter1();
答案:
1
2
1
3
知识点解析:
- 每次调用
createCounter()都会创建新的词法环境 counter1和counter2有各自独立的count- 闭包保持对外部变量的引用
题目 6:循环中的闭包问题
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 100);
}
答案: 3 3 3
知识点解析:
var i在全局/函数作用域,循环结束后i = 3- 所有回调函数共享同一个
i - 事件循环执行时
i已经是最终值
题目 7:循环闭包解决方案
// 方案1:使用IIFE
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, 100);
})(i);
}
// 方案2:使用let
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 100);
}
答案: 0 1 2(两种方案都输出)
知识点解析:
- IIFE:每次循环创建新作用域,捕获当前
i的值 - let:每次循环创建新的块级作用域,每个
i都是独立的
题目 8:模块模式
function createModule() {
let privateVar = 0;
return {
getValue: function() {
return privateVar;
},
setValue: function(val) {
privateVar = val;
},
increment: function() {
privateVar++;
}
};
}
const module = createModule();
console.log(module.getValue());
module.increment();
console.log(module.getValue());
module.setValue(10);
console.log(module.getValue());
答案: 0 1 10
知识点解析:
- 模块模式利用闭包创建私有变量
privateVar只能在模块内部访问- 对外暴露的接口可以操作私有变量
作用域链查找题目
题目 9:多层嵌套作用域
var a = 1;
function level1() {
var a = 2;
function level2() {
var a = 3;
function level3() {
console.log(a);
}
level3();
}
level2();
}
level1();
答案: 3
知识点解析:
- 作用域链:
level3→level2→level1→ 全局 - 变量查找遵循"最近原则"
level3找到level2中的a = 3
题目 10:作用域链中断
var a = 1;
function test() {
console.log(a);
var a = 2;
}
test();
答案: undefined
知识点解析:
- 变量提升:函数内的
var a被提升 - 创建阶段:
a被声明并初始化为undefined - 第一个
console.log时,局部变量a已存在但未赋值
题目 11:全局变量污染
function func1() {
x = 10; // 没有使用 var/let/const
}
function func2() {
console.log(x);
}
func1();
func2();
答案: 10
知识点解析:
- 未声明的变量赋值会创建全局变量
- 在严格模式下会报错
- 应该避免这种隐式全局变量
题目 12:严格模式的影响
'use strict';
function test() {
x = 10; // 这里会发生什么?
console.log(x);
}
test();
答案: ReferenceError: x is not defined
知识点解析:
- 严格模式禁止隐式创建全局变量
- 未声明的变量赋值会抛出错误
- 提高代码质量和安全性
综合题目
题目 13:作用域链 + this
var name = 'Global';
var obj = {
name: 'Object',
getName: function() {
return function() {
return this.name;
};
},
getNameArrow: function() {
return () => {
return this.name;
};
}
};
console.log(obj.getName()());
console.log(obj.getNameArrow()());
答案:
Global
Object
知识点解析:
- 普通函数:
this取决于调用方式,独立调用指向全局 - 箭头函数:
this继承自外层作用域,指向obj
题目 14:闭包内存泄漏
function createHeavyObject() {
const heavyData = new Array(1000000).fill('heavy data');
return function() {
console.log('Closure still holds reference to heavyData');
};
}
let closure = createHeavyObject();
// 如何释放 heavyData 的内存?
答案: 设置 closure = null
知识点解析:
- 闭包会保持对外部变量的引用
- 即使函数不再使用,引用的变量也不会被垃圾回收
- 手动解除引用可以释放内存
题目 15:作用域链性能
function createNestedFunctions(depth) {
let value = 0;
function createLevel(currentDepth) {
if (currentDepth >= depth) {
return function() {
return value;
};
}
return createLevel(currentDepth + 1);
}
return createLevel(0);
}
const deepClosure = createNestedFunctions(1000);
console.log(deepClosure());
答案: 0
知识点解析:
- 深层嵌套的作用域链会影响性能
- 变量查找需要沿着作用域链向上搜索
- 现代 JS 引擎会优化这种访问
题目 16:块级作用域与循环
// 使用 var
console.log('Using var:');
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 使用 let
console.log('Using let:');
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
答案:
Using var:
3
3
3
Using let:
0
1
2
知识点解析:
var:函数作用域,循环结束后i=3let:块级作用域,每次循环创建新的i
题目 17:函数参数作用域
var x = 1;
function test(x) {
console.log(x);
x = 2;
console.log(x);
}
test(10);
console.log(x);
答案:
10
2
1
知识点解析:
- 函数参数相当于在函数作用域内声明变量
- 修改参数不会影响外部变量
- 参数作用域优先级高于外部作用