JavaScript变量的作用域介绍
JavaScript 变量的作用域决定了变量在代码中的可访问性。
var 是 JavaScript 中最早用于声明变量的关键字,它函数作用域或全局作用域。
let 关键字,具有块级作用域、全局作用域。
const关键字,具有块级作用域、全局作用域。
var、let 和 const 关键字对比表
特性 |
var |
let |
const |
作用域 |
函数作用域或全局作用域 |
块级作用域、全局作用域 |
块级作用域、全局作用域 |
变量提升 |
提升到作用域顶部,初始化为 undefined |
声明前访问会报错,因存在“暂时性死区”(Temporal Dead Zone) |
声明前访问会报错,因存在“暂时性死区”(Temporal Dead Zone) |
重复声明 |
合法,后续声明覆盖前一个值 |
不合法,抛出 SyntaxError |
不合法,抛出 SyntaxError |
是否可变 |
可变 |
可变 |
不可变(引用不可变,但内容可变) |
是否成为全局对象属性 |
是(在全局上下文中) |
否 |
否 |
下面展开介绍
1. 作用域类型
全局作用域(Global Scope)
- 定义:在函数外声明的变量。
- 特点:
- 任何地方都可访问,包括函数内部。过度使用全局变量可能导致命名冲突和代码难以维护。
- 在浏览器环境中,全局变量通常与 window 对象关联。
- 浏览器环境中,var 声明的全局变量会成为 window 对象(在浏览器环境中)的属性,let 和 const 不会。
- 注意
避免隐式全局变量,始终显式声明变量。
在非严格模式下,未使用 var、let 或 const 关键字声明的变量会被隐式提升为全局变量。
在严格模式下,未声明的变量会导致 ReferenceError,因此不会创建隐式全局变量。
var:在全局上下文中声明的变量会成为全局对象的属性,具有全局作用域。
let 和 const:在全局上下文中声明的变量是全局变量,但不会成为全局对象的属性; let 或 const 关键字还可以声明块级作用域(见后面)。
- 全局变量的创建方式:
1)隐式全局变量:
myGlobalVar = "Hello, World!"; // 隐式全局变量
console.log(myGlobalVar); // 输出 "Hello, World!"
console.log(window.myGlobalVar); // 输出 "Hello, World!",因为它是 window 的属性
2)显式全局变量:
使用window对象可以明确地创建全局变量。
window.myExplicitGlobalVar = "Hello, again!"; // 显式全局变量
console.log(window.myExplicitGlobalVar); // 输出 "Hello, again!"
console.log(myExplicitGlobalVar); // 输出 "Hello, again!",直接访问也可以。
3) 使用 var、let 或 const 关键字声明全局变量
//使用 var 声明全局变量
var globalVar = "Hello";
console.log(globalVar); // 输出: Hello
console.log(window.globalVar); // 输出: Hello,因为它是 window 对象的属性
//使用 let 声明全局变量
let globalLetVar = "Hello";
console.log(globalLetVar); // 输出: Hello
console.log(window.globalLetVar); // 输出: undefined(不是 window 的属性)
//使用 const 声明全局变量
const globalConstVar = "Hello";
console.log(globalConstVar); // 输出: Hello
console.log(window.globalConstVar); // 输出: undefined(不是 window 的属性)
注意,虽然全局变量在某些情况下是必要的,但为了防止命名冲突和提高代码质量,现代编程实践中通常不鼓励过度依赖全局变量。
函数作用域(Function Scope)
- 定义:在函数内部用var声明的变量和函数参数,作用范围为整个函数。
- 特点:
- 在函数内任何位置(包括嵌套代码块)可访问。
- 示例:
function func() {
if (true) {
var innerVar = '内部变量'; // 属于函数作用域
}
console.log(innerVar); // '内部变量'(正常访问)
}
func();
此例同时说明,var 声明的变量没有块级作用域,即使在块(如 if、for、while 等)中声明,变量仍然属于包含它的函数或全局作用域。
var 声明的变量会被提升(hoisting)到当前作用域的顶部,但赋值不会被提升。这意味着变量在声明之前可以访问,但值为 undefined。例如:
console.log(hoistedVar); // 输出: undefined
var hoistedVar = 'I am hoisted';
块级作用域(ES6+)
- 定义:由 {} 包围的代码块(如 if、for),使用 let 或 const 声明。
- 特点:
- 变量仅在块内有效。
- 避免循环变量泄露等问题。
- 示例:
if (true) {
let blockVar = '块内变量';
const PI = 3.14;
}
console.log(blockVar); // 报错:blockVar未定义
此例说明,let 和 const 不会被提升,存在“暂时性死区”(Temporal Dead Zone),即声明前访问会报错。
注意:
• let 和 const 声明的变量仅在 声明它们的代码块内有效。
• 如果在函数体的最外层(不嵌套在任何代码块中)声明,则作用域为 整个函数体(因为函数体本身是一个块级作用域)。
• 如果在函数内的嵌套代码块中声明(如 if 内部),则作用域仅限该代码块。
var、let和const的区别
- var:
- 具有函数作用域(在函数内部声明的变量只在函数内部有效)。
- 如果在全局作用域中声明,则具有全局作用域。
- 存在变量提升(hoisting),但未初始化的变量会返回undefined。
- 可以重复声明同一个变量。
- let和const:
- 具有块级作用域(在代码块内部声明的变量只在代码块内部有效)。
- 不会被提升到块的顶部。
- 不允许重复声明同一个变量。
- let声明的变量可以重新赋值,而const声明的变量不能重新赋值(具有只读性,且必须在声明时立即赋值,否则报错))。
2.作用域链与闭包
- 作用域链(Scope Chain):函数在定义时确定作用域链,逐级向上查找变量。
- 闭包(Closures):函数保留对外部作用域的引用,即使外部函数已执行完毕。
- 内部函数可以访问外部函数的变量。
- 外部函数执行完毕后,其作用域不会被销毁,而是被内部函数引用。
作用域链示例:
let globalVar = "global";
function outerFunction() {
let outerVar = "outer";
function innerFunction() {
let innerVar = "inner";
console.log(innerVar); // 输出: inner
console.log(outerVar); // 输出: outer
console.log(globalVar); // 输出: global
}
innerFunction();
}
outerFunction();
当访问一个变量时,JavaScript会按照作用域链的顺序查找变量。作用域链是从当前作用域开始,逐级向上查找,直到全局作用域。
闭包示例:
function outerFunction() {
let outerVar = "I am outer";
function innerFunction() {
console.log(outerVar); // 访问外部变量
}
return innerFunction;
}
let myClosure = outerFunction();
myClosure(); // 输出: I am outer
闭包是指函数可以访问其外部作用域的变量,即使外部函数已经执行完毕。
附1、严格模式(使用'use strict')
- 在严格模式下,未声明的变量会导致ReferenceError,从而避免全局变量污染。
- 严格模式是ES5引入的一种特殊的运行模式,使代码在更严格的条件下运行。
- 通过在代码开头添加 "use strict" 声明来启用。
- 可以应用于整个脚本或单个函数。
启用方式
// 整个脚本使用严格模式
"use strict";
// 后续代码...
// 或在函数内使用
function myFunction() {
"use strict";
// 函数代码...
}
示例:非严格模式和严格模式下未声明变量直接赋值的行为差异。
假设我们有以下代码片段:
function myFunction() {
x = 10; // 未声明的变量 x
console.log(x); // 输出 x 的值
}
myFunction();
console.log(x); // 在全局作用域中访问 x
1)非严格模式下的行为
在非严格模式下,如果直接给未声明的变量赋值,JavaScript 会自动将该变量提升为全局变量。
function myFunction() {
x = 10; // 未声明的变量 x
console.log(x); // 输出 10
}
myFunction();
console.log(x); // 输出 10
解释:
在myFunction函数中,变量x没有被声明(没有使用var、let或const),但直接赋值为10。
非严格模式下,JavaScript 会自动将x提升为全局变量。因此,x在函数内部和全局作用域中都可以被访问。
这种行为可能导致意外的全局变量污染。
2)严格模式下的行为
在严格模式下,未声明的变量直接赋值会抛出ReferenceError。
'use strict'; // 启用严格模式
function myFunction() {
x = 10; // 未声明的变量 x
console.log(x); // 抛出 ReferenceError : x is not defined
}
myFunction();
console.log(x); // 这行代码不会被执行
解释:
在严格模式下,JavaScript 不允许直接给未声明的变量赋值。
如果尝试给未声明的变量赋值,JavaScript 会抛出ReferenceError,提示变量未定义。
这种行为可以防止意外创建全局变量,减少潜在的错误和命名冲突。
关于 JavaScript 严格模式的官方文档可见https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode
附2、可变(Mutable)和不可变(Immutable)
“可变”(Mutable)和“不可变”(Immutable)是描述变量或数据是否可以被修改的术语。
1)可变(Mutable)
如果一个变量或数据结构的内容可以被修改,那么它就是可变的。这意味着你可以直接改变变量的值或数据结构中的某些部分,而不会创建新的对象。
示例
// 可变的变量
let number = 10;
number = 20; // 修改变量的值,number 现在是 20
// 可变的对象
let obj = { name: "Alice" };
obj.name = "Bob"; // 修改对象的属性,obj 现在是 { name: "Bob" }
obj.age = 25; // 添加新的属性,obj 现在是 { name: "Bob", age: 25 }
在上面的例子中:
number 是一个可变的变量,因为它的值可以从 10 改为 20。
obj 是一个可变的对象,因为它的属性可以被修改或添加。
2)不可变(Immutable)
如果一个变量或数据结构的内容不能被修改,那么它就是不可变的。这意味着一旦创建,它的值或内容就无法被改变。如果需要“修改”,通常会创建一个新的对象或变量。
示例
// 不可变的变量(使用 const 声明)
const PI = 3.14;
PI = 3.14159; // 抛出 TypeError: Assignment to constant variable.
在上面的例子中:
PI 是一个不可变的变量,因为它被 const 声明,不能被重新赋值。
不可变对象
虽然 JavaScript 中没有原生的不可变对象,但可以通过一些技术手段(如 Object.freeze())使对象的结构和属性不可变。
JavaScript复制
const obj = Object.freeze({ name: "Alice" });
obj.name = "Bob"; // 不会报错,但不会生效,obj 仍然是 { name: "Alice" }
obj.age = 25; // 不会报错,但不会生效,obj 仍然是 { name: "Alice" }
在上面的例子中:
- obj 是一个不可变的对象,因为通过 Object.freeze() 方法,它的属性无法被修改或添加。
3. 可变与不可变的区别
- 可变数据:
- 可以直接修改。
- 修改操作通常会影响原始数据。
- 在某些情况下可能导致意外的副作用(如在函数中修改传入的对象)。
- 不可变数据:
- 不能直接修改。
- 修改操作会创建一个新的对象或数据结构。
- 更安全,避免了意外修改导致的错误。
- 常用于函数式编程,因为函数式编程强调“无副作用”的函数。
小结
- 可变(Mutable):可以被修改。
- 不可变(Immutable):不能被修改。
- 在 JavaScript 中,let 声明的变量是可变的,而 const 声明的变量是不可变的(但对象或数组的内容可以被修改)。
- 不可变性有助于提高代码的安全性和可维护性,尤其是在复杂的应用中。