前言
我是通過阮一峰老師的ES6教程入門的,基本上是把ES6的幾個核心特性過了一遍,但是面試官一問深我就???了,還是實際運用的太少。
本篇文章也偏總結類,結合我親身經歷的高頻面試題,建議大家必須要對箭頭函式、Promise、Generator、async等內容深入理解。
工具:Babel是一個 ES6 轉碼器,可以將 ES6 程式碼轉為 ES5 程式碼,以便相容那些還沒支援ES6的平臺。
String字串優化
新增了字串模板,在拼接大段字串時,用反斜槓取代以往的字串相加的形式,能保留所有空格和換行,使得字串拼接看起來更加直觀,更加優雅。
新增了includes()方法,用於取代傳統的只能用indexOf查詢包含字元的方法, 此外還新增了startsWith(), endsWith(), padStart(),padEnd(),repeat()等方法,可方便的用於查詢,補全字串。
Array陣列優化
陣列解構賦值: 如ES6可以直接以let [a,b,c] = [1,2,3]
形式進行變數賦值,對映關係更清晰。
擴充套件運算子:
- 可以將一個陣列轉為用逗號分隔的引數序列。
console.log(...[1, 2, 3]) // 1 2 3
複製程式碼
-
可以實現陣列的複製和解構賦值
(let a = [2,3,4]; let b = [...a])
-
可以取代arguments物件和apply方法,輕鬆獲取未知引數個數情況下的引數集合。
使用擴充套件運算子替代函式的apply方法:
// ES5 的寫法
function f(x, y, z) {
// ...
}
var args = [0, 1, 2];
f.apply(null, args);
// ES6 寫法
let args = [0, 1, 2];
f(...args);
複製程式碼
JS中遍歷陣列的方法:
- for迴圈
- forEach
myArray.forEach(function(value){
console.log(value);
})
複製程式碼
無法中途跳出forEach迴圈,break命令或return命令都不能奏效。
for... in
for…in主要是為遍歷物件而設計的,不適用於遍歷陣列。
遍歷陣列的缺點:
- 陣列的鍵名是數字,但是for…in迴圈是以字串作為鍵名的。
- 某些情況下,for…in迴圈會以任意順序遍歷鍵名。
for...of
(ES6)
for(let value of myArray){
console.log(value);
}
複製程式碼
- 不同用於forEach方法,它可以與break、continue和return配合使用。
- 提供了遍歷所有資料結構的統一操作介面。
for of 和 for in 的總結:
- 推薦在迴圈物件屬性的時候,使用for...in, 在遍歷陣列的時候的時候使用for...of。
- for...in迴圈出的是key,for...of迴圈出的是value
- 注意,for...of是ES6新引入的特性。修復了ES5引入的for...in的不足
- for...of不能迴圈普通的物件,需要通過和Object.keys()搭配使用
Object型別優化
- 物件屬性變數式宣告:
ES6可以直接以變數形式宣告物件屬性或者方法。比傳統的鍵值對形式宣告更加簡潔,更加方便,語義更加清晰。
let [apple, orange] = ['red appe', 'yellow orange'];
let myFruits = {apple, orange};
// let myFruits = {apple: 'red appe', orange: 'yellow orange'};
複製程式碼
- 物件的擴充套件運算子(...)
可將一個陣列轉為用逗號分隔的引數序列,主要用於函式呼叫。
console.log(...[1, 2, 3]) // 1 2 3
- super 關鍵字:
ES6在Class類裡新增了類似this的關鍵字super。同this總是指向當前函式所在的物件不同,super關鍵字總是指向當前函式所在物件的原型物件。
箭頭函式
基本使用:
如果 return 值就只有一行表示式,可以省去 return,預設表示該行是返回值,否則需要加一個大括號和 return。如果引數只有一個,也可以省去括號,兩個則需要加上括號。
var f = v => v*2;
// 等價於
var f = function(v){
return v*2;
}
// 判斷偶數
var isEven = n => n % 2 == 0;
// 需要加 return
var = (a, b) => {
if(a >= b)
return a;
return b;
}
複製程式碼
ES6 引入 rest 引數(形式為...變數名),用於獲取函式的多餘引數,這樣就不需要使用arguments物件了。
rest 引數搭配的變數是一個陣列,該變數將多餘的引數放入陣列中。
//利用 rest 引數,可以向該函式傳入任意數目的引數。
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
複製程式碼
面試問題:箭頭函式和普通函式的區別
- 箭頭函式沒有自己的this物件
函式中的 this 始終是指向函式執行時所在的物件。比如全域性函式執行時,this 指向 window,物件的方法執行時,this 指向該物件,這就是函式 this 的可變。
而箭頭函式中的 this 是固定的,箭頭函式繼承自己作用域的上一層的this,就是上一級外部函式的 this 的指向。任何方法都改變不了其指向,如call(), bind(), apply()。
一個例子:
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
var id = 21;
foo.call({ id: 42 }); // id: 42
複製程式碼
執行的結果是 42 而不是全域性的 21,表示 setTimeout 函式執行的時候,this 指向的不是 window。因此箭頭函式很好地解決了匿名函式和setTimeout
和setInterval
的this指向問題,不用再去給其用that變數儲存this。
對箭頭函式中關於 this 的總結:在物件的方法中直接使用箭頭函式,會指向 window,其他箭頭函式 this 會指向上一層的 this,箭頭函式並沒有儲存 this。
var obj = {
id: 1,
foo: ()=>{
return this.id;
}
}
var id = 2;
obj.foo(); // 2
複製程式碼
- 箭頭函式不能當做建構函式,不能使用new,因為它沒有自己的this,無法例項化。
- 箭頭函式不繫結
arguments
, 取而代之用rest引數(形式為...變數名)。也沒有super
、new.target
。 - 不可以使用
yield
命令,箭頭函式不可用作Generator
函式。 - 箭頭函式沒有原型屬性。
Set 和 Map
- Set
ES6引入的一種類似Array的新的資料結構,Set例項的成員類似於陣列item成員,區別是Set例項的成員都是唯一,不重複的。這個特性可以輕鬆地實現陣列去重。
Set本身是一個建構函式,用來生成 Set 資料結構。
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
console.log(i);
}
// 2 3 5 4
複製程式碼
- Map
JavaScript 的物件(Object),本質上是鍵值對的集合,但是傳統上只能用字串當作鍵。這給它的使用帶來了很大的限制。
Map
是ES6引入的一種類似Object的新的資料結構,也是鍵值對的集合,但是“鍵”的範圍不限於字串,各種型別的值(包括物件)都可以當作鍵。
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
複製程式碼
let 和 const
沒有塊級作用域回來帶很多難以理解的問題,比如for迴圈var變數洩露,變數覆蓋等問題。
let
宣告的變數擁有自己的塊級作用域,形如for (let x...)的迴圈在每次迭代時都為x建立新的繫結。且修復了var宣告變數帶來的變數提升問題。(必須宣告 'use strict' 後才能使用let宣告變數,否則瀏覽並不能顯示結果)
“變數提升”現象:即變數可以在宣告之前使用,值為undefined。為了糾正這種現象,let命令改變了語法行為,它所宣告的變數一定要在宣告後使用,否則報錯。
ES5 只有全域性作用域和函式作用域,沒有塊級作用域。
塊級作用域的出現,實際上使得獲得廣泛應用的立即執行函式表示式(IIFE)不再必要了。
// IIFE 寫法
(function () {
var tmp = ...;
...
}());
// 塊級作用域寫法
{
let tmp = ...;
...
}
複製程式碼
const
宣告一個只讀的常量。一旦宣告,常量的值就不能改變。
const 宣告的變數不得改變值,這意味著,const一旦宣告變數,就必須立即初始化。
const的作用域與let命令相同:只在宣告所在的塊級作用域內有效。
總結: 使用var宣告的變數,其作用域為該語句所在的函式內,且存在變數提升現象; 使用let宣告的變數,其作用域為該語句所在的程式碼塊內,不存在變數提升; 使用const宣告的是常量,在後面出現的程式碼中不能再修改該常量的值。
Promise
主要作用是用來解決JS回撥機制產生的“回撥地獄”。 回撥地獄帶來的負面作用有以下幾點:
- 程式碼臃腫, 可讀性差, 複用性差, 容易滋生 bug。
- 耦合度過高,可維護性差。
- 只能在回撥裡處理異常。
Promise它不是新的語法功能,而是一種新的寫法,將回撥函式的巢狀,改成鏈式呼叫。
new Promise(請求1)
.then(請求2(請求結果1))
.then(請求3(請求結果2))
.catch(處理異常(異常資訊))
複製程式碼
Promise 使用總結:
- 可以通過兩種方式初始化一個 Promise 物件,都會返回一個 Promise 物件。
- new Promise(fn)
- Promise.resolve(fn)
-
然後呼叫上一步返回的 promise 物件的 then 方法,註冊回撥函式。 then 中的回撥函式可以有一個引數,也可以不帶引數。如果 then 中的回撥函式依賴上一步的返回結果,那麼要帶上引數。
-
最後註冊 catch 異常處理函式,處理前面回撥中可能丟擲的異常。
簡單例子:
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done');
});
}
timeout(100).then((value) => {
console.log(value);
});
複製程式碼
timeout方法返回一個Promise例項,表示一段時間以後才會發生的結果。過了指定的時間(ms引數)以後,Promise例項的狀態變為resolved,就會觸發then方法繫結的回撥函式。
一些常用API:
Promise.race
類方法,多個 Promise 任務同時執行,返回最先執行結束的 Promise 任務的結果,不管這個 Promise 結果是成功還是失敗。
Promise.all
類方法,多個 Promise 任務同時執行。 如果全部成功執行,則以陣列的方式返回所有 Promise 任務的執行結果。 如果有一個 Promise 任務 rejected,則只返回 rejected 任務的結果。
如果後續任務是非同步任務的話,必須return 一個 新的 promise 物件。如果後續任務是同步任務,只需 return 一個結果即可。
new Promise(買菜)
//用買好的菜做飯
.then((買好的菜)=>{
return new Promise(做飯);
})
複製程式碼
一個 Promise 物件有三個狀態,並且狀態一旦改變,便不能再被更改為其他狀態:
- pending,非同步任務正在進行。
- resolved (也可以叫fulfilled),非同步任務執行成功。
- rejected,非同步任務執行失敗。
generator 以及 async/await 語法使非同步處理更加接近同步程式碼寫法,可讀性更好,同時異常捕獲和同步程式碼的書寫趨於一致。
(async ()=>{
let 蔬菜 = await 買菜();
let 飯菜 = await 做飯(蔬菜);
let 送飯結果 = await 送飯(飯菜);
let 通知結果 = await 通知我(送飯結果);
})();
複製程式碼
Generator
Generator 函式會返回一個遍歷器物件,可以依次遍歷 Generator 函式內部的每一個狀態。
形式上,Generator 函式是一個普通函式,但是有兩個特徵。
- function關鍵字與函式名之間有一個星號;
- 函式體內部使用yield表示式,定義不同的內部狀態。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
複製程式碼
Generator 函式的呼叫方法與普通函式一樣,也是在函式名後面加上一對圓括號。不同的是,呼叫 Generator 函式後,該函式並不執行,返回的也不是函式執行結果,而是一個指向內部狀態的指標物件,也就是上一章介紹的遍歷器物件。
下一步,必須呼叫遍歷器物件的next方法,使得指標移向下一個狀態。也就是說,每次呼叫next方法,內部指標就從函式頭部或上一次停下來的地方開始執行,直到遇到下一個yield表示式(或return語句)為止。換言之,Generator 函式是分段執行的,yield表示式是暫停執行的標記,而next方法可以恢復執行。
async和await
ES2017 標準引入了 async 函式,使得非同步操作變得更加方便。
async 函式就是 Generator 函式的語法糖。
async函式就是將 Generator 函式的星號(*)替換成async,將yield替換成await。並且返回一個
Promise
,可以使用then方法新增回撥函式。 當函式執行的時候,一旦遇到await就會先返回,等到非同步操作完成,再接著執行函式體內後面的語句。
async函式對 Generator 函式的改進:
- 內建執行器 async函式自帶執行器,不像 Generator 函式,需要呼叫next方法,或者用co模組,才能真正執行。
- 更好的語義
- 更好的適用性
- 返回值是 Promise 進一步說,async函式完全可以看作多個非同步操作,包裝成的一個 Promise 物件,而await命令就是內部then命令的語法糖。
例子:getJSON函式返回一個promise,這個promise成功resolve時會返回一個json物件。我們只是呼叫這個函式,列印返回的JSON物件,然後返回”done”。
// promise
const makeRequest = () =>
getJSON()
.then(data => {
console.log(data)
return "done"
})
makeRequest()
複製程式碼
//使用Async/Await
const makeRequest = async () => {
console.log(await getJSON())
return "done"
}
makeRequest()
//async函式會隱式地返回一個promise,該promise的reosolve值就是函式return的值。(示例中reosolve值就是字串”done”)
複製程式碼
Async的優缺點:
優勢: 處理 then 的呼叫鏈能夠更清晰準確的寫出程式碼。
缺點: 濫用 await 可能會導致效能問題,因為 await 會阻塞程式碼,也許之後的非同步程式碼並不依賴於前者,但仍然需要等待前者完成,導致程式碼失去了併發性。
Iterator
是ES6中一個很重要概念,它並不是物件,也不是任何一種資料型別。為Set、Map、Array、Object新增一個統一的遍歷API。部署了Iterator介面的物件(可遍歷物件)都可以通過for...of
去遍歷。
class
ES6 的class可以看作只是一個語法糖,它的絕大部分功能,ES5 都可以做到,新的class寫法只是讓物件原型的寫法更加清晰、更像物件導向程式設計的語法而已,可以看做是建構函式的另一種寫法。
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 2);
//ES6的class改寫
class Point {
//構造方法
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
複製程式碼
上面程式碼定義了一個“類”,可以看到裡面有一個constructor方法,這就是構造方法,而this關鍵字則代表例項物件。也就是說,ES5 的建構函式Point,對應 ES6 的Point類的構造方法。
Point類除了構造方法,還定義了一個toString方法。注意,定義“類”的方法的時候,前面不需要加上function這個關鍵字,直接把函式定義放進去了就可以了。另外,方法之間不需要逗號分隔,加了會報錯。
class實現繼承: Class 可以通過extends關鍵字實現繼承,這比 ES5 的通過修改原型鏈實現繼承,要清晰和方便很多。
總結
日常前端程式碼開發中,有哪些值得用ES6去改進的程式設計優化或者規範:
- 常用箭頭函式來取代
var self = this;
的做法。 - 常用let取代var命令。
- 常用陣列/物件的結構賦值來命名變數,結構更清晰,語義更明確,可讀性更好。
- 在長字串多變數組合場合,用模板字串來取代字串累加,能取得更好地效果和閱讀體驗。
- 用Class類取代傳統的建構函式,來生成例項化物件。
- 在大型應用開發中,要保持module模組化開發思維,分清模組之間的關係,常用import、export方法。