ES6新特性浅析

来源:互联网 发布:西安黑马程序员 编辑:程序博客网 时间:2024/05/22 13:34

       ES6新特性浅析


2009年发布的改进版本ES5,引入了Object.create()Object.defineProperty()getterssetters严格模式以及JSON对象。我们已经使用过所有这些新特性。但事实上,这些改进并没有深入影响我编写JS代码的方式,其中一个最大的革新大概就是新的数组方法:.map(). filter()这些。

但是,ES6并非如此!经过持续几年的磨砺,它已成为JS有史以来最实质的升级,新的语言和库特性就像无主之宝,等待有识之士的发掘。新的语言特性涵盖范围甚广,小到受欢迎的语法糖,例如箭头函数(arrow functions)和简单的字符串插值(string interpolation),大到烧脑的新概念,例如代理(proxies)和生成器(generators)。

ES6将彻底改变你编写JS代码的方式!

这一系列旨在向你展示如何仔细审阅ES6提供给JavaScript程序员的这些新特性。

迭代器和for-of循环

早期的数组遍历
for (var index = 0; index < myArray.length; index++) {  console.log(myArray[index]);}

ES5出现先后,我们可以这样遍历
myArray.forEach(function (value) {console.log(value);});
这段代码看起来更加简洁,但这种方法也有一个小缺陷:你不能使用break语句中断循环,也不能使用return语句返回到外层函数。

使用for-in遍历
for (var index in myArray) { // 千万别这样做  console.log(myArray[index]);}

但这是一个不好的选择,为什么?

  • 在这段代码中,赋给index的值不是实际的数字,而是字符串“0”、“1”、“2”,此时很可能在无意之间进行字符串算数计算,例如:“2” + 1 == “21”,这给编码过程带来极大的不便。
  • 作用于数组的for-in循环体除了遍历数组元素外,还会遍历自定义属性。举个例子,如果你的数组中有一个可枚举属性myArray.name,循环将额外执行一次,遍历到名为“name”的索引。就连数组原型链上的属性都能被访问到。
  • 最让人震惊的是,在某些情况下,这段代码可能按照随机顺序遍历数组元素
  • 简而言之,for-in是为普通对象设计的,你可以遍历得到字符串类型的键,因此不适用于数组遍历。
使用ES6 for-of 遍历
为什么会出现这个方法?
目前看来,成千上万的Web网站依赖for-in循环,其中一些网站甚至将其用于数组遍历。如果想通过修正for-in循环增加数组遍历支持会让这一切变得更加混乱,因此,标准委员会在ES6中增加了一种新的循环语法来解决目前的问题。

for (var value of myArray) {  console.log(value);}

与之前的内建方法相比,这种循环方式看起来是否有些眼熟?那好,我们将要探究一下for-of循环的外表下隐藏着哪些强大的功能。现在,只需记住:

  • 这是最简洁、最直接的遍历数组元素的语法
  • 这个方法避开了for-in循环的所有缺陷
  • 与forEach()不同的是,它可以正确响应break、continue和return语句

for-in循环用来遍历对象属性。

for-of循环用来遍历数据—例如数组中的值。

但是,不仅如此!

for-of循环也可以遍历其它的集合

for-of循环不仅支持数组,还支持大多数类数组对象,例如DOM NodeList对象。

for-of循环也支持字符串遍历,它将字符串视为一系列的Unicode字符来进行遍历:

for (var chr of "") {  alert(chr);}

它同样支持Map和Set对象遍历。

Map和Set对象。他们是ES6中新增的类型。后续会讲到两个新的类型。

举个例子,Set对象可以自动排除重复项

// 基于单词数组创建一个set对象var uniqueWords = new Set(words);

生成Set对象后,你可以轻松遍历它所包含的内容:

for (var word of uniqueWords) {   console.log(word);}

Map对象稍有不同:内含的数据由键值对组成,所以你需要使用解构(destructuring)来将键值对拆解为两个独立的变量:

for (var [key, value] of phoneBookMap) {   console.log(key + "'s phone number is: " + value);}

解构也是ES6的新特性,我们将在另一篇文章中讲解。看来我应该记录这些优秀的主题,未来有太多的新内容需要一一剖析。

现在,你只需记住:未来的JS可以使用一些新型的集合类,甚至会有更多的类型陆续诞生,而for-of就是为遍历所有这些集合特别设计的循环语句。

for-of循环不支持普通对象,但如果你想迭代一个对象的属性,你可以用for-in循环(这也是它的本职工作)或内建的Object.keys()方法:

// 向控制台输出对象的可枚举属性for (var key of Object.keys(someObject)) {  console.log(key + ": " + someObject[key]);}

深入理解

ES6始终坚持这样的宗旨:凡是新加入的特性,势必已在其它语言中得到强有力的实用性证明。

正如其它语言中的for/foreach语句一样,for-of循环语句通过方法调用来遍历各种集合。数组、Maps对象、Sets对象以及其它在我们讨论的对象有一个共同点,它们都有一个迭代器方法

你可以给任意类型的对象添加迭代器方法。

迭代器对象

我们简单了解一下迭代器

for-of循环首先调用集合的[Symbol.iterator]()方法,紧接着返回一个新的迭代器对象。迭代器对象可以是任意具有.next()方法的对象;for-of循环将重复调用这个方法,每次循环调用一次。举个例子,这段代码是我能想出来的最简单的迭代器:

var zeroesForeverIterator = { [Symbol.iterator]: function () {   return this;  },  next: function () {  return {done: false, value: 0}; }};

每一次调用.next()方法,它都返回相同的结果,返回给for-of循环的结果有两种可能:(a) 我们尚未完成迭代;(b) 下一个值为0。这意味着(value of zeroesForeverIterator) {}将会是一个无限循环。当然,一般来说迭代器不会如此简单。

迭代器对象也可以实现可选的.return()和.throw(exc)方法。如果for-of循环过早退出会调用.return()方法,异常、break语句或return语句均可触发过早退出。如果迭代器需要执行一些清洁或释放资源的操作,可以在.return()方法中实现。大多数迭代器方法无须实现这一方法。.throw(exc)方法的使用场景就更特殊了:for-of循环永远不会调用它。

现在我们已了解所有细节,可以写一个简单的for-of循环然后按照下面的方法调用重写被迭代的对象。

首先是for-of循环:

for (VAR of ITERABLE) {  一些语句}

然后是一个使用以下方法和少许临时变量实现的与之前大致相当的示例,:

var $iterator = ITERABLE[Symbol.iterator]();var $result = $iterator.next();while (!$result.done) {   VAR = $result.value;   一些语句   $result = $iterator.next(); }

这段代码没有展示.return()方法是如何处理的,我们可以添加这部分代码,可以看到for-of循环用起来很简单,但是其背后有着非常复杂的机制。

ES6生成器(Generators)简介

什么是生成器?

举个例子:

function* quips(name) {  yield "你好 " + name + "!";  yield "我叫派,哈哈哈";  if (name.startsWith("X")) {    yield "你的名字 " + name + "  首字母是X,这很傻!";  }  yield "我们下次再见!";}

这段代码看起来很像一个函数,我们称之为生成器函数,它与普通函数有很多共同点,但是二者有如下区别:

  • 普通函数使用function声明,而生成器函数使用function*声明。
  • 在生成器函数内部,有一种类似return的语法:关键字yield。二者的区别是,普通函数只可以return一次,而生成器函数可以yield多次(当然也可以只yield一次)。在生成器的执行过程中,遇到yield表达式立即暂停,后续可恢复执行状态。

这就是普通函数和生成器函数之间最大的区别,普通函数不能自暂停,生成器函数可以。

生成器做了什么?

当你调用quips()生成器函数时发生了什么?

> var iter = quips("jorendorff");  [object Generator]> iter.next()  { value: "你好 jorendorff!", done: false }> iter.next()  { value: "我叫派", done: false }> iter.next()  { value: "我们下次再见!", done: false }> iter.next()  { value: undefined, done: true }

你大概已经习惯了普通函数的使用方式,当你调用它们时,它们立即开始运行,直到遇到return或抛出异常时才退出执行。

生成器调用看起来非常类似:quips("jorendorff")。但是,当你调用一个生成器时,它并非立即执行,而是返回一个已暂停的生成器对象(上述实例代码中的iter)。你可将这个生成器对象视为一次函数调用,只不过立即冻结了,它恰好在生成器函数的最顶端的第一行代码之前冻结了。

每当你调用生成器对象的.next()方法时,函数调用将其自身解冻并一直运行到下一个yield表达式,再次暂停。

这也是在上述代码中我们每次都调用iter.next()的原因,我们获得了quips()函数体中yield表达式生成的不同的字符串值。

调用最后一个iter.next()时,我们最终抵达生成器函数的末尾,所以返回结果中done的值为true。抵达函数的末尾意味着没有返回值,所以返回结果中value的值为undefined。

值得特别一提的是,生成器不是线程,在支持线程的语言中,多段代码可以同时运行,通通常导致竞态条件和非确定性,不过同时也带来不错的性能。生成器则完全不同。当生成器运行时,它和调用者处于同一线程中,拥有确定的连续执行顺序,永不并发。与系统线程不同的是,生成器只有在其函数体内标记为yield的点才会暂停。

现在,我们了解了生成器的原理,领略过生成器的运行、暂停恢复运行的不同状态。那么,这些奇怪的功能究竟有何用处?

生成器是迭代器

ES6的迭代器,它是ES6中独立的内建类,同时也是语言的一个扩展点,通过实现[Symbol.iterator]()和.next()两个方法你就可以创建自定义迭代器。

实现一个迭代器。举个例子,我们创建一个简单的range迭代器,它可以简单地将两个数字之间的所有数相加。首先是传统的for(;;)循环:

// 应该弹出三次 "ding"for (var value of range(0, 3)) {  alert("Ding! at floor #" + value);}

使用ES6的类的解决方案

class RangeIterator {  constructor(start, stop) {    this.value = start;    this.stop = stop;  }  [Symbol.iterator]() { return this; }  next() {    var value = this.value;    if (value < this.stop) {      this.value++;      return {done: false, value: value};    } else {      return {done: true, value: undefined};    }  }}// 返回一个新的迭代器,可以从start到stop计数。function range(start, stop) {  return new RangeIterator(start, stop);}

这看起来有些难以实现。

既然我们有生成器,是否可以在这里应用它们呢?尝试一下:

function* range(start, stop) {  for (var i = start; i < stop; i++)    yield i;}

以上4行代码实现的生成器完全可以替代之前引入了一整个RangeIterator类的23行代码的实现。可行的原因是:生成器是迭代器。所有的生成器都有内建.next()和[Symbol.iterator]()方法的实现。你只须编写循环部分的行为。

RangeIterator的实现代码很长并且非常奇怪,因为你需要在不借助循环语法的前提下为它添加循环功能的描述。所以生成器是最好的解决方案!

我们如何发挥作为迭代器的生成器所产生的最大效力?

l 使任意对象可迭代。编写生成器函数遍历这个对象,运行时yield每一个值。然后将这个生成器函数作为这个对象的[Symbol.iterator]方法。

l 简化数组构建函数。假设你有一个函数,每次调用的时候返回一个数组结果,就像这样:

// 拆分一维数组icons// 根据长度rowLengthfunction splitIntoRows(icons, rowLength) {  var rows = [];  for (var i = 0; i < icons.length; i += rowLength) {    rows.push(icons.slice(i, i + rowLength));  }  return rows;}

使用生成器创建的代码相对较短:

function* splitIntoRows(icons, rowLength) {  for (var i = 0; i < icons.length; i += rowLength) {    yield icons.slice(i, i + rowLength);  }}

行为上唯一的不同是,传统写法立即计算所有结果并返回一个数组类型的结果,使用生成器则返回一个迭代器,每次根据需要逐一地计算结果。

  • 获取异常尺寸的结果。你无法构建一个无限大的数组,但是你可以返回一个可以生成一个永无止境的序列的生成器,每次调用可以从中取任意数量的值。
  • 重构复杂循环。你是否写过又丑又大的函数?你是否愿意将其拆分为两个更简单的部分?现在,你的重构工具箱里有了新的利刃——生成器。当你面对一个复杂的循环时,你可以拆分出生成数据的代码,将其转换为独立的生成器函数,然后使用for (var data of myNewGenerator(args))遍历我们所需的数据。
  • 构建与迭代相关的工具。ES6不提供用来过滤、映射以及针对任意可迭代数据集进行特殊操作的扩展库。借助生成器,我们只须写几行代码就可以实现类似的工具。

举个例子,假设你需要一个等效于Array.prototype.filter并且支持DOM NodeLists的方法,可以这样写:

function* filter(test, iterable) {  for (var item of iterable) {    if (test(item))      yield item;  }}

你看,生成器魔力四射!借助它们的力量可以非常轻松地实现自定义迭代器,记住,迭代器贯穿ES6的始终,它是数据和循环的新标准。

生成器和异步代码

这是我在一段时间以前写的一些JS代码

         };        })      });    });  });});

可能你已经在自己的代码中见过类似的片段,异步API通常需要一个回调函数,这意味着你需要为每一次任务执行编写额外的异步函数。所以如果你有一段代码需要完成三个任务,你将看到类似的三层级缩进的代码,而非简单的三行代码。

比如这样写:

}).on('close', function () {  done(undefined, undefined);}).on('error', function (error) {  done(error);});

异步API拥有错误处理规则,不支持异常处理。不同的API有不同的规则,大多数的错误规则是默认的;在有些API里,甚至连成功提示都是默认的。

这些是到目前为止我们为异步编程所付出的代价,我们正慢慢开始接受异步代码不如等效同步代码美观又简洁的这个事实。

生成器为你提供了避免以上问题的新思路。

实验性的Q.async()尝试结合promises使用生成器产生异步代码的等效同步代码。举个例子:

// 制造一些噪音的同步代码。function makeNoise() {  shake();  rattle();  roll();}// 制造一些噪音的异步代码。// 返回一个Promise对象// 当我们制造完噪音的时候会变为resolvedfunction makeNoise_async() {  return Q.async(function* () {    yield shake_async();    yield rattle_async();    yield roll_async();  });}

二者主要的区别是,异步版本必须在每次调用异步函数的地方添加yield关键字。

在Q.async版本中添加一个类似if语句的判断或try/catch块,如同向同步版本中添加类似功能一样简单。与其它异步代码编写方法相比,这种方法更自然,不像是学一门新语言一样辛苦。

如果你已经看到这里,你可以试着阅读来自James Long的更深入地讲解生成器的文章。

生成器为我们提供了一个新的异步编程模型思路,这种方法更适合人类的大脑。相关工作正在不断展开。此外,更好的语法或许会有帮助。

模板字符串


反撇号(`)基础知识

ES6引入了一种新型的字符串字面量语法,我们称之为模板字符串(template strings)。除了使用反撇号字符 ` 代替普通字符串的引号 ' 或 " 外,它们看起来与普通字符串并无二致。在最简单的情况下,它们与普通字符串的表现一致:

context.fillText(`Ceci n'est pas une chaîne.`, x, y);

但是我们并没有说:“原来只是被反撇号括起来的普通字符串啊”。模板字符串名之有理,它为JavaScript提供了简单的字符串插值功能,从此以后,你可以通过一种更加美观、更加方便的方式向字符串中插值了。

模板字符串的使用方式成千上万,但是最让我会心一暖的是将其应用于毫不起眼的错误消息提示:

function authorize(user, action) {  if (!user.hasPrivilege(action)) {    throw new Error(      `用户 ${user.name} 未被授权执行 ${action} 操作。`);  }}

在这个示例中,${user.name}和${action}被称为模板占位符,JavaScript将把user.name和action的值插入到最终生成的字符串中,例如:用户jorendorff未被授权打冰球。(这是真的,我还没有获得冰球许可证。)

到目前为止,我们所了解到的仅仅是比 + 运算符更优雅的语法,下面是你可能期待的一些特性细节:

  • 模板占位符中的代码可以是任意JavaScript表达式,所以函数调用、算数运算等这些都可以作为占位符使用,你甚至可以在一个模板字符串中嵌套另一个,我称之为模板套构(template inception)。
  • 如果这两个值都不是字符串,可以按照常规将其转换为字符串。例如:如果action是一个对象,将会调用它的.toString()方法将其转换为字符串值。
  • 如果你需要在模板字符串中书写反撇号,你必须使用反斜杠将其转义:`\``等价于"`"。
  • 同样地,如果你需要在模板字符串中引入字符$和{。无论你要实现什么样的目标,你都需要用反斜杠转义每一个字符:`\$`和`\{`。

与普通字符串不同的是,模板字符串可以多行书写:

$("#warning").html(`  <h1>小心!>/h1>  <p>未经授权打冰球可能受罚  将近${maxPenalty}分钟。</p>`);

模板字符串中所有的空格、新行、缩进,都会原样输出在生成的字符串中。

好啦,我说过要让你们轻松掌握模板字符串,从现在起难度会加大,你可以到此为止,去喝一杯咖啡,慢慢消化之前的知识。真的,及时回头不是一件令人感到羞愧的事情。Lopes Gonçalves曾经向我们证明过,船只不会被海妖碾压,也不会从地球的边缘坠落下去,他最终跨越了赤道,但是他有继续探索整个南半球么?并没有,他回家了,吃了一顿丰盛的午餐,你一定不排斥这样的感觉。

反撇号的未来

当然,模板字符串也并非事事包揽:

  • 它们不会为你自动转义特殊字符,为了避免跨站脚本漏洞,你应当像拼接普通字符串时做的那样对非置信数据进行特殊处理。
  • 它们无法很好地与国际化库(可以帮助你面向不同用户提供不同的语言)相配合,模板字符串不会格式化特定语言的数字和日期,更别提同时使用不同语言的情况了。
  • 它们不能替代模板引擎的地位,例如:Mustache、Nunjucks。

ES6为JS开发者和库设计者提供了一个很好的衍生工具,你可以借助这一特性突破模板字符串的诸多限制,我们称之为标签模板(tagged templates)。

标签模板的语法非常简单,在模板字符串开始的反撇号前附加一个额外的标签即可。我们的第一个示例将添加一个SaferHTML标签,我们要用这个标签来解决上述的第一个限制:自动转义特殊字符。

请注意,ES6标准库不提供类似SaferHTML功能,我们将在下面自己来实现这个功能。

var message =  SaferHTML`<p>${bonk.sender} 向你示好。</p>`;

这里用到的标签是一个标识符SaferHTML;也可以使用属性值作为标签,例如:SaferHTML.escape;还可以是一个方法调用,例如:SaferHTML.escape({unicodeControlCharacters: false})。精确地说,任何ES6的成员表达式(MemberExpression)或调用表达式(CallExpression)都可作为标签使用。

可以看出,无标签模板字符串简化了简单字符串拼接,标签模板则完全简化了函数调用!

上面的代码等效于:

var message =  SaferHTML(templateData, bonk.sender);

templateData是一个不可变数组,存储着模板所有的字符串部分,由JS引擎为我们创建。因为占位符将标签模板分割为两个字符串的部分,所以这个数组内含两个元素,形如Object.freeze(["<p>", " has sent you a bonk.</p>"]。

(事实上,templateData中还有一个属性,在这篇文章中我们不会用到,但是它是标签模板不可分割的一环:templateData.raw,它同样是一个数组,存储着标签模板中所有的字符串部分,如果我们查看源码将会发现,在这里是使用形如\n的转义序列分行,而在templateData中则为真正的新行,标准标签String.raw会用到这些原生字符串。)

如此一来,SaferHTML函数就可以有成千上万种方法来解析字符串和占位符。

在继续阅读以前,可能你苦苦思索到底用SaferHTML来做什么,然后着手尝试去实现它,归根结底,它只是一个函数,你可以在Firefox的开发者控制台里测试你的成果。

以下是一种可行的方案(在gist中查看):

function SaferHTML(templateData) {  var s = templateData[0];  for (var i = 1; i < arguments.length; i++) {    var arg = String(arguments[i]);    // 转义占位符中的特殊字符。    s += arg.replace(/&/g, "&")            .replace(/</g, "<")            .replace(/</g, ">");    // 不转义模板中的特殊字符。    s += templateData[i];  }  return s;}

通过这样的定义,标签模板SaferHTML`<p>${bonk.sender} 向你示好。</p>` 可能扩展为字符串 "<p>ES6<3er 向你示好。</p>"。即使一个恶意命名的用户,例如“黑客Steve<script>alert('xss');</script>”,向其他用户发送一条骚扰信息,无论如何这条信息都会被转义为普通字符串,其他用户不会受到潜在攻击的威胁。

标签模板带来的灵活性远不止于此,要记住,标签函数的参数不会自动转换为字符串,它们如返回值一样,可以是任何值,标签模板甚至不一定要是字符串!你可以用自定义的标签来创建正则表达式、DOM树、图片、以promises为代表的整个异步过程、JS数据结构、GL着色器……

不定参数和默认参数

不定参数

我们通常使用可变参函数来构造API,可变参函数可接受任意数量的参数。例如,String.prototype.concat方法就可以接受任意数量的字符串参数。ES6提供了一种编写可变参函数的新方式——不定参数。

我们通过一个简单的可变参数函数containsAll给大家演示不定参数的用法。函数containsAll可以检查一个字符串中是否包含若干个子串,例如:containsAll("banana", "b", "nan")返回true,containsAll("banana", "c", "nan")返回false。

首先使用传统方法来实现这个函数:

function containsAll(haystack) {  for (var i = 1; i < arguments.length; i++) {    var needle = arguments[i];    if (haystack.indexOf(needle) === -1) {      return false;    }  }  return true;}

在这个实现中,我们用到了神奇的arguments对象,它是一个类数组对象,其中包含了传递给函数的所有参数。这段代码实现了我们的需求,但它的可读性却不是最理想的。函数的参数列表中只有一个参数haystack,我们无法一眼就看出这个函数实际上接受了多个参数。另外,我们一定要注意,应该从1开始迭代,而不是从0开始,因为arguments[0]相当于参数haystack。如果我们想要在haystack前后添加另一个参数,我们一定要记得更新循环体。不定参数恰好可以解决可读性与参数索引的问题。下面是用ES6不定参数特性实现的containsAll函数:

function containsAll(haystack, ...needles) {  for (var needle of needles) {    if (haystack.indexOf(needle) === -1) {      return false;    }  }  return true;}

这一版containsAll函数与前者有相同的行为,但这一版中使用了一个特殊的...needles语法。我们来看一下调用containsAll("banana", "b", "nan")之后的函数调用过程,与之前一样,传递进来的第一个参数"banana"赋值给参数haystack,needles前的省略号表明它是一个不定参数,所有传递进来的其它参数都被放到一个数组中,赋值给变量needles。对于我们的调用示例而言,needles被赋值为["b", "nan"],后续的函数执行过程一如往常。(注意啦,我们已经使用过ES6中for-of循环。)

在所有函数参数中,只有最后一个才可以被标记为不定参数。函数被调用时,不定参数前的所有参数都正常填充,任何“额外的”参数都被放进一个数组中并赋值给不定参数。如果没有额外的参数,不定参数就是一个空数组,它永远不会是undefined。

默认参数

通常来说,函数调用者不需要传递所有可能存在的参数,没有被传递的参数可由感知到的默认参数进行填充。JavaScript有严格的默认参数格式,未被传值的参数默认为undefined。ES6引入了一种新方式,可以指定任意参数的默认值。

下面是一个简单的示例(反撇号表示模板字符串):

function animalSentence(animals2="tigers", animals3="bears") {    return `Lions and ${animals2} and ${animals3}! Oh my!`;}

默认参数的定义形式为[param1[ = defaultValue1 ][, ..., paramN[ = defaultValueN ]]],对于每个参数而言,定义默认值时=后的部分是一个表达式,如果调用者没有传递相应参数,将使用该表达式的值作为参数默认值。相关示例如下:

animalSentence();                       // Lions and tigers and bears! Oh my!animalSentence("elephants");            // Lions and elephants and bears! Oh my!animalSentence("elephants", "whales");  // Lions and elephants and whales! Oh my!

默认参数有几个微妙的细节需要注意:

  • 默认值表达式在函数调用时自左向右求值,这也意味着,默认表达式可以使用该参数之前已经填充好的其它参数值。举个例子,我们优化一下刚刚那个动物语句函数:

function animalSentenceFancy(animals2="tigers",    animals3=(animals2 == "bears") ? "sealions" : "bears"){  return `Lions and ${animals2} and ${animals3}! Oh my!`;}

现在,animalSentenceFancy("bears")将返回“Lions and bears and sealions. Oh my!”。

  • 传递undefined值等效于不传值,所以animalSentence(undefined, "unicorns")将返回“Lions and tigers and unicorns! Oh my!”。

  • 没有默认值的参数隐式默认为undefined,所以

function myFunc(a=42, b) {...}

是合法的,并且等效于

function myFunc(a=42, b=undefined) {...}

停止使用arguments

现在我们已经看到了arguments对象可被不定参数和默认参数完美代替,移除arguments后通常会使代码更易于阅读。除了破坏可读性外,众所周知,针对arguments对象对JavaScript虚拟机进行的优化会导致一些让你头疼不已的问题。

我们期待着不定参数和默认参数可以完全取代arguments,要实现这个目标,标准中增加了相应的限制:在使用不定参数或默认参数的函数中禁止使用arguments对象。曾经实现过arguments的引擎不会立即移除对它的支持,当然,现在更推荐使用不定参数和默认参数。

解构 Destructuring

什么是解构赋值?

解构赋值允许你使用类似数组或对象字面量的语法将数组和对象的属性赋给各种变量。这种赋值语法极度简洁,同时还比传统的属性访问方法更为清晰。

通常来说,你很可能这样访问数组中的前三个元素:

    var first = someArray[0];    var second = someArray[1];    var third = someArray[2];

如果使用解构赋值的特性,将会使等效的代码变得更加简洁并且可读性更高:

    var [first, second, third] = someArray;

数组与迭代器的解构

以上是数组解构赋值的一个简单示例,其语法的一般形式为:

    [ variable1, variable2, ..., variableN ] = array;

这将为variable1到variableN的变量赋予数组中相应元素项的值。如果你想在赋值的同时声明变量,可在赋值语句前加入varletconst关键字,例如:

    var [ variable1, variable2, ..., variableN ] = array;    let [ variable1, variable2, ..., variableN ] = array;    const [ variable1, variable2, ..., variableN ] = array;

事实上,用变量来描述并不恰当,因为你可以对任意深度的嵌套数组进行解构:

    var [foo, [[bar], baz]] = [1, [[2], 3]];    console.log(foo);    // 1    console.log(bar);    // 2    console.log(baz);    // 3

此外,你可以在对应位留空来跳过被解构数组中的某些元素:

    var [,,third] = ["foo", "bar", "baz"];    console.log(third);    // "baz"

而且你还可以通过“不定参数”模式捕获数组中的所有尾随元素:

    var [head, ...tail] = [1, 2, 3, 4];    console.log(tail);    // [2, 3, 4]

当访问空数组或越界访问数组时,对其解构与对其索引的行为一致,最终得到的结果都是:undefined

    console.log([][0]);    // undefined    var [missing] = [];    console.log(missing);    // undefined

请注意,数组解构赋值的模式同样适用于任意迭代器:

    function* fibs() {      var a = 0;      var b = 1;      while (true) {        yield a;        [a, b] = [b, a + b];      }    }    var [first, second, third, fourth, fifth, sixth] = fibs();    console.log(sixth);    // 5

对象的解构

通过解构对象,你可以把它的每个属性与不同的变量绑定,首先指定被绑定的属性,然后紧跟一个要解构的变量。

    var robotA = { name: "Bender" };    var robotB = { name: "Flexo" };    var { name: nameA } = robotA;    var { name: nameB } = robotB;    console.log(nameA);    // "Bender"    console.log(nameB);    // "Flexo"

当属性名与变量名一致时,可以通过一种实用的句法简写:

    var { foo, bar } = { foo: "lorem", bar: "ipsum" };    console.log(foo);    // "lorem"    console.log(bar);    // "ipsum"

与数组解构一样,你可以随意嵌套并进一步组合对象解构:

    var complicatedObj = {      arrayProp: [        "Zapp",        { second: "Brannigan" }      ]    };    var { arrayProp: [first, { second }] } = complicatedObj;    console.log(first);    // "Zapp"    console.log(second);    // "Brannigan"

当你解构一个未定义的属性时,得到的值为undefined

    var { missing } = {};    console.log(missing);    // undefined

请注意,当你解构对象并赋值给变量时,如果你已经声明或不打算声明这些变量(亦即赋值语句前没有letconstvar关键字),你应该注意这样一个潜在的语法错误:

    { blowUp } = { blowUp: 10 };    // Syntax error 语法错误

为什么会出错?这是因为JavaScript语法通知解析引擎将任何以{开始的语句解析为一个块语句(例如,{console}是一个合法块语句)。解决方案是将整个表达式用一对小括号包裹:

    ({ safe } = {});    // No errors 没有语法错误

解构值不是对象、数组或迭代器

当你尝试解构nullundefined时,你会得到一个类型错误:

    var {blowUp} = null;    // TypeError: null has no properties(null没有属性)

然而,你可以解构其它原始类型,例如:布尔值数值字符串,但是你将得到undefined

    var {wtf} = NaN;    console.log(wtf);    // undefined

你可能对此感到意外,但经过进一步审查你就会发现,原因其实非常简单。当使用对象赋值模式时,被解构的值需要被强制转换为对象。大多数类型都可以被转换为对象,但nullundefined却无法进行转换。当使用数组赋值模式时,被解构的值一定要包含一个迭代器。

默认值

当你要解构的属性未定义时你可以提供一个默认值:

    var [missing = true] = [];    console.log(missing);    // true    var { message: msg = "Something went wrong" } = {};    console.log(msg);    // "Something went wrong"    var { x = 3 } = {};    console.log(x);    // 3

(译者按:Firefox目前只实现了这个特性的前两种情况,第三种尚未实现。详情查看bug 932080。)

解构的实际应用

函数参数定义

作 为开发者,我们需要实现设计良好的API,通常的做法是为函数为函数设计一个对象作为参数,然后将不同的实际参数作为对象属性,以避免让API使用者记住 多个参数的使用顺序。我们可以使用解构特性来避免这种问题,当我们想要引用它的其中一个属性时,大可不必反复使用这种单一参数对象。

    function removeBreakpoint({ url, line, column }) {      // ...    }

这是一段来自Firefox开发工具JavaScript调试器(同样使用JavaScript实现——没错,就是这样!)的代码片段,它看起来非常简洁,我们会发现这种代码模式特别讨喜。

配置对象参数

延伸一下之前的示例,我们同样可以给需要解构的对象属性赋予默认值。当我们构造一个提供配置的对象,并且需要这个对象的属性携带默认值时,解构特性就派上用场了。举个例子,jQuery的ajax函数使用一个配置对象作为它的第二参数,我们可以这样重写函数定义:

    jQuery.ajax = function (url, {      async = true,      beforeSend = noop,      cache = true,      complete = noop,      crossDomain = false,      global = true,      // ... 更多配置    }) {      // ... do stuff    };

如此一来,我们可以避免对配置对象的每个属性都重复var foo = config.foo || theDefaultFoo;这样的操作。

与ES6迭代器协议协同使用

ECMAScript 6中定义了一个迭代器协议,当你迭代Maps(ES6标准库中新加入的一种对象)后,你可以得到一系列形如[key, value]的键值对,我们可将这些键值对解构,更轻松地访问键和值:

    var map = new Map();    map.set(window, "the global");    map.set(document, "the document");    for (var [key, value] of map) {      console.log(key + " is " + value);    }    // "[object Window] is the global"    // "[object HTMLDocument] is the document"

只遍历键:

    for (var [key] of map) {      // ...    }

或只遍历值:

    for (var [,value] of map) {      // ...    }

多重返回值

JavaScript语言中尚未整合多重返回值的特性,但是无须多此一举,因为你自己就可以返回一个数组并将结果解构:

    function returnMultipleValues() {      return [1, 2];    }    var [foo, bar] = returnMultipleValues();

或者,你可以用一个对象作为容器并为返回值命名:

    function returnMultipleValues() {      return {        foo: 1,        bar: 2      };    }    var { foo, bar } = returnMultipleValues();

这两个模式都比额外保存一个临时变量要好得多。

    function returnMultipleValues() {      return {        foo: 1,        bar: 2      };    }    var temp = returnMultipleValues();    var foo = temp.foo;    var bar = temp.bar;

或者使用CPS变换:

    function returnMultipleValues(k) {      k(1, 2);    }    returnMultipleValues((foo, bar) => ...);

使用解构导入部分CommonJS模块

你是否尚未使用ES6模块?还用着CommonJS的模块呢吧!没问题,当我们导入CommonJS模块X时,很可能在模块X中导出了许多你根本没打算用的函数。通过解构,你可以显式定义模块的一部分来拆分使用,同时还不会污染你的命名空间:

    const { SourceMapConsumer, SourceNode } = require("source-map");







原创粉丝点击