算是在多线程并行能力方面的基础建设, 分为2部分:
SharedArrayBuffer允许主线程、及WebWorkers之间共享数据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线程里也可以创建共享数据; 另外,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与Object.keys()一致,对属性都有3个限定条件(own && enumerable && non-Symbol-only)。因此,不考虑性能的话,可以更简单的实现:
function values(obj) { return Object.keys(obj).map(key => obj[key]); }除了返回值形式不同以外,与Object.values(obj)一毛一样
应用场景上,Object.entries(obj)可以用来完成mapObject转Map的工作:
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类型属性,用Symbol-only表示仅针对Symbol类型属性JS里围绕对象属性的这3个特点提供了很多工具方法,除了上面提到的Object.keys()、Object.values()、Object.entries()外,还有
Object.getOwnPropertyNames(obj):own && non-Symbol-onlyObject.getOwnPropertySymbols():own && Symbol-onlyReflect.ownKeys(obj):own。等价于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))包括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)实现
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}实际上,类似的变动在ES5.1也发生过: 对象字面量键值对儿列表允许有多余逗号
还有语言最初的语法规则:数组字面量允许有多余逗号
特殊的:
const arr = [1, 2, 3,,,]; arr.length; // 5数据源是异步的,for…of循环就只能拿到一堆Promise
// 异步数据源 let arr = [1, 2, 3].map(n => Promise.resolve(n)); for (let value of arr) { console.log(value); // Promise.{<resolved>: 1}... }SO 新增了3个东西:
实现了AsyncIterator接口的,就叫async iterable,就有能通过for-await-of遍历的特权
// 异步数据源 let arr = [1, 2, 3].map(n => Promise.resolve(n)); // 实现AsyncIterator接口 arr[Symbol.asyncIterator] = () => { let i = 0; return { next() { let done = i === arr.length; return !done ? arr[i++].then(value => ({ value, done })) : Promise.resolve({ value: void 0, done: true }); } } };(async ()=> { for await (const n of arr) { console.log(n); // 1, 2, 3 } })();用起来与同步for…of没太大区别,只是实现AsyncIterator接口有些麻烦,迫切需要一种更方便的方式
P.S.同样,await关键字只能出现在async function里,for-await-of的await也不例外
就是我们迫切想要的异步迭代器的生成器
// 异步数据源 let arr = [1, 2, 3].map(n => Promise.resolve(n)); // 实现AsyncIterator接口 arr[Symbol.asyncIterator] = async function*() { for (let value of arr) { yield value; } }async generator返回值本来就是async iterable(隐式实现了AsyncIterator接口),没必要手动实现该接口
let asyncIterable = async function*() { let arr = [1, 2, 3].map(n => Promise.resolve(n)); for (let value of arr) { yield value; } }(); //类似于同步版本 let iterable = function*() { let arr = [1, 2, 3]; for (let value of arr) { yield value; } }();就具体语法而言,async generator有3个特点
返回async iterable对象,其next、throw、return方法都返回Promise,而不直接返回{ value, done },并且会默认实现Symbol.asyncIterator方法(因此async generator返回async iterable)函数体中允许出现await、for-await-of语句同样支持yield\*拼接迭代器 let asyncIterable = async function*() { let arr = [1, 2, 3].map(n => Promise.resolve(n)); for (let value of arr) { yield value; } // yield*拼接异步迭代器 yield* (async function*() { for (let v of [4, 5, 6]) { yield v; } }()); // 允许出现await let seven = await Promise.resolve(7); yield seven; // 允许出现for-await-of for await (let x of [8, 9]) { yield x; } }();// test (async ()=> { for await (const n of asyncIterable) { console.log(n); // 1, 2, 3...9 } })();注意一个细节,类似于await nonPromise,for-wait-of也能接受非Promise值(同步值)
P.S.另外,async generator里的yield等价于yield await,具体见Suggestion: Make yield Promise.reject(…)
asyncIterator内部维持了一个请求队列,以此保证遍历次序,例如:
const sleep = (ts) => new Promise((resolve) => setTimeout(resolve, ts)); let asyncIterable = async function*() { yield sleep(3000); yield sleep(1000); }(); const now = Date.now(); const time = () => Date.now() - now; asyncIterable.next().then(() => console.log('first then fired at ' + time())); asyncIterable.next().then(() => console.log('second then fired at ' + time())); // first then fired at 3002 // second then fired at 4005第一个next()结果还没完成,立即发起的第二个next(),会被记到队列里,等到前置next()都完成以后,才实际去做
上例相当于:
let iterable = function*() { let first; yield first = sleep(3000); // 排队,等到前置yield promise都完成以后,才开始 yield first.then(() => sleep(1000)); }(); iterable.next().value.then(() => console.log('first then fired at ' + time())); iterable.next().value.then(() => console.log('second then fired at ' + time()));解构赋值与剩余属性的差异,看似等价,实则不然
let { x, y, ...z } = a; // is not equivalent to let { x, ...n } = a; let { y, ...z } = n; let a = Object.create({x: 1, y: 2}); a.z = 3; void (() => { let { x, y, ...z } = a; console.log(x, y, z); // 1 2 {z: 3} })(); void (() => { let { x, ...n } = a; let { y, ...z } = n; console.log(x, y, z); // 1 undefined {z: 3} })();关键区别在于剩余属性只取自身属性,而解构赋值会取自身及原型链上的属性,所以对照组中的y变成undefined了
ES2015增强过一波
Unicode mode (the u flag):实际应用见JavaScript emoji utils | 正则表达式中的Unicodesticky mode (the y flag):严格从lastIndex指定的位置开始匹配the RegExp.prototype.flags getter:获取正则表达式对象所开启的模式标识(gimuy按字母序排列,分别表示全局匹配、忽略大小写、多行匹配、Unicode支持与严格模式) ES2018进一步增强不开s模式的话
/a.c/.test('abc') === true /a.c/.test('a\nc') === false /a.c/.test('a\rc') === false /a.c/.test('a\u2028c') === false /a.c/.test('a\u2029c}') === false要想匹配任意字符的话,只能通过一些技巧绕过,如:
// [^]匹配一个字符,什么都不排除 /a[^]c/s.test('a\nc') === true // [\s\S]匹配一个字符,任意空白字符和非空白字符 /a[^]c/s.test('a\nc') === true开s模式后
const regex = /a.c/s; regex.test('a\nc') === true另外,还有两个属性用来获取该模式是否已开启:
regex.dotAll === true regex.flags === 's'注意,点号通配模式(s)并不影响多行匹配模式(m),二者是完全独立的:
s:只影响.(点号)的匹配行为
m:只影响^$的匹配行为
可以一起用,也互不干扰:
// 不开m时,$匹配串尾 /^c$/.test('a\nc') === false // 开m之后,$能够匹配行尾 /^c$/m.test('a\nc') === true // 同时开sm,各司其职 /^b./sm.test('a\nb\nc') === trueP.S.m模式术语叫增强的行锚点模式:增强的行锚点模式,把段落分割成逻辑行,使得^和$可以匹配每一行的相应位置,而不是整个串的开始和结束位置
正则环视(lookaround)相关的一个特性,环视的特点是不匹配任何字符,只匹配文本中的特定位置
(? <= ...):肯定逆序环视(Positive lookbehind assertions),子表达式能够匹配左侧文本时才成功匹配 (? <! ...):否定逆序环视(Negative lookbehind assertions),子表达式不能匹配左侧文本时才成功匹配一种向后看的能力,典型应用场景如下:
// 从'$10.53'提取10.53,即捕获左侧是$符的数值 '$10.53'.match(/(?<=\$)\d+(\.\d*)?/)[0] === '10.53' // 从'$-10.53 $-10 $0.53'提取正值0.53,即捕获左侧不是负号的数值 '$-10.53 $-10 $0.53'.match(/(?<=\$)(?<!-)\d+(\.\d*)?/g)[0] === '0.53'向前看的能力一直都有,例如:
// (?=…) 肯定顺序环视, 子表达式能够匹配右侧文本 'baaabac'.match(/(?=(a+))a*b\1/)[0] === 'aba' // (?!…) 否定顺序环视,子表达式不能匹配右侧文本 'testRegexp test-feature tesla'.match(/(?<=\s)(?!test-?)\w+/g)[0] === 'tesla'实现上,含逆序环视的正则表达式的匹配顺序是从右向左的,例如:
// 逆序环视,从右向左扫描输入串,所以$2贪婪匹配到了053 '1053'.replace(/(?<=(\d+))(\d+)$/, '[$1,$2]') === '1[1,053]' // 一般情况,从左向右扫描输入串,贪婪匹配$1为105 '1053'.replace(/^(\d+)(\d+)/, '[$1,$2]') === '[105,3]' //从上例能够发现另一个细节:虽然扫描顺序相反,但捕获分组排序都是从左向右的此外,逆序环视场景下反向扫描对反向引用有影响,毕竟只能引用已匹配过的内容,所以要想匹配叠词的话,应该这样做
/(?<=\1(.))/.test('哈哈') === true OR NOT /(?<=(.)\1)/.test('哈8') === true实际上,这里的\1什么都匹配不到,永远是空串(因为从右向左扫,还没捕获哪来的引用),删掉它也没关系(/(?<=(.))/)
常见的日期格式转换场景:
'2017-01-25'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$1/$2/$3') === '2017/01/25'我们通过$n来引用对应的捕获到的内容,存在两个问题
可读性:$n仅表示第几个捕获分组,不含其它语义灵活性:一旦正则表达式中括号顺序发生变化,replacement($1/$2/$3)要跟着变命名捕获分组能够很好的解决这两个问题:
const reDate = /(?<yyyy>\d{4})-(?<mm>\d{2})-(?<dd>\d{2})/; '2017-01-25'.replace(reDate, '$<yyyy>/$<mm>/$<dd>') === '2017/01/25'正则表达式中的捕获分组与replacement中的引用都有了额外语义
另外,匹配结果对象身上也有一份命名捕获内容:
let result = reDate.exec('2017-01-25'); const { yyyy, mm, dd } = result.groups; OR // const { groups: {yyyy, mm, dd} } = result; `${yyyy}/${mm}/${dd}` === '2017/01/25'从语法上看,引入了3个新东西:
(?<name>...):命名捕获型括号 k<name>:命名反向引用 $<name>:命名replacement引用,函数形式的replacement把groups作为最后一个参数,具体见Replacement targetsUnicode字符有一些属性,比如π是希腊文字,在Unicode中对应的属性是Script=Greek
为了支持根据Unicode属性特征匹配字符的场景,提供了两种语法:
\p{UnicodePropertyName=UnicodePropertyValue}:匹配一个Unicode属性名等于指定属性值的字符\p{LoneUnicodePropertyNameOrValue}:匹配一个该Unicode属性值为true的字符 P.S.对应的\P表示补集注意,都要开u模式,不开不认
前者适用于非布尔值(non-binary)属性,后者用于布尔值(binary)属性,例如:
const regexGreekSymbol = /\p{Script=Greek}/u; regexGreekSymbol.test('π') === true // Unicode数字 /\p{Number}{2}/u.test('罗马数字和带圈数字Ⅵ㉜') === true // Unicode版\d /^\p{Decimal_Number}+$/u.test('') === truePending的Promise要么Resolved要么Rejected,而有些时候需要的是Resolved || Rejected,比如只想等到异步操作结束,不论成功失败,此时Promise.prototype.finally就是最合适的解决方案
fetch('http://www.example.com').finally(() => { // 请求回来了(不论成功失败),隐藏loading document.querySelector('#loading').classList.add('hide'); });可以在finally块里做一些清理工作(类似于try-catch-finally的finally),比如隐藏loading、关闭文件描述符、log记录操作已完成
之前类似的场景一般通过then(f, f)来解决,但finally的特点在于:
没有参数(专职清理,不关心参数)没有参数(专职清理,不关心参数)不影响Promise链的状态及结果(而then(() => {}, () => {})会得到Resolved undefined),除非finally块里的throw或者return rejectedPromise会让Promise链变为Rejected error //例如 Promise.resolve(1) .finally(() => 2) .finally((x) => new Promise((resolve) => { setTimeout(() => { resolve(x+1) }, 3000); })) .then( // 3秒后,log 1 res => console.log(res) )Resolved 1始终没被改变,因为从设计上不希望finally影响返回值
其中,returning a value early指的是返回Rejected Promise,例如:
Promise.resolve(1) // returning a value early .finally(() => Promise.reject(2)) .catch(ex => console.log(ex)) .finally(() => { // throwing an exception throw 3; }) .catch(ex => console.log(ex))模板字符串默认识别(尝试去匹配解释)其中的转义字符
\u:Unicode字符序列,如\u00FF或\u{42}\x:十六进制数值,如\xFF\0:八进制,如\101,具体见Octal escape sequencesP.S.实际上,八进制转义序列在模板字面量和严格模式下的字符串字面量都是不合法的
对于不合法的转义序列,会报错:
// Uncaught SyntaxError: Invalid Unicode escape sequence `\uZZZ` // Uncaught SyntaxError: Invalid hexadecimal escape sequence `\xxyz` // Uncaught SyntaxError: Octal escape sequences are not allowed in template strings. `\0999` // 更容易出现的巧合 `windowsPath = c:\usrs\xxx\projects`但是,模板字符串作为ES2015最开放的特性:
标签模板以开放的姿态欢迎库设计者们来创建强有力领域特定语言。这些语言可能看起来不像JS,但是它们仍可以无缝嵌入到JS中并与JS的其它语言特性智能交互。我不知道这一特性将会带领们走向何方,但它蕴藏着无限的可能性,这令我感到异常兴奋!
这种粗暴的默认解析实际上限制了模板字符串的包容能力,例如latex:
let latexDocument = ` \newcommand{\fun}{\textbf{Fun!}} // works just fine \newcommand{\unicode}{\textbf{Unicode!}} // Illegal token! \newcommand{\xerxes}{\textbf{King!}} // Illegal token!Breve over the h goes \u{h}ere // Illegal token! `这是一段合法的latex源码,但其中的\unicode、\xerxes和\u{h}ere会引发报错
针对这个问题,ES2018决定对标签模板去掉这层默认解析,把处理非法转义序列的工作抛到上层
//例如: function tag(strs) { // 解析过的,存在非法转义序列就是undefined strs[0] === undefined // 裸的,与输入完全一致 strs.raw[0] === "\\unicode and \\u{55}"; } tag`\unicode and \u{55}`注意,这个特性仅针对标签模板,普通模板字符串仍然保留之前的行为(遇到非法转义序列会报错):
let bad = `bad escape sequence: \unicode`; // throws early error可选的 catch 绑定提案是为了能够选择性地移除使用不到的 catch 绑定。
try {} catch(err) {} //现在可以删除使用不到的绑定。 try { ...} catch { ...}这个提案的目的是让 JSON 字符串可以包含未转义的 U+2028 LINE SEPARATOR 和 U+2029 PARAGRAPH SEPARATOR 字符,而 ECMAScript 字符串是不能包含这些字符的。在 ES2019 生效之前,这样做会出现“SyntaxError: Invalid or unexpected token”错误。
const LS = eval('"\u2028"'); const PS = eval("'\u2029'");符号是在 ES2015 中引入的,具有非常独特的功能。在 ES2019 中可以提供给定的描述,目的是避免间接从 Symbol.prototype.toString 获取描述。
const mySymbol = Symbol('myDescription'); console.log(mySymbol); // Symbol(myDescription) console.log(mySymbol.toString()); // Symbol(myDescription) console.log(mySymbol.description); // myDescription之前的函数原型已经有 toString 方法,但是在 ES2019 中,它经过了修订,可以包含函数内的注释,不过不适应于箭头函数。
function foo (){} /* Before */ console.log(foo.toString()); // function foo(){} /* Now ES2019 */ console.log(foo.toString()); // function foo (){} /* Arrow Syntaxconst */ bar = () => {} console.log(bar.toString()); // () => {}Object.entries 方法的反向操作,可用于克隆对象。
const obj = { prop1: 1, prop2: 2, }; const entries = Object.entries(obj); console.log(entries); // [ [ 'prop1', 1 ], [ 'prop2', 2 ] ] const fromEntries = Object.fromEntries(entries); console.log(fromEntries); // Object { prop1: 1, prop2: 2 } console.log(obj === fromEntries); // false不过需要注意的是,嵌入式对象 / 数组都只是引用
const obj = { prop1: 1, prop2: 2, deepCopy: { mutateMe: true } }; const entries = Object.entries(obj); const fromEntries = Object.fromEntries(entries); fromEntries.deepCopy.mutateMe = false; console.log(obj.deepCopy.mutateMe); // false使用 JSON 转义序列表示输出结果,而不是返回 UTF-16 代码单元。
/* Before */ console.log(JSON.stringify('\uD800')); // "�" /* Now ES2019 */ console.log(JSON.stringify('\uD800')); // "\ud800"用来移除字符串开头和结尾的空格
/* Trim */ const name = " Codedam "; console.log(name.trim()); // "Codedam" /* Trim Start */ const description = " Unlocks Secret Codes "; console.log(description.trimStart()); // "Unlocks Secret Codes " /* Trim End */ const category = " JavaScript "; console.log(category.trimEnd()); // " JavaScript"通过将所有子数组元素以递归方式连接到指定的深度来创建数组。默认深度为 1,使数组的第一层嵌套展平。
var arr = [1,2,3,[4,5,6,[7,8,9,[1,2,3]]]]; console.log(arr.flat()); console.log(3,arr.flat(3)); console.log(2,arr.flat(2)); console.log(1,arr.flat(1)) //(7) [1, 2, 3, 4, 5, 6, Array(4)] //3 (12) [1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3] //2 (10) [1, 2, 3, 4, 5, 6, 7, 8, 9, Array(3)] //1 (7) [1, 2, 3, 4, 5, 6, Array(4)] // You can use Infinity to flatten all the nested arrays no matter how deep the array is const arrExtreme = [1, [2, [3, [4, [5, 6, 7, [8, 9]]]]]]; arrExtreme.flat(Infinity); // [1, 2, 3, 4, 5, 6, 7, 8, 9]类似于 flat,并且还与 map 相关,它会先映射数组然后将其展平。
const arr = ['Codedam', 'is Awsome', '!']; const mapResult = arr.map(item => item.split(' ')); console.log(mapResult); // [ [ 'Codedam' ], [ 'is', 'Awsome' ], [ '!' ] ] const flatMapResult = arr.flatMap(chunk => chunk.split(' ')); console.log(flatMapResult); // ['Codedam', 'is', 'Awsome', '!'];