【译】ES7和ES8新特性
最近,我写了一篇博客文章 (opens new window),甚至还创建了一个关于 ES6 / ES2015 的在线课程 (opens new window)。你猜怎么着?TC39(强大的 JavaScript 监督者)正在推进 ES8 的发展,因此让我们聊聊 ES7 和 ES8(或按官方正式 叫法应该叫 ES2016 和 ES2017)。幸运的是,它们比最佳标准 ES6 的功能特性要少得多。 这是真的!你看 ES7 只有两个新功能特性!
ES7 功能特性:
Array.prototype.includes
- 求幂运算符
**
截至本文撰写时(2017 年 1 月),ES8 标准尚未最终确定,但我们可以假设所有已完成的 提案(第 4 阶段)和第 3 阶段的大部分(更多关于阶段的详细内容 在这里 (opens new window)和我 的课程 (opens new window)中)。已完成的 2017 年(ES8)提案是:
Object.values
/Object.entries
- 字符串填充
Object.getOwnPropertyDescriptors
- 函数参数列表允许尾随逗号
- 异步函数
在这篇文章中,我不会介绍第 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');
}
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');
}
2
3
4
5
6
使用 ES7 的includes
方法的代码:
let arr = ['react', 'angular', 'vue'];
// Correct
if (arr.includes('react')) {
console.log('Can use React');
}
2
3
4
5
6
开发人员还可以在字符串中使用includes
方法:
let str = 'React Quickly';
// Correct
if (str.toLowerCase().includes('react')) {
// true
console.log('Found "react"');
}
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)
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
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
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
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.values
和Object.entries
,与Object.keys
类似都
返回一个数组,并且数组的顺序与 Object.keys
返回的数组顺序是一样的。
Object.keys
、Object.values
和Object.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]);
});
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]);
}
2
3
4
你也可以使用旧的for/in
(ES5),但这会遍历所有可枚举的属性(如原型中的属性或带名字
的属性--详情
见MDN (opens new window)),
而不仅仅是自己的属性,这可能会意外地用prototype
或toString
之类的意外值破坏结
果。
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
2
或使用for/of
:
let obj = { a: 1, b: 2, c: 3 };
for (let value of Object.values(obj)) {
console.log(value);
}
// 1, 2, 3
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]]"
2
3
我们可以使用 ES6 / ES2015 的解构(请查看这篇文章 (opens new window)或
本课程 (opens new window)中关于深入 ES6 的内容),从一个嵌套数组中
声明key
和value
:
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
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
2
3
4
5
现在从对象中提取值和键值对变得更加容易了。Object.values
和Object.entries
的执
行方式与Object.keys
是相同的(自身属性+顺序相同)。与 for/of
(ES6)一起使用,
我们不仅可以提取还可以进行迭代。
# 使用padStart
和padEnd
对字符串填充
String.prototype.padStart
和String.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
2
这对财务报表来可能是一种有用的方法:
console.log('0.00'.padStart(20));
console.log('10,000.00'.padStart(20));
console.log('250,000.00'.padStart(20));
2
3
结果会像会计分类账一样有很好的格式:
0.00
10,000.00
250,000.00
2
3
让我们在第二个参数中传入一些非空的填充字符,使用一个字符来填充:
console.log('react'.padStart(10, '_')); // "_____react"
console.log('backbone'.padStart(10, '*')); // "**backbone"
2
顾名思义padEnd
将从右侧的结尾处填充字符串。至于第二个参数,你实际上可以使用任何
长度的字符串。例如:
console.log('react'.padEnd(10, ':-)')); // "react:-):-" is 10
console.log('backbone'.padEnd(10, '*')); // "backbone**" is 10
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)
);
2
3
4
或者你可以像下面这样合并两个对象target
和source
:
Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
这就是Object.getOwnPropertyDescriptors
的用法,但是描述符是什么?就是一个描述对
象。这不废话吗。
好吧,我们更深入了解一下描述符。在 JavaScript 中,有两种类型的描述符:
- 数据描述符
- 访问器描述符
访问器描述符有强制属性:get
或set
或同时具有get
和set
,就是你猜到
的getter (opens new window)和setter (opens new window)函
数。访问器描述符还有可选的属性:configurable
和enumerable
。
let azatsBooks = {
books: ['React Quickly'],
get latest() {
let numberOfBooks = this.books.length;
if (numberOfBooks == 0) return undefined;
return this.books[numberOfBooks - 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
2
3
4
5
6
同样,Object.getOwnPropertyDescriptor(azatsBooks, 'latest')
将显示latest
的描
述符。这是latest
的(get)访问器描述符的示例:
Object
configurable: truee
numerable: true
get: latest()
set: undefined
__proto__: Object
2
3
4
5
6
现在,让我们调用新方法来获取所有的描述符:
console.log(Object.getOwnPropertyDescriptors(azatsBooks));
它将给出一个同时包含books
和latest
描述符的对象。
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
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')
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')
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
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));
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)
}
}
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)
})
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!
});
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)。
- 本文章翻译 自ES7 and ES8 Features (opens new window)。
- 本人英文水平有限,翻译不正确不通顺的地方,敬请指出。