本文深入解析 JavaScript 中的作用域与作用域链机制,涵盖全局、函数及块级作用域的区别。重点阐述词法作用域原理,并详细拆解 JavaScript 引擎如何利用词法环境进行变量查找,揭示 ES6 let/const 与 var 在作用域行为上的差异,帮助开发者编写更安全高效的代码。
作用域基础与重要性
作用域与作用域链是 JavaScript 及众多编程语言中的核心基础概念。尽管许多 JavaScript 开发者对其理解尚浅,但掌握这些概念对于精通 JavaScript 至关重要。
正确理解这一概念有助于编写更优质、高效且简洁的代码,从而提升开发者水平。
本文将详细解释作用域与作用域链的定义,并剖析 JavaScript 引擎在内部如何利用它们进行变量操作与查找。
接下来直接进入正文内容。
什么是作用域
JavaScript 中的作用域决定了变量的可访问性与可见性,即程序中哪些部分可以访问该变量,以及变量在哪些范围内可见。
为什么作用域很重要
作用域最重要的价值在于安全性。变量仅在特定区域内可访问,这防止了程序其他部分意外修改变量值。
此外,作用域还能缓解命名冲突压力,允许在不同作用域中定义同名变量。
作用域的类型
JavaScript 主要包含三种作用域:
- 全局作用域
- 函数作用域
- 块级作用域
JavaScript 作用域类型详解
1. 全局作用域
任何未在函数或代码块中声明的变量均处于全局作用域,这些变量可在程序任意位置访问。例如:
// 全局变量
var greeting = 'Hello World!';
function greet() {
console.log(greeting);
}
// 打印 'Hello World!'
greet();
2. 函数作用域
函数作用域亦称局部作用域。若变量在函数内部声明,则其仅在该函数作用域内有效,外部无法访问。例如:
function greet() {
var greeting = 'Hello World!';
console.log(greeting);
}
// 打印 'Hello World!'
greet();
// 报错: Uncaught ReferenceError: greeting is not defined
console.log(greeting);
3. 块级作用域
ES6 引入了 let 和 const 关键字。与 var 不同,使用 let 或 const 在大括号内声明的变量属于块级作用域,大括号外不可访问。示例如下:
{
// 块级作用域中的变量
let greeting = 'Hello World!';
var lang = 'English';
console.log(greeting); // Prints 'Hello World!'
}
// 变量 'English'
console.log(lang);
// 报错:Uncaught ReferenceError: greeting is not defined
console.log(greeting);
上述代码显示,使用 var 声明的变量 lang 可在大括号外访问,说明 var 声明的变量不具备块级作用域特性。
作用域嵌套与词法作用域
作用域嵌套
如同 JavaScript 支持函数嵌套,作用域也可相互嵌套。参见示例:
var name = 'Peter';
function greet() {
var greeting = 'Hello';
{
let lang = 'English';
console.log(`${lang}: ${greeting} ${name}`);
}
}
greet();
此处涉及三层嵌套作用域:最内层为 let 声明的块级作用域,嵌套于 greet 函数作用域内,最外层为全局作用域。
词法作用域
词法作用域(静态作用域)指作用域在词法分析阶段(通常为编译期)确定,而非执行期确定。示例如下:
let number = 42;
function printNumber() {
console.log(number);
}
function log() {
let number = 54;
printNumber();
}
// Prints 42
log();
代码显示,无论 printNumber() 在何处调用,console.log(number) 均打印 42。若为动态作用域,打印结果将取决于 printNumber() 的调用位置。
在动态作用域下,上述 console.log(number) 将打印 54。
采用词法作用域时,仅查看源代码即可确定变量范围;而动态作用域需在代码执行前无法确定变量范围。
C、C++、Java 及 JavaScript 等主流编程语言均支持静态作用域,而 Perl 同时支持动态与静态作用域。
作用域链机制解析
作用域链
当 JavaScript 使用变量时,引擎首先在当前作用域查找;若未找到,则逐层向上层作用域查找,直至找到变量或到达全局作用域。
若在全局作用域仍未找到变量,非严格模式下会隐式声明该变量,严格模式下则直接报错。
例如:
let foo = 'foo';
function bar() {
let baz = 'baz';
// 打印 'baz'
console.log(baz);
// 打印 'foo'
console.log(foo);
number = 42;
console.log(number); // 打印 42
}
bar();
调用 bar() 时,引擎先在当前作用域查找 baz,再查找 foo。当前作用域未找到 foo,遂向外层(全局作用域)查找并成功找到。
随后将 42 赋值给 number。引擎会逐层查找 number 变量(当前及外部作用域均未找到)。
非严格模式下,引擎会创建全局 number 变量并赋值 42;严格模式下则抛出错误。
结论:使用变量时,JavaScript 引擎沿作用域链逐层向上查找,直至找到目标变量。
词法环境与变量查找原理
作用域和作用域链是如何工作的
前文已介绍作用域及其类型,下文将探讨 JavaScript 引擎如何确定作用域链及查找变量的具体机制。
理解 JavaScript 变量查找机制,必须掌握词法环境概念(参考:理解 JavaScript 中的执行上下文和执行栈)。
什么是词法环境
词法环境是一种标识符(变量或函数名)与变量(指向实际对象如函数、数组或基础数据类型)映射的结构。
简言之,词法环境是 JavaScript 引擎存储变量和对象引用的场所。
注意:不要混淆了词法环境和词法作用域,词法作用域是在代码编译阶段确定的作用域(译者注:一个抽象的概念),而词法环境是Javascript引擎用来存储变量和对象引用的地方(译者注:一个具象的概念)。
一个词法环境结构如下:
lexicalEnvironment = {
a: 25,
obj: <ref. to the object>
}
仅当作用域代码执行时,引擎才会为该作用域创建新词法环境,并记录其引用的外部词法环境(即外部作用域)。示例:
lexicalEnvironment = {
a: 25,
obj: <ref. to the object>
outer: <outer lexical environemt>
}
Javascript引擎是如何进行变量查找的
已知作用域、作用域链及词法环境概念后,下文解析引擎如何利用词法环境确定作用域链并查找变量。
结合示例理解上述概念:
let greeting = 'Hello';
function greet() {
let name = 'Peter';
console.log(`${greeting} ${name}`); // Hello Peter
}
greet();
{
let greeting = 'Hello World!'
console.log(greeting); // Hello World!
}
代码加载后,首先创建全局词法环境,包含全局声明的变量与函数。结构如下:
globalLexicalEnvironment = {
greeting: 'Hello'
greet: <ref. to greet function>
outer: <null>
}
此处 outer 字段为 null,因为全局词法环境已是顶层环境。
调用 greet() 函数时,创建新词法环境:
functionLexicalEnvironment = {
name: 'Peter'
outer: <globalLexicalEnvironment>
}
此处 outer 字段指向 globalLexicalEnvironment,因其外部作用域为全局作用域。
执行 console.log(`${greeting} ${name}`) 时,引擎先在当前函数词法环境查找 greeting 和 name,仅找到 name。随后在上层(全局词法环境)查找 greeting 并成功找到。
接着执行大括号内代码,为该块级创建新词法环境。结构如下:
blockLexicalEnvironment = {
greeting: 'Hello World',
outer: <globalLexicalEnvironment>
}
执行 console.log(greeting) 时,引擎在本层词法环境找到 greeting,查找结束,不再向外层(全局作用域)查找。
注意:仅 let 和 const 声明会创建新词法环境;var 声明的变量存储在当前块所在词法环境(全局或函数词法环境)中。
结论:使用变量时,JavaScript 引擎先在当前词法环境查找,未找到则向上层词法环境查找,直至找到为止。
结论
作用域是变量可访问和可见的区域。与函数类似,JavaScript 作用域可嵌套,引擎会逐层遍历作用域以查找变量。
JavaScript 采用词法作用域,即变量作用在编译阶段确定。
JavaScript 引擎在程序执行期间使用词法环境存储变量和函数。
作用域与作用域链是 JavaScript 基础概念,深入理解这些机制有助于成为更优秀的 JavaScript 开发者。
本文内容至此结束,希望对学习有所帮助,欢迎支持本站。