ES8
在es8中主要有6个特性:
主要的有:
Shared memory and atomics (共享内存和原子)
Async Functions(异步函数)
其他的特性:
Object.values/Object.entries (配合Object.keys使用)
String padding (字符串填充)
Object.getOwnPropertyDescriptors()
Trailing commas in function parameter lists and calls(在函数参数列表和调用中减少逗号的使用)
首先我们来介绍一下主要的两个特性:
Shared memory and atomics (共享内存和原子)
在我们先需要主要需要了解SharedArrayBuffer 和 Atomics
SharedArrayBuffer
在了解SharedArrayBuffer 之前我们需要了解下需要share的这个ArrayBuffer:
ArrayBuffer对象代表储存二进制数据的一段内存,它不能直接读写,只能通过视图(TypedArray视图和DataView视图)来读写,视图的作用是以指定格式解读二进制数据。这个接口的原始设计目的,与 WebGL 项目有关。所谓WebGL,就是指浏览器与显卡之间的通信接口,为了满足 JavaScript 与显卡之间大量的、实时的数据交换,它们之间的数据通信必须是二进制的,而不能是传统的文本格式。文本格式传递一个32位整数,两端的 JavaScript 脚本与显卡都要进行格式转化,将非常耗时。这时要是存在一种机制,可以像 C 语言那样,直接操作字节,将4个字节的32位整数,以二进制形式原封不动地送入显卡,脚本的性能就会大幅提升。
那么我们为什么需要SharedArrayBuffer这个function呢?
任何能够从主线程负载减少工作的方法都对代码运行效率有帮助,某些情况下,ArrayBuffers 可以减少大量应该由主线程做的工作(免去了格式转换的耗时和压力直接操作字符),但是也有些时候减少主线程负载是远远不够的,有时你需要增援,你需要分割你的任务,那么我们这个时候就需要传说中的多线程。
在 JavaScript 里,你可以借助 web worker 做这种事,但是web worker是不能共享内存的,那么这意味着如果你想分配你的任务给别的线程,你需要完整把任务复制过去,这可以通过 postMessage 实现,postMessage 把你传给它的任何对象都序列化,发送到其它 web worker,然后那边接收后反序列化并放进内存。可见这个过程也是相当慢的。我们下面来看下这个过程的code [from MDN]:
main.js
var first = document.querySelector('#number1');
var second = document.querySelector('#number2');
var result = document.querySelector('.result');
if (window.Worker) { // Check if Browser supports the Worker api.
// Requires script name as input
var myWorker = new Worker("worker.js");
// onkeyup could be used instead of onchange if you wanted to update the answer every time
// an entered value is changed, and you don't want to have to unfocus the field to update its .value
first.onchange = function() {
myWorker.postMessage([first.value,second.value]); // Sending message as an array to the worker
console.log('Message posted to worker');
};
second.onchange = function() {
myWorker.postMessage([first.value,second.value]);
console.log('Message posted to worker');
};
myWorker.onmessage = function(e) {
result.textContent = e.data;
console.log('Message received from worker');
};
}
worker.js
onmessage = function(e) {
console.log('Message received from main script');
var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
console.log('Posting message back to main script');
postMessage(workerResult);
}
从上面的code可以看出这样传来传去是非常慢的。
那么我们想多个 web worker 就可以同时读写同一块内存,这样我们就不用传来传去的了,这就是SharedArrayBuffers 为我们提供的。
我们来看下使用使用shareArrayBuffers是怎样实现这个乘法的
main.js
"use strict";
var first = document.querySelector('#number1');
var second = document.querySelector('#number2');
var result = document.querySelector('.result');
if (window.Worker) { // Check if Browser supports the Worker api.
// Requires script name as input
var myWorker = new Worker("worker.js");
// onkeyup could be used instead of onchange if you wanted to update the answer every time
// an entered value is changed, and you don't want to have to unfocus the field to update its .value
var sharedBuffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
first.onchange = function() {
addNumber();
}
second.onchange = function() {
addNumber();
};
const addNumber = () => {
myWorker.postMessage({
aTopic: [first.value, second.value],
aBuf: sharedBuffer // The array buffer that we passed to the transferrable section 3 lines below
});
console.log('Message posted to worker');
result.textContent = new Int32Array(sharedBuffer)[0];
console.log('Message received from worker');
}
}
worker.js
onmessage = function(e) {
console.log('Message received from main script');
var workerResult = e.data.aTopic[0] * e.data.aTopic[1];
new Int32Array(e.data.aBuf)[0] = workerResult;
}
我们这个时候也不用担心postMessage 伴有时延的通信。但是多个worker同时访问一块内存这个时候会出现竞争的问题的。
从 CPU 层面看,增加一个变量值需要三条指令,这是因为计算机同时有长期存储器(内存)和短期存储器(寄存器所有的线程共享同一个长期存储器(内存),但是短期存储器(寄存器)并不是共享的。每个线程需要把值先从内存搬到寄存器,之后就可以在寄存器上进行计算了,再然后会把计算后的值写回内存)。
这个因为竞争的关系会产生错误的结果,那么我们应该怎样让SharedArrayBuffers 发挥他应有的价值呢?
这个时候伴随它诞生的还有:
Atomics
原子操作做的一件事就是在多线程中让计算机按照人所想的单操作方式工作。
这就是为什么被叫做原子操作,因为它可以让一个包含多条指令(指令可以暂停和恢复)的操作执行起来像是一下子就完了,就好像一条指令,类似一个不可分割的原子。
var sab = new SharedArrayBuffer(1024);
var ta = new Uint8Array(sab);
//The static Atomics.add() method adds a given value at a given //position in the array and returns the old value at that position.
Atomics.add(ta, 0, 1); // returns 0, the old value
Atomics.load(ta, 0); // 1
async 函数
首先我们来看下在async 函数诞生之前我们是怎么处理异步的,我先简单列举几个编程模型:
回调函数:
// more code
function loading(callback) {
// wait 3s
setTimeout(function () {
callback();
}, 3000);
}
function show() {
// show the data.
}
loading(show);
// more code
在这种情况下容易出现大家所熟知的回调黑洞:
A(function () {
B(function () {
C(function() {
D(function() {
// ...
})
})
})
})
Promise模式
所谓 Promise,就是一个对象,用来传递异步操作的消息。它代表了某个未来才会知道结果的事件(通常是一个异步操作),并且这个事件提供统一的 API,可供进一步处理。
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('Resolved.');
});
console.log('Hi!');
// Promise
// Hi!
// Resolved
这种实现方式和AngularJs中的Promise用法相近。
下面简单提下ES6中的Generator 函数的使用:
function* gen(x) {
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }
gen函数返回不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象(Iterator Object)。
其实在ES8之前我们还有许多其他的异步实现方式,比如:
在angualrJs中我们实现的subscribe, unsubscribe, publish这种消息订阅/发布模式其实也是实现异步的一种方式。
下面我们来说说今天的主角:async 函数
关键词:await async
那么我们先看下await, await后面可以是Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作),当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
async函数返回一个 Promise 对象,可以使用then方法添加回调函数。
我们看下下面的代码来理解上面的话:
function resolveAfter2Seconds(x) {
return new Promise(resolve => {
setTimeout(() => {
resolve(x);
}, 2000);
});
}
async function add(x) {
var a = await resolveAfter2Seconds(20);
var b = await resolveAfter2Seconds(30);
return x + a + b;
}
add(10).then(v => {
console.log(v); // prints 60 after 4 seconds.
});
Object.values/Object.entries
ES5 引入了Object.keys方法,返回一个数组,成员是参数对象自身的所有可遍历(enumerable)属性的键名。
var obj = { foo: 'bar', baz: 42 };
Object.keys(obj)
// ["foo", "baz"]
es8中新添加的Object.values/Object.entries作为遍历对象的一种补充手段。
Object.values方法返回一个数组,成员是参数对象自身的所有可遍历(enumerable)属性的键值。
var obj = { 100: 'a', 'a': 'b', 7: 'c' };
Object.values(obj)
// ["c", "a", "b"]
Object.entries方法返回一个数组,成员是参数对象自身的所有可遍历(enumerable)属性的键值对数组。
var obj = { 'foo': 'bar', 'baz': 42 , '1': 43};
Object.entries(obj)
// [['1', 43], ["foo", "bar"], ["baz", 42] ]
以上的遍历对象的属性,都遵守同样的属性遍历的次序规则。
首先遍历所有属性名为数值的属性,按照数字排序。
其次遍历所有属性名为字符串的属性,按照生成时间排序。
最后遍历所有属性名为 Symbol 值的属性,按照生成时间排序。
(Object.keys,Object.values,Object.entries都会过滤属性名为 Symbol(一种新的原始数据类型, 表示独一无二的值) 值的属性。)
String padding
字符填充函数,在es8中引入的两个方法: String.padStart 和String.padEnd, 这两个放发的主要是为了在一定程度上填充字符串的长度, 语法如下:
str.padStart(targetLength [, padString])
str.padEnd(targetLength [, padString])
这个方法主要是为了实现字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。
'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'
'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'
//如果原字符串的长度,等于或大于指定的最小长度,则返回原字符串。
'xxx'.padStart(2, 'ab') // 'xxx'
'xxx'.padEnd(2, 'ab') // 'xxx'
//如果省略第二个参数,默认使用空格补全长度。
'x'.padStart(4) // ' x'
'x'.padEnd(4) // 'x '
//用途: 为数值补全指定位数。
'1'.padStart(10, '0') // "0000000001"
'12'.padStart(10, '0') // "0000000012"
'123456'.padStart(10, '0') // "0000123456"
//另一个用途是提示字符串格式。
'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
'09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"
getOwnPropertyDescriptors函数
getOwnPropertyDescriptors函数: 返回指定对象所有自身属性(非继承属性)的描述对象。
ES5 有一个Object.getOwnPropertyDescriptor方法,返回某个对象属性的描述对象(descriptor)。
var obj = { p: 'a' };
Object.getOwnPropertyDescriptor(obj, 'p')
// Object { value: "a",
// writable: true,
// enumerable: true,
// configurable: true
// }
ES2017 引入了Object.getOwnPropertyDescriptors方法,返回指定对象所有自身属性(非继承属性)的描述对象。
const obj = {
foo: 123,
get bar() { return 'abc' }
};
Object.getOwnPropertyDescriptors(obj)
// { foo:
// { value: 123,
// writable: true,
// enumerable: true,
// configurable: true },
// bar:
// { get: [Function: bar],
// set: undefined,
// enumerable: true,
// configurable: true } }
value
包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。该特性的默认值为undefined。直接在对象上定义的属性,该特性被设置为指定的值
writable
表示能否修改属性的值。直接在对象上定义的属性,该特性默认为true
get
获取该属性的访问器函数(getter)。如果没有访问器, 该值为undefined。(仅针对包含访问器或设置器的属性描述有效)
set
获取该属性的设置器函数(setter)。 如果没有设置器, 该值为undefined。(仅针对包含访问器或设置器的属性描述有效)
configurable
表示能否通过delete删除属性从而重新定义属性。,能否修改属性的特性,或者能否把属性修改为访问器属性。直接在对象上定义的属性,该特性默认为true;
enumerable
表示能否通过for-in循环返回属性。直接在对象上定义的属性,该特性默认为true
应用:浅拷贝
const shallowClone = (obj) => Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
);
在装饰器上应用
Trailing commas in function parameter lists and calls(结尾逗号)
此处结尾逗号指的是在函数参数列表中最后一个参数之后的逗号以及函数调用时最后一个参数之后的逗号。ES8 允许在函数定义或者函数调用时,最后一个参数之后存在一个结尾逗号而不报 SyntaxError 的错误。示例代码如下:
函数声明时
function es8(var1, var2, var3,) {
// ...
}
函数调用时
es8(10, 20, 30,);
ES8的这项新特性受启发于对象或者数组中最后一项内容之后的逗号,如 [10, 20, 30,] 和 { x: 1, } 。