回顾一下 ECMAScript 的发展历程:
- 1997 年 6 月,ES1 发布
- 1998 年 6 月,ES2 发布
- 1999 年 12 月,ES3 发布
- 然后,ES4 胎死腹中
- 2009 年 12 月,ES5 发布
- 2015 年 6 月,ES6(ES2015)发布
- ES2016
- ES2017
- ES2018
- ……
经过 ES3 到 ES5 10 年的停滞以后,从 ES6 (ES2015) 开始,又开始小跑向前,连版本命名都改为年份的方式。跑那么快,作为从业者,有点担心扯着蛋。
下面就来了解一下 ES6 之后,又新增那些东西(参见TC39 提案列表)。
ECMAScript 2016
Array.prototype.includes()
includes
用于判断某个元素是否存在于数组中(包括 NaN
)。
const arr = [1, 2, 3, NaN];
arr.includes(3); // true
arr.includes(NaN); // true
arr.indexOf(NaN); // -1
注意:indexOf()
查找 NaN
时始终返回 -1
,includes()
则不同。如果基于 NaN === NaN
为 false
,indexOf()
的结果似乎更合理。不知 includes()
为何没有遵循这个逻辑。
番外:此 API 原本想命名为
contains
的,但 Mootools 已经在数组上扩展了contains
方法,为了避免对历史代码产生影响,改用includes
。
所以,不建议在 ES 内置对象(包括其原型)上定义属性、方法,除非做 polyfill,在老浏览器实现已经成为标准的功能。
乘方中缀操作符
ES 中已经有 ++
、--
操作符,ES2016 引入了 **
操作符进行乘方操作,作为 Math.pow
的一种替代。
Math.pow(7, 2); // 49
7 ** 2 // 49
ECMAScript 2017
Object.values()
Object.keys()
返回一个对象的 key 数组,Object.values()
则返回值的数组。
const cars = { BENZ: 3, Tesla:2, Honda: 1 };
// ES2015
const carVals = Object.keys(cars).map(key => cars[key]);
// [3, 2, 1]
// ES2017
const vals = Object.values(cars);
Object.entries()
以二维数组的形式返回对象的键值对。
const cars = { BENZ: 3, Tesla:2, Honda: 1 };
// ES 5.1
Object.keys(cars).forEach(key => {
console.log(`key: ${key} value: ${cars[key]}`);
});
// ES2017
for (let [key, value] of Object.entries(cars)) {
console.log(`key: ${key} value: ${value}`);
}
// ES2015
const map1 = new Map();
Object.keys(cars).forEach(key => {
map1.set(key, cars[key]);
});
// ES2017
const map = new Map(Object.entries(cars));
字符填充
— String.prototype.padStart(numberOfCharcters [,stringForPadding]);
- String.prototype.padEnd(numberOfCharcters [,stringForPadding]);
前置/后置填充,第一个参数为字符串的目标长度,第二个参数为填充的字符,默认为空格。如果目标长度小于字符串的长度,则不做处理,直接返回原始字符串。
字符串填充一般用于界面中对齐文字,美化展示效果。
let uid = '12345';
const len = 8;
// 这个判断其实没有必要
if (uid.length < len) {
uid = uid.padStart(len, '0');
}
'5'.padStart(10); // ' 5'
'5'.padStart(10, '=*'); //'=*=*=*=*=5'
'5'.padEnd(10); // '5 '
'5'.padEnd(10, '=*'); // '5=*=*=*=*='
const cars = {
'🚙BMW': '10',
'🚘Tesla': '5',
'🚖Lamborghini': '0'
}
Object.entries(cars).map(([name, count]) => {
//padEnd appends ' -' until the name becomes 20 characters
//padStart prepends '0' until the count becomes 3 characters.
console.log(`${name.padEnd(20, ' -')} Count: ${count.padStart(3, '0')}`)
});
//Prints..
// 🚙BMW - - - - - - - Count: 010
// 🚘Tesla - - - - - - Count: 005
// 🚖Lamborghini - - - Count: 000
Emoji 等双字节字符填充:
Emoji 等使用多字节 unicode 呈现,padStart
和 padEnd
可能无法得到预期的结果。
看下面的例子:
'heart'.padStart(10, '❤️'); // prints.. '❤️❤️❤heart'
上面的代码执行结果是 2 个 ❤️ 和 1 个 ❤,而不是五个 ️❤️。这是因为 ❤️ 码点长度为 2 ('\u2764\uFE0F'
),heart
的长度为 5,总共只能填充 5 个字符,也就是 '\u2764\uFE0F\u2764\uFE0F\u2764'
。
附:unicode 在线转换。
Object.getOwnPropertyDescriptors
返回对象所有属性的详细信息(包括 getter
、setter
)。
Object.assign
执行浅拷贝时,不会包含 getter
、setter
,Object.getOwnPropertyDescriptors
的主要目的就是解决这一问题。
var Car = {
name: 'BMW',
price: 1000000,
set discount(x) {
this.d = x;
},
get discount() {
return this.d;
},
};
//Print details of Car object's 'discount' property
console.log(Object.getOwnPropertyDescriptor(Car, 'discount'));
//prints..
// {
// get: [Function: get],
// set: [Function: set],
// enumerable: true,
// configurable: true
// }
//Copy Car's properties to ElectricCar using Object.assign
const ElectricCar = Object.assign({}, Car);
//Print details of ElectricCar object's 'discount' property
console.log(Object.getOwnPropertyDescriptor(ElectricCar, 'discount'));
//prints..
// {
// value: undefined,
// writable: true,
// enumerable: true,
// configurable: true
// }
//⚠️Notice that getters and setters are missing in ElectricCar object for 'discount' property !👎👎
//Copy Car's properties to ElectricCar2 using Object.defineProperties
//and extract Car's properties using Object.getOwnPropertyDescriptors
const ElectricCar2 = Object.defineProperties({}, Object.getOwnPropertyDescriptors(Car));
//Print details of ElectricCar2 object's 'discount' property
console.log(Object.getOwnPropertyDescriptor(ElectricCar2, 'discount'));
//prints..
// { get: [Function: get], 👈🏼👈🏼👈🏼
// set: [Function: set], 👈🏼👈🏼👈🏼
// enumerable: true,
// configurable: true
// }
// Notice that getters and setters are present in the ElectricCar2 object for 'discount' property!
函数参数尾部逗号
函数参数尾部逗号主要是解决 git blame
等工具使用问题。
// 问题
// 开发者 #1 创建了函数
function Person(
name,
age //<-- ES2017 以前的版本添加参数尾部逗号会抛出错误
) {
this.name = name;
this.age = age;
}
// 开发者 #2 填加了一个参数
function Person(
name,
age, // 添加了逗号 <--- 由于这个逗号,开发者 #1 会被 git blame 等工具标记为修改者
address
) {
this.name = name;
this.age = age;
this.address = address;
}
// ES2017 支持参数尾部逗号,解决这一问题
function Person(
name,
age, //<--- 不会报错,开发者 #2 不需要再修改此行
) {
// ...
}
函数调用时添加尾部逗号也是允许的。
Math.max(10, 20,);
Async/Await
这应该是最重要、最有用的功能了。先是 callback 噩梦,然后有了 Promise 链式写法,一路跌跌撞撞,总算有了更清晰明了的 Async/Await。
async
关键字声明一个异步函数,JS 编译器每次执行到包含 await
的语句时都会暂定,等待 await
后面的语句返回的 Promise resolve
或 reject
后才继续往下执行。
// ES2015 Promise
function getAmount(userId) {
getUset(userId)
.then(getBankBabance)
.then(amount => {
console.log(amount);
});
}
// ES2017
async function getAmount7(userId) {
const user = await getUser(userId);
const amount = getBankBalance(user);
console.log(amount);
}
getAmount('1'); // $1000
getAmount7('1'); // $1000
function getUser(userId) {
return new Promise(resolve => {
setTimeout(() => {
resolve('john');
}, 1000);
});
}
function getBankBalance(user) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (user === 'john') {
resolve('$1000');
} else {
reject('unknown user');
}
}, 1000);
});
}
Async 函数本身返回 Promise
Async 函数的返回结果需要使用 Promise 的 .then
方法处理。
function getUser(userId) {
return new Promise(resolve => {
setTimeout(() => {
resolve('john');
}, 1000);
});
}
async function getUserName(userId) {
const name = await getUser(userId);
// 其他操作
// ...
return name;
}
getUserName(userId)
.then(console.log);
并行调用
可以使用 Promise.all
并行调用 await
后面的语句。
function doubleAfter1Sec(param) {
return new Promise(resolve => {
setTimeout(resolve(param * 2), 1000);
});
}
async function doubleAndAdd(a, b) {
[a, b] = await Promise.all([doubleAfter1Sec(a), doubleAfter1Sec(b)]);
return a + b;
}
doubleAndAdd(1, 2)
.then(console.log); // 6
错误处理
方式 1:在函数内部使用 try catch
// Option 1 - Use try catch within the function
async function doubleAndAdd(a, b) {
try {
a = await doubleAfter1Sec(a);
b = await doubleAfter1Sec(b);
} catch (e) {
return NaN; // return something
}
return a + b;
}
// 🚀Usage:
doubleAndAdd('one', 2).then(console.log); // NaN
doubleAndAdd(1, 2).then(console.log); // 6
function doubleAfter1Sec(param) {
return new Promise((resolve, reject) => {
setTimeout(function() {
let val = param * 2;
isNaN(val) ? reject(NaN) : resolve(val);
}, 1000);
});
}
方式 2:捕获每个 await
表达式
//Option 2 - *Catch* errors on every await line
//as each await expression is a Promise in itself
async function doubleAndAdd(a, b) {
a = await doubleAfter1Sec(a).catch(e => console.log('"a" is NaN')); // 👈
b = await doubleAfter1Sec(b).catch(e => console.log('"b" is NaN')); // 👈
if (!a || !b) {
return NaN;
}
return a + b;
}
// Usage:
doubleAndAdd('one', 2).then(console.log); // NaN and logs: "a" is NaN
doubleAndAdd(1, 2).then(console.log); // 6
function doubleAfter1Sec(param) {
return new Promise((resolve, reject) => {
setTimeout(function() {
let val = param * 2;
isNaN(val) ? reject(NaN) : resolve(val);
}, 1000);
});
}
方式 3:捕获整个 async-await 函数
// Option 3 - Dont do anything but handle outside the function
// since async / await returns a promise, we can catch the whole function's error
async function doubleAndAdd(a, b) {
a = await doubleAfter1Sec(a);
b = await doubleAfter1Sec(b);
return a + b;
}
// Usage:
doubleAndAdd('one', 2)
.then(console.log)
.catch(console.log); // 👈👈 <------- use "catch"
function doubleAfter1Sec(param) {
return new Promise((resolve, reject) => {
setTimeout(function() {
let val = param * 2;
isNaN(val) ? reject(NaN) : resolve(val);
}, 1000);
});
}
ESMAScript 2018
ES2018 目前出于最终草案,发布时间为 2018 年 6 月或 7 月。
Shared memory and atomics
这是一个庞大、非常高级的功能,对于 JS 引擎而言是核心增强。
主要思想是 JavaScript 引入某些多线程功能,让开发者可以自行管理内存,取代 JS 引擎内部的内存管理,以编写高性能、并发的程序。
这个功能通过一个新的全局对象 SharedArrayBuffer 实现,其本质上是将数据存储在共享内存空间。所以这些数据可以在 JS 主线程和 web-worker 线程之间共享。
迄今为止,如果想要在 JS 主线程和 web-worker 之间共享数据,必须拷贝数据然后使用 postMessage
发送。有了 SharedArrayBuffer,这样历史就一去不复还了。
但是线程间共享内存会导致竞态条件,为避免这个问题,引入了 Atomics 全局对象。Atomics 提供各种方法锁定线程正在使用数据的共享内存,同时提供安全更新共享内存里的数据的方法。
建议通过某些库使用这个功能,但目前还没有基于此功能封装的库。
更多参考链接:
- From Workers to Shared Memory
- A cartoon intro to SharedArrayBuffers
- A cartoon intro to SharedArrayBuffers
移除标签式模板字面量(Tagged Template literal)限制
标签式模板字面量示例如下(更多细节参见 MDN 文档)。
// A "Tag" function returns a custom string literal.
// In this example, greet calls timeGreet() to append Good //Morning/Afternoon/Evening depending on the time of the day.
function greet(hardCodedPartsArray, ...replacementPartsArray) {
console.log(hardCodedPartsArray); // [ 'Hello ', '!' ]
console.log(replacementPartsArray); // [ 'Raja' ]
let str = '';
hardCodedPartsArray.forEach((string, i) => {
if (i < replacementPartsArray.length) {
str += `${string} ${replacementPartsArray[i] || ''}`;
} else {
str += `${string} ${timeGreet()}`; // <-- append Good morning/afternoon/evening here
}
});
return str;
}
// 🚀Usage:
const firstName = 'Raja';
const greetings = greet`Hello ${firstName}!`; // 👈 <-- Tagged literal
console.log(greetings); //'Hello Raja! Good Morning!' 🔥
function timeGreet() {
const hr = new Date().getHours();
return hr < 12
? 'Good Morning!'
: hr < 18 ? 'Good Afternoon!' : 'Good Evening!';
}
但是,ES2015、ES2016 规范不允许使用转义字符,如 "\u"
(unicode), "\x"
(十六进制),除非完全像 \u00A9
或 \u{2F804}
或 \xA9
这几种形式。
如果在某些领域(如终端)使用形如 \ubla123abla
的标签式模板字符串,会抛出语法错误。
ES2018 解除了这一限制。
function myTagFunc(str) {
return { "cooked": "undefined", "raw": str.raw[0] }
}
const str = myTagFunc `hi \ubla123abla`; // call myTagFunc
// { cooked: "undefined", raw: "hi \\unicode" }
正则表达式 dotAll
标记(s
)
当前 JS 的正则中,.
匹配任意字符,但不包含换行符 \n
、\r
、\u2028
、\u2029
。
ES2018 添加了 s
标记,让 .
可以匹配包括换行符在内的所有字符(提案详情)。
// Before
/first.second/.test('first\nsecond'); // false
// ECMAScript 2018,注意 s 标记
/first.second/s.test('first\nsecond'); // true
正则表达式命名捕获组
正则表达式命名捕获组是从 Python、Java 等语言中借鉴来的功能,以 (?<name>...)
的形式标识正则中的分组,然后通过组名获取该组的匹配的值。
基本示例
下面的例子中使用命名捕获组获取年、月、日。
// 未使用命名捕获组
const reg1 = /(\d{4})-(\d{2})-(\d{2})/;
const result1 = reg1.exec('2015-01-02');
console.log(result1);
// ["2015-01-02", "2015", "01", "02", index: 0, input: "2015-01-02", groups: undefined]
// 使用 ES2018 命名捕获组
const reg2 = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const result2 = reg2.exec('2018-05-07');
// ["2018-05-07", "2018", "05", "07", index: 0, input: "2018-05-07", groups: {year: "2018", month: "05", day: "07"}]
// 剩下的你应该知道怎么用了吧
在正则内部使用命名捕获组
在正则内容可以通过 \k<group name>
的形式反向引用捕获组。
// 命名捕获组 `fruit` 可以捕获 `apple` 或者 `orange`
// 通过 `\k<fruit>` 发现引用 `fruit` 捕获组,以判断左右是否一致
const sameFruit = /(?<fruit>apple|orange)==\k<fruit>/u;
sameFruit.test('apple==apple'); // true
sameFruit.test('orange==orange'); // true
sameFruit.test('apple==orange'); // false
在 String.prototype.replace()
中使用命名捕获组
const reg = /(?<firtName>[A-Za-z]+) (?<lastName>[A-Za-z]+$)/u;
'Minwe Luo'.replace(reg, '$<lastName>, $<firstName>'); // 'Luo Minwe'
参考链接:
展开/剩余操作符 ...
展开/剩余操作符 ...
用得比较普通,应该都很熟悉了。
Tip:赋值等号右边的为展开操作,左边为剩余操作
Object Rest/Spread Properties for ECMAScript
正则表达式向后断言(Lookbehind Assertions)
向后断言可用于判断特定字符是否出现在某些字符之前。
肯定断言(?<=...)
:
/(?<=#).*/.test('hello'); // false
/(?<=#).*/.test('#hello'); // true
'#hello'.match(/#.*/)[0]; // '#hello' 匹配的字符串包含 `#`
'#hello'.match(/(?<=#).*/)[0]; // 匹配 'hello',不包含 `#`
否定断言 (?<!...)
:下面的正则提取前面不包含 $
的数字。
// 不匹配,数字前面是 `$`
'A gallon of milk is $3.00'.match(/(?<!\$)\d+.?\d+/); // null
// 匹配,数字前面非 `$`
'A gallon of milk is ¥3.43'.match(/(?<!\$)\d+.?\d+/)[0]; // 3.43
RegExp Unicode Property Escapes
参考链接:
Promise.prototype.finally()
finally()
方法用于 Promise 无论是 resove
还是 reject
都执行一个回调,一般用于清理性操作。
resove
的情形:
let started = true;
let myPromise = new Promise(resolve => resolve('all good'))
.then(value => console.log(value)) // 输出 'all good'
.catch(e => console.log(e)) // 跳过
.finally(() => {
console.log('done');
started = false; // 清理工作
});
reject
的情形:
let started = true;
let myPromise = new Promise(resolve => resolve('reject'))
.then(value => console.log(value)) // 输出 'reject'
.catch(e => console.log(e)) // 跳过
.finally(() => {
console.log('done');
started = false; // 清理工作
});
Promise 中抛出异常的情形:
let started = true;
let myPromise = new Promise(() => {
throw new Error('error')
})
.then(value => console.log(value)) // 跳过
.catch(e => console.log(e)) // 捕获错误
.finally(() => {
console.log('done'); // 执行,但是没有值传下来
started = false; // 清理工作
});
catch
抛出异常的情形:
let started = true;
let myPromise = new Promise(() => {
throw new Error('error')
})
.then(value => console.log(value)) // 跳过
.catch(e => {
throw new Error('error in catch');
}) // 捕获错误
.finally(() => {
console.log('done'); // 仍然执行
started = false; // 清理工作
// catch 中抛出的异常需要在其他地方处理
});
异步迭代
新增的 for-await-of
循环执行异步迭代。
const promises = [
new Promise((resolve => resolve(1))),
new Promise((resolve => resolve(2))),
new Promise((resolve => resolve(3))),
];
// for-of 同步迭代:输出三个 promise 对象
async function test1() {
for (const obj of promises) {
console.log(obj);
}
}
// for-await-of 异步迭代,等待 Promise resove 并返回结果
async function test2() {
for await (const obj of promises) {
console.log(obj);
}
}
test1(); // promise, promise, promise
test2(); // 1, 2, 3
参考链接:
- tc39 - Asynchronous Iterators for JavaScript
- ES2018: asynchronous iteration
- Async iterators and generators
- 2ality > ES2018 Archive
呃,列举下来还真不少,是不是看得有点头大?
使用这些功能时,某些可以通过 Babel 转换,某些则需要注意增加相应的 polyfill,不然代码到老一点的浏览器就挂了。换个角度看,如果需要考虑兼容性,Array.prototype.includes()
之类的 API 并不值得使用,毕竟和 indexOf()
差别不大,引入 polyfill 的成本远大于其带来的便利。
VIA: Here are examples of everything new in ECMAScript 2016, 2017, and 2018