【译】ES7和ES8新特性

最近,我写了一篇博客文章 (opens new window),甚至还创建了一个关于 ES6 / ES2015 的在线课程 (opens new window)。你猜怎么着?TC39(强大的 JavaScript 监督者)正在推进 ES8 的发展,因此让我们聊聊 ES7 和 ES8(或按官方正式 叫法应该叫 ES2016 和 ES2017)。幸运的是,它们比最佳标准 ES6 的功能特性要少得多。 这是真的!你看 ES7 只有两个新功能特性!

ES7 功能特性:

  1. Array.prototype.includes
  2. 求幂运算符**

截至本文撰写时(2017 年 1 月),ES8 标准尚未最终确定,但我们可以假设所有已完成的 提案(第 4 阶段)和第 3 阶段的大部分(更多关于阶段的详细内容 在这里 (opens new window)和我 的课程 (opens new window)中)。已完成的 2017 年(ES8)提案是:

  1. Object.values/Object.entries
  2. 字符串填充
  3. Object.getOwnPropertyDescriptors
  4. 函数参数列表允许尾随逗号
  5. 异步函数

在这篇文章中,我不会介绍第 3 阶段的提案,但是你可以 在这里 (opens new window)查看第 1 到第 3 阶段的提案情况。

让我们深入了解提案及特性。

# Array.prototype.includes

使用Array.prototype.includes可以使一切变得容易简单。它是indexOf方法的替代者 ,过去开发人员使用indexOf方法检查数组中是否存在某个元素。indexOf方法使用起来 有点笨拙,因为它返回元素所在数组的索引,或者在找不到该元素的情况下返回-1,这么 看来返回结果是一个数字类型的值,而非布尔类型的值,这让开发人员还需要进行额外的判 断。在 ES6 中,要检查元素是否存在,你必须像下面的代码一样,因为当匹配不到时 ,Array.prototype.indexOf返回-1,-1 是真值(转化为布尔值是 true),但是当匹配 的元素的索引为 0 时,数组中确实包含该元素,但 0 转化为布尔值是false

let arr = ['react', 'angular', 'vue'];

// WRONG
if (arr.indexOf('react')) {
  // 0 -> evaluates to false, definitely as we expected
  console.log('Can use React'); // this line would never be executed
}

// Correct
if (arr.indexOf('react') !== -1) {
  console.log('Can use React');
}
1
2
3
4
5
6
7
8
9
10
11
12

或者使用一个小技巧,按位求反运算符会使代码更简洁紧凑,因为对任何数字的( 按位求反)等于-(a +1)

let arr = ['react', 'angular', 'vue'];

// Correct
if (~arr.indexOf('react')) {
  console.log('Can use React');
}
1
2
3
4
5
6

使用 ES7 的includes方法的代码:

let arr = ['react', 'angular', 'vue'];

// Correct
if (arr.includes('react')) {
  console.log('Can use React');
}
1
2
3
4
5
6

开发人员还可以在字符串中使用includes方法:

let str = 'React Quickly';

// Correct
if (str.toLowerCase().includes('react')) {
  // true
  console.log('Found "react"');
}
1
2
3
4
5
6
7

有趣的是,许多 JavaScript 库已经有了includes方法或类似的contains方法 (由于 MooTools 库的原因 (opens new window),TC39 决定不使用 contains 这个名称):

  • jQuery 的:$.inArray
  • Underscore.js:_.contains
  • Lodash:_.includes(在版本 3 和更低版本中,_.contains方法跟 Underscore 中 的一样)
  • CoffeeScript:in运算符(示例 (opens new window)
  • Dart:list.contains示例 (opens new window)

除了更加具有说服力和为开发人员提供布尔值(而非索引值)之外,include方法还可以 和NaN一起使用。最后,include方法具有第二个可选参数fromIndex,这有利于简化 代码,因为它允许从指定的位置开始查找匹配项。

更多示例:

console.log([1, 2, 3].includes(2)); // === true)
console.log([1, 2, 3].includes(4)); // === false)

console.log([1, 2, NaN].includes(NaN)); // === true)

console.log([1, 2, -0].includes(+0)); // === true)
console.log([1, 2, +0].includes(-0)); // === true)

console.log(['a', 'b', 'c'].includes('a')); // === true)
console.log(['a', 'b', 'c'].includes('a', 1)); // === false)
1
2
3
4
5
6
7
8
9
10

总而言之,include方法几乎为所有开发人员在需要检索元素是否在数组/列表中时提供了 便利……。让我们一起欢呼吧 ✌️!

# 求幂运算符**

这个运算符主要是为开发人员做一些数学运算,在 3D、虚拟现实、SVG 或数据可视化的情 况下很有用。在 ES6 及之前的版本中,你必须创建一个循环,创建一个递归函数或使用 Math.pow。求幂就是把同一个数字(底数)乘以自身多次(指数)。例如,7 的 3 次幂 是 7 * 7 * 7

在 ES6 / ES2015 中,你可以使用Math.pow或创建一个小的递归箭头函数:

calculateExponent = (base, exponent) =>
  base * (--exponent > 1 ? calculateExponent(base, exponent) : base);
console.log(calculateExponent(7, 12) === Math.pow(7, 12)); // true
console.log(calculateExponent(2, 7) === Math.pow(2, 7)); // true
1
2
3
4

现在在 ES7 / ES2016 中,面向数学的开发人员可以使用较短的语法:

let a = 7 ** 12;
let b = 2 ** 7;
console.log(a === Math.pow(7, 12)); // true
console.log(b === Math.pow(2, 7)); // true
1
2
3
4

开发人员也可以用来赋值:

let a = 7;
a **= 12;
let b = 2;
b **= 7;
console.log(a === Math.pow(7, 12)); // true
console.log(b === Math.pow(2, 7)); // true
1
2
3
4
5
6

ES 的很多新功能是从其他语言中借鉴过来的(CoffeeScript-最爱,Ruby 等)。如你所料 其他语言中也会存在求幂运算符:

  • Python: x ** y
  • CoffeeScript: x ** y
  • F#: x ** y
  • Ruby: x ** y
  • Perl: x ** y
  • Lua, Basic, MATLAB: x ^ y

对我个人来说,在 JavaScript 中没有求幂运算符从来都不是问题。:)在我写了 15 年的 JavaScript 生涯中,除了面试和像这样的教程外,我从未写过任何关于指数的东西……求幂 运算符对你来说是不可或缺的吗?

# Object.values/Object.entries

ECMAScript2017 规范中的Object.valuesObject.entries,与Object.keys类似都 返回一个数组,并且数组的顺序与 Object.keys返回的数组顺序是一样的。

Object.keysObject.valuesObject.entries返回数组中的每一项,都相应地包含 了对象自身可枚举属性的键、值和键值对。

在 ES8/ES2017 之前,如果 JavaScript 开发人员需要迭代对象的自身属性,就必须使 用Object.keys,然后对其返回的数组进行迭代,并使用obj[key]来访问每个值:

let obj = { a: 1, b: 2, c: 3 };
Object.keys(obj).forEach((key, index) => {
  console.log(key, obj[key]);
});
1
2
3
4

或使用 ES6 / ES2015 的for/of会更好一些:

let obj = { a: 1, b: 2, c: 3 };
for (let key of Object.keys(obj)) {
  console.log(key, obj[key]);
}
1
2
3
4

你也可以使用旧的for/in(ES5),但这会遍历所有可枚举的属性(如原型中的属性或带名字 的属性--详情 见MDN (opens new window)), 而不仅仅是自己的属性,这可能会意外地用prototypetoString之类的意外值破坏结 果。

Object.values返回一个对象自身可枚举属性的数组。我们可以使 用Array.prototype.forEach对其进行迭代,但要使用 ES6 的箭头函数和隐式返回:

let obj = { a: 1, b: 2, c: 3 };
Object.values(obj).forEach((value) => console.log(value)); // 1, 2, 3
1
2

或使用for/of

let obj = { a: 1, b: 2, c: 3 };
for (let value of Object.values(obj)) {
  console.log(value);
}
// 1, 2, 3
1
2
3
4
5

Object.entries 则会返回一个对象自身可枚举属性键值对(作为一个数组)的数 组,返回结果数组中的每个一项也都是一个数组。

let obj = {a: 1, b: 2, c: 3}
JSON.stringify(Object.entries(obj))
"[["a",1],["b",2],["c",3]]"
1
2
3

我们可以使用 ES6 / ES2015 的解构(请查看这篇文章 (opens new window)或 本课程 (opens new window)中关于深入 ES6 的内容),从一个嵌套数组中 声明keyvalue

let obj = { a: 1, b: 2, c: 3 };
Object.entries(obj).forEach(([key, value]) => {
  console.log(`${key} is ${value}`);
});
// a is 1, b is 2, c is 3
1
2
3
4
5

如你所料,我们也可以使用 ES6 的 for/of(毕竟是用于数组的!)来迭代 Object.entrents 的结果:

let obj = { a: 1, b: 2, c: 3 };
for (let [key, value] of Object.entries(obj)) {
  console.log(`${key} is ${value}`);
}
// a is 1, b is 2, c is 3
1
2
3
4
5

现在从对象中提取值和键值对变得更加容易了。Object.valuesObject.entries的执 行方式与Object.keys是相同的(自身属性+顺序相同)。与 for/of(ES6)一起使用, 我们不仅可以提取还可以进行迭代。

# 使用padStartpadEnd对字符串填充

String.prototype.padStartString.prototype.padEnd使得在 JavaScript 中处理字 符串的体验更加愉悦,并有助于避免依赖外部 的 (opens new window)

padStart()通过在开头插入填充字符返回指定长度(targetLength)的字符串。填充 字符是一个指定的字符串,如果需要的话会重复使用,直到达到所需的长度。左侧是字符串 的开头(至少在大多数西方语言中是这样的)。一个典型的示例使用空格填充:

console.log('react'.padStart(10).length); // "     react" is 10
console.log('backbone'.padStart(10).length); // "  backbone" is 10
1
2

这对财务报表来可能是一种有用的方法:

console.log('0.00'.padStart(20));
console.log('10,000.00'.padStart(20));
console.log('250,000.00'.padStart(20));
1
2
3

结果会像会计分类账一样有很好的格式:

                0.00
           10,000.00
          250,000.00
1
2
3

让我们在第二个参数中传入一些非空的填充字符,使用一个字符来填充:

console.log('react'.padStart(10, '_')); // "_____react"
console.log('backbone'.padStart(10, '*')); // "**backbone"
1
2

顾名思义padEnd将从右侧的结尾处填充字符串。至于第二个参数,你实际上可以使用任何 长度的字符串。例如:

console.log('react'.padEnd(10, ':-)')); // "react:-):-" is 10
console.log('backbone'.padEnd(10, '*')); // "backbone**" is 10
1
2

# Object.getOwnPropertyDescriptors

新的Object.getOwnPropertyDescriptors返回对象obj所有自身属性的描述符。它 是Object.getOwnPropertyDescriptor(obj,propName) (opens new window)( 只返回对象obj指定属性propName的描述符)的复数版本。

在我们这个不可变编程的时代,这个方法很有用(记住,对象在 JavaScript 中是引用传递 的!)。在 ES5 中,开发人员使用Object.assign()复制对象。但是 ,Object.assign()不仅会复制或定义新的属性,还会分配属性。当使用更复杂的对象或 类的原型时,这可能会导致问题。

Object.getOwnPropertyDescriptors允许创建对象的真正的浅层副本并创建子类。它是 通过给开发人员提供描述符来实现的。把描述符放 在Object.create(prototype,object)中,可以得到一个真正的浅层副本:

Object.create(
  Object.getPrototypeOf(obj),
  Object.getOwnPropertyDescriptors(obj)
);
1
2
3
4

或者你可以像下面这样合并两个对象targetsource

Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
1

这就是Object.getOwnPropertyDescriptors的用法,但是描述符是什么?就是一个描述对 象。这不废话吗。

好吧,我们更深入了解一下描述符。在 JavaScript 中,有两种类型的描述符:

  1. 数据描述符
  2. 访问器描述符

访问器描述符有强制属性:getset或同时具有getset,就是你猜到 的getter (opens new window)setter (opens new window)函 数。访问器描述符还有可选的属性:configurableenumerable

let azatsBooks = {
  books: ['React Quickly'],
  get latest() {
    let numberOfBooks = this.books.length;
    if (numberOfBooks == 0) return undefined;
    return this.books[numberOfBooks - 1];
  },
};
1
2
3
4
5
6
7
8

Object.getOwnPropertyDescriptor(azatsBooks, 'books')生成的books数据描述符 的示例:

Object
	configurable: true
	enumerable: true
	value: Array[1]
	writable: true
	__proto__: Object
1
2
3
4
5
6

同样,Object.getOwnPropertyDescriptor(azatsBooks, 'latest')将显示latest的描 述符。这是latest的(get)访问器描述符的示例:

Object
	configurable: truee
	numerable: true
	get: latest()
	set: undefined
	__proto__: Object
1
2
3
4
5
6

现在,让我们调用新方法来获取所有的描述符:

console.log(Object.getOwnPropertyDescriptors(azatsBooks));
1

它将给出一个同时包含bookslatest描述符的对象。

Object
  books: Object
    configurable: true
    enumerable: true
    value: Array[1]
    writable: true
    __proto__: Object
  latest: Object
    configurable: true
    enumerable: true
    get: latest()
    set: undefined
    __proto__: Object
  __proto__: Object
1
2
3
4
5
6
7
8
9
10
11
12
13
14

或者,如果你喜欢 DevTools 的格式,请看截图:

# 参数(包括形参和实参)列表尾随逗号

函数定义中的参数列表尾随逗号是纯粹的语法变化。在 ES5 中,正确的 JavaScript 函数 定义语法,在最后一个函数参数后面不应该有逗号

var f = function(a,
  b,
  c,
  d) { // NO COMMA!
  // ...
  console.log(d)
}
f(1,2,3,'this')
1
2
3
4
5
6
7
8

在 ES8 中,可以使用尾随逗号:

var f = function(a,
  b,
  c,
  d,
) { // COMMA? OK!
  // ...
  console.log(d)
}
f(1,2,3,'this')
1
2
3
4
5
6
7
8
9

现在,函数中的尾随逗号与数组(ES3)和对象字面量(ES5)中的尾随逗号规则是一致的:

var arr = [1,  // Length == 3
  2,
  3,
]  // <--- ok
let obj = {a: 1,  // Only 3 properties
  b: 2,
  c: 3,
}  // <--- ok
1
2
3
4
5
6
7
8

更不用说它对 git 非常友好!

当使用多行样式(通常带有很多长参数名)时,最能凸显尾随逗号的作用。开发人员终于可 以忘记看起来很奇怪的逗号优先的使用方式,在 ES5 及之前的版本中函数定义使用尾随逗 号会发生错误,所以开发者不得不使用逗号优先的方式。现在,你可以在任何地方使用逗号 ,甚至在最后一个参数后面

# 异步函数

异步函数(或 async/await)特性是基 于Promise (opens new window)的 语法糖,所以你可能需要阅读一下 Promise,或者看一个视频课程来复习一下。异步函数是 为了简化异步代码的编写,因为......好吧,因为人类的大脑不擅长并行无序的思考方式。 它只是没有进化成那样。

就我个人而言,我从来不喜欢 Promises。与回调函数相比 Promise 非常啰嗦,所以我从来 没有使用过 Promise。幸运的是,ES8 的异步函数更具有说服力。开发人员可以定义一 个async函数,该函数可以包含也可以不包含对基于 promise 异步操作的await。在代 码的背后,异步函数其实返回一个 Promise,然而你并不会在异步函数的函数体中看到关键 字 Promise(当然,除非你明确使用它)。

例如,在 ES6 中,我们可以使用 Promise 和Axios (opens new window)库向 GraphQL 服务器发送请求:

axios
  .get(`/q?query=${query}`)
  .then((response) => response.data)
  .then((data) => {
    this.props.processfetchedData(data); // Defined somewhere else
  })
  .catch((error) => console.log(error));
1
2
3
4
5
6
7

任何 Promise 库都能与新的异步函数兼容。我们可以使用同步代码 try/catch 来处理错误 :

async fetchData(url) => {
  try {
    const response = await axios.get(`/q?query=${query}`)
    const data = response.data
    this.props.processfetchedData(data)
  } catch (error) {
    console.log(error)
  }
}
1
2
3
4
5
6
7
8
9

异步函数返回一个 Promise,因此我们可以像这样继续执行流程:

async fetchData(query) => {
  try {
    const response = await axios.get(`/q?query=${query}`)
    const data = response.data
  	return data
  } catch (error) {
    console.log(error)
  }
}
fetchData(query).then(data => {
  this.props.processfetchedData(data)
})
1
2
3
4
5
6
7
8
9
10
11
12

你可以在(Babel REPL (opens new window))中看到下面这段代码。需要注意的是, 这个示例是模拟实现 Axios 库的功能,在代码中调用了setTimeout模拟实现,并没有进 行真正的 HTTP 请求:

let axios = {
  // mocks
  get: function (x) {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve({ data: x });
      }, 2000);
    });
  },
};
let query = 'mangos';
async function fetchData(query) {
  try {
    const response = await axios.get(`/q?query=${query}`);
    const data = response.data;
    return data;
  } catch (error) {
    console.log(error);
  }
}
fetchData(query).then((data) => {
  console.log(data); // Got data 2s later... Can use data!
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

使用 async/await,你的代码是异步执行的,但看起来像同步的。从上到下阅读这样的代码 ,更容易理解它在做什么,因为结果出现的顺序和函数体的执行顺序都是从上到下。

# 总结

这就是 ES8(未最终确定)和 ES7(已发布)的所有功能。如果你使用 Babel、Traceur 或 类似的转译器,你现在就可以使用所有这些功能以及更多的 0-3 阶段的功能特性,而无需 等待浏览器来实现它们。ES7 和 ES8 的代码将简单地转换为 ES5 兼容代码。甚至在 Internet Explorer 9 也可以使用。😃

一些需要注意的 ES8 功能特性,因为它们目前还处于第 3 阶段,但很可能最终会出现在 ES8/ES2017 中:

  • 共享内存和原子
  • SIMD.JS - SIMD APIs
  • Function.prototype.toString
  • 解除模板字符串的限制
  • global
  • Rest/Spread 属性
  • 异步迭代
  • import()

你可以 在已进入正式流程的提案 (opens new window)已完成提案 (opens new window)中 查看状态。

PS:如果想了解 Promise,箭头函数,let / const 等其他 ES6 / ES2015 功能特性,请阅 读我的博客文章或观看视频。

PS2:如果你喜欢视频 ​​ 形式的学习,请报名参加 Node.University 的ES6/ES2015 课程 (opens new window),并关注即将推出 的ES7 和 ES8 视频课程 (opens new window)

【译】CSS继承:介绍

【译】CSS继承:介绍

【译】如何使用Chrome中的移动设备仿真器

【译】如何使用Chrome中的移动设备仿真器