ES2017又是一个重要的版本,它的重要性不亚于ES2015,因为它带来了async/await
异步编程。
新语法
函数最后一个参数可以尾随一个逗号
原本JS的数组、对象字面量都允许最后一个元素尾随一个逗号,现在函数的形参、实参都允许尾随一个逗号了,这主要是为了方便服务器端生成JS脚本。
'use strict';
function f(a, b,) {
alert(a + '\n' + b);
}
f(1, 2,);
f(3, 4);
async/await 异步编程
这个语法相当于ES2015带来的Promise
的语法糖衣:
'use strict';
function es2015way() {
return new Promise((resolve, reject) => {
setTimeout(() => {
alert('Promise Step...');
if (Math.random() >= 0.3)
resolve('Promise Step OK!');
else
reject('Promise Step FAIL!');
}, 1);
});
}
async function es2017way() { // async新关键字必须与函数声明同一行
alert('Async Step...');
if (Math.random() >= 0.3)
return 'Async Step OK!'; // 直接使用return相当于Promise的resolve回调
else
throw 'Async Step FAIL!'; // 直接使用throw相当于Promise的reject回调
}
// 可以见到新旧两种调用方法其实是等价的
es2015way().then(alert).catch(alert).finally(() => {
const p1 = es2017way().then(alert).catch(alert).finally(async function () { // 匿名函数也可以声明为异步
alert(p1 instanceof Promise);
// 当然,如果只允许ES2015的调用方法,不会有显著差异
// 但是使用await的时候,就会显得简洁很多了
try { // 但是要注意,await只能够使用在async的函数内
const r = await es2017way();
alert(r);
} catch (e) {
alert(e);
} finally {
alert('Done!');
}
// 同理,await也可以作用普通的Promise
try {
const r = await es2015way();
alert(r);
} catch (e) {
alert(e);
}
// 其实,非Promise也可以使用await来调用
alert(await (() => 1)());
try { // 但是要注意,函数默认参数是不可以使用await的,不论是否在async函数内
eval('async function f(x = await es2017way()) {}');
} catch (e) {
alert(e);
}
// 任何可以声明函数的地方都可以将函数声明为async的
const obj = {
async f(ff) {
return await ff();
}
};
class C {
async f() {
}
static async f() {
}
}
alert(obj.f(async () => 1) instanceof Promise &&
C.f() instanceof Promise &&
(new C()).f() instanceof Promise);
try { // 但是要注意,构造函数肯定是不允许使用async的
eval('class C { async constructor() {} }');
} catch (e) {
alert(e);
}
});
});
新API
Object新API
values()与entries()静态方法
这对方法主要是为了让所有对象都能够以集合/数组的行为进行迭代:
'use strict';
const obj = Object.create({ a: 'a', d: 'd' }); // 不能枚举的属性
obj.a = '_a';
obj.b = 'b';
obj.c = 'c';
// 可以看到只有能枚举的属性才可以获取
alert(JSON.stringify(Object.values(obj)));
const entries = Object.entries(obj);
alert(JSON.stringify(entries));
const map = new Map(entries);
alert(map.size);
// 这对方法不能直接用在集合上
alert(JSON.stringify(Object.values(map)));
alert(JSON.stringify(Object.entries(map)));
// 实际上数组的values()和entries()方法返回都是迭代器
alert(entries.values());
alert(entries.entries());
// 实际上集合的values()和entries()方法返回都是迭代器
alert(map.values());
alert(map.entries());
getOwnPropertyDescriptors()静态方法
这个是Reflect.ownKeys()
与Reflect.getOwnPropertyDescriptor()/Object.getOwnPropertyDescriptor()
方法的组合的方便方法:
'use strict';
const obj = { a: 1, b: 2 };
alert(JSON.stringify(Object.getOwnPropertyDescriptors(obj), null, 4));
const result = {};
for (const k of Reflect.ownKeys(obj))
result[k] = Reflect.getOwnPropertyDescriptor(obj, k);
alert(JSON.stringify(result, null, 4));
String新API
padStart()与padEnd()方法
这对方法可以在字符串的前/后补上空格或你指定的字符串:
'use strict';
let s = '1234';
s = s.padStart(8); // 让字符串变成8个字符长,前面不够的以空格填补
alert(`|${s}|`);
s = s.padEnd(12, '_'); // 让字符串变成12个字符长,后面不够的以“_”填补
alert(`|${s}|`);
s = s.padStart(16, '123'); // 还可以让多个字符串轮流来填补
alert(`|${s}|`);
共享内存与原子操作
有鉴于Promise
与async/await
的普及,ES2017为JS带来了共享内存与原子操作,以便并发编程。但是Mozilla的页面提到,这套API很有可能会在新浏览器中被禁用:
原因是有可能会受到Spectre旁路攻击。但目前Chrome还可以使用这套API,而且Chrome团队还承诺Web时钟问题解决了之后还会继续提供这套API。
诚如阿里云栖社区所述,在云的环境下Spectre攻击还是挺可怕的,因为黑客可以通过一个正常的VPS/容器旁路嗅探到其它服务的内存内容,但是SSH已经添了反Spectre的支持,我相信一众云厂商都会有各种办法防止这个问题出现。至于浏览器环境其实我不是很担心,因为现在已经HTTPS普及化了,Web页面会被恶意加载奇怪第三方代码的可能性已经很低了——除非手贱,那剩下来最怕的就是恶意网站通过多个页签的方法旁路嗅探目标地址的内容,恐怕浏览器的砂箱机制在不远将来就会解决这个问题。当然,少上那些“奇怪”的网站更好。
这套API在未来的Web应用中还是不可或缺的,因为WebGL与WebWorker都要使用共享内存(以后有机会再系统学习、整理一些文章),而ArrayBuffer
并不可以跨线程/进程共享,且%Typed%Array
和DataView
的操作都不是原子化的。(但这样做实际上还是会受到Spectre的影响,各大芯片厂商就不能努力一把合力解决这个问题吗?)
显然它的接口是与ArrayBuffer
一样的,只不过它可以跨线程/进程共享。
'use strict';
const buf1 = new SharedArrayBuffer(64); // 创建一个64字节的缓冲区
alert(buf1.byteLength === 64);
const buf2 = buf1.slice(16, 48); // 将buf1中间的32字节复制到buf2
alert(buf2.byteLength === 32 && buf2 instanceof SharedArrayBuffer);
Atomics新内置对象
ECMA委员会将所有原子化操作都封装在Atomics
的静态方法中,这中做法与Java的java.util.concurrent.atomic
包有点类似(不过Java在语言级别支持volatile
关键字来实现内存屏障,ES貌似并没有提供这样的功能的打算),都是基于“比较-交换-自旋”(CompareAndSwap-Spin)的。不过有趣的是,Atomics
支持的只有整数类型的数组,且这些数组背后只可以使用SharedArrayBuffer
。
'use strict';
const buf1 = new SharedArrayBuffer(4);
const a1 = new Int32Array(buf1);
alert(Atomics.store(a1, 0, Math.random() * 10000)); // 原子化的 a1[0] = Math.random() * 10000;
alert(Atomics.load(a1, 0)); // 原子化的 a1[0];
Atomics.add(a1, 0, 1); // 原子化的 a[0] += 1;
alert(Atomics.load(a1, 0));
Atomics.sub(a1, 0, 1); // 原子化的 a[0] -= 1;
alert(Atomics.load(a1, 0));
Atomics.and(a1, 0, Math.random() * 10000); // 原子化的 a[0] &= Math.random() * 10000;
alert(Atomics.load(a1, 0));
Atomics.or(a1, 0, Math.random() * 10000); // 原子化的 a[0] |= Math.random() * 10000;
alert(Atomics.load(a1, 0));
Atomics.xor(a1, 0, Math.random() * 10000); // 原子化的 a[0] ^= Math.random() * 10000;
alert(Atomics.load(a1, 0));
// 原子化的
// const oldValue = a1[0];
// a[0] = 111;
// return oldValue;
alert(Atomics.load(a1, 0) === Atomics.exchange(a1, 0, 111));
// 原子化的
// if (a1[0] === 111) {
// a1[0] = 1111;
// return 111;
// }
alert(Atomics.compareExchange(a1, 0, 111, 1111) === 111);
// 事实上,Atomics也不一定都使用CAS免锁操作来实现原子化
// 对于带类型数组的元素为奇数字节长的情况都要使用锁来实现
// 而多于4字节的情况也必须使用锁,我不清楚是否与芯片的位长有关
// 至少我的CPU一直都是64位的
for (let bytes = 1; bytes <= 8; ++bytes)
alert(`Atomics.isLockFree(${bytes}) => ${Atomics.isLockFree(bytes)}`);
// Atomics.wait/notify操作只支持Int32Array
// 且Atomics.wait不能在浏览器渲染的主线程的上下文调用
// 一般都在WebWorker中等待,这样即便阻塞也是阻塞后台线程
const worker = new Worker(window.URL.createObjectURL(new Blob([`
onmessage = function (e) {
const a1 = e.data;
if (a1 instanceof Int32Array && a1.buffer instanceof SharedArrayBuffer)
postMessage(Atomics.wait(a1, 0, 110/*, timeout = Infinity*/));
};
`], { type: 'application/javascript' })));
worker.onmessage = function (e) {
alert('Atomics.wait => ' + e.data);
};
Atomics.store(a1, 0, 0);
worker.postMessage(a1); // 把数组传到后台,让后台等待
Atomics.store(a1, 0, 110); // 设置目标值
setTimeout(() => {
alert('Atomics.notify => ' + Atomics.notify(a1, 0/*, count = Infinity*/) + ' threads');
}, 100);
打赏作者