06.JavaScript 作用域与变量详解
一、什么是作用域
1. 作用域的定义
作用域(Scope)是指程序中定义变量的区域,它决定了变量的可见性和生命周期。
2. 作用域的主要功能
- 变量可见性:确定在代码的哪些部分可以访问变量
- 变量生命周期:确定变量何时被创建和销毁
- 命名空间管理:防止变量命名冲突
二、JavaScript 作用域类型
1. 全局作用域(Global Scope)
// 全局作用域中的变量
var globalVar = "我是全局变量";
let globalLet = "我也是全局变量";
const globalConst = "我还是全局变量";
function showGlobal() {
console.log(globalVar); // 可以访问
console.log(globalLet); // 可以访问
console.log(globalConst); // 可以访问
}
showGlobal();
console.log(globalVar); // 在函数外部也可以访问
特点:
- 在代码最外层声明的变量
- 在任何地方都可以访问
- 生命周期:页面加载时创建,页面关闭时销毁
- 浏览器中全局变量会成为
window对象的属性(使用var声明时)
2. 函数作用域(Function Scope)
function myFunction() {
// 函数作用域中的变量
var functionVar = "我是函数作用域变量";
let functionLet = "我也是函数作用域变量";
console.log(functionVar); // 可以访问
console.log(functionLet); // 可以访问
function innerFunction() {
console.log(functionVar); // 可以访问外层函数的变量
let innerLet = "内层函数变量";
console.log(innerLet); // 可以访问
}
innerFunction();
// console.log(innerLet); // 错误!无法访问内层函数的变量
}
myFunction();
// console.log(functionVar); // 错误!无法访问函数内部的变量
特点:
- 在函数内部声明的变量
- 只能在函数内部访问
- 生命周期:函数调用时创建,函数执行完毕后销毁
var声明的变量具有函数作用域
3. 块级作用域(Block Scope,ES6+)
// 块级作用域示例
if (true) {
// 块级作用域
let blockLet = "我是块级作用域变量";
const blockConst = "我也是块级作用域变量";
var blockVar = "我使用var声明,不是块级作用域";
console.log(blockLet); // 可以访问
console.log(blockConst); // 可以访问
console.log(blockVar); // 可以访问
}
console.log(blockVar); // 可以访问(var没有块级作用域)
// console.log(blockLet); // 错误!无法访问块级作用域变量
// console.log(blockConst); // 错误!
// 循环中的块级作用域
for (let i = 0; i < 3; i++) {
// 每个循环迭代都有独立的 i
setTimeout(function() {
console.log(i); // 0, 1, 2
}, 100);
}
// 对比:使用var
for (var j = 0; j < 3; j++) {
setTimeout(function() {
console.log(j); // 3, 3, 3(所有回调共享同一个j)
}, 100);
}
特点:
- 由一对花括号
{}定义的区域 - 使用
let和const声明的变量具有块级作用域 - 生命周期:进入块时创建,离开块时销毁
- 常见于:
if、for、while、switch等语句
三、全局变量 vs 局部变量
对比表格
| 特性 | 全局变量 | 局部变量 |
|---|---|---|
| 声明位置 | 函数外部 | 函数内部或代码块内部 |
| 作用域 | 全局作用域 | 函数作用域或块级作用域 |
| 访问范围 | 任何地方都可以访问 | 只能在声明的作用域内访问 |
| 生命周期 | 页面加载时创建,页面关闭时销毁 | 函数/块执行时创建,执行完毕后销毁 |
| 内存占用 | 长期占用内存 | 临时占用,执行完释放 |
| 命名冲突 | 容易冲突 | 相对安全 |
| 推荐程度 | 尽量避免使用 | 推荐使用 |
代码示例对比
// 全局变量 - 不推荐
var globalCounter = 0;
function incrementGlobal() {
globalCounter++;
console.log("全局计数器:", globalCounter);
}
function resetGlobalCounter() {
globalCounter = 0; // 可能在其他地方被意外修改
console.log("重置全局计数器");
}
incrementGlobal(); // 全局计数器: 1
incrementGlobal(); // 全局计数器: 2
resetGlobalCounter(); // 重置全局计数器
console.log(globalCounter); // 0
// 局部变量 - 推荐
function createCounter() {
let localCounter = 0; // 局部变量,外部无法访问
return {
increment: function() {
localCounter++;
console.log("局部计数器:", localCounter);
return localCounter;
},
reset: function() {
localCounter = 0;
console.log("计数器已重置");
},
getValue: function() {
return localCounter;
}
};
}
const myCounter = createCounter();
myCounter.increment(); // 局部计数器: 1
myCounter.increment(); // 局部计数器: 2
myCounter.reset(); // 计数器已重置
console.log(myCounter.getValue()); // 0
// console.log(localCounter); // 错误!无法直接访问局部变量
四、作用域链(Scope Chain)
1. 作用域链的概念
当访问一个变量时,JavaScript 引擎会按照以下顺序查找:
- 当前作用域
- 外层作用域
- 再外层作用域
- 直到全局作用域
2. 作用域链示例
// 全局作用域
let globalValue = "全局";
function outerFunction() {
// 外层函数作用域
let outerValue = "外层";
function middleFunction() {
// 中间函数作用域
let middleValue = "中间";
function innerFunction() {
// 内层函数作用域
let innerValue = "内层";
console.log(innerValue); // "内层" - 当前作用域
console.log(middleValue); // "中间" - 向外一层
console.log(outerValue); // "外层" - 向外两层
console.log(globalValue); // "全局" - 全局作用域
}
innerFunction();
}
middleFunction();
}
outerFunction();
3. 作用域链可视化
全局作用域 [globalValue]
↑
外层函数作用域 [outerValue]
↑
中间函数作用域 [middleValue]
↑
内层函数作用域 [innerValue]
五、变量提升(Hoisting)
1. var 的变量提升
console.log(myVar); // undefined(不会报错)
var myVar = "Hello";
console.log(myVar); // "Hello"
// 实际执行顺序相当于:
// var myVar; // 声明提升到顶部
// console.log(myVar); // undefined
// myVar = "Hello"; // 赋值保持原位
// console.log(myVar); // "Hello"
2. let 和 const 的暂时性死区(TDZ)
// console.log(myLet); // 错误!Cannot access 'myLet' before initialization
let myLet = "Hello";
// console.log(myConst); // 错误!
const myConst = "World";
3. 函数提升
sayHello(); // "Hello!"(可以正常调用)
function sayHello() {
console.log("Hello!");
}
// 函数表达式不会提升
// sayGoodbye(); // 错误!Cannot access 'sayGoodbye' before initialization
const sayGoodbye = function() {
console.log("Goodbye!");
};
六、闭包(Closure)与作用域
1. 闭包的基本概念
闭包是指函数能够记住并访问其词法作用域,即使该函数在其词法作用域之外执行。
2. 闭包示例
function createCounter() {
let count = 0; // 局部变量,外部无法直接访问
// 返回一个函数,形成闭包
return function() {
count++; // 可以访问外层函数的count变量
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// count变量被闭包"记住"了,不会被垃圾回收
3. 闭包的实用场景
// 1. 数据封装
function createPerson(name) {
let age = 0; // 私有变量
return {
getName: () => name,
getAge: () => age,
setAge: (newAge) => {
if (newAge >= 0) age = newAge;
},
birthday: () => age++
};
}
const person = createPerson("Alice");
person.setAge(25);
person.birthday();
console.log(person.getName(), person.getAge()); // "Alice", 26
// 2. 函数工厂
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// 3. 事件处理中的闭包
function setupButtons() {
for (let i = 1; i <= 3; i++) {
// 使用let创建块级作用域,每个循环都有独立的i
const button = document.createElement("button");
button.textContent = `按钮 ${i}`;
button.onclick = function() {
alert(`你点击了按钮 ${i}`);
};
document.body.appendChild(button);
}
}
七、严格模式(Strict Mode)
1. 启用严格模式
"use strict"; // 在整个脚本或函数开头添加
// 或者只在函数内启用
function strictFunction() {
"use strict";
// 严格模式代码
}
2. 严格模式对作用域的影响
"use strict";
// 1. 不允许未声明就赋值(防止创建意外全局变量)
// undeclaredVar = 10; // 错误!ReferenceError
// 2. 不允许删除变量、函数、函数参数
var x = 10;
// delete x; // 错误!SyntaxError
// 3. 函数参数不能重名
// function duplicateParams(a, a, b) { } // 错误!SyntaxError
// 4. 禁止使用 with 语句
// with (Math) { console.log(PI); } // 错误!SyntaxError
// 5. 保留字不能用作变量名
// let let = 10; // 错误!SyntaxError
八、最佳实践
1. 变量声明最佳实践
// 1. 优先使用 const,其次 let,避免 var
const PI = 3.14159; // 不会改变的常量
let username = "John"; // 可能改变的值
// var oldWay = "avoid this"; // 避免使用
// 2. 声明时初始化
let count = 0; // 好的做法
let total; // 避免(除非必要)
// total = calculateTotal(); // 稍后赋值
// 3. 使用块级作用域限制变量可见性
{
const temp = calculateValue();
console.log(temp);
}
// temp在这里不可访问,减少命名冲突
// 4. 避免全局变量污染
// 不好的做法:很多全局变量
// var user, config, cache, logger, ...;
// 好的做法:使用命名空间或模块
const App = {
user: {},
config: {},
cache: {},
init: function() { /* ... */ }
};
2. 作用域管理技巧
// 1. IIFE(立即调用函数表达式)创建私有作用域
(function() {
// 私有变量,不会污染全局作用域
const privateData = "secret";
function privateHelper() {
console.log(privateData);
}
// 暴露公共接口
window.MyModule = {
publicMethod: function() {
privateHelper();
}
};
})();
// 2. 模块模式
const CounterModule = (function() {
let privateCount = 0;
function changeBy(val) {
privateCount += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCount;
}
};
})();
CounterModule.increment();
console.log(CounterModule.value()); // 1
// 3. 使用闭包时注意内存泄漏
function createHeavyClosure() {
const largeData = new Array(1000000).fill("data");
return function() {
// 闭包引用了largeData,即使不再需要也不会被回收
console.log(largeData.length);
};
}
// 解决方案:在不再需要时清除引用
function createSafeClosure() {
let largeData = new Array(1000000).fill("data");
const closure = function() {
if (largeData) {
console.log(largeData.length);
}
};
// 提供清理方法
closure.cleanup = function() {
largeData = null;
};
return closure;
}
3. 现代JavaScript模块作用域
// ES6模块(module.js)
// 每个模块有自己的作用域
const privateVariable = "模块私有";
export const publicVariable = "模块公开";
export function publicFunction() {
console.log(privateVariable); // 可以访问模块私有变量
}
// 在另一个文件中
import { publicVariable, publicFunction } from './module.js';
console.log(publicVariable); // 可以访问
publicFunction(); // 可以调用
// console.log(privateVariable); // 错误!无法访问模块私有变量
九、常见问题与解决方案
1. 循环中的闭包问题
// 问题:使用var,所有闭包共享同一个i
function createFunctionsProblem() {
var functions = [];
for (var i = 0; i < 3; i++) {
functions.push(function() {
console.log(i); // 所有函数都输出3
});
}
return functions;
}
// 解决方案1:使用let(块级作用域)
function createFunctionsSolution1() {
var functions = [];
for (let i = 0; i < 3; i++) {
functions.push(function() {
console.log(i); // 0, 1, 2
});
}
return functions;
}
// 解决方案2:IIFE创建独立作用域
function createFunctionsSolution2() {
var functions = [];
for (var i = 0; i < 3; i++) {
(function(index) {
functions.push(function() {
console.log(index); // 0, 1, 2
});
})(i);
}
return functions;
}
// 解决方案3:使用forEach(每次迭代都有独立作用域)
function createFunctionsSolution3() {
var functions = [];
[0, 1, 2].forEach(function(i) {
functions.push(function() {
console.log(i); // 0, 1, 2
});
});
return functions;
}
2. 全局变量污染问题
// 问题:多个脚本可能使用相同的全局变量名
// script1.js
var config = { /* ... */ };
// script2.js(可能由其他人编写)
var config = { /* ... */ }; // 覆盖了第一个config!
// 解决方案1:使用命名空间
var MyApp = MyApp || {};
MyApp.config = { /* ... */ };
// 解决方案2:IIFE封装
(function() {
var config = { /* ... */ };
// 你的代码...
})();
// 解决方案3:使用ES6模块(最佳方案)
3. 变量遮蔽(Variable Shadowing)
let x = 10;
function test() {
let x = 20; // 遮蔽了外层的x
console.log(x); // 20
}
test();
console.log(x); // 10(外层的x没有改变)
// 避免过度遮蔽
function calculate(radius) {
// 不要这样:let radius = parseFloat(radius); // 遮蔽参数
// 应该这样:
let r = parseFloat(radius);
return Math.PI * r * r;
}
十、总结
1. 作用域核心要点
- 全局作用域:最外层,随处可访问,生命周期最长
- 函数作用域:函数内部,使用
var声明 - 块级作用域:
{}内部,使用let/const声明
2. 变量声明建议
| 关键字 | 作用域 | 是否提升 | 可重新赋值 | 推荐程度 |
|---|---|---|---|---|
var |
函数作用域 | 是 | 是 | 避免使用 |
let |
块级作用域 | 否(有TDZ) | 是 | 推荐(需要重新赋值时) |
const |
块级作用域 | 否(有TDZ) | 否 | 推荐(优先使用) |
3. 最佳实践总结
- 优先使用
const,需要重新赋值时用let - 避免使用
var和全局变量 - 使用严格模式(
"use strict") - 合理使用闭包,注意内存管理
- 利用模块化(ES6模块)组织代码
- 保持作用域清晰,避免过度嵌套
- 使用IIFE或块级作用域隔离代码
4. 记忆口诀
- "全局变量处处跑,局部变量家里蹲"
- "var 提升爱乱跑,let/const 守规矩"
- "闭包记住老家,作用域链寻亲"
- "严格模式把关,避免意外全局"
// 最终示例:良好的作用域实践
(function() {
"use strict";
// 模块私有变量
const MODULE_NAME = "MyModule";
let instanceCount = 0;
// 公共接口
window.MyModule = {
createInstance: function(config) {
instanceCount++;
// 实例私有变量
const instanceId = instanceCount;
let settings = Object.assign({}, config);
return {
getId: () => instanceId,
getSettings: () => ({ ...settings }),
updateSettings: (newSettings) => {
settings = Object.assign(settings, newSettings);
}
};
},
getInstanceCount: () => instanceCount
};
})();
// 使用
const instance1 = MyModule.createInstance({ color: "red" });
const instance2 = MyModule.createInstance({ color: "blue" });
console.log(instance1.getId()); // 1
console.log(instance2.getId()); // 2
console.log(MyModule.getInstanceCount()); // 2







