前段时间换工作,面试了几家公司,有一道题发现基本是必问的,就是说一说平时用到的那些 es6 7 8 9 等的特性。一直没有做总结,现在就整理一下平时自己在工作中用到的比较多的那些新特性。
const and let
声明变量新增的两个关键词,与 var 不同的一点在于,在 JS 函数中的 var 声明,其作用域是函数体的全部,而 const 和 let 是有块作用域的。
看一个经典的闭包问题,现在用 let 就可以轻易地避免了。
// before
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // => 3, 3, 3
});
}
// now
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 0, 1, 2
});
}
const 是常量的声明,表示的是变量不可被再次赋值,而变量的值是可以改变的
const obj = { a: 1 };
obj = 'a'; // => Uncaught TypeError: Assignment to constant variable.
obj.b = 2;
Template Literals
允许嵌入表达式的字符串字面量,可以使用多行字符串和字符串插值功能。
const name = 'laohan';
const grettings = `hello ${name}`;
console.log(grettings); // => hello laohan
const html = `
<div>
利用字面量,可以很方便的实现多行字符串
</div>
`;
String padding
用另一个字符串填充当前字符串(重复,如果需要的话),以便产生的字符串达到给定的长度。
String.prototype.padStart
str.padStart(targetLength [, padString])
'abc'.padStart(10); // ' abc'
'abc'.padStart(10, "foo"); // 'foofoofabc'
String.prototype.padEnd
str.padEnd(targetLength [, padString])
'abc'.padEnd(10); // 'abc '
'abc'.padEnd(10, "foo"); // 'abcfoofoof'
padStart 与 padEnd 的不同在于填充的字符串是在当前字符串的左边还是右边。padStart 在左,padEnd 在右。
Exponentiation infix operator (指数运算符)
// before
Math.pow(2, 3); // => 8
// now
2 ** 3; // => 8
Array and Object destructing
解构赋值
const arr = [1, 2, 3];
// before
var a = arr[0];
var b = arr[1];
var c = arr[2];
// now
const [a, b, c] = arr;
console.log(a, b, c); // => 1, 2, 3
const obj = {
a: 1,
b: 2,
c: 3,
};
// before
var a = obj.a;
var b = obj.b;
var c = obj.c;
// now
const { a, b, c } = obj;
console.log(a, b, c); // => 1, 2, 3
Arrow Functions
更简短的函数并且不绑定 this。 箭头函数没有自己的 this,适用于那些本来需要匿名函数的地方。
// before
function log(name) {
console.log(name);
}
// now
// 当参数只有一个的时候,()是可以省略的
const fn = name => {
console.log(name);
};
箭头函数写起来更简短,但也不能到处用。有些不该用的地方使用了箭头函数,会得到不是你想要的结果
const obj = {
name: 'laohan',
getName() {
console.log(this.name);
},
};
obj.getName(); // => laohan
const obj = {
name: 'laohan',
getName: () => {
console.log(this.name);
},
};
obj.getName(); // => undefined
Object.assign()
将所有可枚举属性的值从一个或多个源对象复制到目标对象
const obj1 = { a: 1, b: 2 };
const obj2 = { c: { d: 3 } };
const obj3 = Object.assign({}, obj1, obj2);
// 这里也可以利用解构来实现
const obj4 = { ...obj1, ...obj2 };
console.log(obj3); // => {a: 1, b: 2, c: {d: 3}}
// ps: Object.assign是浅复制的
obj2.c.d = 4;
console.log(obj3); // => {a: 1, b: 2, c: {d: 4}}
Object.keys()
返回一个由一个给定对象的自身可枚举属性组成的数组。
const obj = {
a: 1,
b: 2,
c: 3,
};
const keys = Object.keys(obj);
console.log(keys); // => ['a', 'b', 'c']
Object.values()
返回一个给定对象自己的所有可枚举属性值的数组。
const obj = {
a: 1,
b: 2,
c: 3,
};
const values = Object.values(obj);
console.log(values); // => [1, 2, 3]
Object.entries()
返回一个给定对象自身可枚举属性的键值对数组
const obj = {
a: 1,
b: 2,
c: 3,
};
const entries = Object.entries(obj);
console.log(entries); // => [['a', 1], ['b', 2], ['c', 3]]
Array.prototype.includes
用于判断数组中是否包含某一个指定的值,根据情况,如果包含则返回 true,否则返回 false。
const arr = [1, 2, 3, NaN];
// before
if (arr.indexOf(3) !== -1) {
console.log(true);
}
// now
if (arr.includes(3)) {
console.log(true);
}
// ps: indexOf 对于无法判断是否包含 NaN
arr.includes(NaN); // => true
arr.indexOf(NaN); // => -1
Promise
Promise 对象用于表示一个异步操作的最终状态(完成或失败),以及其返回的值。 Promise 的出现可以解决‘回调 - 地狱’的问题,通过 then 的链式调用,使得我们的代码看起来更加美观一些。
function asyncFunc() {
return new Promise((resolve, reject) => {
...
resolve(result)
...
reject(error)
})
}
asyncFunc()
.then(result => { ··· })
.catch(error => { ··· });
这篇文章 We have a problem with promises 可以看一看,理解里面的问题就算懂了。
Map
跟 Object 类似,用于保存键值对。但还是有一些区别
- Object 的 key 只能是字符串或者 Symbols, Map 的 key 可以是任意值,包含数字、对象等
- Map 可以通过 size 获得键值对的个数,而 Object 不行
- Map 是可迭代的,而 Object 的迭代需要先获取它的键数组然后再进行迭代
const m = new Map();
const obj = {};
const fn = () => {};
m.set(obj, 'object');
m.set(fn, 'function');
m.set(1, 'number');
m.size; // => 3
m.get(obj); // => object
m.get(fn); // => function
const mm = new Map();
mm.set(1, 'a');
mm.set(2, 'b');
for (var [key, value] of mm) {
console.log(key + ' = ' + value);
}
// output
// 1 = a
// 2 = b
Set
与 Array 类似,不同的在于 Set 中的元素只会出现一次,即元素是唯一的
const s = new Set();
mySet.add(1); // Set(1) {1}
mySet.add(5); // Set(2) {1, 5}
mySet.add(5); // Set { 1, 5 }
我们经常看到数组去重的方法是利用 Set 来完成的
const arr = [1, 2, 3, 3, 4, 4, 5];
const uniArr = Array.from(new Set(arr));
console.log(uniArr); // => [1, 2, 3, 4, 5]
for...of
新的循环方式,代替之前的 for...in 和 forEach 方法。 可在可迭代对象(包括 Array,Map,Set,String,arguments 对象等等)进行迭代循环
const arr = ['a', 'b'];
for (const x of arr) {
console.log(x);
}
// Output
// a
// b
break 和 continue 也可以用于 for...of 循环
const arr = ['a', '', 'b'];
for (const x of arr) {
if (x.length === 0) break;
console.log(x);
}
// Output:
// a
如果需要在循环中同时获取 index 索引的时候,可以这么写
const arr = ['a', 'b'];
for (const [index, element] of arr.entries()) {
console.log(`${index}. ${element}`);
}
// Output:
// 0. a
// 1. b
需要注意的是,for...of 仅可用于可迭代对象
// Array-like, but not iterable!
const arrayLike = { length: 2, 0: 'a', 1: 'b' };
for (const x of arrayLike) {
console.log(x);
}
// => TypeError: arrayLike is not iterable
for (const x of Array.from(arrayLike)) {
console.log(x); // OK
}
上面的 arrayLike 虽然跟 Array 相似,有 length 属性,也有索引等,为什么没有办法像 Array 那样实现 for...of 的循环呢? 其实,Array、String 能使用 for...of 是由于其原型中有一个[Symbol.iterator]的属性,如果我们给上面的 arrayLike 也添加这个属性,那么也是可以实现 for...of 操作的。
const arrayLike = {
length: 2,
0: 'a',
1: 'b',
[Symbol.iterator]() {
let i = 0;
return {
next() {
if (i < arrayLike.length) {
return { done: false, value: arrayLike[i++] };
}
return { done: true };
},
};
},
};
for (const x of arrayLike) {
console.log(x);
}
Generator Functions
function* genFunc() {
console.log('First');
yield;
console.log('Second');
}
const genObj = genFunc();
genObj.next();
// Output: First
genObj.next();
// output: Second
function* 是一个新的'关键词',标识 generator 函数。
生成器对象是由一个 generator function 返回的,并且它符合可迭代协议和迭代器协议,主要有两点:
- 为迭代提供更高层次的抽象
- 提供新的控制流程来帮助解决“回调 - 地狱”问题。
在之前,我们是通过添加[Symbol.iterator]属性来实现可迭代的。其实还可以通过 generator 来实现
const obj = {
length: 2,
0: 'a',
1: 'b',
// generator 会返回一个 iterator
*getIterator() {
let i = 0;
while (i < this.length) {
yield this[i++];
}
},
};
for (const x of obj.getIterator()) {
console.log(x);
}
我们之前写异步函数的时候,经常都是利用 callback 的形式去完成。利用 generator,就可以有一种新的方式了。
const fs = require('fs');
const path = require('path');
function* logFiles(dir) {
for (const fileName of fs.readdirSync(dir)) {
const filePath = path.resolve(dir, fileName);
yield filePath;
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
yield* logFiles(filePath);
}
}
}
for (const p of logFiles(process.argv[2])) {
console.log(p);
}
async / await
async / await 的出现是为了帮我们解决“回调 - 地狱”的问题,让我们可以编写出看似同步的代码来实现异步调用。
async 关键词告诉 js 编译器区别对待这个函数。 当调用一个 async 函数时,会返回一个 Promise 对象,async 函数中可能也会有 await 表达式,这会使 async 函数暂停执行,等待表达式中的 Promise 解析完成后继续执行 async 函数并返回解决结果。
function getUserName() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('laohan');
}, 1000);
});
}
function getUserPhone() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1234567);
}, 1000);
});
}
async function getUserInfo() {
const name = await getUserName();
const phone = await getUserPhone();
console.log(name); // => 'laohan'
console.log(phone); // => 1234567
}
虽然使用 async 可以让我们的函数看起来像同步一样的,但对于一些新手来说,如果不小心的话,会犯一些小错误。 如上面的代码,getUserName 和 getUserPhone 两者是没有关联的,并不需要等到 getUserName 完成之后才去调用 getUserPhone,这两个是可以并行执行的。
async function getUserInfo() {
const [name, phone] = await Promise.all([getUserName(), getUserPhone()]);
console.log(name); // => 'laohan'
console.log(phone); // => 1234567
}
欢迎讨论
讨论地址 ES的那些新特性 Issue
参考资料
JavaScript Symbols, Iterators, Generators, Async/Await, and Async Iterators — All Explained Simply Here are examples of everything new in ECMAScript 2016, 2017, and 2018 Coding recipe: extracting loop functionality (via callbacks and generators)