在前面的文章中,我们提到 JavaScript 在创建执行上下文时会进行以下操作:
- 生成变量对象(VO)
- 确定作用域(Scope)
- 确定 this 的值
那么,这里提到的作用域到底是什么呢?今天我们就来深入了解一下这个重要概念。
什么是作用域?
举个例子
想象一下,你家里有客厅、卧室、厨房等不同的房间。每个房间里的东西,只有在那个房间里才能使用。比如,你把钥匙放在卧室的桌子上,那么只有在卧室里你才能找到这把钥匙,在客厅是找不到的。
在 JavaScript 中,作用域就像是这些房间,它决定了变量在哪里可以被访问和使用。
让我们通过一个简单的例子来理解:
function myFunction() {
let localVar = '函数内部变量'
}
myFunction()
console.log(localVar) // Uncaught ReferenceError: localVar is not defined
从上面的例子可以看出,变量 localVar
就像是放在函数这个"房间"里的东西,在函数外面是访问不到的,所以会报错。
简单来说,作用域就是变量的"活动范围"。它的主要作用是:
- 隔离变量:不同作用域的变量互不干扰
- 保护变量:防止变量被意外修改
- 避免命名冲突:不同作用域可以有同名变量
JavaScript 中的作用域类型
在 ES6 之前,JavaScript 只有两种作用域:
- 全局作用域:整个程序都能访问
- 函数作用域:只在函数内部能访问
ES6 新增了块级作用域,通过 let
和 const
关键字实现。
三种作用域详解
1. 全局作用域
全局作用域就像是家里的客厅,所有人都可以进入和使用。在代码中任何地方都能访问到的变量就拥有全局作用域。
以下几种情况会创建全局作用域:
最外层定义的变量和函数
const globalVariable = '我是最外层变量'
function outerFunction() {
// 最外层函数
const innerVariable = '内层变量'
function nestedFunction() {
//内层函数
console.log(innerVariable)
}
nestedFunction()
}
console.log(globalVariable) // 我是最外层变量
outerFunction() // 内层变量
console.log(innerVariable) // innerVariable is not defined
nestedFunction() // nestedFunction is not defined
忘记用 var/let/const 声明的变量(不推荐)
(function testFunction() {
globalVar = '未定义直接赋值的变量'
let localVar2 = '内层变量2'
})()
console.log(globalVar) // 未定义直接赋值的变量
console.log(localVar2) // localVar2 is not defined
window 对象的属性
浏览器中,window 对象的属性都拥有全局作用域,比如 window.name
、window.location
等。
全局作用域的问题
全局作用域就像是把所有东西都放在客厅里,时间长了就会很乱:
// 开发者A写的代码中
let userInfo = { id: 100 }
// 开发者B写的代码中
let userInfo = { active: true }
这就是为什么 jQuery、Zepto 等库都会把代码包在 (function(){...})()
中,就像给自己的代码建了一个独立的房间,避免和其他代码产生冲突。
2. 函数作用域
函数作用域就像是你的卧室,只有你自己能进入,外人无法访问里面的东西。
function performTask() {
const userName = 'zhangsan'
function displayName() {
console.log(userName)
}
displayName()
}
console.log(userName) // userName is not defined
displayName() // displayName is not defined
重要特性:作用域的层级关系
内层作用域可以访问外层作用域的变量,但外层不能访问内层的变量。
这就像是:
- 你在卧室里可以去客厅拿东西
- 但客厅里的人不能进入你的卧室拿东西
用气泡框来理解作用域层级:
最后输出的结果为 2、4、12
- 气泡 1 是全局作用域,有标识符 foo
- 气泡 2 是作用域 foo,有标识符 a、bar、b
- 气泡 3 是作用域 bar,仅有标识符 c
注意:ES6 之前的"坑"
在 ES6 之前,if
、for
、while
等语句的大括号 {}
不会创建新的作用域!
if (true) {
// 'if' 条件语句块不会创建一个新的作用域
var username = 'Hammad' // username 依然在全局作用域中
}
console.log(username) // logs 'Hammad'
这经常让初学者感到困惑,也容易产生 bug。所以 ES6 引入了块级作用域来解决这个问题。
3. 块级作用域 - ES6 的"新房间"
块级作用域是 ES6 带来的新特性,通过 let
和 const
关键字创建。现在 {}
真的可以创建独立的作用域了!
什么时候会创建块级作用域?
- 函数内部
- 任何用
{}
包裹的代码块
块级作用域的特点:
不会变量提升
let
和 const
不像 var
那样会提升到顶部,必须先声明再使用。
function getColor(flag) {
if (flag) {
const color = 'blue'
return color
} else {
// color 在此处不可用
return null
}
// color 在此处不可用
}
不允许重复声明
同一个作用域内,不能用 let
重复声明同名变量:
var counter = 30
let counter = 40 // Uncaught SyntaxError: Identifier 'counter' has already been declared
但在不同的作用域内可以有同名变量:
var counter = 30
// 不会抛出错误
if (condition) {
let counter = 40
// 其他代码
}
解决循环中的经典问题
块级作用域最大的用处之一就是解决循环中的变量问题。看这个经典的例子:
<button>测试1</button>
<button>测试2</button>
<button>测试3</button>
const buttons = document.getElementsByTagName('button')
for (var index = 0; index < buttons.length; index++) {
buttons[index].onclick = function () {
console.log('第' + (index + 1) + '个')
}
}
期望效果:点击第几个按钮就显示"第几个" 实际效果:点击任何按钮都显示"第 4 个"
原因:var i
是全局变量,循环结束后 i
的值是 3,所以所有按钮的点击事件都使用这个值。
解决方案:用 let
声明 i
for (let index = 0; index < buttons.length; index++) {
buttons[index].onclick = function () {
console.log('第' + (index + 1) + '个')
}
}
作用域链 - 变量的"寻找路径"
作用域链就是 JavaScript 引擎查找自由变量的路径。
什么是自由变量?
自由变量就是在当前作用域中没有定义,但需要使用的变量。
就像你在卧室里找不到钥匙,就需要到客厅去找一样。
const num = 100
function myFunc() {
const localNum = 200
console.log(num) // 这里的 num 在这里就是一个自由变量
console.log(localNum)
}
myFunc()
什么是作用域链?
当在当前作用域找不到变量时,JavaScript 会:
- 先在当前作用域找
- 找不到就去父级作用域找
- 还找不到继续往上找
- 直到全局作用域
- 如果全局作用域也没有,就报错
这种一层层向上查找的路径,就叫做作用域链。
const globalVar = 100
function outerFunc() {
const outerVar = 200
function innerFunc() {
const innerVar = 300
console.log(globalVar) // 自由变量 100,顺作用域链向父作用域找
console.log(outerVar) // 自由变量 200,顺作用域链向父作用域找
console.log(innerVar) // 300 本作用域的变量
}
innerFunc()
}
outerFunc()
重要概念:静态作用域
这里有个非常重要的概念需要理解:
let value = 10
function testFn() {
console.log(value)
}
function execute(callback) {
let value = 20
;(function () {
callback() // 10,而不是 20
})()
}
execute(testFn)
函数 testFn
中的变量 value
应该从哪里取值?
答案是:从创建 testFn
函数时的作用域中取值,而不是调用时的作用域!
这就是 JavaScript 的静态作用域(也叫词法作用域):
- 作用域在写代码时就确定了
- 不是在运行时确定的
再来看一个例子:
const meal = 'rice'
const consume = function () {
console.log(`eat ${meal}`)
}
;(function () {
const meal = 'noodle'
consume() // eat rice
})()
结果是 eat rice
,因为 consume
函数是在全局作用域中创建的,所以它使用全局的 meal
变量。
如果我们把函数的创建位置改一下:
const meal = 'rice'
;(function () {
const meal = 'noodle'
const consume = function () {
console.log(`eat ${meal}`)
}
consume() // eat noodle
})()
这时结果是 eat noodle
,因为 consume
函数是在立即执行函数内部创建的,所以使用的是内部的 meal
变量。
作用域 vs 执行上下文
很多人容易混淆这两个概念,其实它们是完全不同的:
作用域(Scope)
- 什么时候确定:写代码时就确定了
- 会不会变化:不会变化,是静态的
- 主要作用:决定变量的访问范围
执行上下文(Execution Context)
- 什么时候确定:代码运行时才确定
- 会不会变化:会变化,是动态的
- 主要作用:决定
this
的指向、变量的值等
简单记忆
- 作用域:在哪里能找到变量(位置)
- 执行上下文:变量的具体值是什么(内容)
总结
作用域的核心要点
- 作用域是变量的活动范围,决定了变量在哪里可以被访问
- 三种类型:全局作用域、函数作用域、块级作用域(ES6+)
- 层级关系:内层可以访问外层,外层不能访问内层
- 静态特性:在写代码时就确定,不会因为调用位置而改变
作用域链的核心要点
- 查找机制:从当前作用域开始,逐层向上查找变量
- 查找顺序:当前作用域 → 父级作用域 → ... → 全局作用域
- 静态绑定:查找路径在函数创建时就确定了
希望这篇文章能帮助你更好地理解 JavaScript 的作用域机制!如果有任何疑问,欢迎在评论区讨论。