JavaScript变量的作用域介绍
JavaScript变量的作用域介绍
JavaScript 变量的作用域决定了变量在代码中的可访问性。
var 是 JavaScript 中最早用于声明变量的关键字,它函数作用域或全局作用域。
let 关键字,具有块级作用域、全局作用域。
const关键字,具有块级作用域、全局作用域。
var、let 和 const 关键字对比表
特性 | var | let | const |
作用域 | 函数作用域或全局作用域 | 块级作用域、全局作用域 | 块级作用域、全局作用域 |
变量提升 | 提升到作用域顶部,初始化为 undefined | 声明前访问会报错,因存在“暂时性死区”(Temporal Dead Zone) | 声明前访问会报错,因存在“暂时性死区”(Temporal Dead Zone) |
重复声明 | 合法,后续声明覆盖前一个值 | 不合法,抛出 SyntaxError | 不合法,抛出 SyntaxError |
是否可变 | 可变 | 可变 | 不可变(引用不可变,但内容可变)【注】 |
是否成为全局对象属性 | 是(在全局上下文中) | 否 | 否 |
【注:const 声明的变量不可重新赋值,但若为对象/数组,其内容可修改。示例:
const arr = [1, 2];
arr.push(3); // 允许
arr = [4, 5]; // 报错 】
下面展开介绍
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声明的变量不能重新赋值(具有只读性,且必须在声明时立即赋值,否则报错))。
模块作用域(Module scope)
模块作用域的定义
- 定义:每个 ES6 模块(以 .mjs 扩展名或 <script type="module"> 标签引入)拥有独立的作用域,模块内声明的变量、函数、类等默认仅在模块内可见,不会污染全局作用域。
- 核心规则:
- 模块内的顶级变量(var、let、const)不会自动成为全局对象的属性(如 window 或 global)。
- 模块间需要通过 export 导出和 import 导入来共享变量或函数。
浏览器环境,需使用 <script type="module"> 标签加载模块。例如
<script type="module" src="app.mjs"></script>
模块作用域的特点
(1) 默认隔离性
// moduleA.mjs
let privateVar = "模块A的私有变量"; // 仅模块A可见
export const publicVar = "模块A的公开变量";
// moduleB.mjs
import { publicVar } from './moduleA.mjs';
console.log(publicVar); // "模块A的公开变量"
console.log(privateVar); // 报错:privateVar未定义
(2) 不会污染全局对象
// 模块内声明变量
var moduleVar = "模块变量";
console.log(window.moduleVar); // undefined(浏览器环境)
(3) 隐式严格模式
ES6 模块默认启用严格模式,无需显式添加 'use strict'。
// 以下代码在模块中直接报错,非模块脚本可能不会
undeclaredVar = 10; // 报错:未声明变量
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())使对象的结构和属性不可变。
const obj = Object.freeze({ name: "Alice" });
obj.name = "Bob"; // 不会报错,但不会生效,obj 仍然是 { name: "Alice" }
obj.age = 25; // 不会报错,但不会生效,obj 仍然是 { name: "Alice" }
obj = {}; // 报错
在上面的例子中:
obj 是一个不可变的对象,因为通过 Object.freeze() 方法,它的属性无法被修改或添加。
3. 可变与不可变的区别
- 可变数据:
- 可以直接修改。
- 修改操作通常会影响原始数据。
- 在某些情况下可能导致意外的副作用(如在函数中修改传入的对象)。
- 不可变数据:
- 不能直接修改。
- 修改操作会创建一个新的对象或数据结构。
- 更安全,避免了意外修改导致的错误。
- 常用于函数式编程,因为函数式编程强调“无副作用”的函数。
小结
- 可变(Mutable):可以被修改。
- 不可变(Immutable):不能被修改。
- 在 JavaScript 中,let 声明的变量是可变的,而 const 声明的变量是不可变的(但对象或数组的内容可以被修改)。
- 不可变性有助于提高代码的安全性和可维护性,尤其是在复杂的应用中。
附录3、模块及在浏览器环境测试运行
假设有如下两个模块math.mjs和app.mjs
// math.mjs模块文件
export const PI = 3.1415;
export function sum(a, b) { return a + b; }
// app.mjs入口模块文件
import { PI, sum } from './math.mjs';
console.log(PI); // 3.1415
console.log(sum(2, 3)); // 5
如何在浏览器环境测试运行?
创建文件目录结构
D:/My-project/
│
├── index.html
├── app.mjs
└── math.mjs
编写 HTML 入口文件,在 index.html 中通过 <script type="module"> 引入入口模块:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ES Modules Test</title>
</head>
<body>
<h1>ES Modules Test</h1>
<script type="module" src="./app.mjs"></script>
</body>
</html>
特别提示,浏览器出于安全限制,禁止直接通过 file:// 协议加载 ES 模块——不能直接用浏览器打开index.html。你需要通过本地 HTTP 服务器运行代码。需要使用启动本地服务器方式,这里使用 Python 内置服务器,需要你已安装了Python。
a.在cmd中用cd命令(其中/d 参数用于切换到其他盘符),进入项目目录
cd /d D:/my-project
Python的HTTP服务器
b.再在cmd中用如下命令
python -m http.server 8000
参见下图:
c.然后(不要退出Python的HTTP服务器)在浏览器中访问http://127.0.0.1:8000
打开浏览器的开发者工具(按 F12 或右键选择“检查”)。
切换到 Console(控制台) 标签。
如果一切正常,你会看到输出:
3.1415
5
OJK!