ES2015 最大的特性之一就是有了一个全新的作用域。在这个章节里,我们将开始学习什么是作用域。我们将继续学习如何创建新的作用域类型,以及给我们代码带来的好处
作用域描述为一个变量,函数,标识符可以被访问的区域。JavaScript 传统上有两种作用域类型:全局作用域和函数作用域,你定义变量的位置会影响其他代码是否可以访问。让我们来看一个简单的例子来阐述作用域的概念。想象一下,你的 JavaScript 文件只包含以下代码:
var globalVariable = 'This is global';
function globalFunction1() {
var innerVariable1 = 'Non-global variable 1';
}
function globalFunction2() {
var innerVariable2 = 'Non-global variable 2';
}
在上面的代码中,我们首先声明了一个变量 globalVariable
。这个语句不在函数内部,所以会自动存到全局作用域中。浏览器用 window
对象创建了一个全局作用域,除了可以用 globalVariable
访问,我们还可以通过挂在 window
对象上的 window.globalVariable
访问。我们可以在文件的任何地方访问这个变量,这两个函数的之前或之后,甚至是在函数的内部(这就是为什么我们说全局变量是 “隐藏的”,我们可以在任何地方正确的访问他们),甚至是在附在同一页面的其他 JavaScript 文件
在全局作用域里,我们定义了两个函数,globalFunction1
和 globalFunction2
,就像全局变量一样,他们是 “可见的” 并且可以在这个文件的任何地方调用,也可以被同一页面的其他 JavaScript 文件调用。然而,当 JavaScript 引擎解析这些函数时,会分别创建他们自己的作用域。因吹斯听,这两个新的函数作用域被嵌套在全局作用域下,成为子作用域。这也就意味着函数内的代码可以访问全局变量,就像是和在函数 “内部的” 定义变量一样
当我们试图访问 JavaScript 里的标识符时,浏览器会首先在当前作用域中查找。如果没有找到,浏览器会在当前作用域的父作用域中查找,并且继续向上查找,直到找到这个变量,或者到达全局作用域为止。如果这个变量在全局作用域里依旧没有找到的话,那么浏览器会抛出一个 ReferenceError
错误。这种嵌套的作用域被称作作用域链,而这个检查当前作用域和父作用域的过程被称作变量查找。这种查找只会向上查找作用域链,它永远不会在它的子作用域里查找
在上面的作用域链查找方向我们得知,例子中的 innerVariable1
变量只能在 globalFunction1
函数内部被访问,innerVariable2
变量只能在 globalFunction2
函数内部被访问。innerVariable1
变量不能在 globalFunction2
函数内部或全局作用域内被访问,innerVariable2
变量也不能在 globalFunction1
函数内部或全局作用域内被访问
下面的图片是上面代码中作用域的抽象表示:
让我们来看下另一个简短的代码示例,彻底的了解下到目前为止我们所介绍到的作用域概念。假设 JavaScript 文件只包含如下代码:
function outer() {
var variable1;
function inner() {
var variable2;
}
}
在这段代码里,我们在全局作用域里声明了一个叫 outer
的函数。因为它是一个函数,所以它创建了一个函数作用域,嵌套在全局作用域下。在这个作用域下,我们又声明了一个叫 variable1
的变量和 一个叫 inner
的函数。因为 inner
也是一个函数,所以一个新的作用域又被创建了,嵌套在 outer
函数的作用域下
在 inner
函数中,我们既可以访问 variable2
也可以访问 variable1
。当我们在 inner
函数中访问 variable1
时,浏览器首先会在它的作用域里查找这个变量;当这个变量没有被找到时,会继续向上在父作用域里查找(也就是 outer
函数的作用域)。代码里作用域如下图所示:
这个示例中的作用域链比较长,从 inner
函数延伸到 outer
函数,直到全局对象 window
在 JavaScript 中,一个块是由一个或多个语句用大括号包裹起来的。诸如 if
,for
,while
的条件表达式,都是用块基于特定的条件来执行块语句
其他流行的常见的编程语言都有块作用域,JavaScript 作用域中,直到如今却只有全局作用域和函数作用域,因此使我们变得很困惑。ES2015 在 JavaScript 新增了块作用域,对于我们的代码来说有很大的影响,并且对于那些熟悉其他编程语言的开发者来说变得更直观
块作用域意味着一个块可以创建它自己的作用域,而不是简单的存在于它最近到父级函数作用域或全局作用域下。让我们在认识块作用域是如何工作的之前,先来了解下传统上块里的 JavaScript 是如何工作的:
function fn() {
var x = 'function scope';
if (true) {
var y = 'not block scope';
}
function innerFn() {
console.log(x, y); // function scope not block scope
}
innerFn();
}
var
语句是不能够创建块作用域的,即使是在块里,因此 console.log
语句可以访问到 x
和 y
变量。 fn
函数创建了一个函数作用域而且 x
和 y
变量都是可以通过作用域内的作用域链访问到
理解提升的概念是理解 JavaScript 如何工作的基础。JavaScript 有两个阶段:解析阶段(JavaScript 引擎读取所有的代码)、执行阶段(执行已解析的代码)。大多数的事情都发生在第二阶段;例如,当你使用 console.log
语句时,实际的日志消息会在执行阶段打印到控制台
然而,一些重要的事情也会在解析阶段发生,包括变量的内存分配、作用域创建。提升这个术语指的是 JavaScript 引擎在遇到标识符,如变量、函数声明时所发生到事情;当发生声明提升时,它的行为就像是把它定义的字面量提升到当前作用域的顶部。鉴于此,上面到代码示例实际会变成如下情况:
function fn() {
var x;
var y;
x = 'function scope';
if (true) {
y = 'not block scope';
}
function innerFn() {
console.log(x, y); // function scope not block scope
}
innerFn();
}
只有变量到声明会提升到它的作用域的顶部;在这个例子的 if
语句中,变量赋值依然发生在我们所赋值的地方。当然,我们到变量并不会移动,而是引擎行为表现如此,因此这样可以更好的帮助我们理解代码
除了变量,函数声明也会被提升。结果就是,从 JavaScript 引擎到角度来看,代码实际上看起来是这样的:
function fn() {
var x;
var y;
function innerFn() {
console.log(x, y); // function scope not block scope
}
x = 'function scope';
if (true) {
y = 'not block scope';
}
innerFn();
}
innerFn
的声明也被提升到了它的作用域的顶部。但是,记住它仅仅是函数声明被提升了,函数调用没有被提升。上面的代码并不会报任何错,因为 innerFn
在 x
和 y
赋值之前并没有被调用
即使使用了 ES2015,var
声明也不会创建块作用域。为了创建块作用域,我们需要在块里使用 let
或 const
声明。我们一会再看 const
,首先来看下 let
表面上,let
和 var
(我们用它来声明变量)的行为很相似:
function fn() {
var variable1;
let variable2;
}
在这个简单的例子中,var
和 let
声明都做了相同的事情(在 fn
创建的作用域下初始化了一个新的变量)。为了创建一个新的块作用域,我们需要在块里使用 let
:
function fn() {
var variable1 = 'function scope';
if (true) {
let variable2 = 'block scope';
}
console.log(variable1, variable2); // Uncaught ReferenceError: variable2 is not defined
}
fn();
在这个代码示例中,抛出了一个引用错误(reference error);让我们来探索下为什么会这样。fn
函数创建了一个新作用域,里面声明了变量 variable1
。然后我们在 if
语句的块里,声明了变量 variable2
。然而,因为我们在块里使用了 let
声明,因此一个新的块作用域在 fn
的作用域下被创建了
如果 console.log
语句也在 if
块中的话,那么它就和 variable2
在相同的作用域下了,也能够通过作用域链找到 variable1
。但是因为 console.log
在外头,因此它不能访问 variable2
,所以会抛出一个引用错误
块作用域和函数作用域的行为相同,但是他们是为块创建的,而不是函数
当一个用 var
声明的常规变量被创建时,会被提升到它的作用域的顶部,然后并初始化一个 undefined
值,这样就允许我们能够在它赋值之前引用一个�常规变量
console.log(x); // undefined
var x = 10;
记住,由于存在声明提升,代码实际看起来是这样的:
var x = undefined;
console.log(x); // undefined
x = 10;
这个行为会阻止抛出引用错误 ReferenceError
用 let
声明的变量也被提升了,但重要的是,他们并不会自动初始化值 undefined
,因此意味着下面的代码会产生一个错误:
console.log(x); // Uncaught ReferenceError: x is not defined
let x = 10;
这个错误是由暂时性死区(TDZ)引起的。TDZ 存在于作用域初始化到变量声明期间。为了修复这个错误(ReferenceError
),我们需要在访问它前声明它:
译者注:TDZ
let x;
console.log(x); // undefined
x = 10;
TDZ 这样设计是为了使开发更容易(试图引用一个还没声明的变量通常视为一个错误,而不是故意为之),因此这个错误可以立即提醒我们
新的 const
被用来声明一个不可再次赋值的变量。它和 let
的在 TDZ 的行为非常相似,�但是,const
变量必须初始化一个�值
const VAR1 = 'constant';
从现在开始, 变量 VAR1
的值将永远是 “constant” 这个字符串。如果我们试图�再次对它赋值,�我们会得到一个错误:
TypeError: Assignment to
constant
variable
如果我们试图创建一个没有初始化的 const
变量,我们将看到�一个语法错误:
SyntaxError: Missing initializer in
const
declaration
相似地,一个 const
变量�不能被再次声明。如果我们试图再次用 const
声明一个相同变量时,我们将得到一个不同类型的语法错误
SyntaxError: Identifier ‘VAR1′ has already been declared
和其他编程语言一样,常量是被用来保存我们的程序在生命周期里不希望改变的值
记住 let
和 const
�都是 JavaScript �的保留词,因此在严格模式下,是不能被用作标识符名称的(变量名,函数名等)。随着 ES2015 越来越普遍,let
和 const
�优于 var
�已形成一个共识,因为变量�创建的作用域�更与其他现代编程语言看齐,并且代码的行为也更好预测。�
因此,在大多数情况下尽可能的�避免使用 var
用 const
声明的变量不能被再次赋值的,但是 const
声明的变量并不是完全不可变的。如果我们用对象或数组初始化了一个 const
变量,我们依然可以修改对象的属性和增加删除数组的元素
- 在
for
循环里用let
来初始化计数器变量 - 修复下面
const
的错误:
const VAR1 = 'constant';
const VAR1 = 'constant2';
const VAR2;
VAR2 = 'constant';
- 本文仅代表原作者个人观点,译者不发表任何观点
- Markdown 文件由译者手动整理,如有勘误,欢迎指正
- 译文和原文采用一样协议,侵删