快速掌握es6+新特性及es6核心語法盤點

徐小夕發表於2019-10-06

首先先祝各位國慶快樂,好好去體驗生活的快樂,也祝祖國生日快樂,越變越強大,越來越繁榮。

快速掌握es6+新特性及es6核心語法盤點
接下來我會總結一些工作中常用也比較核心的es6+的語法知識,後面又要慢慢開始工作之旅了,希望在總結自己經驗的過程中大家會有所收穫~

正文

1. let和const

let

用法類似於var,但是所宣告的變數,只在let命令所在的程式碼塊內有效,即let宣告的是一個塊作用域內的變數。

特點:

  • 不存在變數提升
  • 暫時性死區——只要塊級作用域記憶體在let命令,它所宣告的變數就“繫結”(binding)這個區域,不再受外部的影響
  • 不允許重複宣告
  • 塊級作用域——被{}包裹的,外部不能訪問內部

應用案例與分析:

// 使用var
for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  });
}   // => 5 5 5 5 5

// 使用let
for (let i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  });
}   // => 0 1 2 3 4
複製程式碼

上面使用let的程式碼中,變數i是let宣告的,當前的i只在本輪迴圈有效,所以每一次迴圈的i其實都是一個新的變數,JavaScript 引擎內部會記住上一輪迴圈的值,初始化本輪的變數i時,就在上一輪迴圈的基礎上進行計算所以最後能正常輸出i的值。

注意:

  1. for迴圈還有一個特別之處,就是設定迴圈變數的那部分是一個父作用域,而迴圈體內部是一個單獨的子作用域,所以我們可以在迴圈體內部訪問到i的值。
  2. let和var全域性宣告時,var可以通過window的屬性訪問而let不能。

const

const宣告一個只讀的常量。一旦宣告,常量的值就不能改變。const實際上保證的是變數指向的那個記憶體地址所儲存的資料不得改動。對於簡單型別的資料(數值、字串、布林值),值就儲存在變數指向的那個記憶體地址,因此等同於常量。但對於複合型別的資料(主要是物件和陣列),變數指向的記憶體地址,儲存的只是一個指向實際資料的指標,const只能保證這個指標是固定的,至於它指向的資料結構是不是可變的,就完全不能控制了。因此,將一個物件宣告為常量必須非常小心。

因此,我們使用const時,不能只宣告而不初始化值,否則會報錯:

const a;
// SyntaxError: Missing initializer in const declaration
複製程式碼

const的其他特性和let很像,一般推薦用它來宣告常量,並且常量名大寫。

2. 數值的擴充套件

ES6 在Number物件上,新提供了Number.isFinite()和Number.isNaN()兩個方法。

Number.isFinite()

Number.isFinite()用來檢查一個數值是否為有限的(finite),即不是Infinity。 注意,如果引數型別不是數值,Number.isFinite一律返回false。

Number.isFinite(0.8); // true
Number.isFinite(NaN); // false
Number.isFinite(Infinity); // false
Number.isFinite('hello');  // false
Number.isFinite(true);  // false
複製程式碼

Number.isNaN()

用來檢查一個值是否為NaN,如果引數型別不是NaN,Number.isNaN一律返回false

Number.isFinite和Number.isNaN與傳統的全域性方法isFinite()和isNaN()的區別在於,傳統方法先呼叫Number()將非數值的值轉為數值,再進行判斷,而這兩個新方法只對數值有效,Number.isFinite()對於非數值一律返回false, Number.isNaN()只有對於NaN才返回true,非NaN一律返回false。

isFinite(11) // true
isFinite("11") // true
Number.isFinite(11) // true
Number.isFinite("11") // false

isNaN(NaN) // true
isNaN("NaN") // true
Number.isNaN(NaN) // true
Number.isNaN("NaN") // false
Number.isNaN(10) // false
複製程式碼

Number.parseInt(), Number.parseFloat()

ES6 將全域性方法parseInt()和parseFloat(),移植到Number物件上面,行為完全保持不變,這樣做的目的,是逐步減少全域性性方法,使得語言逐步模組化

Number.isInteger()

Number.isInteger()用來判斷一個數值是否為整數;JavaScript 內部,整數和浮點數採用的是同樣的儲存方法,所以 25 和 25.0 被視為同一個值;如果引數不是數值,Number.isInteger返回false;由於 JavaScript 數值儲存為64位雙精度格式,數值精度最多可以達到 53 個二進位制位,如果數值的精度超過這個限度,第54位及後面的位就會被丟棄,這種情況下,Number.isInteger可能會誤判。

Number.isInteger(15) // true
Number.isInteger(15.1) // false
Number.isInteger(15.0) // true
Number.isInteger('10') // false
Number.isInteger(true) // false
// 超出精度範圍會誤判
Number.isInteger(5.0000000000000002) // true
複製程式碼

Math.trunc()

Math.trunc方法用於去除一個數的小數部分,返回整數部分;對於非數值,Math.trunc內部使用Number方法將其先轉為數值;對於空值和無法擷取整數的值,返回NaN

Math.trunc(2.1) // 2
Math.trunc(-2.9) // -2
Math.trunc(-0.1254) // -0
Math.trunc('125.456') // 125
Math.trunc(true) //1
Math.trunc(false) // 0
Math.trunc(null) // 0
Math.trunc(NaN);      // NaN
Math.trunc('bar');    // NaN
Math.trunc();         // NaN
Math.trunc(undefined) // NaN
複製程式碼

Math.cbrt()

Math.cbrt方法用於計算一個數的立方根;對於非數值,Math.cbrt方法內部也是先使用Number方法將其轉為數值

Math.cbrt(-1) // -1
Math.cbrt(0)  // 0
Math.cbrt(8)  // 2
Math.cbrt('8') // 2
Math.cbrt('hello') // NaN
複製程式碼

Math.hypot()

Math.hypot方法返回所有引數的平方和的平方根

Math.hypot(3, 4);        // 5
Math.hypot(3, 4, 5);     // 7.0710678118654755
Math.hypot();            // 0
Math.hypot(NaN);         // NaN
Math.hypot(3, 4, 'foo'); // NaN
Math.hypot(3, 4, '5');   // 7.0710678118654755
Math.hypot(-3); 
複製程式碼

有了這個api,我們算一個n維勾股定理是不是很方便了呢?

指數運算子

ES2016 新增了一個指數運算子(**)。這個運算子的一個特點是右結合,而不是常見的左結合。多個指數運算子連用時,是從最右邊開始計算的。

// 相當於 2 ** (3 ** 2)
2 ** 3 ** 2  // 512
// => Math.pow(2, Math.pow(3,2))
複製程式碼

es6+還擴充套件了更多的數值api,感興趣的可以自己去學習研究。

3. 陣列的擴充套件

擴充套件運算子

擴充套件運算子(spread)是三個點(...),將一個陣列轉為用逗號分隔的引數序列

應用:

  1. 複製陣列
const a1 = [1, 2];
const a2 = [...a1];
複製程式碼
  1. 合併陣列
const arr1 = ['1', '2'];
const arr2 = ['c', {a:1} ];

// ES6 的合併陣列
[...arr1, ...arr2]
複製程式碼

注:這兩種方法都是淺拷貝,使用的時候需要注意。

  1. 將字串轉化為陣列

使用擴充套件運算子能夠正確識別四個位元組的 Unicode 字元。凡是涉及到操作四個位元組的 Unicode 字元的函式,都有這個問題。因此,最好都用擴充套件運算子改寫。

[...'xuxi']
// [ "x", "u", "x", "i" ]
複製程式碼
  1. 實現了 Iterator 介面的物件
let nodeList = document.querySelectorAll('div');
let arr = [...nodeList];
複製程式碼

上面程式碼中,querySelectorAll方法返回的是一個NodeList物件。它不是陣列,而是一個類似陣列的物件。擴充套件運算子可以將其轉為真正的陣列,原因就在於NodeList物件實現了 Iterator 。

Array.from()

Array.from方法用於將類物件轉為真正的陣列:類似陣列的物件和可遍歷的物件(包括 ES6 新增的資料結構 Set 和 Map)。

實際應用中我們更多的是將Array.from用於DOM 操作返回的 NodeList 集合,以及函式內部的arguments物件。

// NodeList物件
let nodeList = document.querySelectorAll('p')
let arr = Array.from(nodeList)

// arguments物件
function say() {
  let args = Array.from(arguments);
}
複製程式碼

Array.from還可以接受第二個引數,作用類似於陣列的map方法,用來對每個元素進行處理,將處理後的值放入返回的陣列。

Array.from([1, 2, 4], (x) => x + 1)
// [2, 3, 5]
複製程式碼

Array.of()

Array.of方法用於將一組值,轉換為陣列。Array.of基本上可以用來替代Array()或new Array(),並且不存在由於引數不同而導致的過載。它的行為非常統一。

Array.of() // []
Array.of(undefined) // [undefined]
Array.of(2) // [2]
Array.of(21, 2) // [21, 2]
複製程式碼

陣列例項的 copyWithin()

陣列例項的copyWithin()方法,在當前陣列內部,將指定位置的成員複製到其他位置(會覆蓋原有成員),然後返回當前陣列。也就是說,使用這個方法,會修改當前陣列。

它接受三個引數:

  • target(必需):從該位置開始替換資料。如果為負值,表示倒數。
  • start(可選):從該位置開始讀取資料,預設為 0。如果為負值,表示從末尾開始計算。
  • end(可選):到該位置前停止讀取資料,預設等於陣列長度。如果為負值,表示從末尾開始計算。
[11, 21, 31, 41, 51].copyWithin(0, 3)  // => [41, 51, 31, 41, 51]
// 將3號位複製到0號位
[1, 2, 3, 4, 5].copyWithin(0, 3, 4)
複製程式碼

陣列例項的 find() 和 findIndex()

陣列例項的find方法用於找出第一個符合條件的陣列成員。它的引數是一個回撥函式,所有陣列成員依次執行該回撥函式,直到找出第一個返回值為true的成員,然後返回該成員。如果沒有符合條件的成員,則返回undefined。陣列例項的findIndex方法的用法與find方法非常類似,返回第一個符合條件的陣列成員的位置,如果所有成員都不符合條件,則返回-1。這兩個方法都可以接受第二個引數,用來繫結回撥函式的this物件。

// find
[1, 5, 8, 12].find(function(value, index, arr) {
  return value > 9;
}) // 12

// findIndex
[1, 5, 5, 15].findIndex(function(value, index, arr) {
  return value > 9;
}) // 3

// 第二個引數
function f(v){
  return v > this.age;
}
let person = {name: 'John', age: 20};
[10, 12, 26, 15].find(f, person);    // 26
複製程式碼

陣列例項的 fill()

fill方法使用給定值,填充一個陣列。fill方法還可以接受第二個和第三個引數,用於指定填充的起始位置和結束位置。注意,如果填充的型別為物件,那麼被賦值的是同一個記憶體地址的物件,而不是深拷貝物件。

new Array(3).fill(7)
// [7, 7, 7]

['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']

// 填充引用型別
let arr = new Array(2).fill({name: "xuxi"});
arr[0].name = "xu";
arr
// [{name: "xu"}, {name: "xu"}]

let arr = new Array(2).fill([]);
arr[0].push(1);
arr
// [[1], [1]]
複製程式碼

陣列例項的 includes()

Array.prototype.includes方法返回一個布林值,表示某個陣列是否包含給定的值。該方法的第二個參數列示搜尋的起始位置,預設為0。如果第二個引數為負數,則表示倒數的位置,如果這時它大於陣列長度(比如第二個引數為-4,但陣列長度為3),則會重置為從0開始。

[1, 4, 3].includes(3)     // true
[1, 2, 4].includes(3)     // false
[1, 5, NaN, 6].includes(NaN) // true
複製程式碼

陣列例項的 flat(),flatMap()

flat()用於將巢狀的陣列“拉平”,變成一維的陣列。該方法返回一個新陣列,對原資料沒有影響。flat()預設只會“拉平”一層,如果想要“拉平”多層的巢狀陣列,可以將flat()方法的引數寫成一個整數,表示想要拉平的層數,預設為1。如果不管有多少層巢狀,都要轉成一維陣列,可以用Infinity關鍵字作為引數。flatMap()方法對原陣列的每個成員執行一個函式,然後對返回值組成的陣列執行flat()方法。該方法返回一個新陣列,不改變原陣列。flatMap()只能展開一層陣列。flatMap()方法還可以有第二個引數,用來繫結遍歷函式裡面的this。

[1, 2, [3, [4, 5]]].flat()
// [1, 2, 3, [4, 5]]

[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]

[1, [2, [3]]].flat(Infinity)
// [1, 2, 3]

// 如果原陣列有空位,flat()方法會跳過空位
[1, 2, , 4, 5].flat()
// [1, 2, 4, 5]

// flatMap
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]
複製程式碼

4. 函式的擴充套件

函式引數的預設值

function say(name = 'xuxi') {
    alert(name)
}
複製程式碼

注意點:

  • 引數變數是預設宣告的,所以不能用let或const再次宣告
  • 使用引數預設值時,函式不能有同名引數
  • 引數預設值不是傳值的,而是每次都重新計算預設值表示式的值。也就是說,引數預設值是惰性求值的
  • 引數如果傳入undefined,將觸發該引數等於預設值,null則沒有這個效果。 關鍵點

函式的 length 屬性

指定了預設值以後,函式的length屬性,將返回沒有指定預設值的引數個數。也就是說,指定了預設值後,length屬性將失真;如果設定了預設值的引數不是尾引數,那麼length屬性也不再計入後面的引數了。

(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
// 引數不是尾引數
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
複製程式碼

作用域

一旦設定了引數的預設值,函式進行宣告初始化時,引數會形成一個單獨的作用域。等到初始化結束,這個作用域就會消失。在不設定引數預設值時,是不會出現的。

箭頭函式

由於箭頭函式的用法比較簡單,我們來看看注意點:

  • 函式體內的this物件,就是定義時所在的物件,而不是使用時所在的物件。

  • 不可以當作建構函式,也就是說,不可以使用new命令,否則會丟擲一個錯誤。

  • 不可以使用arguments物件,該物件在函式體內不存在。如果要用,可以用 rest 引數代替。

  • 不可以使用yield命令,因此箭頭函式不能用作 Generator 函式。

不適合場景:

// 定義物件的方法,且該方法內部包括this,
// 因為物件不構成單獨的作用域,導致say箭頭函式定義時的作用域就是全域性作用域
const person = {
  year: 9,
  say: () => {
    this.year--
  }
}

// 需要動態this的時候,也不應使用箭頭函式
// 程式碼執行時,點選按鈕會報錯,因為button的監聽函式是一個箭頭函式,導致裡面的this是全域性物件
var btn = document.getElementById('btn');
btn.addEventListener('click', () => {
  this.classList.add('on');
});
複製程式碼

5. 物件的擴充套件

物件的擴充套件運算子

物件的擴充套件運算子(...)用於取出引數物件的所有可遍歷屬性,拷貝到當前物件之中;等同於使用Object.assign()方法

let a = {w: 'xu', y: 'xi'}
let b = {name: '12'}
let ab = { ...a, ...b };
// 等同於
let ab = Object.assign({}, a, b);
複製程式碼

Object.is()

用來比較兩個值是否嚴格相等,與嚴格比較運算子(===)的行為基本一致;不同之處只有兩個:一是+0不等於-0,二是NaN等於自身。

+0 === -0 //true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
複製程式碼

Object.assign()

用於物件的合併,將源物件的所有可列舉屬性,複製到目標物件; 如果只有一個引數,Object.assign會直接返回該引數; 由於undefined和null無法轉成物件,所以如果它們作為引數,就會報錯; 其他型別的值(即數值、字串和布林值)不在首引數,也不會報錯。但是,除了字串會以陣列形式,拷貝入目標物件,其他值都不會產生效果。

// 合併物件
const target = { a: 1, b: 1 };

const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

// 非物件和字串的型別將忽略
const a1 = '123';
const a2 = true;
const a3 = 10;

const obj = Object.assign({}, a1, a2, a3);
console.log(obj); // { "0": "1", "1": "2", "2": "3" }
複製程式碼

注意點:

  • Object.assign方法實行的是淺拷貝,而不是深拷貝。也就是說,如果源物件某個屬性的值是物件,那麼目標物件拷貝得到的是這個物件的引用
  • 對於巢狀的物件,遇到同名屬性,Object.assign的處理方法是替換,而不是新增
  • Object.assign可以用來處理陣列,但是會把陣列視為物件
Object.assign([1, 2, 3], [4, 5])
// [4, 5, 3]
複製程式碼
  • Object.assign只能進行值的複製,如果要複製的值是一個取值函式,那麼將求值後再複製
const a = {
  get num() { return 1 }
};
const target = {};

Object.assign(target, a)
// { num: 1 }
複製程式碼

應用場景:

  • 為物件新增屬性和方法
  • 克隆/合併物件
  • 為屬性指定預設值

Object.keys()

返回一個陣列,成員是引數物件自身的(不含繼承的)所有可遍歷屬性的鍵名

Object.values()

返回一個陣列,成員是引數物件自身的(不含繼承的)所有可遍歷屬性的鍵值。注意:返回陣列的成員順序:如果屬性名為數值的屬性,是按照數值大小,從小到大遍歷的

const obj = { 100: '1', 2: '2', 7: '3' };
Object.values(obj)
// ["2", "3", "1"]
複製程式碼

Object.entries()

返回一個陣列,成員是引數物件自身的(不含繼承的)所有可遍歷屬性的鍵值對陣列;如果原物件的屬性名是一個 Symbol 值,該屬性會被忽略

const obj = { a: '1', b: 1, [Symbol()]: 123 };
Object.entries(obj)
// [ ["a", "1"], ["b", 1] ]
複製程式碼

Object.fromEntries()

Object.fromEntries()方法是Object.entries()的逆操作,用於將一個鍵值對陣列轉為物件

Object.fromEntries([
  ['a', '1'],
  ['b', 2]
])
// { a: "1", b: 2 }
複製程式碼

應用場景:

  • 將鍵值對的資料結構還原為物件,因此特別適合將 Map 結構轉為物件
  • 配合URLSearchParams物件,將查詢字串轉為物件
Object.fromEntries(new URLSearchParams('name=xuxi&year=24'))
// { name: "xuxi", year: "24" }
複製程式碼

6. symbol

ES6 引入了一種新的原始資料型別Symbol,表示唯一的值。它是 JavaScript 語言的第七種資料型別,前六種是:undefined、null、布林值(Boolean)、字串(String)、數值(Number)、物件(Object)。Symbol 值通過Symbol函式生成。凡是屬性名屬於 Symbol 型別,就都是獨一無二的,可以保證不會與其他屬性名產生衝突。

注意點:

  • Symbol函式前不能使用new命令,否則會報錯
  • 由於 Symbol 值不是物件,所以不能新增屬性。本質上,它是一種類似於字串的資料型別
  • Symbol函式可以接受一個字串作為引數,表示對 Symbol 例項的描述,方便區分
  • Symbol函式的引數只是表示對當前 Symbol 值的描述,因此相同引數的Symbol函式的返回值是不相等的
  • Symbol 值不能與其他型別的值進行運算,會報錯
  • Symbol 值作為物件屬性名時,不能用點運算子
  • 在物件的內部,使用 Symbol 值定義屬性時,Symbol 值必須放在方括號之中
  • Symbol 值作為屬性名時,該屬性還是公開屬性,不是私有屬性
  • Symbol 作為屬性名時屬性不會出現在for...in、for...of迴圈中,也不會被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。但是,它也不是私有屬性,使用Object.getOwnPropertySymbols方法可以獲取指定物件的所有 Symbol 屬性名。

Symbol.for(),Symbol.keyFor()

Symbol.for()接受一個字串作為引數,然後搜尋有沒有以該引數作為名稱的 Symbol 值。如果有,就返回這個 Symbol 值,否則就新建並返回一個以該字串為名稱的 Symbol 值。Symbol.keyFor方法返回一個已登記的 Symbol 型別值的key

let a1 = Symbol.for('123');
let a2 = Symbol.for('123');

a1 === a2 // true

// Symbol.for()與Symbol()這兩種寫法,都會生成新的Symbol。
// 它們的區別是,前者會被登記在全域性環境中供搜尋,後者不會
Symbol.keyFor(a1) // "123"
let c2 = Symbol("f");
Symbol.keyFor(c2) // undefined
複製程式碼

7. set和map資料結構

set

ES6提供了新的資料結構Set,類似於陣列,但是成員的值都是唯一的,沒有重複的值。Set本身是一個建構函式,用來生成Set資料結構。

例項屬性和方法:

  • add(value):新增某個值,返回Set結構本身。
  • delete(value):刪除某個值,返回一個布林值,表示刪除是否成功。
  • has(value):返回一個布林值,表示該值是否為Set的成員。
  • clear():清除所有成員,沒有返回值。
s.add(1).add(3).add(3);
// 注意3被加入了兩次

s.size // 2

s.has(1) // true
s.has(2) // false

s.delete(3);
s.has(3) // false
複製程式碼

遍歷操作:

  • keys():返回鍵名的遍歷器
  • values():返回鍵值的遍歷器
  • entries():返回鍵值對的遍歷器
  • forEach():使用回撥函式遍歷每個成員

Set的遍歷順序就是插入順序,這個特性有時非常有用,比如使用Set儲存一個回撥函式列表,呼叫時就能保證按照新增順序呼叫。

應用場景:

// 陣列去重
let arr = [1223];
let unique = [...new Set(arr)];
// or
function dedupe(array) {
  return Array.from(new Set(array));
}

let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);

// 並集
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}

// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
// set {2, 3}

// 差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}
複製程式碼

map

類似於物件,也是鍵值對的集合,各種型別的值(包括物件)都可以當作鍵。Map結構提供了“值—值”的對應,是一種更完善的Hash結構實現。

例項屬性和方法:

  • size屬性: 返回Map結構的成員總數
  • set(key, value): set方法設定key所對應的鍵值,然後返回整個Map結構。如果key已經有值,則鍵值會被更新,否則就新生成該鍵,set方法返回的是Map本身,因此可以採用鏈式寫法
  • get(key) : get方法讀取key對應的鍵值,如果找不到key,返回undefined
  • has(key) : has方法返回一個布林值,表示某個鍵是否在Map資料結構中
  • delete(key) : delete方法刪除某個鍵,返回true。如果刪除失敗,返回false
  • clear() : clear方法清除所有成員,沒有返回值

遍歷方法和set類似,Map結構轉為陣列結構,比較快速的方法是結合使用擴充套件運算子(...):

let map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

[...map.keys()]
// [1, 2, 3]

[...map.values()]
// ['one', 'two', 'three']

[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]

[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]
複製程式碼

陣列轉map:

new Map([[true, 7], [{foo: 3}, ['abc']]])
// Map {true => 7, Object {foo: 3} => ['abc']}
複製程式碼

Map轉為物件:

function strMapToObj(strMap) {
  let obj = Object.create(null);
  for (let [k,v] of strMap) {
    obj[k] = v;
  }
  return obj;
}

let myMap = new Map().set('yes', true).set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }
複製程式碼

物件轉為Map

function objToStrMap(obj) {
  let strMap = new Map();
  for (let k of Object.keys(obj)) {
    strMap.set(k, obj[k]);
  }
  return strMap;
}

objToStrMap({yes: true, no: false})
// [ [ 'yes', true ], [ 'no', false ] ]
複製程式碼

8. Proxy 和 Reflect

關於Proxy 和 Reflect的介紹,我會單獨寫個完整的模組來介紹及其應用。

9. promise物件

Promise是非同步程式設計的一種解決方案,簡單說就是一個容器,裡面儲存著某個未來才會結束的事件的結果。從語法上說,Promise是一個物件,從它可以獲取非同步操作的訊息。

特點:

  • 物件的狀態不受外界影響。Promise物件代表一個非同步操作,有三種狀態:Pending(進行中)、Resolved(已完成,又稱Fulfilled)和Rejected(已失敗)。只有非同步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。
  • 一旦狀態改變,就不會再變,任何時候都可以得到這個結果。整個過程不可逆。

使用:

  • 基本用法
// 實現非同步載入圖片
function loadImageAsync(url) {
 return new Promise(function(resolve, reject) {
   var image = new Image();

   image.onload = function() {
     resolve(image);
   };

   image.onerror = function() {
     reject(new Error('圖片載入失敗'));
   };

   image.src = url;
 });
}
// 使用
loadImageAsync('http://xxxx/api').then((data) => {
   // some code
}).catch(err => console.log(err))
複製程式碼

resolve函式將Promise物件的狀態從“未完成”變為“成功”,即Pending => Resolved,並將非同步操作的結果作為引數傳遞出去;reject函式將Promise物件的狀態從“未完成”變為“失敗”,即Pending => Rejected,並將非同步操作報出的錯誤作為引數傳遞出去。

Promise例項生成以後,可以用then方法分別指定Resolved狀態和Reject狀態的回撥函式。then方法的第一個引數是Resolved狀態的回撥函式,第二個引數(可選)是Rejected狀態的回撥函式。

then方法返回的是一個新的Promise例項(注意,不是原來那個Promise例項)。因此可以採用鏈式寫法,即then方法後面再呼叫另一個then方法。catch方法是.then(null, rejection)的別名,用於指定發生錯誤時的回撥函式。

  • Promise.all()

Promise.all方法用於將多個Promise例項,包裝成一個新的Promise例項。只有Promise.all內的所有promise狀態都變成fulfilled,它的狀態才會變成fulfilled,此時內部promise的返回值組成一個陣列,傳遞給Promise.all的回撥函式。只要Promise.all內部有一個promise被rejected,Promise.all的狀態就變成rejected,此時第一個被reject的例項的返回值,會傳遞給p的回撥函式。

  • Promise.race()

Promise.race方法同樣是將多個Promise例項,包裝成一個新的Promise例項。只要Promise.race中有一個例項率先改變狀態,Promise.race的狀態就跟著改變。那個率先改變的 Promise 例項的返回值,就傳遞給Promise.race的回撥函式。

實現請求超時處理:

const ajaxWithTime = (url, ms) => Promise.race([
  fetch(url),
  new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('request timeout')), ms)
  })
])
ajaxWithTime('http://xxx', 5000).then(response => console.log(response))
.catch(error => console.log(error))
複製程式碼
  • Promise.resolve()

將現有物件轉為Promise物件。如果引數是Promise例項,那麼Promise.resolve將不做任何修改、原封不動地返回這個例項;如果引數是一個原始值,或者是一個不具有then方法的物件,則Promise.resolve方法返回一個新的Promise物件,狀態為Resolved;需要注意的是,立即resolve的Promise物件,是在本輪“事件迴圈”(event loop)的結束時,而不是在下一輪“事件迴圈”的開始時。

setTimeout(function () {
  console.log('3');
}, 0);

Promise.resolve().then(function () {
  console.log('2');
});

console.log('1');

// 1
// 2
// 3
複製程式碼
  • finally()

finally方法用於指定不管Promise物件最後狀態如何,都會執行的操作。它接受一個普通的回撥函式作為引數,該函式不管怎樣都必須執行

10. async函式

async函式就是Generator函式的語法糖,async函式的await命令後面,可以是Promise物件和原始型別的值(數值、字串和布林值,但這時等同於同步操作)。async函式的返回值是Promise物件,你可以用then方法指定下一步的操作。進一步說,async函式完全可以看作多個非同步操作,包裝成的一個Promise物件,而await命令就是內部then命令的語法糖。

應用案例:

1.指定時間後返回資料

function timeout(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value)
}

asyncPrint('xu xi', 5000);
複製程式碼

注意事項:

  • await命令後面的Promise物件,執行結果可能是rejected,所以最好把await命令放在try...catch程式碼塊中
  • 多個await命令後面的非同步操作,如果不存在繼發關係,最好讓它們同時觸發
let [a, b] = await Promise.all([a(), b()]);
複製程式碼
  • await命令只能用在async函式之中,如果用在普通函式,就會報錯

應用場景:

  1. 按順序完成非同步操作
async function fetchInOrder(urls) {
  // 併發讀取遠端請求
  const promises = urls.map(async url => {
    const res = await fetch(url);
    return res.text();
  });

  // 按次序輸出
  for (const promise of promises) {
    console.log(await promise);
  }
}
複製程式碼

11. class

通過class關鍵字,可以定義類。ES6的class可以看作只是一個語法糖,它的絕大部分功能,ES5都可以做到,class寫法只是讓物件原型的寫法更加清晰、更像物件導向程式設計的語法而已。類的資料型別就是function,類本身就指向建構函式。建構函式的prototype屬性,在ES6的“類”上面繼續存在。類的所有方法都定義在類的prototype屬性上面。另外,類的內部所有定義的方法,都是不可列舉的.

class Person {
  constructor(){
    // ...
  }

  toString(){
    // ...
  }
}

// 等同於

Person.prototype = {
  toString(){},
  toValue(){}
};

// 不可列舉
Object.keys(Person.prototype)
// []
Object.getOwnPropertyNames(Person.prototype)
// ["constructor","toString"]
// 注:ES5的寫法,toString方法是可列舉的
複製程式碼

constructor方法

方法是類的預設方法,通過new命令生成物件例項時,自動呼叫該方法。一個類必須有constructor方法,如果沒有顯式定義,一個空的constructor方法會被預設新增。constructor方法預設返回例項物件(this),可以指定返回另外一個物件

注:類的建構函式,不使用new是沒法呼叫的,會報錯。這是它跟普通建構函式的一個主要區別,後者不用new也可以執行。

不存在變數提升

this的指向

類的方法內部如果含有this,它預設指向類的例項。但是,必須非常小心,一旦單獨使用該方法,很可能報錯

// 解決this指向問題
class Say {
  constructor() {
    // 在構造方法中繫結this或者使用箭頭函式
    this.sayName = this.sayName.bind(this);
  }
}
複製程式碼

Class的繼承

Class之間可以通過extends關鍵字實現繼承,子類必須在constructor方法中呼叫super方法,否則新建例項時會報錯。這是因為子類沒有自己的this物件,而是繼承父類的this物件,如果不呼叫super方法,子類就得不到this物件。

class Color extends Point {
  constructor(x, y, name) {
    super(x, y); // 呼叫父類的constructor(x, y)
    this.name = name;
  }

  toString() {
    return this.name + ' ' + super.toString(); // 呼叫父類的toString()
  }
}
複製程式碼

Class的取值函式(getter)和存值函式(setter)

與ES5一樣,在Class內部可以使用get和set關鍵字,對某個屬性設定存值函式和取值函式,攔截該屬性的存取行為

class MyHTMLElement {
  constructor(element) {
    this.element = element;
  }

  get html() {
    return this.element.innerHTML;
  }

  set html(value) {
    console.log('success', value)
    this.element.innerHTML = value;
  }
}

const el = new MyHTMLElement(el);
el.html('1111')  // success 1111
複製程式碼

Class的靜態方法

如果在一個方法前,加上static關鍵字,就表示該方法不會被例項繼承,而是直接通過類來呼叫,這就稱為“靜態方法”。

class Hi {
  static say() {
    return 'hello';
  }
}

Hi.say() // 'hello'

let hi = new Hi();
hi.say()
// TypeError: foo.classMethod is not a function
複製程式碼

12. 修飾器Decorator

類的修飾

修飾器是一個用來修改類的行為的函式。其對類的行為的改變,是程式碼編譯時發生的,而不是在執行時。

function get(target) {
  target.get = 'GET';
}

@get
class MyHttpClass {}

console.log(MyHttpClass.get) // GET

// 如果覺得一個引數不夠用,可以在修飾器外面再封裝一層函式
function get(type) {
  return function(target) {
    target.type = type;
    // 新增例項屬性
    target.prototype.isDev = true;
  }
}

@get('json')
class MyHttpClass {}
MyHttpClass.type // 'json'

let http = new MyHttpClass();
http.isDev // true
複製程式碼

方法的修飾

修飾器不僅可以修飾類,還可以修飾類的屬性,修飾器只能用於類和類的方法,不能用於函式,因為存在函式提升。如果同一個方法有多個修飾器,會像剝洋蔥一樣,先從外到內進入,然後由內向外執行。

修飾類的屬性時,修飾器函式一共可以接受三個引數,第一個引數是所要修飾的目標物件,第二個引數是所要修飾的屬性名,第三個引數是該屬性的描述物件。

// 下面的@log修飾器,可以起到輸出日誌的作用
class Kinds {
  list = []
  @log
  add(name) {
    return this.list.push(name)
  }
}

function log(target, name, descriptor) {
  var oldValue = descriptor.value;
  console.log('old', oldValue);
  // ...
  return descriptor;
}

const dog = new Kinds();

dog.add('dog');

// 多個裝飾器的洋蔥式執行順序
function printN(id){
    console.log('print-0', id);
    return (target, property, descriptor) => console.log('print-1', id);
}

class A {
    @printN(1)
    @printN(2)
    say(){}
}
// print-0 1
// print-0 2
// print-1 2
// print-1 1
複製程式碼

13. module

模組功能主要由兩個命令構成:export和import。export命令用於規定模組的對外介面,import命令用於輸入其他模組提供的功能。

export

如果你希望外部能夠讀取模組內部的某個變數,就必須使用export關鍵字輸出該變數

// lib.js
// 直接匯出
export var name = 'xu xi';

// 優先考慮下面這種寫法,更清晰優雅
var year = 1958;
export {year};

//匯出函式
export function multiply(x, y) {
  return x * y;
};

// 使用as關鍵字重新命名
export {
  year as y
};

// 注意:export命令規定的是對外的介面,必須與模組內部的變數建立一一對應關係
var a = 1;
// 報錯,因為通過變數a,直接輸出的是1,1只是一個值,不是介面
export a;
// 同理,下面的也會報錯
function f() {}
export f;

// 另外,export語句輸出的介面,與其對應的值是動態繫結關係,
// 即通過該介面,可以取到模組內部實時的值
export var a = 1;
setTimeout(() => a = 2, 1000);  // 1s之後a變為2
複製程式碼

export命令可以出現在模組的任何位置,只要處於模組頂層就可以。如果處於塊級作用域內,就會報錯,import也是如此。這是因為處於條件程式碼塊之中,就沒法做靜態優化了,違背了ES6模組的設計初衷。

import

使用export命令定義了模組的對外介面後,其他 JS 檔案就可以通過import命令載入這個模組。import命令具有提升效果,會提升到整個模組的頭部,首先執行。是因為import命令是編譯階段執行的,在程式碼執行之前。如果多次重複執行同一句import語句,那麼只會執行一次,而不會執行多次

// main.js
import {other, year} from './lib';
// 將輸入的變數重新命名
import { year as y } from './lib';

// 提升
say()
import { say } from './lib';

// 整體載入模組
import * as lib from './lib';
console.log(lib.year, lib.say())
複製程式碼

export default 命令

為模組指定預設輸出

// a.js
export default function () {
  console.log('xu xi');
}

// b.js
import a from './a'
複製程式碼

最後

由於工作中90%以上的業務都可以用以上的es6新特性解決,後期會不斷優化完善,並出一個完全使用es6完成的實戰專案。更多前端問題在公眾號《趣談前端》加入我們一起討論吧!

快速掌握es6+新特性及es6核心語法盤點

更多推薦

相關文章