作用域决定了变量的可访问范围。JavaScript 采用词法作用域,变量的作用域在代码编写时就确定了。
🎯 什么是作用域#
作用域是变量和函数的可访问区域。它决定了代码中变量的可见性和生命周期。
// 全局变量:任何地方都能访问const globalVar = '全局'
function outer() { // 函数内变量:只在函数内可访问 const outerVar = '外层'
function inner() { // 内层函数可以访问外层变量 const innerVar = '内层' console.log(globalVar) // "全局" console.log(outerVar) // "外层" console.log(innerVar) // "内层" }
inner() // console.log(innerVar) // ReferenceError}
outer()// console.log(outerVar) // ReferenceError全局作用域#
全局变量#
// 方式1:在最外层声明const globalA = 'a'let globalB = 'b'var globalC = 'c'
// 方式2:挂载到全局对象(不推荐)window.globalD = 'd' // 浏览器global.globalE = 'e' // Node.js
// 方式3:未声明直接赋值(严格模式报错)function bad() { implicitGlobal = '隐式全局' // 危险!}全局对象#
// 浏览器中的全局对象console.log(window === globalThis) // true
// var 声明的变量成为全局对象属性var x = 1console.log(window.x) // 1
// let/const 不会let y = 2const z = 3console.log(window.y) // undefinedconsole.log(window.z) // undefined
// 全局函数也是全局对象的方法function greet() {}console.log(window.greet) // ƒ greet()全局污染#
// 🔶 问题:全局变量可能被覆盖var name = '张三'// ... 很多代码后var name = '李四' // 覆盖了!
// ✅ 解决方案1:使用 IIFE;(function () { var name = '张三' // 只在函数内可用})()
// ✅ 解决方案2:使用模块// module.jsexport const name = '张三'
// ✅ 解决方案3:使用块级作用域{ const name = '张三'}函数作用域#
基本概念#
function outer() { var x = 1
function inner() { var y = 2 console.log(x) // 1(可以访问外层) console.log(y) // 2 }
inner() console.log(x) // 1 // console.log(y) // ReferenceError}
outer()// console.log(x) // ReferenceErrorvar 的函数作用域#
function example() { if (true) { var x = 1 // 函数作用域,不是块作用域 } console.log(x) // 1(可以访问)
for (var i = 0; i < 3; i++) { // ... } console.log(i) // 3(可以访问)}变量提升#
function hoisting() { console.log(x) // undefined(不是 ReferenceError) var x = 1 console.log(x) // 1}
// 相当于function hoisting() { var x // 声明提升到顶部 console.log(x) // undefined x = 1 // 赋值保持原位 console.log(x) // 1}
// 函数声明也会提升sayHello() // "Hello!"(可以在声明前调用)function sayHello() { console.log('Hello!')}
// 但函数表达式不会完全提升// greet() // TypeError: greet is not a functionvar greet = function () { console.log('Hi!')}块级作用域#
let 和 const#
// 块级作用域{ let x = 1 const y = 2 var z = 3}
// console.log(x) // ReferenceError// console.log(y) // ReferenceErrorconsole.log(z) // 3(var 不受块限制)
// if 语句if (true) { let blockVar = 'block'}// console.log(blockVar) // ReferenceError
// for 循环for (let i = 0; i < 3; i++) { console.log(i)}// console.log(i) // ReferenceError暂时性死区(TDZ)#
// let/const 声明的变量在声明前不可访问console.log(x) // ReferenceError: Cannot access 'x' before initializationlet x = 1
// 即使外层有同名变量let y = 'outer'{ // TDZ 开始 // console.log(y) // ReferenceError let y = 'inner' // TDZ 结束 console.log(y) // "inner"}
// typeof 也会触发 TDZ// console.log(typeof undeclared) // "undefined"// console.log(typeof z) // ReferenceErrorlet z = 1循环中的块级作用域#
// var:共享同一个变量for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100)}// 输出:3, 3, 3
// let:每次迭代创建新的绑定for (let j = 0; j < 3; j++) { setTimeout(() => console.log(j), 100)}// 输出:0, 1, 2
// ES5 解决方案:IIFEfor (var k = 0; k < 3; k++) { ;(function (k) { setTimeout(() => console.log(k), 100) })(k)}// 输出:0, 1, 2词法作用域#
JavaScript 使用词法作用域(静态作用域),作用域在代码编写时确定。
const x = 'global'
function outer() { const x = 'outer'
function inner() { console.log(x) // 查找定义时的作用域链 }
return inner}
const fn = outer()fn() // "outer"(不是 "global")
// 对比动态作用域(JavaScript 不是)// 动态作用域会输出调用时的 x作用域链#
const a = 1
function outer() { const b = 2
function middle() { const c = 3
function inner() { const d = 4 // 作用域链:inner -> middle -> outer -> global console.log(a, b, c, d) // 1, 2, 3, 4 }
inner() }
middle()}
outer()变量查找#
const x = 'global'
function foo() { console.log(x) // 当前作用域没有,向上查找}
function bar() { const x = 'bar' foo() // 还是输出 "global"}
bar()
// foo 的作用域链在定义时确定:foo -> global// 不是在调用时:foo -> bar -> global模块作用域#
// ES6 模块有自己的作用域
const privateVar = '私有'export const publicVar = '公开'
export function greet() { console.log(privateVar) // 可以访问私有变量}
// main.jsimport { publicVar, greet } from './module.js'console.log(publicVar) // "公开"// console.log(privateVar) // ReferenceError
// 模块顶层的 this 是 undefinedconsole.log(this) // undefined(在模块中)作用域实践#
最小化全局变量#
// 🔶 不好:污染全局var config = {}var utils = {}var app = {}
// ✅ 使用命名空间var MyApp = { config: {}, utils: {}, init: function () {},}
// ✅ 使用模块// config.jsexport default { api: 'https://api.example.com' }
// ✅ 使用 IIFEconst App = (function () { const private = '私有'
return { public: '公开', getPrivate() { return private }, }})()避免变量遮蔽#
const name = '全局'
function greet() { // 🔶 遮蔽外层变量可能导致困惑 const name = '局部' console.log(name)}
// ✅ 使用更具描述性的名称const globalName = '全局'
function greet() { const userName = '局部' console.log(userName)}利用块级作用域#
// 临时变量隔离function process(data) { // 处理阶段1 { const temp = data.map((x) => x * 2) // 使用 temp } // temp 不再可访问
// 处理阶段2 { const temp = data.filter((x) => x > 0) // 使用 temp }}
// switch 语句中的块作用域switch (action) { case 'increment': { const step = 1 result += step break } case 'decrement': { const step = 1 result -= step break }}常见陷阱#
🙋 意外创建全局变量#
function bad() { // 忘记声明 oops = '全局变量' // 非严格模式下创建全局变量}
// 使用严格模式防止;('use strict')function good() { oops = '...' // ReferenceError}🙋 循环中的闭包#
// 经典问题const buttons = document.querySelectorAll('button')
// 🔶 错误:所有按钮都输出相同的值for (var i = 0; i < buttons.length; i++) { buttons[i].onclick = function () { console.log(i) // 总是 buttons.length }}
// ✅ 使用 letfor (let i = 0; i < buttons.length; i++) { buttons[i].onclick = function () { console.log(i) // 正确的索引 }}
// ✅ 使用闭包for (var i = 0; i < buttons.length; i++) { ;(function (j) { buttons[j].onclick = function () { console.log(j) } })(i)}🙋 函数声明在块内#
// 在块内的函数声明行为不一致if (true) { function foo() { return 'if' }} else { function foo() { return 'else' }}
// 不同浏览器可能有不同结果// 推荐使用函数表达式let fooif (true) { foo = function () { return 'if' }} else { foo = function () { return 'else' }}总结#
| 作用域类型 | 创建方式 | 变量声明 |
|---|---|---|
| 全局作用域 | 最外层代码 | var/let/const |
| 函数作用域 | function 关键字 | var/let/const |
| 块级作用域 | 代码块 | let/const |
| 模块作用域 | ES6 模块文件 | let/const |
| 声明方式 | 作用域 | 提升 | TDZ | 重复声明 |
|---|---|---|---|---|
| var | 函数 | 是 | 无 | 允许 |
| let | 块 | 否 | 有 | 不允许 |
| const | 块 | 否 | 有 | 不允许 |
核心要点:
- JavaScript 使用词法作用域,在代码编写时确定
- 优先使用
const,需要修改时用let,避免使用var - 利用块级作用域隔离变量
- 理解作用域链进行变量查找