前段時間換工作,面試了幾家公司,有一道題發現基本是必問的,就是說一說平時用到的那些 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)