ECMAScript 2016, 2017, and 2018一
1. Array.prototype.includes
includes
是一个简单的检测元素是否在数组中的方法,包括NaN
。(不像indexOf
)
let a = [1, 2, 3, NaN, 4];
a.includes(NaN); // true
a.indexOf(NaN); // -1
2. 求幂中缀操作符
算数操作,例如+
和-
都是中缀操作符,与他们类似**
被用来求幂操作。
Math.pow(7,2) // 49
7**2 // 49
1. Object.values()
Object.values()
与Object.keys()
类似,不过返回了对象的所有值。
const cars = {BMW: 3, Tesla: 2, Toyota: 1}
// ES2015
const vals = Object.keys(cars).map(key => cars[key]);
console.log(vals) // [3, 2, 1]
// ES2016
const values = Object.values(cars);
console.log(values) // [3, 2, 1]
2. Object.entries()
Object.entries()
与Object.keys()
相关,但不像Object.keys()
只返回key,这个方法连value一起返回。
Example 1
const cars = {BMW: 3, Tesla: 2, Toyota: 1}
// ES2015
Object.keys(cars).forEach(key => {
console.log('key: ' + key + ' value: ' + cars[key])
})
// ES2017
for (let [key, value] of Object.entries(cars)){
console.log(`key: ${key} value: ${value}`)
}
Example 2
const cars = {BMW: 3, Tesla: 2, Toyota: 1}
// ES2015
const map1 = new Map();
Object.keys(cars).forEach(key => {
map1.set(key, cars[key]);
});
// ES2017
const map2 = new Map(Object.entries(cars));
// Map(3) {"BMW" => 3, "Tesla" => 2, "Toyota" => 1}
Object.entries
返回二维数组,把key value全返回回来。
3. String padding
String.prototype.padStart
和String.prototype.padEnd
允许在字符串头或尾添加空白或指定字符串。(一共这么长,而不是添加这么长)
'someString'.padStart(numberOfCharcters [,stringForPadding]);
'5'.padStart(10) // ' 5'
'5'.padStart(10, '=*') //'=*=*=*=*=5'
'5'.padEnd(10) // '5 '
'5'.padEnd(10, '=*') //'5=*=*=*=*='
总有一些情况下我们想要补齐输出,美化效果,这是一个很好用的方法
Example 1
const formated = [1, 12, 123, 1234, 12345].map(
num => num.toString().padStart(10, '0')
)
// 输出
// "0000000001"
// "0000000012"
// "0000000123"
// "0000001234"
// "0000012345"
Example 2
const cars = {BMW: 10, Tesla: 5, Toyota: 0}
Object.entries(cars).map(([name, count]) => {
console.log(`${name.padEnd(20, '-')} Count: ${count.toString().padStart(3, '0')} `)
})
// BMW----------------- Count: 010
// Tesla--------------- Count: 005
// Toyota-------------- Count: 000
需要注意的问题
在Emoji和其他双字节字符上,此方法有问题。
'heart'.padStart(10, "❤️"); // prints.. '❤️❤️❤heart'
并没如预期添加五个❤️,这是因为❤️由两字节 ('\u2764\uFE0F' )组成,最终添加上去了两个'\u2764\uFE0F'和一个'\u2764'
4. Object.getOwnPropertyDescriptors
Object.getOwnPropertyDescriptor()
方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)
这个方法返回对象属性的所有细节(包括setter和getter)。这个api的动机就是浅拷贝时拷贝getter和setter(与Object.assign
不同)。
Object.assign
浅拷贝原对象除了getter和setter之外的所有属性
var Car = {
name: 'BMW',
price: 1000000,
set discount(x) {
this.d = x;
},
get discount() {
return this.d;
},
};
//Print details of Car object's 'discount' property
console.log(Object.getOwnPropertyDescriptor(Car, 'discount'));
//prints..
// {
// get: [Function: get],
// set: [Function: set],
// enumerable: true,
// configurable: true
// }
//Copy Car's properties to ElectricCar using Object.assign
const ElectricCar = Object.assign({}, Car);
//Print details of ElectricCar object's 'discount' property
console.log(Object.getOwnPropertyDescriptor(ElectricCar, 'discount'));
//prints..
// {
// value: undefined,
// writable: true,
// enumerable: true,
// configurable: true
// }
//⚠️Notice that getters and setters are missing in ElectricCar object for 'discount' property !👎👎
//Copy Car's properties to ElectricCar2 using Object.defineProperties
//and extract Car's properties using Object.getOwnPropertyDescriptors
const ElectricCar2 = Object.defineProperties({}, Object.getOwnPropertyDescriptors(Car));
//Print details of ElectricCar2 object's 'discount' property
console.log(Object.getOwnPropertyDescriptor(ElectricCar2, 'discount'));
//prints..
// { get: [Function: get], 👈🏼👈🏼👈🏼
// set: [Function: set], 👈🏼👈🏼👈🏼
// enumerable: true,
// configurable: true
// }
// Notice that getters and setters are present in the ElectricCar2 object for 'discount' property!
5. 在函数参数中添加尾随逗号
这个主要是用来帮助git,让显示更加美观,在添加属性后只改变添加的那一行,而不是上一行也改变。如下面的例子
//问题
// #1 开发者创建
function Person(
name,
age
) {
this.name = name;
this.age = age;
}
//#2 开发者添加address
function Person(
name,
age, // 添加尾随逗号
address //添加新的参数
) {
this.name = name;
this.age = age;
this.address = address; //添加的这行
}
//ES2017 解决
//允许#1开发者添加尾随逗号
//#1 创建如下
function Person(
name,
age, //
) {
this.name = name;
this.age = age;
}
6. Async/Await
这个是目前为止最重要,最有用的特性。Async使我们不必处理回调地狱,可以让这个代码看起来更整洁。
async
关键词告诉JS引擎采用不同的方法处理这个函数。编译器一旦在函数中遇见await
就暂停。它假定await
后的表达式返回一个Promise,于是一直等待这个Promise直到resolved或者rejected。
在下面的例子中,getAmount
方法调用了两个异步函数getUser
和getBankBalance
。我们可以使用Promise完成这个功能,但是使用async await
更优雅更简单。
// ES2015
function getAmount1(userId) {
getUser(userId)
.then(getBankBalance)
.then(amount => {
console.log(amount);
});
}
// ES2017
// 译者注: 这个async不可省略
// 否则报错 Syntax Error: await is a reserved word
async function getAmount2(userId) {
var user = await getUser(userId);
var amount = await getBankBalance(user);
console.log(amount);
}
function getUser(userId) {
return new Promise(resolve => {
setTimeout(() => {
resolve('john');
}, 1000)
})
}
function getBankBalance(user) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (user === 'john') {
resolve('$1000');
} else {
resolve('unknown user');
}
}, 1000)
})
}
getAmount1('1') // $1000
getAmount2('1') // $1000
6.1 Async函数本身返回Promise
如果你想使用async函数的返回值,你需要使用Promise的then
来获取。
在下面的例子,我们想要在doubleAndAdd
函数外输出他的结果,所以我们需要使用then
语法。
// async本身返回promise!
async function doubleAndAdd(a, b) {
a = await doubleAfter1Sec(a);
b = await doubleAfter1Sec(b);
return a + b;
}
function doubleAfter1Sec(param) {
return new Promise(resolve => {
setTimeout(resolve(param * 2), 1000);
})
}
doubleAndAdd(1, 2).then(console.log); // 6
6.2 并行调用 async/await
上面面的例子调用await两遍,每次等待1秒(共两秒)。但是,我们其实可以使用Promise.all
并行的调用a和b,因为他们互不依赖,这样时间就缩短为1秒。
// async本身返回promise!
async function doubleAndAdd(a, b) {
// a = await doubleAfter1Sec(a);
// b = await doubleAfter1Sec(b);
[a, b] = await Promise.all([doubleAfter1Sec(a), doubleAfter1Sec(b)])
return a + b;
}
function doubleAfter1Sec(param) {
return new Promise(resolve => {
setTimeout(() => {
resolve(param * 2)
}, 1000);
})
}
doubleAndAdd(1, 2).then(console.log); // 6
6.3 错误处理
使用async await
时有多种错误处理方法。
Option 1. 方法内使用try catch
async function doubleAndAdd(a, b) {
try {
a = await doubleAfter1Sec(a);
b = await doubleAfter1Sec(b);
} catch (e) {
console.log(e);
return "Error!!";
}
// [a, b] = await Promise.all([doubleAfter1Sec(a), doubleAfter1Sec(b)])
return a + b;
}
function doubleAfter1Sec(param) {
return new Promise((resolve, reject) => {
setTimeout(() => {
let val = param * 2;
isNaN(val) ? reject('error msg') : resolve(val);
}, 1000);
})
}
doubleAndAdd('one', 2).then(console.log); // Error!! 和log 'error msg'
doubleAndAdd(1, 2).then(console.log); // 6
Option 2. 每个await表达式都Catch
// 对每一个await catch
// 因为每一个await内部都是一个Promise
async function doubleAndAdd(a, b) {
a = await doubleAfter1Sec(a).catch(e => console.log('"a" is NaN')); // 👈
b = await doubleAfter1Sec(b).catch(e => console.log('"b" is NaN')); // 👈
if (!a || !b) {
return NaN;
}
return a + b;
}
//🚀Usage:
doubleAndAdd('one', 2).then(console.log); // NaN and logs: "a" is NaN
doubleAndAdd(1, 2).then(console.log); // 6
function doubleAfter1Sec(param) {
return new Promise((resolve, reject) => {
setTimeout(function() {
let val = param * 2;
isNaN(val) ? reject(NaN) : resolve(val);
}, 1000);
});
}
Option 3. 对整个的async-await函数Catch
// 除了在函数外部之外,不做任何处理
// 因为async await返回一个Promise 我们可以对他整体进行处理
async function doubleAndAdd(a, b) {
a = await doubleAfter1Sec(a);
b = await doubleAfter1Sec(b);
return a + b;
}
//🚀Usage:
doubleAndAdd('one', 2)
.then(console.log)
.catch(console.log); // 👈👈🏼<------- use "catch"
function doubleAfter1Sec(param) {
return new Promise((resolve, reject) => {
setTimeout(function() {
let val = param * 2;
isNaN(val) ? reject(NaN) : resolve(val);
}, 1000);
});
}
ECMAScript最近已经进入终稿,2018年6或7月会推出。所有下面的features都是在Stage-4中提到的,而且将会是ECMAScript2018的一部分。
1. 共享内存和atomics
这是一个大的先进的提升,而且是对JS引擎的核心提升。
核心观点是为JavaScript带来某种意义上(some sort of)的多线程特性,从而允许JS开发人员未来可通过自己管理内存而不是靠JS引擎来管理内存,从而写一些高性能的并发代码
这是通过一个新类型的全局变量SharedArrayBuffer,在共享内存空间上存储数据。这样这些数据就可以被JS主线程和web-werker线程共同使用。
目前为止,如果我们想在主线程和worker间共享变量只能通过拷贝变量然后通过postMessage
传递。再也不用这样了!
只需要简单的使用SharedArrayBuffer,数据就会瞬间被主线程和web-worker访问到。
但是共享内存会带来竞争条件(race conditions)。为了避免它,引入了“Atomics
”全局变量。当一个线程使用共享内存中的数据时,Atomics
提供了多种方式来锁住内存。同时它也提供了多种方式在共享内存中安全地更新数据。
推荐使用类库来使用这个特性,但是目前没有类库能兼容这个特性。
如果你感兴趣,推荐阅读
- From Workers to Shared Memory — lucasfcosta
- A cartoon intro to SharedArrayBuffers — Lin Clark
- Shared memory and atomics — Dr. Axel Rauschmayer
2. 移除标记模板字面量(Tagged Template literal)的限制
首先,我们需要解释一下什么叫做“Tagged Template literal”,这样才能更好的理解这个特性。
在ES2015+,有一个特性叫做标签模板字面量(Tagged Template literal),他允许开发人员定制字符串将如何被插入。例如,标准的方式,我们这样插入字符串。
const firstName = 'NowhereToRun';
const greetings = `Hello ${firstName}`;
console.log(greetings); // Hello NowhereToRun
在标签字面量中,你可以写一个方法接收硬编码的字符串,例如['hello', '!'],和一个替换的变了,例如:['NowhereToRun']
,返回任何你想让这个function返回的东西。
下面的例子展示了我们的“Tag”函数greet
在输入的字符串后添加当时的时间。
// A "Tag" function returns a custom string literal.
// In this example, greet calls timeGreet() to append Good //Morning/Afternoon/Evening depending on the time of the day.
function greet(hardCodedPartsArray, ...replacementPartsArray) {
console.log(hardCodedPartsArray); //["Hello", ", I'm", "!", raw: Array(3)]
console.log(replacementPartsArray); //["Raja", "Eason"]
let str = '';
hardCodedPartsArray.forEach((string, i) => {
if (i < replacementPartsArray.length) {
str += `${string} ${replacementPartsArray[i] || ''}`;
} else {
str += `${string} ${timeGreet()}`; //<-- append Good morning/afternoon/evening here
}
});
return str;
}
// Usage:
const firstName = 'Raja';
const you = 'Eason'
const greetings = greet `Hello${firstName}, I'm${you}!`; // <-- Tagged literal
console.log(greetings); // 'Hello Raja! Good Morning!'
function timeGreet() {
const hr = new Date().getHours();
return hr < 12 ?
'Good Morning!' :
hr < 18 ? 'Good Afternoon!' : 'Good Evening!';
}
现在我们来讨论什么是"Tagged"函数,很多人想在不同的环境下使用这个特性,例如命令行终端,或获取HTTP请求的URIs,等等。
Tagged String literal存在的问题
ES2015和ES2016不逊于使用转义字符(escape characters)。例如“\u” (unicode),“\x”(hexadecimal) 除非他们看起来像一个准确的字符,例如\u00A9
或 \u{2F804} or \xA9。
所以如果你的Tagged函数内部有其他领域的规则(例如终端的),他可能需要使用类似** \ubla123abla**的字符,并不是像\u0049 或 \u{@F804}一样准确,你会被报语法错误。
在ES2018中,这个规则被去掉了,只要Tagged函数返回一个对象,包含cooked
属性(非法变量存储为"undefined"),和raw
属性(保存任意你想保存的)就允许使用看起来不合法的转义字符。
function myTagFunc(str) {
return { "cooked": "undefined", "raw": str.raw[0] }
}
var str = myTagFunc `hi \ubla123abla`; //call myTagFunc
str // { cooked: "undefined", raw: "hi \\unicode" }
3. 正则表达式“dotall”标记
目前正则表达式中,即时dot(".")表示匹配任意单个字符,他还是不能匹配换行符,例如\n \r \f
等。
// Before
console.log(/first.second/.test('first\nsecond')); // false
这个特性使得dot操作符可以匹配所有单字符。为了使这个不造成非常大的影响,我们需要在创建正则的时候添加\s
标志。
//ECMAScript 2018
/first.second/s.test('first\nsecond'); //true Notice: /s
ECMAScript 2018 — 允许通过\s标记来使 . 操作符匹配换行符
4. RegExp命名组捕获
这个增强为JS带来了和其他编程例如Java,Python语言一样的“命名组(Named Groups)”。这个特性允许开发者写正则表达式时以(?<name>...)
为不同的组提供名称(标识符)。可以使用这个名称去方便地获取捕获。
4.1 基本的命名组的例子
在下面的例子中,我们使用(?<year>) (?<month>) and (?day)
去命名日期的不同部分。匹配结果返回值会有一个property
属性,其中有属性year
,month
,day
,值为他们对应的匹配值。
let re1 = /(\d{4})-(\d{2})-(\d{2})/;
let result1 = re1.exec('2015-01-02');
console.log(result1);
// ["2015-01-02", "2015", "01", "02", index: 0, input: "2015-01-02", groups: undefined]
let re2 = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
let result2 = re2.exec('2015-01-02');
console.log(result2);
// ["2015-01-02", "2015", "01", "02", index: 0, input: "2015-01-02", groups: {year: "2015", month: "01", day: "02"}]
4.2 在正则表达式内部使用命名组的值
我们可以使用\k<group name>
的格式向前寻找刚才正则匹配的值。
let sameWorks = /(?<fruit>apple|orange)==\k<fruit>/u;
sameWorks.test('apple==apple') // true
sameWorks.test('orange==apple') //false
sameWorks.test('orange==orange') //true
4.3 在String.prototype.replace中使用命名组
命名组的特性已经集成到了String的replace
实例方法中。我们可以方便地使用它交换字符位置。
例如,把“firstName,lastName”变成“lastName,firstName”。
let re = /(?<firstName>[A-Za-z]+) (?<lastName>[A-Za-z]+$)/u;
'HaHa NowhereToRun'.replace(re, '$<lastName>, $<firstName>');
// "NowhereToRun, HaHa"
5. 对Object获取剩余属性(Rest properties)
剩余操作符(Rest operator) ...
(或者称三点操作符)可以用来获取Object的没有被解析出的属性。
5.1 使用...获取你想要的属性
// 提取出firstName和age, 把剩下的存储到'remaining'变量中
let {
firstName,
age,
...remaining
} = {
firstName: 'haha',
lastName: 'NowhereToRun',
age: 20,
height: '5.10',
race: 'martian'
}
console.log(firstName) // haha
console.log(age) // 20
console.log(remaining) // {lastName: "NowhereToRun", height: "5.10", race: "martian"}
5.2 使用...移除不想要的元素
例如我想要一个新的对象不包含SSN属性,我们不必遍历旧对象的每一个属性,直接可以用... 来获取去除不需要的属性之外的属性
let {
SSN,
...cleanObj
} = {
firstName: 'haha',
lastName: 'NowhereToRun',
age: 20,
height: '5.10',
race: 'martian',
SSN: '123-45-67890'
}
console.log(cleanObj); // {firstName: "haha", lastName: "NowhereToRun", age: 20, height: "5.10", race: "martian"}
6. 对象的扩展属性 (Spread properties for Objects)
可以使用 ...
操作符来展开对象的属性。与Object.assign等操作类似,后面的会覆盖前面的同名属性。
扩展操作符(本节)用于等号的右侧。Rest操作符(上节)用于等号的左侧
// person 和 account 合并
const person = { fName: 'john', age: 20, name: 'haha' };
const account = { name: 'bofa', amount: '$1000' };
// 通过扩展运算符提取person和account的属性,并将其添加到一个新对象中。
const personAndAccount = { ...person, ...account };
console.log(personAndAccount); // {fName: 'john', age: 20, name: 'bofa', amount: '$1000' }
7. RegExp向后断言
这是对RegExp的增强,它允许我们确保某些字符串立即存在于其他字符串之前。
可以使用一个组(?<=...)
(问号,小于,等于)来查找肯定的声明。
此外使用(?<!...)
(问号,小于,感叹号)来查看否定声明。
从本质上讲,只要-ve声明通过,就会匹配。(-ve是啥... 查查)
肯定断言
比如我们希望#
号后面紧跟着winner
,即#winner
。而且希望正则表达式只返回winner不包含#
号。
console.log(/(?<=#).*/.test('winner')); // false
console.log(/(?<=#).*/.test('#winner')); // true
//before
console.log('#winner'.match(/#.*/)[0]); //'#winner' 匹配值 包含 #
//After ECMAScript 2018
console.log('#winner'.match(/(?<=#).*/)[0]); // 'winner'
否定断言
假设我们想从具有€符号之后提取数字而不是$符号之后提取数字。
(这个例子Chrome中执行结果并不一样,Chrome还没支持?)
'A gallon of milk is €3.00'.match(/(?<!\$)\d+\.?\d+/)[0];
//3.00
'A gallon of milk is $3.00'.match(/(?<!\$)\d+\.?\d+/);
// 输出 ["00", index: 23, input: "A gallon of milk is $3.00", groups: undefined]