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

ES2017

免費2026-03-05#JS#ES8#ServiceWorker共享数据#JS线程锁#JS原子操作#JS transaction

一。特性概覽

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...in:enumerable && 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.

參考資料

評論

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

提交評論