翻译:理解作用域和作用域链

翻译说明

很多资料都没有接清楚作用域和作用域链。今天参考 understanding-scope-and-scope-chain-in-javascript 简单翻译了一下文章。

版权属于作者。

什么是作用域

作用域的作用是控制程序中变量是否可见和可使用。

为什么作用域很重要

  1. 最主要是安全性: 作用域让变量在不同程序块直接隔离,减少混乱。

  2. 减少名字空间冲突,我们可以在不同作用域使用相同变量名。

作用域类型

有三种类中, 全局作用域、函数作用域、块作用域。

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开发者。

参考:

Mark24

Everything can Mix.