【译】深刻理解 ES2015,第一趴:块做用域 let 和 const


ES2015 最大的特性之一就是有了一个全新的做用域。在这个章节里,咱们将开始学习什么是做用域。咱们将继续学习如何建立新的做用域类型,以及给咱们代码带来的好处javascript

快速了解做用域

做用域描述为一个变量,函数,标识符能够被访问的区域。JavaScript 传统上有两种做用域类型:全局做用域和函数做用域,你定义变量的位置会影响其余代码是否能够访问。让咱们来看一个简单的例子来阐述做用域的概念。想象一下,你的 JavaScript 文件只包含如下代码:java

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 文件git

在全局做用域里,咱们定义了两个函数,globalFunction1globalFunction2,就像全局变量同样,他们是 “可见的” 而且能够在这个文件的任何地方调用,也能够被同一页面的其余 JavaScript 文件调用。然而,当 JavaScript 引擎解析这些函数时,会分别建立他们本身的做用域。因吹斯听,这两个新的函数做用域被嵌套在全局做用域下,成为子做用域。这也就意味着函数内的代码能够访问全局变量,就像是和在函数 “内部的” 定义变量同样github

当咱们试图访问 JavaScript 里的标识符时,浏览器会首先在当前做用域中查找。若是没有找到,浏览器会在当前做用域的父做用域中查找,而且继续向上查找,直到找到这个变量,或者到达全局做用域为止。若是这个变量在全局做用域里依旧没有找到的话,那么浏览器会抛出一个 ReferenceError 错误。这种嵌套的做用域被称做做用域链,而这个检查当前做用域和父做用域的过程被称做变量查找。这种查找只会向上查找做用域链,它永远不会在它的子做用域里查找编程

在上面的做用域链查找方向咱们得知,例子中的 innerVariable1 变量只能在 globalFunction1 函数内部被访问,innerVariable2 变量只能在 globalFunction2 函数内部被访问。innerVariable1 变量不能在 globalFunction2 函数内部或全局做用域内被访问,innerVariable2 变量也不能在 globalFunction1 函数内部或全局做用域内被访问数组

下面的图片是上面代码中做用域的抽象表示:浏览器

js-scopes

全局做用域包含了 globalVariable 以及两个内嵌的函数做用域。每一个内嵌的函数做用域又包含本身的变量,可是这些变量不能被全局做用域访问。虚线表示的是做用域链的查找方向

让咱们来看下另外一个简短的代码示例,完全的了解下到目前为止咱们所介绍到的做用域概念。假设 JavaScript 文件只包含以下代码:编程语言

function outer() {
  var variable1;

  function inner() {
    var variable2;
  }
}
复制代码

在这段代码里,咱们在全局做用域里声明了一个叫 outer 的函数。由于它是一个函数,因此它建立了一个函数做用域,嵌套在全局做用域下。在这个做用域下,咱们又声明了一个叫 variable1 的变量和 一个叫 inner 的函数。由于 inner 也是一个函数,因此一个新的做用域又被建立了,嵌套在 outer 函数的做用域下函数

inner 函数中,咱们既能够访问 variable2 也能够访问 variable1。当咱们在 inner 函数中访问 variable1 时,浏览器首先会在它的做用域里查找这个变量;当这个变量没有被找到时,会继续向上在父做用域里查找(也就是 outer 函数的做用域)。代码里做用域以下图所示:学习

js-scopes2

函数做用域能够嵌套在其余的函数做用域里,可是做用域链查找规则是同样的,所以在 inner 做用域下能够访问到 variable1variable2,可是在 outer 做用域下只能访问 variable1

这个示例中的做用域链比较长,从 inner 函数延伸到 outer 函数,直到全局对象 window

JavaScript 的新做用域

在 JavaScript 中,一个块是由一个或多个语句用大括号包裹起来的。诸如 ifforwhile 的条件表达式,都是用块基于特定的条件来执行块语句

其余流行的常见的编程语言都有块做用域,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 语句能够访问到 xy 变量。 fn 函数建立了一个函数做用域并且 xy 变量都是能够经过做用域内的做用域链访问到

声明提高

理解提高的概念是理解 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 的声明也被提高到了它的做用域的顶部。可是,记住它仅仅是函数声明被提高了,函数调用没有被提高。上面的代码并不会报任何错,由于 innerFnxy 赋值以前并无被调用

使用 let

即便使用了 ES2015,var 声明也不会建立块做用域。为了建立块做用域,咱们须要在块里使用 letconst 声明。咱们一会再看 const,首先来看下 let

表面上,letvar(咱们用它来声明变量)的行为很类似:

function fn() {
  var variable1;
  let variable2;
}
复制代码

在这个简单的例子中,varlet 声明都作了相同的事情(在 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

新的 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

和其余编程语言同样,常量是被用来保存咱们的程序在生命周期里不但愿改变的值

记住 letconst 都是 JavaScript 的保留词,所以在严格模式下,是不能被用做标识符名称的(变量名,函数名等)。随着 ES2015 愈来愈广泛,letconst 优于 var 已造成一个共识,由于变量建立的做用域更与其余现代编程语言看齐,而且代码的行为也更好预测。 所以,在大多数状况下尽量的避免使用 var

不可变性

const 声明的变量不能被再次赋值的,可是 const 声明的变量并非彻底不可变的。若是咱们用对象或数组初始化了一个 const 变量,咱们依然能够修改对象的属性和增长删除数组的元素

练习

  1. for 循环里用 let 来初始化计数器变量
  2. 修复下面 const 的错误:
const VAR1 = 'constant';
const VAR1 = 'constant2';
const VAR2;
VAR2 = 'constant';
复制代码

成功是经过不断的练习和知识的积累,而非智力


  • 本文仅表明原做者我的观点,译者不发表任何观点
  • Markdown 文件由译者手动整理,若有勘误,欢迎指正
  • 译文和原文采用同样协议,侵删
相关文章
相关标签/搜索