跳到主要內容
黯羽輕揚每天積累一點點

ES2017

免費2018-11-11#JS#ES8#ServiceWorker共享数据#JS线程锁#JS原子操作#JS transaction

Async functions 終於在 ES2017 加入豪華午餐了,多線程方面的基礎建設也在逐步完善

一。特性概覽

2 個主要特性:

4 個小特性:

二。Async functions

一個里程碑式的特性,標誌著 JS 異步編程體驗上升到了一個新高度,具體見 [從 Generator 到 Async function](/articles/從 generator 到 async-function/)

三。Shared memory and atomics

算是在多線程並行能力方面的基礎建設,分為 2 部分:

  • SharedArrayBuffer 允許主線程、及 [WebWorkers](/articles/理解 web-workers/) 之間共享數據

  • Atomic operations(原子操作)用來解決數據同步的問題,如加鎖、事務

例如:

// 主線程
var w = new Worker("myworker.js");
var sab = new SharedArrayBuffer(1024);  // 1KiB shared memory
// 同樣通過 postMessage 給 worker 線程丟過去
w.postMessage(sab);

// worker 線程(myworker.js)
var sab;
onmessage = function (ev) {
  sab = ev.data;  // 1KiB shared memory, the same memory as in the parent
}

之前

線程之間傳遞的是值 copy,而不是共享引用

現在可以通過 SharedArrayBuffer 共享同一份數據,並且在 worker 線程裡也可以創建共享數據:

Memory can be created in any agent and then shared with any other agent, and can be shared among many agents simultaneously.

另外,SharedArrayBuffer 可以作為 ArrayBuffer 使用,所以也可以共享 TypedArray

var sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 100000); // 100000 primes
var ia = new Int32Array(sab);  // ia.length == 100000
var primes = new PrimeGenerator();
for ( let i=0 ; i < ia.length ; i++ )
  ia[i] = primes.next();
w.postMessage(ia);

由於數據是多線程共享的,勢必面臨數據同步的問題,通過 Atomics 全域對象提供的一些方法來解決:

// 讀
Atomics.load(typedArray, index)
// 寫
Atomics.store(typedArray, index, value)
// 寫,返回舊值
Atomics.exchange(array, index, value)
// 條件寫,僅當舊值等於 oldval 時才寫,返回舊值
compareExchange(array, index, oldval, newval)
// 帶讀寫鎖的運算(加、減、與、或、異或)
Atomics.add(array, index, value)
Atomics.sub(array, index, value)
Atomics.and(array, index, value)
Atomics.or(array, index, value)
Atomics.xor(array, index, value)

這些原子操作不會被打斷(not interruptible),在此基礎上可以實現:

  • 保證連續讀寫操作的順序

  • 避免寫操作「丟失」(比如寫到臟數據上了)

此外,還允許掛起/喚醒(更友好的線程等待方式,不多佔資源):

Atomics.wait(typedArray, index, value[, timeout])
Atomics.wake(typedArray, index, count)

例如:

// A 線程寫
console.log(ia[37]);  // Prints 163
Atomics.store(ia, 37, 123456);
Atomics.wake(ia, 37, 1);

// B 線程等著讀
Atomics.wait(ia, 37, 163);
console.log(ia[37]);  // Prints 123456

而不需要靠死循環來實現阻塞式等待:

while (Atomics.load(ia, 37) == 163);
console.log(ia[37]);  // Prints 123456

P.S. 有意思的一點,主線程不允許掛起:

The specification allows the browser to deny wait on the main thread, and it is expected that most browsers will eventually do so. A denied wait throws an exception.

P.S. 關於 Shared memory and atomics 特性的更多信息,請查看:

四。小特性

Object.values/Object.entries

// 返回 (1) 自身的 (2) 可枚舉的 (3) 非 Symbol 類型的 屬性的值
Object.values(obj)

polyfill 實現 大致如下:

function values(obj) {
  var vals = [];
  for (var key in obj) {
    if (obj.hasOwnProperty(key) && obj.propertyIsEnumerable(key)) {
      vals.push(obj[key]);
    }
  }
  return vals;
}

Object.keys() 一致,對屬性都有 3 個限定條件(own && enumerable && non-Symbol-only)。因此,不考慮性能的話,可以實現更簡單的 polyfill:

function values(obj) {
  return Object.keys(obj).map(key => obj[key]);
}

類似的,還提供了:

// 返回 (1) 自身的 (2) 可枚舉的 (3) 非 Symbol 類型的 屬性的鍵值對兒
Object.entries(obj)

polyfill 也類似:

function entries(obj) {
  var entrys = [];
  for (var key in obj) {
    if (obj.hasOwnProperty(obj, key) && obj.propertyIsEnumerable(obj, key)) {
      entrys.push([key, obj[key]]);
    }
  }
  return entrys;
};

除了返回值形式不同以外,與 Object.values(obj)一毛一樣

應用場景上,Object.entries(obj) 可以用來完成 mapObject 轉 [Map](/articles/集合(set 和 map)-es6 筆記 8/#articleHeader3) 的工作:

new Map(Object.entries({
    one: 1,
    two: 2,
}))
// 輸出 Map(2)?{"one" => 1, "two" => 2}

枚舉性,原型屬性與 Symbol

  • 枚舉性:通過 obj.propertyIsEnumerable(key) 來檢查,下面用 enumerable 表示可枚舉

  • 是不是原型屬性:通過 obj.hasOwnProperty(key) 來檢查,下面用 own 表示僅針對非原型屬性

  • 是不是 Symbol:通過 typeof key === 'symbol' 來檢查,下面用 non-Symbol-only 表示僅針對非 [Symbol](/articles/symbol-es6 筆記 7/) 類型屬性,用 Symbol-only 表示僅針對 Symbol 類型屬性

JS 裡圍繞對象屬性的這 3 個特點提供了很多工具方法,除了上面提到的 Object.keys()Object.values()Object.entries() 外,還有:

以及 1 種遍歷方式:

  • for...inenumerable && non-Symbol-only

P.S. 想起了 for...of?這個東西與對象關係不大,僅針對 iterable,如類數組對象(arguments、DOMNodeList 等)

Object.getOwnPropertyDescriptors

// 以對象字典形式返回 (1) 自身的 所有屬性的描述符
Object.getOwnPropertyDescriptors(obj)

包括 Symbol 類型屬性與不可枚舉屬性,例如:

const obj = {
  [Symbol('foo')]: 123
};
Object.defineProperty(obj, 'bar', {
  value: 42,
  enumerable: false
});
console.log(Object.getOwnPropertyDescriptors(obj));
// 輸出
// {
//   bar: {value: 42, writable: false, enumerable: false, configurable: false},
//   Symbol(foo): {value: 123, writable: true, enumerable: true, configurable: true}
// }
// 而 Object.keys(obj).length === 0

可以通過 Reflect.ownKeys(obj) 實現 polyfill:

function getOwnPropertyDescriptors(obj) {
    const result = {};
    for (let key of Reflect.ownKeys(obj)) {
        result[key] = Object.getOwnPropertyDescriptor(obj, key);
    }
    return result;
}

應用場景上,主要用來完成精細的對象拷貝工作:

// 連帶屬性描述符原樣搬過去
function clone(obj) {
  return Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
}
// 會丟失不可枚舉屬性以及原描述符
function copy(obj) {
  return Object.assign({}, obj);
}

區別如下:

const obj = {};
Object.defineProperty(obj, 'bar', {
  value: 42,
  enumerable: false
});
Object.defineProperty(obj, 'foo', {
  value: 24,
  enumerable: true,
  writable: false
});

Object.getOwnPropertyDescriptors(clone(obj));
// 屬性保持原狀
// bar: {value: 42, writable: false, enumerable: false, configurable: false}
// foo: {value: 24, writable: false, enumerable: true, configurable: false}

Object.getOwnPropertyDescriptors(copy(obj));
// 不可枚舉的 bar 丟了,foo 的屬性描述符被重置回預設了
// foo: {value: 24, writable: true, enumerable: true, configurable: true}

String padding

曾經引發 npm 風波 的 left-pad 模塊,以後用不著了:

str.padStart(targetLength [, padString])
str.padEnd(targetLength [, padString])

有一些小細節,例如:

// 預設補空格(U+0020)
'1'.padStart(4) === '1'.padStart(4, ' ')
//  也可以填充指定串
'1'.padEnd(4, 0) === '1000'
// 填充串的長度不限於一個字符,太長會被裁剪掉
'1'.padEnd(4, 'abcde') === '1abc'
// 不用補就不補
'1345'.padStart(2) === '1345'

Trailing commas in function parameter lists and calls

基本語法的 2 個小變動:

function foo(
  param1,
  param2, // 形參列表允許有多餘逗號
) {
  foo(
    'abc',
    'def',  // 實參列表允許有多餘逗號
  );
}

實際上,類似的變動在 ES5.1 也發生過:

const object = { 
  foo: "bar", 
  baz: "qwerty",
  age: 42,  // 對象字面量鍵值對兒列表允許有多餘逗號
};

除了上面 3 種,還有語言最初的語法規則:

const arr = [
  1,
  2,
  3,  // 數組字面量允許有多餘逗號
];

arr; // [1, 2, 3]
arr.length; // 3

特殊的:

const arr = [1, 2, 3,,,];
arr.length; // 5

在字面量形式的稀疏數組中,最後一個逗號屬於trailing commas(末尾多餘逗號)被忽略掉,因此數組大小是 5

P.S. 關於 trailing commas 的更多信息,見 Trailing commas

五。總結

Async functions 終於在 ES2017 加入豪華午餐了,多線程方面的基礎建設也在逐步完善

此外,還有三個無關緊要的 Object 方法,一個字符串 padding 方法,參數列表末尾允許有多餘逗號。對於這些錦上添花的東西,看到一個端正的態度

Are people seriously considering to extend the language for something that can be implemented in 7 lines of code ?

Convenience matters, as does eliminating redundant code. JavaScript's runtime library is still very spartan compared to other programming languages.

參考資料

評論

暫無評論,快來發表你的看法吧

提交評論