Skip to content

作用域

作用域决定了变量的可访问范围。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 = 1
console.log(window.x) // 1
// let/const 不会
let y = 2
const z = 3
console.log(window.y) // undefined
console.log(window.z) // undefined
// 全局函数也是全局对象的方法
function greet() {}
console.log(window.greet) // ƒ greet()

全局污染#

// 🔶 问题:全局变量可能被覆盖
var name = '张三'
// ... 很多代码后
var name = '李四' // 覆盖了!
// ✅ 解决方案1:使用 IIFE
;(function () {
var name = '张三'
// 只在函数内可用
})()
// ✅ 解决方案2:使用模块
// module.js
export 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) // ReferenceError

var 的函数作用域#

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 function
var greet = function () {
console.log('Hi!')
}

块级作用域#

let 和 const#

// 块级作用域
{
let x = 1
const y = 2
var z = 3
}
// console.log(x) // ReferenceError
// console.log(y) // ReferenceError
console.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 initialization
let 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) // ReferenceError
let 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 解决方案:IIFE
for (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

模块作用域#

module.js
// ES6 模块有自己的作用域
const privateVar = '私有'
export const publicVar = '公开'
export function greet() {
console.log(privateVar) // 可以访问私有变量
}
// main.js
import { publicVar, greet } from './module.js'
console.log(publicVar) // "公开"
// console.log(privateVar) // ReferenceError
// 模块顶层的 this 是 undefined
console.log(this) // undefined(在模块中)

作用域实践#

最小化全局变量#

// 🔶 不好:污染全局
var config = {}
var utils = {}
var app = {}
// ✅ 使用命名空间
var MyApp = {
config: {},
utils: {},
init: function () {},
}
// ✅ 使用模块
// config.js
export default { api: 'https://api.example.com' }
// ✅ 使用 IIFE
const 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
}
}
// ✅ 使用 let
for (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 foo
if (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不允许

核心要点