Mark24
翻译:理解作用域和作用域链
翻译说明
很多资料都没有接清楚作用域和作用域链。今天参考 understanding-scope-and-scope-chain-in-javascript 简单翻译了一下文章。
版权属于作者。
什么是作用域
作用域的作用是控制程序中变量是否可见和可使用。
为什么作用域很重要
-
最主要是安全性: 作用域让变量在不同程序块直接隔离,减少混乱。
-
减少名字空间冲突,我们可以在不同作用域使用相同变量名。
作用域类型
有三种类中, 全局作用域、函数作用域、块作用域。
1. 全局作用域
那些不在块作用域或者函数作用域的变量,就在全局作用域里。
全局作用域里的变量,可以在程序的任何地方访问。
var greeting = 'Hello World!';function greet() {
console.log(greeting);
}// Prints 'Hello World!'
greet();
本地作用域(Local Scope) 或 函数作用域(Function Scope)
函数中声明的变量就在本地作用域中。他只能在函数内部访问,无法在外部访问。
function greet() {
var greeting = 'Hello World!';
console.log(greeting);
}// Prints 'Hello World!'
greet();// Uncaught ReferenceError: greeting is not defined
console.log(greeting);
3. 块作用域
ES6引入了 let const
关键字,不像var
,他们可以和最近的大括号形成块作用域。块作用域的变量,在大括号外不可见。
{
let greeting = 'Hello World!';
var lang = 'English';
console.log(greeting); // Prints 'Hello World!'
}// Prints 'English'
console.log(lang);// Uncaught ReferenceError: greeting is not defined
console.log(greeting);
我们可以看到,var声明的变量在块外部可以访问,即var变量不在 块作用域中。
嵌套作用域
就像JavaScript中的函数,作用域也可以被嵌套。
var name = 'Peter';function greet() {
var greeting = 'Hello';
{
let lang = 'English';
console.log(`${lang}: ${greeting} ${name}`);
}
}greet();
这里有三个彼此嵌套的作用域。
全局作用域,函数作用域,以及函数内的块作用域(let关键字声明)。
词法作用域
词法作用域(Lexial Scope)也叫静态作用域。从字面上讲是指作用域是在词法化时间(通常称为编译)而不是在运行时确定的。
注:简单理解为书写代码决定。
let number = 42;
function printNumber() {
console.log(number);
}
function log() {
let number = 54;
printNumber();
}// Prints 42
log();
这里 console.log(number) 将会打印42 不论 printNumber在何处调用。这个不同于一些使用动态作用域的编程语言,其中printNumber打印值依赖于在哪里调用它。
如果上面的代码是动态作用域语言写的, console.log(number)打印的结果将会是54。
使用词法作用域,我们只要通过看代码,就可以确定一个变量的作用域。
大多数编程语言支持词法作用域(静态作用域)比如 C/C++/Java/JavaScript。 Perl支持静态作用域和动态作用域。
作用域链
当一个变量在JS中被调用,JS引擎就会尝试在 变量的当前作用域中寻找他。如果没有找到,他就会进入外部作用域去找,如此往复,直到找到或者到全局作用域。
如果它仍然找不到变量,它将在全局作用域内隐式声明变量(如果不是严格模式)或返回错误。
例如:
let foo = 'foo';
function bar() {
let baz = 'baz'; // Prints 'baz'
console.log(baz); // Prints 'foo'
console.log(foo);
number = 42;
console.log(number); // Prints 42
}
bar();
当函数 bar被执行,JS引擎会在当前作用中 查找 baz变量。 接着,它会继续找 foo变量,foo找不到,他将会在外部作用域查找(这里是全局作用域)。
接着,我们把42赋值给number,JS引擎在当前作用域查找number,然后在外部查找。
如果不是严格模式,JS引擎会创建一个number变量把42赋值给它或者返回一个报错。
所以,当使用一个变量,JS引擎会沿着作用域链,直到查找到它。
作用域 和 作用域链 如何工作的?
到目前为止,我们已经讨论作用域以及他的类型。 现在让我们了解JavaScript引擎如何确定变量作用域,以及如何进行变量查找。
在理解JS引擎如何查找变量,我们需要先理解词法环境的概念。
什么是词法环境(Lexical Environment)?
词法环境是一个存储 标识符和变量映射的数据结构。这里的标识符指 变量/函数名称。并且变量是那些实际对象包括函数对象、数组对象或者原始值的引用。
注:必要把 词法作用域、词法环境 搞混淆。
词法作用域是代码决定了。 词法环境是程序执行期间存储变量的地方。
概念上,词法环境看起来就像:
lexicalEnvironment = {
a: 25,
obj: <ref. to the object>
}
代码执行的过程中,代码的每一个词法作用域,都会创建一个新的词法环境。
词法环境还拥有一个 outer属性,指向它外部的词法环境。
例如:
lexicalEnvironment = {
a: 25,
obj: <ref. to the object> outer: <outer lexical environemt>
}
JS引起如何实现变量查找?
现在我们知道了作用域、作用域链、词法环境。
让我们来学习JS引擎如何使用词法环境来确定作用域和作用域链。
通过下面例子说明:
let greeting = 'Hello';
function greet() {
let name = 'Peter';
console.log(`${greeting} ${name}`);
}
greet();
{
let greeting = 'Hello World!'
console.log(greeting);
}
当上面的脚本被加载,全局的词法环境被创建。
全局词法环境包括变量、全局作用域中定义的函数, 就像下面:
globalLexicalEnvironment = {
greeting: 'Hello'
greet: <ref. to greet function> outer: <null>
}
这里 outer属性 设置为null,因为全局作用域没有外部作用域。
接着,geet函数指向,并且创建词法环境。如下:
functionLexicalEnvironment = {
name: 'Peter'
outer: <globalLexicalEnvironment>
}
这里outer词法环境被设置为 globalLexicalEnvironment
因为他的外部作用域是 全局作用域。
接着,JS引擎执行
console.log(`${greeting} ${name}`)
JS引擎尝试在 函数的词法环境中查找 greeting、name变量。
JS引擎在当前词法环境中找到name,但是没有找到greeting。
于是它顺着 outer指向的 词法环境继续查找 greeting变量,然后找到了。
接着,JS执行块中的代码。他创建了一个块的词法环境。
blockLexicalEnvironment = {
greeting: 'Hello World', outer: <globalLexicalEnvironment>
}
接着 console.log(greeting)
执行,JS因为在当前词法环境中找到变量并使用,所以没有再进入outer中查找。
注: let和const会创建新的词法环境, var不会。var声明会被加入到当前的词法环境(即 全局或者函数词法环境)。
译者注:也就是说,var声明的都绑定在 function的词法环境中。而let、const会在内部创建新的块词法环境,块中的变量查找,优先块中。 块引用了外部环境,查找不到的情况下会进入外部函数的词法环境中查找。
因此,再程序中使用变量时,JS引擎会尝试在当前词法环境中查找变量,它无法找到时会在outer指向的外部词法环境中查找。
这就是JS引擎查找变量的工作原理。
总结
简单讲,作用域是变量是否可见、可获得的区域。和函数类似,作用域可以被嵌套,JS引起会顺着作用域链查找变量。
JS使用词法作用域(静态作用域),也就是代码决定了变量作用域。代码执行过程中,JS使用词法环境存储变量。
作用域和作用域链是JS的基础概念,每一个JS开发者应该理解。对这些概念理解,能帮助你称为更优秀的JS开发者。