JavaScript中級指南-02 ES6常用知識點(2W餘字學習筆記)

只會番茄炒蛋發表於2019-09-20

ECMAScript 6.0(簡稱ES6),作為下一代JavaScript的語言標準正式釋出於2015 年 6 月,至今已經發布4年多了,但是因為蘊含的語法之廣,完全消化需要一定的時間,這裡我總結了自己在學習ES6過程中的一些知識點,以及ES6以後新語法的知識點,使用場景,希望對各位有所幫助.

俗話說得好,好記性不如爛筆頭,自己多敲一遍程式碼記得快,印象也深刻

第一章 嚴格模式

嚴格模式是ES5引入, 嚴格模式主要有以下限制;

  • 變數必須宣告後在使用
  • 函式的引數不能有同名屬性, 否則報錯
  • 不能使用with語句 (說實話我基本沒用過)
  • 不能對只讀屬性賦值, 否則報錯
  • 不能使用字首0表示八進位制數,否則報錯 (說實話我基本沒用過)
  • 不能刪除不可刪除的資料, 否則報錯
  • 不能刪除變數delete prop, 會報錯, 只能刪除屬性delete global[prop]
  • eval不會在它的外層作用域引入變數
  • eval和arguments不能被重新賦值
  • arguments不會自動反映函式引數的變化
  • 不能使用arguments.caller (說實話我基本沒用過)
  • 不能使用arguments.callee (說實話我基本沒用過)
  • 禁止this指向全域性物件
  • 不能使用fn.caller和fn.arguments獲取函式呼叫的堆疊 (說實話我基本沒用過)
  • 增加了保留字(比如protected、static和interface)

第二章 let 和 const命令

基本用法跟 es5var 一樣,但是 let 宣告不存在變數提升現象

var 宣告存在變數提升現象, letconst則不會有這種情況

暫時性死區 簡稱 TDZ

只要塊級作用域記憶體在let命令, 它所宣告的變數就"繫結"(binding)這個區域,不再受外部的影響

var num = 123
if (true) {
    num = 'abc' // Cannot access 'num' before initialization
    let num
}
複製程式碼

ES6明確規定, 如果區塊中存在let和const命令, 這個區塊對這些命令宣告的變數,從一開始就形成了封閉作用域,凡是在宣告之前就使用這些變數,就會報錯

暫時性死去的本質就是, 只要一進入當前作用域,所有使用的變數就已經存在了, 但是不可獲取, 只有等宣告變數的那一行程式碼出現,才可以獲取和使用該變數

不允許重複宣告

let 不允許在相同的作用域內, 重複宣告同一個變數

// 報錯
function fn() {
    let a = 1
    var a = 2
}

// 報錯
function fn() {
    let a = 1
    let a = 2
}
複製程式碼

因此,不能在函式內部重新宣告引數。

function fn(arg) {
  let arg; // 報錯
}

function fn(arg) {
  {
    let arg; // 不報錯
  }
}
複製程式碼

塊級作用域

為什麼需要快作用域

ES5只有全域性作用域和函式作用域, 沒有塊作用域, 這種情況帶來了很多不合理的場景

第一種場景, 內層變數可能會覆蓋外層變數

var num = 123
function fn() {
    console.log(num)
    if (false) { // 內部宣告變數覆蓋了全域性, 由於是var宣告出現變數提升上面的num值為undefined
        var num = 456  
    }
}
fn() // undefined
複製程式碼

第二種場景, 用來計數的迴圈變數洩露成為全域性變數

var s = 'hello';
for (var i = 0; i < s.length; i++) {  // 這裡var宣告的變數自動掛載到了全域性
  console.log(s[i]); 
}
console.log(i); // 5
複製程式碼

ES6 的塊級作用域

function fn() {
    let n = 5
    if (true) {
        let n = 10
    }
    console.log(n)
}
fn() // 5
複製程式碼

ES6 允許塊級作用域的任意巢狀。

塊級作用域的出現,實際上使得獲得廣泛應用的立即執行函式表示式(IIFE / 立即呼叫的函式表示式)不再必要了。

// IIFE 寫法
(function () {
  var tmp = ...;
  ...
}());

// 塊級作用域寫法
{
  let tmp = ...;
  ...
}
複製程式碼

塊級作用域不返回值,除非t是全域性變數。

const命令

const宣告一個只讀的常量。

const除了以下兩點與let不同外,其他特性均與let相同:

1. const一旦宣告變數,就必須立即初始化,不能留到以後賦值。
2. 一旦宣告,常量的值就不能改變。
複製程式碼

本質

const限定的是賦值行為。

const a = 1;
a = 2; // 報錯

const arr = [];
arr.push(1) // [1] 
//在宣告引用型資料為常量時,const儲存的是變數的指標,只要保證指標不變就不會儲存。下面的行為就會報錯

arr = []; // 報錯 因為是賦值行為。變數arr儲存的指標改變了。
複製程式碼

頂層物件屬性與全域性變數

頂層物件, 在瀏覽器環境指的是window物件, 在Node環境指的是global物件, ES5之中,頂層物件的屬性與全域性變數是等價的

window.a = 1
a // 1
a = 2;
window.a // 2
複製程式碼

頂層物件的屬性與全域性變數掛鉤, 被認為是JavaScript語言最大的設計敗筆之一

為了解決這個問題, ES6引入的let cosnt class宣告的全域性變數不再屬於頂層物件的屬性

而同時為了向下相容, var和function宣告的變數依然屬於全域性物件的屬性

var a = 1;
window.a // 1

let b = 1;
window.b // undefined
複製程式碼

第三章 變數的解構賦值

基本用法

ES6允許按照一定模式, 從陣列和物件中提取值, 對變數進行賦值, 這被稱為解構(Destructuring)

ES5一次宣告多個變數

var a = 1,
    b = 2,
    c = 3;
複製程式碼

ES6一次宣告多個變數

let [a, b, c] = [1, 2, 3]
// a = 1
// b = 2
// c = 3
複製程式碼

本質上,這種寫法屬於“模式匹配”,只要等號兩邊的模式相同,左邊的變數就會被賦予對應的值。

let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3

let [ , , third] = ["foo", "bar", "baz"];
third // "baz"

let [x, , y] = [1, 2, 3];
x // 1
y // 3

let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]

let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []
複製程式碼

如果解構不成功,變數的值就等於undefined。

let [foo] = [];
let [bar, foo] = [1];
// foo 都是undefined
複製程式碼

另一種情況是不完全解構,即等號左邊的模式,只匹配一部分的等號右邊的陣列。這種情況下,解構依然可以成功。

let [x, y] = [1, 2, 3];
x // 1
y // 2

let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4

//上面兩個例子,都屬於不完全解構,但是可以成功。
複製程式碼

如果等號的右邊不是陣列,那麼將會報錯。

// 報錯
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
複製程式碼

預設值

解構賦值允許指定預設值。

let [foo = true] = [];
foo // true

let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
複製程式碼

注意,ES6 內部使用嚴格相等運算子(===),判斷一個位置是否有值。所以,如果一個陣列成員不嚴格等於undefined,預設值是不會生效的。

let [x = 1] = [undefined];
x // 1

let [x = 1] = [null];
x // null
複製程式碼

如果預設值是一個表示式,那麼這個表示式是惰性求值的,即只有在用到的時候,才會求值。

function f() {
  console.log('aaa');
}

let [x = f()] = [1]; // [1]
//等價於
let x;
if ([1][0] === undefined) {
  x = f();
} else {
  x = [1][0];
}
複製程式碼

預設值可以引用解構賦值的其他變數,但該變數必須已經宣告。

let [x = 1, y = x] = [];     // x=1; y=1
let [x = 1, y = x] = [2];    // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = [];     // ReferenceError
//上面最後一個表示式之所以會報錯,是因為x用到預設值y時,y還沒有宣告
複製程式碼

物件的解構賦值

解構不僅可以用於陣列, 還可以用於物件

let { foo, bar } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
複製程式碼

物件的解構與陣列有一個重要的不同。陣列的元素是按次序排列的,變數的取值由它的位置決定;而物件的屬性沒有次序,變數必須與屬性同名,才能取到正確的值。

let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"

let { baz } = { foo: "aaa", bar: "bbb" };
baz // undefined
複製程式碼

如果變數名與屬性名不一致,必須寫成下面這樣。

let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"

let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'
複製程式碼

實際上說明,物件的解構賦值是下面形式的簡寫

let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };
複製程式碼

也就是說,物件的解構賦值的內部機制,是先找到同名屬性,然後再賦給對應的變數。真正被賦值的是後者,而不是前者。

let { foo: baz } = { foo: "aaa", bar: "bbb" };
baz // "aaa"
foo // error: foo is not defined
複製程式碼

與陣列一樣,解構也可以用於巢狀結構的物件。

let obj = {
  p: [
    'Hello',
    { y: 'World' }
  ]
};

let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"
複製程式碼

物件的解構也可以指定預設值。

var {x = 3} = {};
x // 3

var {x, y = 5} = {x: 1};
x // 1
y // 5

var {x: y = 3} = {};
y // 3

var {x: y = 3} = {x: 5};
y // 5

var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"
複製程式碼

預設值生效的條件是,物件的屬性值嚴格等於undefined。

var {x = 3} = {x: undefined};
x // 3

var {x = 3} = {x: null};
x // null
複製程式碼

如果解構模式是巢狀的物件,而且子物件所在的父屬性不存在,那麼將會報錯。

// 報錯
let {foo: {bar}} = {baz: 'baz'};
//等號左邊物件的foo屬性,對應一個子物件。該子物件的bar屬性,解構時會報錯。原因很簡單,因為foo這時等於undefined,再取子屬性就會報錯,
複製程式碼

由於陣列本質是特殊的物件,因此可以對陣列進行物件屬性的解構。

let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3
複製程式碼

字串的解構賦值

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
複製程式碼

類似陣列的物件都有一個length屬性,因此還可以對這個屬性解構賦值。

let {length : len} = 'hello';
len // 5
複製程式碼

函式引數的解構賦值

function add([a,b]){
  return a + b;
}
add([2,3])//5
複製程式碼

函式引數的解構也可以使用預設值。

function move({x = 0, y = 0} = {}) {
  return [x, y];
}

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]
複製程式碼

注意,下面的寫法會得到不一樣的結果。

function move({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]
複製程式碼

上面程式碼是為函式move的引數指定預設值,而不是為變數x和y指定預設值,所以會得到與前一種寫法不同的結果。

數值和布林值的解構賦值

解構賦值時,如果等號右邊是數值和布林值,則會先轉為物件。

let {toString: s} = 123;
s === Number.prototype.toString // true

let {toString: s} = true;
s === Boolean.prototype.toString // true
複製程式碼

圓括號

解構賦值雖然很方便,但是解析起來並不容易。對於編譯器來說,一個式子到底是模式,還是表示式,沒有辦法從一開始就知道,必須解析到(或解析不到)等號才能知道。

由此帶來的問題是,如果模式中出現圓括號怎麼處理。ES6 的規則是,只要有可能導致解構的歧義,就不得使用圓括號。

但是,這條規則實際上不那麼容易辨別,處理起來相當麻煩。因此,建議只要有可能,就不要在模式中放置圓括號。

不能使用圓括號的情況以下三種解構賦值不得使用圓括號。

1) 變數宣告語句
// 全部報錯
let [(a)] = [1];

let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {};

let { o: ({ p: p }) } = { o: { p: 2 } };

2)函式引數---函式引數也屬於變數宣告,因此不能帶有圓括號。
// 報錯
function f([(z)]) { return z; }
// 報錯
function f([z,(x)]) { return x; }

3) 賦值語句的模式
// 全部報錯
({ p: a }) = { p: 42 };
([a]) = [5];
//上面程式碼將整個模式放在圓括號之中,導致報錯。
// 報錯
[({ p: a }), { x: c }] = [{}, {}];
複製程式碼

**可以使用圓括號的情況 ** 可以使用圓括號的情況只有一種:賦值語句的非模式部分,可以使用圓括號。

[(b)] = [3]; // 正確
({ p: (d) } = {}); // 正確
[(parseInt.prop)] = [3]; // 正確
複製程式碼

上面三行語句都可以正確執行,因為首先它們都是賦值語句,而不是宣告語句;其次它們的圓括號都不屬於模式的一部分。第一行語句中,模式是取陣列的第一個成員,跟圓括號無關;第二行語句中,模式是p,而不是d;第三行語句與第一行語句的性質一致。

用途

1. 除了可以一次定義多個變數
2. 還可以讓函式返回多個值
3. 可以方便地讓函式的引數跟值對應起來
4. 提取json資料
5. 函式引數的預設值
複製程式碼

第四章 字串擴充套件

includes()、startsWith()、endsWith()

傳統上,JavaScript 只有indexOf方法,可以用來確定一個字串是否包含在另一個字串中。ES6 又提供了三種新方法。

  • includes():返回布林值,表示是否找到了引數字串。
  • startsWith():返回布林值,表示引數字串是否在原字串的頭部。
  • endsWith():返回布林值,表示引數字串是否在原字串的尾部。
let s = 'Hello world!';

s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true
複製程式碼

這三個方法都支援第二個引數,表示開始搜尋的位置。

let s = 'Hello world!';

s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false
複製程式碼

上面程式碼表示,使用第二個引數n時,endsWith的行為與其他兩個方法有所不同。它針對前n個字元,而其他兩個方法針對從第n個位置直到字串結束。

repeat

repeat方法返回一個新字串,表示將原字串重複n次。

'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""
複製程式碼
  • 引數如果是小數,會被向下取整。
  • 如果repeat的引數是負數或者Infinity,會報錯。
  • 0 到-1 之間的小數,則等同於 0,這是因為會先進行取整運算。0 到-1 之間的小數,取整以後等於-0,repeat視同為 0。
  • 引數NaN等同於 0。
  • 如果repeat的引數是字串,則會先轉換成數字。

padStart()、padEnd()

ES2017 引入了字串補全長度的功能。如果某個字串不夠指定長度,會在頭部或尾部補全。padStart()用於頭部補全,padEnd()用於尾部補全。

'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'

'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'
複製程式碼
  • 如果原字串的長度,等於或大於指定的最小長度,則返回原字串。
  • 如果用來補全的字串與原字串,兩者的長度之和超過了指定的最小長度,則會截去超出位數的補全字串。
  • 如果省略第二個引數,預設使用空格補全長度。

模板字串 (這個我專案中經常用到)

es5的字串模板輸出通常是使用+拼接。

這樣的缺點顯然易見:字串拼接內容多的時候,過於混亂,易出錯。

而ES6 引入了模板字串解決這個問題。

var name = "番茄",trait = "帥氣";
//es5dDdD
var str = "他叫"+name+",人非常"+trait+",說話又好聽";

//es6
var str2 = `他叫 ${name} ,人非常 ${trait} ,說話又好聽`;
複製程式碼

模板字串是增強版的字串,用反引號(`)標識。它可以當作普通字串使用,也可以用來定義多行字串,或者在字串中嵌入變數。

  • 如果在模板字串中需要使用反引號,則前面要用反斜槓轉義。
  • 如果使用模板字串表示多行字串,所有的空格和縮排都會被保留在輸出之中。
  • 模板字串中嵌入變數,需要將變數名寫在${}之中。
  • 大括號內部可以放入任意的 JavaScript 表示式,可以進行運算,以及引用物件屬性。
  • 模板字串之中還能呼叫函式。
  • 如果大括號中的值不是字串,將按照一般的規則轉為字串。比如,大括號中是一個物件,將預設呼叫物件的toString方法。
  • 如果模板字串中的變數沒有宣告,將報錯。

標籤模板

模板字串可以緊跟在一個函式名後面,該函式將被呼叫來處理這個模板字串。這被稱為“標籤模板”功能。

alert`123`
// 等同於
alert(123)
複製程式碼

標籤模板其實不是模板,而是函式呼叫的一種特殊形式。“標籤”指的就是函式,緊跟在後面的模板字串就是它的引數。

如果模板字元裡面有變數,就不是簡單的呼叫了,而是會將模板字串先處理成多個引數,再呼叫函式。

let a = 5;
let b = 10;

tag`Hello ${ a + b } world ${ a * b }`;
// 等同於
tag(['Hello ', ' world ', ''], 15, 50);
複製程式碼

第五章 數值的擴充套件

Number.isFinite()、Number.isNaN()

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

Number.isFinite()用來檢查一個數值是否為有限的(finite)。

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

Number.isNaN()用來檢查一個值是否為NaN。

和全域性函式 isNaN() 相比,該方法不會強制將引數轉換成數字,只有在引數是真正的數字型別,且值為 NaN 的時候才會返回 true。

Number.isNaN(NaN);        // true
Number.isNaN(Number.NaN); // true
Number.isNaN(0 / 0)       // true

// 下面這幾個如果使用全域性的 isNaN() 時,會返回 true。
Number.isNaN("NaN");      // false,字串 "NaN" 不會被隱式轉換成數字 NaN。
Number.isNaN(undefined);  // false
Number.isNaN({});         // false
Number.isNaN("blabla");   // false

// 下面的都返回 false
Number.isNaN(true);
Number.isNaN(null);
Number.isNaN(37);
Number.isNaN("37");
Number.isNaN("37.37");
Number.isNaN("");
Number.isNaN(" ");
複製程式碼

Number.parseInt()、Number.parseFloat()

ES6 將全域性方法parseInt()和parseFloat(),移植到Number物件上面,行為完全保持不變。

// ES5的寫法
parseInt('12.34') // 12
parseFloat('123.45#') // 123.45

// ES6的寫法
Number.parseInt('12.34') // 12
Number.parseFloat('123.45#') // 123.45
複製程式碼

這樣做的目的,是逐步減少全域性性方法,使得語言逐步模組化。

Number.parseInt === parseInt // true
Number.parseFloat === parseFloat // true
複製程式碼

Number.isInteger()

Number.isInteger()用來判斷一個值是否為整數。需要注意的是,在 JavaScript 內部,整數和浮點數是同樣的儲存方法,所以 3 和 3.0 被視為同一個值。

Number.isInteger(25) // true
Number.isInteger(25.0) // true
Number.isInteger(25.1) // false
Number.isInteger("15") // false
Number.isInteger(true) // false
複製程式碼

Math 物件的擴充套件

ES6 在 Math 物件上新增了 17 個與數學相關的方法。所有這些方法都是靜態方法,只能在 Math 物件上呼叫。

Math.trunc()
Math.trunc方法用於去除一個數的小數部分,返回整數部分。

Math.trunc(4.1) // 4
Math.trunc(4.9) // 4
Math.trunc(-4.1) // -4
Math.trunc(-4.9) // -4
Math.trunc(-0.1234) // -0
複製程式碼
  • 對於非數值,Math.trunc內部使用Number方法將其先轉為數值。
  • 對於空值和無法擷取整數的值,返回NaN。

Math.sign() Math.sign方法用來判斷一個數到底是正數、負數、還是零。對於非數值,會先將其轉換為數值。

它會返回五種值。

  • 引數為正數,返回+1;
  • 引數為負數,返回-1;
  • 引數為 0,返回0;
  • 引數為-0,返回-0;
  • 其他值,返回NaN。
Math.sign(-5) // -1
Math.sign(5) // +1
Math.sign(0) // +0
Math.sign(-0) // -0
Math.sign(NaN) // NaN

Math.sign('')  // 0
Math.sign(true)  // +1
Math.sign(false)  // 0
Math.sign(null)  // 0
Math.sign('9')  // +1
Math.sign('foo')  // NaN
Math.sign()  // NaN
Math.sign(undefined)  // NaN
複製程式碼

Math.cbrt() Math.cbrt方法用於計算一個數的立方根。

對於非數值,Math.cbrt方法內部也是先使用Number方法將其轉為數值。

Math.cbrt(-1) // -1
Math.cbrt(0)  // 0
Math.cbrt(1)  // 1
Math.cbrt(2)  // 1.2599210498948734
複製程式碼

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);          // 3
複製程式碼

指數運算子 ES2016 新增了一個指數運算子(**)。

2 ** 2 // 4
2 ** 3 // 8
複製程式碼

指數運算子可以與等號結合,形成一個新的賦值運算子(**=)。

let a = 1.5;
a **= 2;
// 等同於 a = a * a;

let b = 4;
b **= 3;
// 等同於 b = b * b * b;
複製程式碼

第六章 函式的擴充套件

基本用法

ES6 之前,不能直接為函式的引數指定預設值,只能採用變通的方法。

function log(x, y = 'World') {
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
複製程式碼

與解構賦值預設值結合使用

function foo({x, y = 5}) {
  console.log(x, y);
}

foo({}) // undefined 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo() // TypeError: Cannot read property 'x' of undefined
複製程式碼

引數預設值的位置

通常情況下,定義了預設值的引數,應該是函式的尾引數。因為這樣比較容易看出來,到底省略了哪些引數。如果非尾部的引數設定預設值,實際上這個引數是沒法省略的。

// 例一
function f(x = 1, y) {
  return [x, y];
}

f() // [1, undefined]
f(2) // [2, undefined])
f(, 1) // 報錯
f(undefined, 1) // [1, 1]

// 例二
function f(x, y = 5, z) {
  return [x, y, z];
}

f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // 報錯
f(1, undefined, 2) // [1, 5, 2]
複製程式碼

函式的 length 屬性

指定了預設值以後,函式的length屬性,將返回沒有指定預設值的引數個數。也就是說,指定了預設值後,length屬性將失真。

(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
複製程式碼
  • fn.length 返回形參個數
  • arguments.length 返回實參個數

作用域

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

var x = 1;

function f(x, y = x) {
  console.log(y);
}

f(2) // 2
複製程式碼

上面程式碼中,引數y的預設值等於變數x。呼叫函式f時,引數形成一個單獨的作用域。在這個作用域裡面,預設值變數x指向第一個引數x,而不是全域性變數x,所以輸出是2。

let x = 1;

function f(y = x) {
  let x = 2;
  console.log(y);
}

f() // 1
複製程式碼

上面程式碼中,函式f呼叫時,引數y = x形成一個單獨的作用域。這個作用域裡面,變數x本身沒有定義,所以指向外層的全域性變數x。函式呼叫時,函式體內部的區域性變數x影響不到預設值變數x。

var x = 1;

function foo(x = x) {
  // ...
}

foo() // ReferenceError: x is not defined
複製程式碼

上面程式碼中,引數x = x形成一個單獨作用域。實際執行的是let x = x,由於暫時性死區的原因,這行程式碼會報錯”x 未定義“。

var x = 1;
function foo(x, y = function() { x = 2; }) {
  var x = 3;
  y();
  console.log(x);
}

foo()//3
x//1
複製程式碼

如果將var x = 3的var去除,函式foo的內部變數x就指向第一個引數x,與匿名函式內部的x是一致的,所以最後輸出的就是2,而外層的全域性變數x依然不受影響。

var x = 1;
function foo(x, y = function() { x = 2; }) {
  x = 3;
  y();
  console.log(x);
}

foo() // 2
x // 1
複製程式碼

rest 引數

ES6 引入 rest 引數(形式為...變數名),用於獲取函式的多餘引數,這樣就不需要使用arguments物件了。rest 引數搭配的變數是一個陣列,該變數將多餘的引數放入陣列中。

function add(...values) {
  let sum = 0;

  for (var val of values) {
    sum += val;
  }

  return sum;
}

add(2, 5, 3) // 10
複製程式碼

arguments物件不是陣列,而是一個類似陣列的物件。所以為了使用陣列的方法,必須使用Array.prototype.slice.call先將其轉為陣列。rest 引數就不存在這個問題,它就是一個真正的陣列,陣列特有的方法都可以使用。下面是一個利用 rest 引數改寫陣列push方法的例子。

function push(array, ...items) {
  items.forEach(function(item) {
    array.push(item);
    console.log(item);
  });
}

var a = [];
push(a, 1, 2, 3)
複製程式碼

注意,rest 引數之後不能再有其他引數(即只能是最後一個引數),否則會報錯。

// 報錯
function f(a, ...b, c) {
  // ...
}
複製程式碼

函式的length屬性,不包括 rest 引數。

(function(a) {}).length  // 1
(function(...a) {}).length  // 0
(function(a, ...b) {}).length  // 1
複製程式碼

嚴格模式

從 ES5 開始,函式內部可以設定為嚴格模式。

ES2016 做了一點修改,規定只要函式引數使用了預設值、解構賦值、或者擴充套件運算子,那麼函式內部就不能顯式設定為嚴格模式,否則會報錯

name 屬性

返回函式名。

function foo() {}
foo.name // "foo"

var f = function () {}; // "f"
複製程式碼

箭頭函式

  • 基本用法
ES6 允許使用“箭頭”(=>)定義函式。
var f = v => v;
//上面的箭頭函式等同於
var f = function(v) {
  return v;
};

如果箭頭函式不需要引數或需要多個引數,就使用一個圓括號代表引數部分。
var f = () => 5;
// 等同於
var f = function () { return 5 };

var sum = (num1, num2) => num1 + num2;
// 等同於
var sum = function(num1, num2) {
  return num1 + num2;
};

如果箭頭函式的程式碼塊部分多於一條語句,就要使用大括號將它們括起來,並且使用return語句返回。
var sum = (num1, num2) => { return num1 + num2; }

由於大括號被解釋為程式碼塊,所以如果箭頭函式直接返回一個物件,必須在物件外面加上括號,否則會報錯。
// 報錯
let getTempItem = id => { id: id, name: "Temp" };

// 不報錯
let getTempItem = id => ({ id: id, name: "Temp" });
複製程式碼
  • 使用注意點 箭頭函式有幾個使用注意點。
  1. 函式體內的this物件,就是定義時所在的物件,而不是使用時所在的物件。
  2. 不可以當作建構函式,也就是說,不可以使用new命令,否則會丟擲一個錯誤。
  3. 不可以使用arguments物件,該物件在函式體內不存在。如果要用,可以用 rest 引數代替。
  4. 不可以使用yield命令,因此箭頭函式不能用作 Generator 函式。

this指向的固定化,並不是因為箭頭函式內部有繫結this的機制,實際原因是箭頭函式根本沒有自己的this,導致內部的this就是外層程式碼塊的this。正是因為它沒有this,所以也就不能用作建構函式。

箭頭函式轉成 ES5 的程式碼如下。

// ES6
function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

// ES5
function foo() {
  var _this = this;

  setTimeout(function () {
    console.log('id:', _this.id);
  }, 100);
}
//轉換後的 ES5 版本清楚地說明了,箭頭函式裡面根本沒有自己的this,而是引用外層的this。
複製程式碼

由於箭頭函式沒有自己的this,所以當然也就不能用call()、apply()、bind()這些方法去改變this的指向。

函式引數的尾逗號

ES2017 允許函式的最後一個引數有尾逗號(trailing comma)。

這樣的規定也使得,函式引數與陣列和物件的尾逗號規則,保持一致了。

function clownsEverywhere(
  param1,
  param2,
) { /* ... */ }

clownsEverywhere(
  'foo',
  'bar',
);
複製程式碼

第七章 陣列的擴充套件

擴充套件運算子

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

console.log(...[1, 2, 3])
// 1 2 3

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

[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]
複製程式碼

該運算子將一個陣列,變為引數序列。

擴充套件運算子後面還可以放置表示式。

var x = 1
const arr = [...(x > 0 ? ['a'] : [], 'b')]
console.log(arr) // ['a', 'b']
複製程式碼

如果擴充套件運算子後面是一個空陣列,則不產生任何效果。

var arr = [...[], 1]
console.log(arr) // [1]
複製程式碼
  • 替代陣列的 apply 方法 (將陣列當作引數傳入函式中) 由於擴充套件運算子可以展開陣列,所以不再需要apply方法,將陣列轉為函式的引數了。
// ES5 的寫法
function fn(x, y, z) {
    // ...
}
var arr = [1, 2, 3]
fn.apply(null, arr)

// ES6 的寫法
function fn(x, y, z) {
    // ...
}
var arr = [1, 2, 3]
fn(...arr)
複製程式碼

es5的時候大家的利用Math.max拿陣列最大值

//es5
Math.max.apply(null,[1,5,2,8]) // 8
//es6
Math.max(...[1,5,2,8]) // 8
//上面兩種方法等同於
Math.max(1,5,2,8)
複製程式碼
  • 擴充套件運算子的應用
// 複製陣列
// ES5 的方法
var arr = [1, 2, 3]
var arr1 = arr.concat()
arr1[arr1.length] = 5
console.log(arr) // [1, 2, 3]
console.log(arr1) // [1, 2, 3, 5]

// 擴充套件運算子提供了複製陣列的簡便寫法。
// 方法一
var arr = [1, 2, 3]
var arr1 = [...arr]
console.log(arr1) // [1, 2, 3]

// 方法二
var arr = [1, 2, 3]
var [...arr1] = arr
console.log(arr1) // [1, 2, 3]
複製程式碼
  • 合併陣列 擴充套件運算子提供了陣列合並的新寫法。
// ES5
[1, 2].concat(more)
// ES6
[1, 2, ...more]

var arr1 = ['a', 'b'];
var arr2 = ['c'];
var arr3 = ['d', 'e'];

// ES5的合併陣列
arr1 = arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]

// ES6的合併陣列
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]
複製程式碼
  • 與解構賦值結合

擴充套件運算子可以與解構賦值結合起來,用於生成陣列。

const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest  // [2, 3, 4, 5]

const [first, ...rest] = [];
first // undefined
rest  // []

const [first, ...rest] = ["foo"];
first  // "foo"
rest   // []

// 錯誤用法
const [...butLast, last] = [1, 2, 3, 4, 5];
// 報錯

const [first, ...middle, last] = [1, 2, 3, 4, 5];
// 報錯
複製程式碼
  • 字串 擴充套件運算子還可以將字串轉為真正的陣列
[...'hello']
// ["h", "e", "l", "l", "o"]
複製程式碼
  • 將類陣列轉為陣列
// 平時我們獲取dom節點的陣列是一個類陣列, 無法使用陣列的方法
let nodeList = document.querySelectorAll('div');
// 通過擴充套件雲演算法轉換為陣列
let array = [...nodeList];
複製程式碼
  • Array.from()

Array.from方法用於將兩類物件轉為真正的陣列

// NodeList物件
let ps = document.querySelectorAll('p');
Array.from(ps).forEach(function (p) {
  console.log(p);
});

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

引數:

  • 第一個引數:一個類陣列物件,用於轉為真正的陣列
  • 第二個引數:類似於陣列的map方法,用來對每個元素進行處理,將處理後的值放入返回的陣列。
  • 第三個引數:如果map函式裡面用到了this關鍵字,還可以傳入Array.from的第三個引數,用來繫結this。

  • Array.of()

Array.of方法用於將一組值,轉換為陣列。

這個方法的主要目的,是彌補陣列建構函式Array()的不足。因為引數個數的不同,會導致Array()的行為有差異。

//Array
Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]

Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1
複製程式碼
  • 陣列例項的 copyWithin()
陣列例項的copyWithin方法,在當前陣列內部,將指定位置的成員複製到其他位置(會覆蓋原有成員),然後返回當前陣列。也就是說,使用這個方法,會修改當前陣列。

它接受三個引數。

- target(必需):從該位置開始替換資料。
- start(可選):從該位置開始讀取資料,預設為 0。如果為負值,表示倒數。
- end(可選):到該位置前停止讀取資料,預設等於陣列長度。如果為負值,表示倒數。

這三個引數都應該是數值,如果不是,會自動轉為數值。

[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]    // 改變第一個數字, 值從第三個值賦值
複製程式碼
  • 陣列例項的 find() 和 findIndex()
// find()
陣列例項的find方法,用於找出第一個符合條件的陣列成員。
它的引數是一個回撥函式,所有陣列成員依次執行該回撥函式,直到找出第一個返回值為true的成員,
然後返回該成員。如果沒有符合條件的成員,則返回undefined。

[1, 4, -5, 10].find((n) => n < 0)
// -5
[1, 5, 10, 15].find(function(value, index, arr) {
  return value > 9;
}) // 10
//find方法的回撥函式可以接受三個引數,依次為當前的值、當前的位置和原陣列。

// findIndex方法的用法與find方法非常類似,返回第一個符合條件的陣列成員的位置,
如果所有成員都不符合條件,則返回-1。
[1, 5, 10, 15].findIndex(function(value, index, arr) {
  return value > 9;
}) // 2
複製程式碼

我自己一般使用find方法比較多

這兩個方法都可以接受第二個引數,用來繫結回撥函式的this物件。

  • 陣列例項的 fill()
fill方法使用給定值,填充一個陣列。
fill方法用於空陣列的初始化非常方便。陣列中已有的元素,會被全部抹去。
['a', 'b', 'c'].fill(7)
// [7, 7, 7]

new Array(3).fill(7)
// [7, 7, 7]
fill方法還可以接受第二個和第三個引數,用於指定填充的起始位置和結束位置。
var arr = [1, 2, 3]
arr.fill(6, 1, 2) // [1, 6, 3]
arr.fill(6, 1) // [1, 6, 6]
複製程式碼
  • for...of
es6引入的作為遍歷所有資料結構的統一的方法。

一個資料結構只要部署了Symbol.iterator屬性,就被視為具有 iterator 介面,
就可以用for...of迴圈遍歷它的成員。也就是說,for...of迴圈內部呼叫的是資料結構的Symbol.iterator方。
複製程式碼
  • 陣列例項的 entries(),keys() 和 values()

entries(),keys()和values()——用於遍歷陣列。它們都返回一個遍歷器物件,可以用for...of迴圈進行遍歷,唯一的區別是keys()是對鍵名的遍歷、values()是對鍵值的遍歷,entries()是對鍵值對的遍歷。

for (let index of ['a', 'b'].keys()) {
  console.log(index);
}
// 0
// 1

for (let elem of ['a', 'b'].values()) {
  console.log(elem);
}
// 'a'
// 'b'

for (let [index, elem] of ['a', 'b'].entries()) {
  console.log(index, elem);
}
// 0 "a"
// 1 "b"
複製程式碼
  • 陣列例項的 includes()

方法返回一個布林值,表示某個陣列是否包含給定的值,與字串的includes方法類似。ES2016 引入了該方法。

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

該方法的第二個參數列示搜尋的起始位置,預設為0。如果第二個引數為負數,則表示倒數的位置,如果這時它大於陣列長度(比如第二個引數為-4,但陣列長度為3),則會重置為從0開始。

沒有該方法之前,我們通常使用陣列的indexOf方法,檢查是否包含某個值。

indexOf方法有兩個缺點,一是不夠語義化,它的含義是找到引數值的第一個出現位置,所以要去比較是否不等於-1,表達起來不夠直觀。二是,它內部使用嚴格相等運算子(===)進行判斷,這會導致對NaN的誤判。

  • 陣列的空位 陣列的空位指,陣列的某一個位置沒有任何值。

注意,空位不是undefined,一個位置的值等於undefined,依然是有值的。空位是沒有任何值,in運算子可以說明這一點。

0 in [undefined, undefined, undefined] // true
0 in [, , ,] // false
複製程式碼

ES5 對空位的處理,已經很不一致了,大多數情況下會忽略空位。

  • forEach(), filter(), reduce(), every() 和some()都會跳過空位。
  • map()會跳過空位,但會保留這個值
  • join()和toString()會將空位視為undefined,而undefined和null會被處理成空字串。

ES6 則是明確將空位轉為undefined。

Array.from方法會將陣列的空位,轉為undefined,也就是說,這個方法不會忽略空位。

Array.from方法會將陣列的空位,轉為undefined,也就是說,這個方法不會忽略空位。

Array.from(['a',,'b'])
// [ "a", undefined, "b" ]
複製程式碼

擴充套件運算子(...)也會將空位轉為undefined。

[...['a',,'b']]
// [ "a", undefined, "b" ]
複製程式碼

copyWithin()會連空位一起拷貝。

[,'a','b',,].copyWithin(2,0) // [,"a",,"a"]
複製程式碼

fill()會將空位視為正常的陣列位置。

new Array(3).fill('a') // ["a","a","a"]
複製程式碼

for...of迴圈也會遍歷空位。

let arr = [, ,];
for (let i of arr) {
  console.log(1);
}
// 1
// 1
複製程式碼

上面程式碼中,陣列arr有兩個空位,for...of並沒有忽略它們。如果改成map方法遍歷,空位是會跳過的。

entries()、keys()、values()、find()和findIndex()會將空位處理成undefined。

// entries()
[...[,'a'].entries()] // [[0,undefined], [1,"a"]]

// keys()
[...[,'a'].keys()] // [0,1]

// values()
[...[,'a'].values()] // [undefined,"a"]

// find()
[,'a'].find(x => true) // undefined

// findIndex()
[,'a'].findIndex(x => true) // 0
複製程式碼

由於空位的處理規則非常不統一,所以建議避免出現空位。

第八章 物件的擴充套件

  • 屬性的簡潔表示法

ES6 允許直接寫入變數和函式,作為物件的屬性和方法。這樣的書寫更加簡潔。

const foo = 'bar';
const baz = {foo};
baz // {foo: "bar"}

// 等同於
const baz = {foo: foo};
複製程式碼

方法也可以簡寫。

const o = {
  method() {
    return "Hello!";
  }
};

// 等同於
const o = {
  method: function() {
    return "Hello!";
  }
};
複製程式碼
  • 屬性名錶達式

JavaScript 定義物件的屬性,有兩種方法。

// 方法一
obj.foo = true;

// 方法二
obj['a' + 'bc'] = 123;
複製程式碼

但是,如果使用字面量方式定義物件(使用大括號),在 ES5 中只能使用方法一(識別符號)定義屬性。

var obj = {
  foo: true,
  abc: 123
};
複製程式碼

ES6 允許字面量定義物件時,用方法二(表示式)作為物件的屬性名,即把表示式放在方括號內。

let propKey = 'foo';

let obj = {
  [propKey]: true,
  ['a' + 'bc']: 123
};
複製程式碼

表示式還可以用於定義方法名。

let obj = {
  ['h' + 'ello']() {
    return 'hi';
  }
};

obj.hello() // hi
複製程式碼

注意,屬性名錶達式與簡潔表示法,不能同時使用,會報錯。

// 報錯
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };

// 正確
const foo = 'bar';
const baz = { [foo]: 'abc'};
複製程式碼
  • Object.is()

ES5 比較兩個值是否相等,只有兩個運算子:相等運算子(==)和嚴格相等運算子(===)。它們都有缺點,前者會自動轉換資料型別,後者的NaN不等於自身,以及+0等於-0。JavaScript 缺乏一種運算,在所有環境中,只要兩個值是一樣的,它們就應該相等。

ES6 提出同值相等演算法,用來解決這個問題。Object.is就是部署這個演算法的新方法。它用來比較兩個值是否嚴格相等,與嚴格比較運算子(===)的行為基本一致。

Object.is('foo', 'foo')
// true
Object.is({}, {})
// false
複製程式碼

不同之處只有兩個:一是+0不等於-0,二是NaN等於自身。

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

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
複製程式碼
  • Object.assign() 這個常用

Object.assign方法用於物件的合併,將源物件(source)的所有可列舉屬性,複製到目標物件(target)。

const target = { a: 1 };

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

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

如果只有一個引數,Object.assign會直接返回該引數。

const obj = {a: 1};
Object.assign(obj) === obj // true
複製程式碼

由於undefined和null無法轉成物件,所以如果它們作為引數,就會報錯。

Object.assign(undefined) // 報錯
Object.assign(null) // 報錯
複製程式碼

注意:Object.assign可以用來處理陣列,但是會把陣列視為物件。

Object.assign([1, 2, 3], [4, 5])
// [4, 5, 3]
//把陣列視為屬性名為 0、1、2 的物件,因此源陣列的 0 號屬性4覆蓋了目標陣列的 0 號屬性1。
複製程式碼
  • Object.keys() ES5 引入了Object.keys方法,返回一個陣列,成員是引數物件自身的(不含繼承的)所有可遍歷(enumerable)屬性的鍵名。
var obj = { foo: 'bar', baz: 42 };
Object.keys(obj)
// ["foo", "baz"]
複製程式碼

ES2017 引入了跟Object.keys配套的Object.values和Object.entries,作為遍歷一個物件的補充手段,供for...of迴圈使用。

let {keys, values, entries} = Object;
let obj = { a: 1, b: 2, c: 3 };

for (let key of keys(obj)) {
  console.log(key); // 'a', 'b', 'c'
}

for (let value of values(obj)) {
  console.log(value); // 1, 2, 3
}

for (let [key, value] of entries(obj)) {
  console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]
}
複製程式碼
  • Object.values()

Object.values方法返回一個陣列,成員是引數物件自身的(不含繼承的)所有可遍歷(enumerable)屬性的鍵值。

const obj = { foo: 'bar', baz: 42 };
Object.values(obj)
// ["bar", 42]
複製程式碼

返回陣列的成員順序

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

上面程式碼中,屬性名為數值的屬性,是按照數值大小,從小到大遍歷的,因此返回的順序是b、c、a。

  • Object.entries

Object.entries方法返回一個陣列,成員是引數物件自身的(不含繼承的)所有可遍歷(enumerable)屬性的鍵值對陣列。

const obj = { foo: 'bar', baz: 42 };
Object.entries(obj)
// [ ["foo", "bar"], ["baz", 42] ]
複製程式碼

除了返回值不一樣,該方法的行為與Object.values基本一致。

物件的擴充套件運算子

  • 解構賦值
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }
複製程式碼

由於解構賦值要求等號右邊是一個物件,所以如果等號右邊是undefined或null,就會報錯,因為它們無法轉為物件。

let { x, y, ...z } = null; // 執行時錯誤
let { x, y, ...z } = undefined; // 執行時錯誤
複製程式碼

解構賦值必須是最後一個引數,否則會報錯。

let { ...x, y, z } = obj; // 句法錯誤
let { x, ...y, ...z } = obj; // 句法錯誤
複製程式碼

注意,解構賦值的拷貝是淺拷貝,即如果一個鍵的值是複合型別的值(陣列、物件、函式)、那麼解構賦值拷貝的是這個值的引用,而不是這個值的副本。

let obj = { a: { b: 1 } };
let { ...x } = obj;
obj.a.b = 2;
x.a.b // 2
複製程式碼
  • 擴充套件運算子

擴充套件運算子(...)用於取出引數物件的所有可遍歷屬性,拷貝到當前物件之中。

let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }
複製程式碼

這等同於使用Object.assign方法。

let aClone = { ...a };
// 等同於
let aClone = Object.assign({}, a);
複製程式碼

第九章 symbol ES6新增的資料型別

ES5裡面物件的屬性名都是字串,如果你需要使用一個別人提供的物件,你對這個物件有哪些屬性也不是很清楚,但又想為這個物件新增一些屬性,那麼你新增的屬性名就很可能和原來的屬性名傳送衝突,顯然我們是不希望這種情況發生的。所以,我們需要確保每個屬性名都是獨一無二的,這樣就可以防止屬性名的衝突了。因此,ES6裡就引入了Symbol,用它來產生一個獨一無二的值。

Symbol是什麼

Symbol實際上是ES6引入的一種原始資料型別,除了Symbol,JavaScript還有其他5種原始資料型別,分別是Undefined、Null、Boolean、String、Number、物件,這5種資料型別都是ES5中就有的。

怎麼生成一個Symbol型別的值

Symbol值是通過Symbol函式生成的,如下:

let s = Symbol();
console.log(s);  // Symbol()
typeof s;  // "symbol"
複製程式碼

Symbol函式前不能用new

Symbol函式不是一個建構函式,前面不能用new操作符。所以Symbol型別的值也不是一個物件,不能新增任何屬性,它只是一個類似於字元型的資料型別。如果強行在Symbol函式前加上new操作符,會報錯,如下:

let s = new Symbol();
// Uncaught TypeError: Symbol is not a constructor(…)
複製程式碼

Symbol函式的引數

字串作為引數

用上面的方法生成的Symbol值不好進行區分,Symbol函式還可以接受一個字串引數,來對產生的Symbol值進行描述,方便我們區分不同的Symbol值。

let s1 = Symbol('s1');
let s2 = Symbol('s2');
console.log(s1);  // Symbol(s1)
console.log(s2);  // Symbol(s2)
s1 === s2;  //  false
let s3 = Symbol('s2');
s2 === s3;  //  false
複製程式碼
  1. 給Symbol函式加了引數之後,控制檯輸出的時候可以區分到底是哪一個值;
  2. Symbol函式的引數只是對當前Symbol值的描述,因此相同引數的Symbol函式返回值是不相等的;

物件作為引數

如果Symbol函式的引數是一個物件,就會呼叫該物件的toString方法,將其轉化為一個字串,然後才生成一個Symbol值。所以,說到底,Symbol函式的引數只能是字串。

ymbol值不可以進行運算

既然Symbol是一種資料型別,那我們一定想知道Symbol值是否能進行運算。告訴你,Symbol值是不能進行運算的,不僅不能和Symbol值進行運算,也不能和其他型別的值進行運算,否則會報錯。 Symbol值可以顯式轉化為字串和布林值,但是不能轉為數值。

var mysym1 = Symbol('my symbol');

mysym1.toString() //  'Symbol('my symbol')'

String(mysym1)  //  'Symbol('my symbol')'

var mysym2 = Symbol();

Boolean(mysym2);  // true

Number(mysym2)  // TypeError: Cannot convert a Symbol value to a number(…)
複製程式碼

Symbol作屬性名

Symbol就是為物件的屬性名而生,那麼Symbol值怎麼作為物件的屬性名呢?有下面幾種寫法:

let a = {};
let s4 = Symbol();
// 第一種寫法
a[s4] = 'mySymbol';
// 第二種寫法
a = {
    [s4]: 'mySymbol'
}
// 第三種寫法
Object.defineProperty(a, s4, {value: 'mySymbol'});
a.s4;  //  undefined
a.s4 = 'mySymbol';
a[s4]  //  undefined
a['s4']  // 'mySymbol'
複製程式碼
  1. 使用物件的Symbol值作為屬性名時,獲取相應的屬性值不能用點運算子;
  2. 如果用點運算子來給物件的屬性賦Symbol型別的值,實際上屬性名會變成一個字串,而不是一個Symbol值;
  3. 在物件內部,使用Symbol值定義屬性時,Symbol值必須放在方括號之中,否則只是一個字串。

Symbol值作為屬性名的遍歷

使用for...in和for...of都無法遍歷到Symbol值的屬性,Symbol值作為物件的屬性名,也無法通過Object.keys()、Object.getOwnPropertyNames()來獲取了。但是,不同擔心,這種平常的需求肯定是會有解決辦法的。我們可以使用Object.getOwnPropertySymbols()方法獲取一個物件上的Symbol屬性名。也可以使用Reflect.ownKeys()返回所有型別的屬性名,包括常規屬性名和 Symbol屬性名。

let s5 = Symbol('s5');
let s6 = Symbol('s6');
let a = {
    [s5]: 's5',
    [s6]: 's6'
}
Object.getOwnPropertySymbols(a);   // [Symbol(s5), Symbol(s6)]
a.hello = 'hello';
Reflect.ownKeys(a);  //  ["hello", Symbol(s5), Symbol(s6)]
複製程式碼

利用Symbol值作為物件屬性的名稱時,不會被常規方法遍歷到這一特性,可以為物件定義一些非私有的但是又希望只有內部可用的方法。

Symbol.for()和Symbol.keyFor()

Symbol.for()函式也可以用來生成Symbol值,但該函式有一個特殊的用處,就是可以重複使用一個Symbol值。

let s1 = Symbol.for("s11");
let s2 = Symbol.for("s22");

console.log(s1===s2)//false

let s3 = Symbol("s33");
let s4 = Symbol("s33");

console.log(s3===s4)//false

console.log(Symbol.keyFor(s3))//undefined
console.log(Symbol.keyFor(s2))//"s22"
console.log(Symbol.keyFor(s1))//"s11"
複製程式碼

**Symbol.for()**函式要接受一個字串作為引數,先搜尋有沒有以該引數作為名稱的Symbol值,如果有,就直接返回這個Symbol值,否則就新建並返回一個以該字串為名稱的Symbol值。

**Symbol.keyFor()**函式是用來查詢一個Symbol值的登記資訊的,Symbol()寫法沒有登記機制,所以返回undefined;而Symbol.for()函式會將生成的Symbol值登記在全域性環境中,所以Symbol.keyFor()函式可以查詢到用Symbol.for()函式生成的Symbol值。

內建Symbol值

ES6提供了11個內建的Symbol值,分別是Symbol.hasInstance 、Symbol.isConcatSpreadable 、Symbol.species 、Symbol.match 、Symbol.replace 、Symbol.search 、Symbol.split 、Symbol.iterator 、Symbol.toPrimitive 、Symbol.toStringTag 、Symbol.unscopables 等。 有興趣的可以自行了解: 地址

第十章 Set 和 Map 資料結構 (也就用過[...new Set([1, 2, 3, 1])]去重)

Set

  • 基本用法

ES6 提供了新的資料結構 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
//通過add方法向 Set 結構加入成員,結果表明 Set 結構不會新增重複的值。
複製程式碼

Set 函式可以接受一個陣列作為引數,用來初始化。

// 例一
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]

// 例二
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5

// 例三
function divs () {
  return [...document.querySelectorAll('div')];
}

const set = new Set(divs());
set.size // 56

// 類似於
divs().forEach(div => set.add(div));
set.size // 56
複製程式碼

對陣列去重

// 去除陣列的重複成員
[...new Set(array)]
複製程式碼
  1. 向 Set 加入值的時候,不會發生型別轉換,所以5和"5"是兩個不同的值。
  2. Set 內部判斷兩個值是否不同,使用的演算法叫做“Same-value equality”,它類似於精確相等運算子(===),主要的區別是NaN等於自身,而精確相等運算子認為NaN不等於自身。

注意:兩個物件總是不相等的。

Set 例項的屬性和方法

  • Set 結構的例項有以下屬性:
- Set.prototype.constructor:建構函式,預設就是Set函式。
- Set.prototype.size:返回Set例項的成員總數。
Set 例項的方法分為兩大類:操作方法(用於運算元據)和遍歷方法(用於遍歷成員)。

複製程式碼
  • 四個操作方法:
- add(value):新增某個值,返回 Set 結構本身。
- delete(value):刪除某個值,返回一個布林值,表示刪除是否成功。
- has(value):返回一個布林值,表示該值是否為Set的成員。
- clear():清除所有成員,沒有返回值。

複製程式碼
  • Array.from()可以將 Set 結構轉為陣列。
const items = new Set([1, 2, 3, 4, 5]);
const array = Array.from(items);
複製程式碼
  • 遍歷操作
Set 結構的例項有四個遍歷方法,可以用於遍歷成員。

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

keys(),values(),entries()

keys方法、values方法、entries方法返回的都是遍歷器物件。
由於 Set 結構沒有鍵名,只有鍵值(或者說鍵名和鍵值是同一個值),所以keys方法和values方法的行為完全一致。

let set = new Set(['red', 'green', 'blue']);

for (let item of set.keys()) {
  console.log(item);
}
// red
// green
// blue

for (let item of set.values()) {
  console.log(item);
}
// red
// green
// blue

for (let item of set.entries()) {
  console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]
複製程式碼

Set 結構的例項預設可遍歷,它的預設遍歷器生成函式就是它的values方法。

  • forEach()

Set 結構的例項與陣列一樣,也擁有forEach方法,用於對每個成員執行某種操作,沒有返回值。

let set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key + ' : ' + value))
// 1 : 1
// 4 : 4
// 9 : 9
複製程式碼

forEach方法還可以有第二個引數,表示繫結處理函式內部的this物件。

WeakSet (本人表示基本沒用過)

WeakSet 結構與 Set 類似,也是不重複的值的集合。但是,它與 Set 有兩個區別:

  1. WeakSet 的成員只能是物件,而不能是其他型別的值。
  2. WeakSet 中的物件都是弱引用,即垃圾回收機制不考慮 WeakSet 對該物件的引用,也就是說,如果其他物件都不再引用該物件,那麼垃圾回收機制會自動回收該物件所佔用的記憶體,不考慮該物件還存在於 WeakSet 之中。

由於上面這個特點,WeakSet 的成員是不適合引用的,因為它會隨時消失。另外,由於 WeakSet 內部有多少個成員,取決於垃圾回收機制有沒有執行,執行前後很可能成員個數是不一樣的,而垃圾回收機制何時執行是不可預測的,因此 ES6 規定 WeakSet 不可遍歷。

用法跟set差不多

const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]}

//下面的寫法不行
const b = [3, 4];
const ws = new WeakSet(b);
// Uncaught TypeError: Invalid value used in weak set(…)
複製程式碼

WeakSet 結構有以下三個方法。

  • WeakSet.prototype.add(value):向 WeakSet 例項新增一個新成員。
  • WeakSet.prototype.delete(value):清除 WeakSet 例項的指定成員。
  • WeakSet.prototype.has(value):返回一個布林值,表示某個值是否在 WeakSet 例項之中。

WeakSet 沒有size屬性,沒有辦法遍歷它的成員。

Map (基本沒用過)

含義和基本用法

JavaScript 的物件(Object),本質上是鍵值對的集合(Hash 結構),但是傳統上只能用字串當作鍵。這給它的使用帶來了很大的限制。

為了解決這個問題,ES6 提供了 Map 資料結構。它類似於物件,也是鍵值對的集合,但是“鍵”的範圍不限於字串,各種型別的值(包括物件)都可以當作鍵。也就是說,Object 結構提供了“字串—值”的對應,Map 結構提供了“值—值”的對應,是一種更完善的 Hash 結構實現。如果你需要“鍵值對”的資料結構,Map 比 Object 更合適。

const m = new Map();
const o = {p: 'Hello World'};

m.set(o, 'content')
m.get(o) // "content"

m.has(o) // true
m.delete(o) // true
m.has(o) // false
複製程式碼

作為建構函式,Map 也可以接受一個陣列作為引數。該陣列的成員是一個個表示鍵值對的陣列。

const map = new Map([
  ['name', '張三'],
  ['title', 'Author']
]);

map.size // 2
map.has('name') // true
map.get('name') // "張三"
map.has('title') // true
map.get('title') // "Author"
複製程式碼

注意,只有對同一個物件的引用,Map 結構才將其視為同一個鍵。這一點要非常小心。

const map = new Map();

map.set(['a'], 555);
map.get(['a']) // undefined
複製程式碼

如果 Map 的鍵是一個簡單型別的值(數字、字串、布林值),則只要兩個值嚴格相等,Map 將其視為一個鍵,比如0和-0就是一個鍵,布林值true和字串true則是兩個不同的鍵。另外,undefined和null也是兩個不同的鍵。雖然NaN不嚴格相等於自身,但 Map 將其視為同一個鍵。

例項的屬性和操作方法

1. size屬性		返回成員總數
2. set(key,value)       設定鍵值對,返回Map結構
3. get(key)             讀取key對應的值,找不到就是undefined
4. has(key)             返回布林值,表示key是否在Map中
5. delete(key)          刪除某個鍵,返回true,失敗返回false
6. clear()              清空所有成員,沒有返回值
複製程式碼

遍歷方法

Map 結構原生提供三個遍歷器生成函式和一個遍歷方法。

- keys():返回鍵名的遍歷器。
- values():返回鍵值的遍歷器。
- entries():返回所有成員的遍歷器。
- forEach():遍歷 Map 的所有成員。

需要特別注意的是,Map 的遍歷順序就是插入順序。遍歷行為基本與set的一致。
複製程式碼

與其他資料結構的互相轉換

  • Map 轉為陣列
const myMap = new Map()
  .set(true, 7)
  .set({foo: 3}, ['abc']);
[...myMap]
複製程式碼
  • 陣列 轉為 Map
new Map([
  [true, 7],
  [{foo: 3}, ['abc']]
])
// Map {
//   true => 7,
//   Object {foo: 3} => ['abc']
// }
複製程式碼
  • Map 轉為物件 如果所有 Map 的鍵都是字串,它可以轉為物件。
function strMapToObj(strMap) {
  let obj = Object.create(null);
  for (let [k,v] of strMap) {
    obj[k] = v;
  }
  return obj;
}

const 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})
// Map {"yes" => true, "no" => false}
複製程式碼
  • Map 轉為 JSON Map 轉為 JSON 要區分兩種情況。一種情況是,Map 的鍵名都是字串,這時可以選擇轉為物件 JSON。
function strMapToJson(strMap) {
  return JSON.stringify(strMapToObj(strMap));
}

let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap)
// '{"yes":true,"no":false}'

複製程式碼

另一種情況是,Map 的鍵名有非字串,這時可以選擇轉為陣列 JSON。

function mapToArrayJson(map) {
  return JSON.stringify([...map]);
}

let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'
複製程式碼
  • JSON 轉為 Map JSON 轉為 Map,正常情況下,所有鍵名都是字串。
function jsonToStrMap(jsonStr) {
  return objToStrMap(JSON.parse(jsonStr));
}

jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}
複製程式碼

但是,有一種特殊情況,整個 JSON 就是一個陣列,且每個陣列成員本身,又是一個有兩個成員的陣列。這時,它可以一一對應地轉為 Map。這往往是陣列轉為 JSON 的逆操作。

function jsonToMap(jsonStr) {
  return new Map(JSON.parse(jsonStr));
}

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

WeakMap 的語法 (同樣沒用過)

WeakMap只有四個方法可用:get()、set()、has()、delete()。

無法被遍歷,因為沒有size。無法被清空,因為沒有clear(),跟WeakSet相似。

WeakMap 應用的典型

let myElement = document.getElementById('logo');
let myWeakmap = new WeakMap();

myWeakmap.set(myElement, {timesClicked: 0});

myElement.addEventListener('click', function() {
  let logoData = myWeakmap.get(myElement);
  logoData.timesClicked++;
}, false);
複製程式碼

上面程式碼中,myElement是一個 DOM 節點,每當發生click事件,就更新一下狀態。我們將這個狀態作為鍵值放在 WeakMap 裡,對應的鍵名就是myElement。一旦這個 DOM 節點刪除,該狀態就會自動消失,不存在記憶體洩漏風險。

第十一章 Proxy

Proxy 用於修改某些操作的預設行為,等同於在語言層面做出修改,所以屬於一種“超程式設計”,即對程式語言進行程式設計。

Proxy 可以理解成,在目標物件之前架設一層“攔截”,外界對該物件的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。Proxy 這個詞的原意是代理,用在這裡表示由它來“代理”某些操作,可以譯為“代理器”。 Vue3.0使用了proxy

var obj = new Proxy({}, {
  get: function (target, key, receiver) {
    console.log(`getting ${key}!`);
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, value, receiver) {
    console.log(`setting ${key}!`);
    return Reflect.set(target, key, value, receiver);
  }
});
// target表示要攔截的資料
// key表示要攔截的屬性
// value表示要攔截的屬性的值
// receiver表示Proxy{}
//上面程式碼對一個空物件架設了一層攔截,重定義了屬性的讀取(get)和設定(set)行為
obj.count = 1
//  setting count!
++obj.count
//  getting count!
//  setting count!
//  2
複製程式碼

上面程式碼說明,Proxy 實際上過載(overload)了點運算子,即用自己的定義覆蓋了語言的原始定義。

ES6 原生提供 Proxy 建構函式,用來生成 Proxy 例項。

let proxy = new Proxy(target, handler);
Proxy 物件的所有用法,都是上面這種形式,不同的只是handler引數的寫法。其中,new Proxy()表示生成一個Proxy例項,target參數列示所要攔截的目標物件,handler引數也是一個物件,用來定製攔截行為。
複製程式碼

Proxy 物件的所有用法,都是上面這種形式,不同的只是handler引數的寫法。其中,new Proxy()表示生成一個Proxy例項,target參數列示所要攔截的目標物件,handler引數也是一個物件,用來定製攔截行為。

var proxy = new Proxy({}, {
  get: function(target, property) {
    return 35;
  }
});
proxy.time = 10
proxy.time // 35  // 攔截了所有的獲取屬性,都會返回35
proxy.name // 35
proxy.title // 35
複製程式碼

如果handler沒有設定任何攔截,那就等同於直接通向原物件。

var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"
複製程式碼

上面程式碼中,handler是一個空物件,沒有任何攔截效果,訪問proxy就等同於訪問target。

同一個攔截器函式,可以設定攔截多個操作。

對於可以設定、但沒有設定攔截的操作,則直接落在目標物件上,按照原先的方式產生結果。

下面是 Proxy 支援的攔截操作一覽,一共 13 種:

  • get(target, propKey, receiver):攔截物件屬性的讀取,比如proxy.foo和proxy['foo']。
  • set(target, propKey, value, receiver):攔截物件屬性的設定,比如proxy.foo = v或proxy['foo'] = v,返回一個布林值。
  • has(target, propKey):攔截propKey in proxy的操作,返回一個布林值。
  • deleteProperty(target, propKey):攔截delete proxy[propKey]的操作,返回一個布林值。
  • ownKeys(target):攔截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy),返回一個陣列。該方法返回目標物件所有自身的屬性的屬性名,而Object.keys()的返回結果僅包括目標物件自身的可遍歷屬性。
  • getOwnPropertyDescriptor(target, propKey):攔截Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性的描述物件。
  • defineProperty(target, propKey, propDesc):攔截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一個布林值。
  • preventExtensions(target):攔截Object.preventExtensions(proxy),返回一個布林值。
  • getPrototypeOf(target):攔截Object.getPrototypeOf(proxy),返回一個物件。
  • isExtensible(target):攔截Object.isExtensible(proxy),返回一個布林值。
  • setPrototypeOf(target, proto):攔截Object.setPrototypeOf(proxy, proto),返回一個布林值。如果目標物件是函式,那麼還有兩種額外操作可以攔截。
  • apply(target, object, args):攔截 Proxy 例項作為函式呼叫的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
  • construct(target, args):攔截 Proxy 例項作為建構函式呼叫的操作,比如new proxy(...args)。

例如:

deleteProperty方法用於攔截delete操作,如果這個方法丟擲錯誤或者返回false,當前屬性就無法被delete命令刪除。

apply方法攔截函式的呼叫、call和apply操作。

get方法用於攔截某個屬性的讀取操作。

let obj2 = new Proxy(obj,{
  get(target,property,a){
    //return 35;
    /*console.log(target)
   				console.log(property)*/
    let Num = ++wkMap.get(obj).getPropertyNum;
    console.log(`當前訪問物件屬性次數為:${Num}`)
    return target[property]

  },
  deleteProperty(target,property){
    return false;
  },
  apply(target,ctx,args){
    return Reflect.apply(...[target,[],args]);;
  }

})
複製程式碼

Proxy.revocable()

Proxy.revocable方法返回一個可取消的 Proxy 例項。

let target = {};
let handler = {};

let {proxy, revoke} = Proxy.revocable(target, handler);

proxy.foo = 123;
proxy.foo // 123

revoke();
proxy.foo // TypeError: Revoked
複製程式碼

Proxy.revocable方法返回一個物件,該物件的proxy屬性是Proxy例項,revoke屬性是一個函式,可以取消Proxy例項。上面程式碼中,當執行revoke函式之後,再訪問Proxy例項,就會丟擲一個錯誤。

Proxy.revocable的一個使用場景是,目標物件不允許直接訪問,必須通過代理訪問,一旦訪問結束,就收回代理權,不允許再次訪問。

this 問題

雖然 Proxy 可以代理針對目標物件的訪問,但它不是目標物件的透明代理,即不做任何攔截的情況下,也無法保證與目標物件的行為一致。主要原因就是在 Proxy 代理的情況下,目標物件內部的this關鍵字會指向 Proxy 代理。

const target = {
  m: function () {
    console.log(this === proxy);
  }
};
const handler = {};

const proxy = new Proxy(target, handler);

target.m() // false
proxy.m()  // true
//一旦proxy代理target.m,後者內部的this就是指向proxy,而不是target。
複製程式碼

第十二章 Promise (重點章節)

概念

Promise 是非同步程式設計的一種解決方案,比傳統的解決方案——回撥函式和事件——更合理和更強大。

所`Promise,簡單說就是一個容器,裡面儲存著某個未來才會結束的事件(通常是一個非同步操作)的結果。

特點

  1. 物件的狀態不受外界影響。
  2. 一旦狀態改變,就不會再變,任何時候都可以得到這個結果。

狀態

Promise物件代表一個非同步操作,有三種狀態:

pending(進行中)、fulfilled(已成功)和rejected(已失敗)。

只有非同步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。

缺點

  1. 無法取消Promise,一旦新建它就會立即執行,無法中途取消。
  2. 如果不設定回撥函式,Promise內部丟擲的錯誤,不會反應到外部。
  3. 當處於pending狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)

用法

寫js必然不會對非同步事件陌生。

settimeout(()=>{
  console.log("123")
},0)

console.log("abc")
//先輸出誰?
複製程式碼

答案我想我不用說,大家都知道

如果abc需要在123執行結束後再輸出怎麼辦?

當然,可以使用callback,但是callback使用起來是一件很讓人絕望的事情。

這時:Promise這個為非同步程式設計而生的物件站了出來....

let p = new Promise((resolve,reject)=>{
  //一些非同步操作
  setTimeout(()=>{
    console.log("123")
    resolve("abc");
    reject("我是錯誤資訊")
  },0)
})
.then(function(data){
  //resolve狀態
  console.log(data)
},function(err){
  //reject狀態
  console.log(err)
})
//'123'
//'abc'
// 我是錯誤資訊
複製程式碼

這時候你應該有兩個疑問:

1.包裝這麼一個函式有毛線用?

2.resolve('123');這是幹毛的?

Promise例項生成以後,可以用then方法分別指定resolved狀態和rejected狀態的回撥函式。

也就是說,狀態由例項化時的引數(函式)執行來決定的,根據不同的狀態,看看需要走then的第一個引數還是第二個。

resolve()和reject()的引數會傳遞到對應的回撥函式的data或err

then返回的是一個新的Promise例項,也就是說可以繼續then

鏈式操作的用法

所以,從表面上看,Promise只是能夠簡化層層回撥的寫法,而實質上,Promise的精髓是“狀態”,用維護狀態、傳遞狀態的方式來使得回撥函式能夠及時呼叫,它比傳遞callback函式要簡單、靈活的多。所以使用Promise的正確場景是這樣的:

runAsync1()
.then(function(data){
    console.log(data);
    return runAsync2();
})
.then(function(data){
    console.log(data);
    return runAsync3();
})
.then(function(data){
    console.log(data);
});
//非同步任務1執行完成
//隨便什麼資料1
//非同步任務2執行完成
//隨便什麼資料2
//非同步任務3執行完成
//隨便什麼資料3
複製程式碼

runAsync1、runAsync2、runAsync3長這樣↓

function runAsync1(){
    var p = new Promise(function(resolve, reject){
        //做一些非同步操作
        setTimeout(function(){
            console.log('非同步任務1執行完成');
            resolve('隨便什麼資料1');
        }, 1000);
    });
    return p;            
}
function runAsync2(){
    var p = new Promise(function(resolve, reject){
        //做一些非同步操作
        setTimeout(function(){
            console.log('非同步任務2執行完成');
            resolve('隨便什麼資料2');
        }, 2000);
    });
    return p;            
}
function runAsync3(){
    var p = new Promise(function(resolve, reject){
        //做一些非同步操作
        setTimeout(function(){
            console.log('非同步任務3執行完成');
            resolve('隨便什麼資料3');
        }, 2000);
    });
    return p;            
}
複製程式碼

在then方法中,你也可以直接return資料而不是Promise物件,在後面的then中也可以接收到資料:

runAsync1()
.then(function(data){
    console.log(data);
    return runAsync2();
})
.then(function(data){
    console.log(data);
    return '直接返回資料';  //這裡直接返回資料
})
.then(function(data){
    console.log(data);
});
//非同步任務1執行完成
//隨便什麼資料1
//非同步任務2執行完成
//隨便什麼資料2
//直接返回資料
複製程式碼

reject的用法

前面的例子都是隻有“執行成功”的回撥,還沒有“失敗”的情況,reject的作用就是把Promise的狀態置為rejected,這樣我們在then中就能捕捉到,然後執行“失敗”情況的回撥。

let num = 10;
let p1 = function() {
   	return new Promise((resolve,reject)=>{
      if (num <= 5) {
        resolve("<=5,走resolce")
        console.log('resolce不能結束Promise')
      }else{
        reject(">5,走reject")
        console.log('reject不能結束Promise')
      }
    }) 
}

p1()
  .then(function(data){
    console.log(data)
  },function(err){
    console.log(err)
  })
//reject不能結束Promise
//>5,走reject
複製程式碼

resolve和reject永遠會在當前環境的最後執行,所以後面的同步程式碼會先執行。

如果resolve和reject之後還有程式碼需要執行,最好放在then裡。

然後在resolve和reject前面寫上return。

Promise.prototype.catch()

Promise.prototype.catch方法是.then(null, rejection)的別名,用於指定發生錯誤時的回撥函式。

// 接著上面的例子
p1()
  .then(function(data){
    console.log(data)
  })
  .catch(function(err){
  	console.log(err)
  })
//reject不能結束Promise
//>5,走reject 	
複製程式碼

Promise.all()

Promise.all方法用於將多個 Promise 例項,包裝成一個新的 Promise 例項。

const p = Promise.all([p1, p2, p3]);
複製程式碼

p的狀態由p1、p2、p3決定,分成兩種情況。

  1. 只有p1、p2、p3的狀態都變成fulfilled,p的狀態才會變成fulfilled。 此時p1、p2、p3的返回值組成一個陣列,傳遞給p的回撥函式。
  2. 只要p1、p2、p3之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的例項的返回值,會傳遞給p的回撥函式。

promises是包含 3 個 Promise 例項的陣列,只有這 3 個例項的狀態都變成fulfilled,或者其中有一個變為rejected,才會呼叫Promise.all方法後面的回撥函式。

如果作為引數的 Promise 例項,自己定義了catch方法,那麼它一旦被rejected,並不會觸發Promise.all()的catch方法,如果沒有引數沒有定義自己的catch,就會呼叫Promise.all()的catch方法。

Promise.race()

Promise.race方法同樣是將多個 Promise 例項,包裝成一個新的 Promise 例項。

const p = Promise.race([p1, p2, p3]);
// 上面程式碼中,只要p1、p2、p3之中有一個例項率先改變狀態,p的狀態就跟著改變。
// 那個率先改變的 Promise 例項的返回值,就傳遞給p的回撥函式。
複製程式碼

Promise.resolve()

有時需要將現有物件轉為 Promise 物件,Promise.resolve方法就起到這個作用。

const jsPromise = Promise.resolve('123');
複製程式碼

上面程式碼將123轉為一個 Promise 物件。

Promise.resolve等價於下面的寫法。

Promise.resolve('123')
// 等價於
new Promise(resolve => resolve('123'))
複製程式碼
Promise.resolve方法的引數分成四種情況。
  • 引數是一個 Promise 例項
  • 引數是一個thenable物件

thenable物件指的是具有then方法的物件,比如下面這個物件。

let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};

複製程式碼

Promise.resolve方法會將這個物件轉為 Promise 物件,然後就立即執行thenable物件的then方法。

let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};

let p1 = Promise.resolve(thenable);
p1.then(function(value) {
  console.log(value);  // 42
});
複製程式碼

上面程式碼中,thenable物件的then方法執行後,物件p1的狀態就變為resolved,從而立即執行最後那個then方法指定的回撥函式,輸出 42。

  • 引數不是具有then方法的物件,或根本就不是物件

如果引數是一個原始值,或者是一個不具有then方法的物件,則Promise.resolve方法返回一個新的 Promise 物件,狀態為resolved。

const p = Promise.resolve('Hello');

p.then(function (s){
  console.log(s)
});
// Hello
複製程式碼

上面程式碼生成一個新的 Promise 物件的例項p。由於字串Hello不屬於非同步操作(判斷方法是字串物件不具有 then 方法),返回 Promise 例項的狀態從一生成就是resolved,所以回撥函式會立即執行。Promise.resolve方法的引數,會同時傳給回撥函式。

  • 不帶有任何引數

Promise.resolve方法允許呼叫時不帶引數,直接返回一個resolved狀態的 Promise 物件。

所以,如果希望得到一個 Promise 物件,比較方便的方法就是直接呼叫Promise.resolve方法。

const p = Promise.resolve();

p.then(function () {
  // ...
});
複製程式碼

上面程式碼的變數p就是一個 Promise 物件。

需要注意的是,立即resolve的 Promise 物件,是在本輪“事件迴圈”(event loop)的結束時,而不是在下一輪“事件迴圈”的開始時。

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

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

console.log('one');

// one
// two
// three
複製程式碼

上面程式碼中,setTimeout(fn, 0)在下一輪“事件迴圈”開始時執行,Promise.resolve()在本輪“事件迴圈”結束時執行,console.log('one')則是立即執行,因此最先輸出。

Promise.reject()

Promise.reject(reason)方法也會返回一個新的 Promise 例項,該例項的狀態為rejected。

const p = Promise.reject('出錯了');
// 等同於
const p = new Promise((resolve, reject) => reject('出錯了'))

p.then(null, function (s) {
  console.log(s)
});
// 出錯了

複製程式碼

上面程式碼生成一個 Promise 物件的例項p,狀態為rejected,回撥函式會立即執行。

注意,Promise.reject()方法的引數,會原封不動地作為reject的理由,變成後續方法的引數。這一點與Promise.resolve方法不一致。

const thenable = {
  then(resolve, reject) {
    reject('出錯了');
  }
};

Promise.reject(thenable)
.catch(e => {
  console.log(e === thenable)
})
// true
複製程式碼

上面程式碼中,Promise.reject方法的引數是一個thenable物件,執行以後,後面catch方法的引數不是reject丟擲的“出錯了”這個字串,而是thenable物件。

第十三章 Iterator

####概念 迭代器是一種介面、是一種機制。

為各種不同的資料結構提供統一的訪問機制。任何資料結構只要部署 Iterator 介面,就可以完成遍歷操作(即依次處理該資料結構的所有成員)。

Iterator 的作用有三個:

  1. 為各種資料結構,提供一個統一的、簡便的訪問介面;
  2. 使得資料結構的成員能夠按某種次序排列;
  3. 主要供for...of消費。

Iterator本質上,就是一個指標物件。

過程是這樣的:

(1)建立一個指標物件,指向當前資料結構的起始位置。

(2)第一次呼叫指標物件的next方法,可以將指標指向資料結構的第一個成員。

(3)第二次呼叫指標物件的next方法,指標就指向資料結構的第二個成員。

(4)不斷呼叫指標物件的next方法,直到它指向資料結構的結束位置。

  • 普通函式實現Iterator
function myIter(obj){
  let i = 0;
  return {
    next(){
      let done = (i>=obj.length);
      let value = !done ? obj[i++] : undefined;
      return {
        value,
        done,
      }
    }
  }
}
複製程式碼

原生具備 Iterator 介面的資料結構如下。

  • Array
  • Map
  • Set
  • String
  • 函式的 arguments 物件
  • NodeList 物件

下面的例子是陣列的Symbol.iterator屬性。

let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();

iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }
複製程式碼

下面是另一個類似陣列的物件呼叫陣列的Symbol.iterator方法的例子。

let iterable = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
  console.log(item); // 'a', 'b', 'c'
}
複製程式碼

注意,普通物件部署陣列的Symbol.iterator方法,並無效果。

let iterable = {
  a: 'a',
  b: 'b',
  c: 'c',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
  console.log(item); // undefined, undefined, undefined
}
複製程式碼

字串是一個類似陣列的物件,也原生具有 Iterator 介面。

var someString = "hi";
typeof someString[Symbol.iterator]
// "function"

var iterator = someString[Symbol.iterator]();

iterator.next()  // { value: "h", done: false }
iterator.next()  // { value: "i", done: false }
iterator.next()  // { value: undefined, done: true }
複製程式碼

第十四章 Generator

基本概念

Generator 函式是 ES6 提供的一種非同步程式設計解決方案,語法行為與傳統函式完全不同。

執行 Generator 函式會返回一個遍歷器物件,也就是說,Generator 函式還是一個遍歷器物件生成函式。返回的遍歷器物件,可以依次遍歷 Generator 函式內部的每一個狀態。

  • 跟普通函式的區別
  1. function關鍵字與函式名之間有一個星號;
  2. 函式體內部使用yield表示式,定義不同的內部狀態。
  3. Generator函式不能跟new一起使用,會報錯。
function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();
複製程式碼

上面程式碼定義了一個 Generator 函式helloWorldGenerator,它內部有兩個yield表示式(hello和world),即該函式有三個狀態:hello,world 和 return 語句(結束執行)。

呼叫 Generator 函式後,該函式並不執行,返回的也不是函式執行結果,而是一個指向內部狀態的指標物件,也就是遍歷器物件。

下一步,必須呼叫遍歷器物件的next方法,使得指標移向下一個狀態。也就是說,每次呼叫next方法,內部指標就從函式頭部或上一次停下來的地方開始執行,直到遇到下一個yield表示式(或return語句)為止。換言之,Generator 函式是分段執行的,yield表示式是暫停執行的標記,而next方法可以恢復執行。

ES6 沒有規定,function關鍵字與函式名之間的星號,寫在哪個位置。這導致下面的寫法都能通過。

function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }
複製程式碼

yield 表示式

由於 Generator 函式返回的遍歷器物件,只有呼叫next方法才會遍歷下一個內部狀態,所以其實提供了一種可以暫停執行的函式。yield表示式就是暫停標誌。

遍歷器物件的next方法的執行邏輯如下。

(1)遇到yield表示式,就暫停執行後面的操作,並將緊跟在yield後面的那個表示式的值,作為返回的物件的value屬性值。

(2)下一次呼叫next方法時,再繼續往下執行,直到遇到下一個yield表示式。

(3)如果沒有再遇到新的yield表示式,就一直執行到函式結束,直到return語句為止,並將return語句後面的表示式的值,作為返回的物件的value屬性值。

(4)如果該函式沒有return語句,則返回的物件的value屬性值為undefined。

yield表示式與return語句既有相似之處

都能返回緊跟在語句後面的那個表示式的值。

不同之處

每次遇到yield,函式暫停執行,下一次再從該位置繼續向後執行,而return語句不具備位置記憶的功能。一個函式裡面,只能執行一次(或者說一個)return語句,但是可以執行多次(或者說多個)yield表示式。正常函式只能返回一個值,因為只能執行一次return;Generator 函式可以返回一系列的值,因為可以有任意多個yield。

注意

yield表示式只能用在 Generator 函式裡面,用在其他地方都會報錯。

另外,yield表示式如果用在另一個表示式之中,必須放在圓括號裡面。

console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield 123)); // OK
複製程式碼

與 Iterator 介面的關係

由於 Generator 函式就是遍歷器生成函式,因此可以把 Generator 賦值給物件的Symbol.iterator屬性,從而使得該物件具有 Iterator 介面。

Object.prototype[Symbol.iterator] = function* (){
  for(let i in this){
    yield this[i];
  }
}
//--------------
function* iterEntries(obj) {
  let keys = Object.keys(obj);
  for (let i=0; i < keys.length; i++) {
    let key = keys[i];
    yield [key, obj[key]];
  }
}

let myObj = { foo: 3, bar: 7 };

for (let [key, value] of iterEntries(myObj)) {
  console.log(key, value);
}

複製程式碼

next 方法的引數

function* f() {
  for(var i = 0; true; i++) {
    var reset = yield i;
    if(reset) { i = -1; }
  }
}

var g = f();

g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }
複製程式碼

這個功能有很重要的語法意義。

Generator 函式從暫停狀態到恢復執行,它的上下文狀態(context)是不變的。通過next方法的引數,就有辦法在 Generator 函式開始執行之後,繼續向函式體內部注入值。

function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}

var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
複製程式碼

for...of 迴圈

for...of迴圈可以自動遍歷 Generator 函式時生成的Iterator物件,且此時不再需要呼叫next方法。

function *foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5
複製程式碼
function* fibonacci() {
  let [prev, curr] = [1, 1];
  while(true){
    [prev, curr] = [curr, prev + curr];
    yield curr;
  }
}

for (let n of fibonacci()) {
  if (n > 10000000) break;
  console.log(n);
}
複製程式碼

Generator.prototype.return()

Generator 函式返回的遍歷器物件,還有一個return方法,可以返回給定的值,並且終結遍歷 Generator 函式。

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

var g = gen();

g.next()        // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next()        // { value: undefined, done: true }
複製程式碼

yield*

如果在 Generator 函式內部,呼叫另一個 Generator 函式,預設情況下是沒有效果的。

function* foo() {
  yield 'a';
  yield 'b';
}

function* bar() {
  yield 'x';
  foo();
  yield 'y';
}

for (let v of bar()){
  console.log(v);
}
// "x"
// "y"
複製程式碼

foo和bar都是 Generator 函式,在bar裡面呼叫foo,是不會有效果的。

這個就需要用到yield*表示式,用來在一個 Generator 函式裡面執行另一個 Generator 函式。

function* bar() {
  yield 'x';
  yield* foo();
  yield 'y';
}

// 等同於
function* bar() {
  yield 'x';
  yield 'a';
  yield 'b';
  yield 'y';
}

// 等同於
function* bar() {
  yield 'x';
  for (let v of foo()) {
    yield v;
  }
  yield 'y';
}

for (let v of bar()){
  console.log(v);
}
// "x"
// "a"
// "b"
// "y"
複製程式碼

再來看一個對比的例子。

function* inner() {
  yield 'hello!';
}

function* outer1() {
  yield 'open';
  yield inner();
  yield 'close';
}

var gen = outer1()
gen.next().value // "open"
gen.next().value // 返回一個遍歷器物件
gen.next().value // "close"

function* outer2() {
  yield 'open'
  yield* inner()
  yield 'close'
}

var gen = outer2()
gen.next().value // "open"
gen.next().value // "hello!"
gen.next().value // "close"
複製程式碼

上面例子中,outer2使用了yield*,outer1沒使用。結果就是,outer1返回一個遍歷器物件,outer2返回該遍歷器物件的內部值。

從語法角度看,如果yield表示式後面跟的是一個遍歷器物件,需要在yield表示式後面加上星號,表明它返回的是一個遍歷器物件。這被稱為yield*表示式。

作為物件屬性的 Generator 函式

如果一個物件的屬性是 Generator 函式,可以簡寫成下面的形式。

let obj = {
  * myGeneratorMethod() {
    ···
  }
};
複製程式碼

說實話學習了async await之後Generator 函式基本可以不用了

第十五章 async 函式 (重點這個,賊好用,我專案中基本都用這個了)

ES2017 標準引入了 async 函式,使得非同步操作變得更加方便。

async 函式是 Generator 函式的語法糖。

什麼是語法糖?

意指那些沒有給計算機語言新增新功能,而只是對人類來說更“甜蜜”的語法。語法糖往往給程式設計師提供了更實用的編碼方式,有益於更好的編碼風格,更易讀。不過其並沒有給語言新增什麼新東西。

反向還有語法鹽:

主要目的是通過反人類的語法,讓你更痛苦的寫程式碼,雖然同樣能達到避免程式碼書寫錯誤的效果,但是程式設計效率很低,畢竟提高了語法學習門檻,讓人齁到憂傷。。。

async函式使用時就是將 Generator 函式的星號(*)替換成async,將yield替換成await,僅此而已。

async函式對 Generator 函式的區別:

(1)內建執行器。

Generator 函式的執行必須靠執行器,而async函式自帶執行器。也就是說,async函式的執行,與普通函式一模一樣,只要一行。

(2)更好的語義。

async和await,比起星號和yield,語義更清楚了。async表示函式裡有非同步操作,await表示緊跟在後面的表示式需要等待結果。

(3)正常情況下,await命令後面是一個 Promise 物件。如果不是,會被轉成一個立即resolve的 Promise 物件。

(4)返回值是 Promise。

async函式的返回值是 Promise 物件,這比 Generator 函式的返回值是 Iterator 物件方便多了。你可以用then方法指定下一步的操作。

進一步說,async函式完全可以看作多個非同步操作,包裝成的一個 Promise 物件,而await命令就是內部then命令的語法糖。

錯誤處理

如果await後面的非同步操作出錯,那麼等同於async函式返回的 Promise 物件被reject。

async function f() {
  await new Promise(function (resolve, reject) {
    throw new Error('出錯了');
  });
}

f()
.then(v => console.log(v))
.catch(e => console.log(e))
// Error:出錯了
複製程式碼

上面程式碼中,async函式f執行後,await後面的 Promise 物件會丟擲一個錯誤物件,導致catch方法的回撥函式被呼叫,它的引數就是丟擲的錯誤物件。具體的執行機制,可以參考後文的“async 函式的實現原理”。

防止出錯的方法,也是將其放在try...catch程式碼塊之中。

async function f() {
  try {
    await new Promise(function (resolve, reject) {
      throw new Error('出錯了');
    });
  } catch(e) {
  }
  return await('hello world');
}
複製程式碼

如果有多個await命令,可以統一放在try...catch結構中。

async function main() {
  try {
    const val1 = await firstStep();
    const val2 = await secondStep(val1);
    const val3 = await thirdStep(val1, val2);

    console.log('Final: ', val3);
  }
  catch (err) {
    console.error(err);
  }
}
複製程式碼

應用

var fn = function (time) {
  console.log("開始處理非同步");
  setTimeout(function () {
    console.log(time);
    console.log("非同步處理完成");
    iter.next();
  }, time);

};

function* g(){
  console.log("start");
  yield fn(3000)
  yield fn(500)
  yield fn(1000)
  console.log("end");
}

let iter = g();
iter.next();
複製程式碼

下面是async函式的寫法

var fn = function (time) {
  return new Promise(function (resolve, reject) {
    console.log("開始處理非同步");
    setTimeout(function () {
      resolve();
      console.log(time);
      console.log("非同步處理完成");
    }, time);
  })
};

var start = async function () {
  // 在這裡使用起來就像同步程式碼那樣直觀
  console.log('start');
  await fn(3000);
  await fn(500);
  await fn(1000);
  console.log('end');
};

start();
複製程式碼

第十六章 Class (重點這個,賊好用,我專案中基本都用這個了)

class跟let、const一樣:不存在變數提升、不能重複宣告...

es5物件導向寫法跟傳統的面嚮物件語言(比如 C++ 和 Java)差異很大,很容易讓新學習這門語言的程式設計師感到困惑。

ES6 提供了更接近傳統語言的寫法,引入了 Class(類)這個概念,作為物件的模板。通過class關鍵字,可以定義類。

ES6 的class可以看作只是一個語法糖,它的絕大部分功能,ES5 都可以做到,新的class寫法只是讓物件原型的寫法更加清晰、更像物件導向程式設計的語法而已。

//es5
function Fn(x, y) {
  this.x = x;
  this.y = y;
}

Fn.prototype.add = function () {
  return this.x + this.y;
};
//等價於
//es6
class Fn{
  constructor(x,y){
    this.x = x;
    this.y = y;
  }
  
  add(){
    return this.x + this.y;
  }
}

var F = new Fn(1, 2);
console.log(F.add()) //3
複製程式碼

建構函式的prototype屬性,在 ES6 的“類”上面繼續存在。事實上,類的所有方法都定義在類的prototype屬性上面。

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

  add() {
    // ...
  }

  sub() {
    // ...
  }
}

// 等同於

Fn.prototype = {
  constructor() {},
  add() {},
  sub() {},
};
複製程式碼

類的內部所有定義的方法,都是不可列舉的(non-enumerable),這與es5不同。

//es5
var Fn = function (x, y) {
  // ...
};

Point.prototype.add = function() {
  // ...
};

Object.keys(Fn.prototype)
// ["toString"]
Object.getOwnPropertyNames(Fn.prototype)
// ["constructor","add"]

//es6
class Fn {
  constructor(x, y) {
    // ...
  }

  add() {
    // ...
  }
}

Object.keys(Fn.prototype)
// []
Object.getOwnPropertyNames(Fn.prototype)
// ["constructor","add"]
複製程式碼

嚴格模式

類和模組的內部,預設就是嚴格模式,所以不需要使用use strict指定執行模式。只要你的程式碼寫在類或模組之中,就只有嚴格模式可用。

考慮到未來所有的程式碼,其實都是執行在模組之中,所以 ES6 實際上把整個語言升級到了嚴格模式。

constructor

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

class Fn {
}

// 等同於
class Fn {
  constructor() {}
}
複製程式碼

constructor方法預設返回例項物件(即this),完全可以指定返回另外一個物件。

class Foo {
  constructor() {
    return Object.create(null);
  }
}

new Foo() instanceof Foo
// false
//constructor函式返回一個全新的物件,結果導致例項物件不是Foo類的例項。
複製程式碼

類必須使用new呼叫

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

class Foo {
  constructor() {
    return Object.create(null);
  }
}

Foo()
// TypeError: Class constructor Foo cannot be invoked without 'new'
複製程式碼

Class 表示式

與函式一樣,類也可以使用表示式的形式定義。

const MyClass = class Me {
  getClassName() {
    return Me.name;
  }
};
複製程式碼

上面程式碼使用表示式定義了一個類。需要注意的是,這個類的名字是MyClass而不是Me,Me只在 Class 的內部程式碼可用,指代當前類。

let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined
複製程式碼

如果類的內部沒用到的話,可以省略Me,也就是可以寫成下面的形式。

const MyClass = class { /* ... */ };
複製程式碼

採用 Class 表示式,可以寫出立即執行的 Class。

let person = new class {
  constructor(name) {
    this.name = name;
  }

  sayName() {
    console.log(this.name);
  }
}('張三');

person.sayName(); // "張三"

複製程式碼

上面程式碼中,person是一個立即執行的類的例項。

私有方法和私有屬性

私有方法/私有屬性是常見需求,但 ES6 不提供,只能通過變通方法模擬實現。(以後會實現)

通常是在命名上加以區別。

class Fn {

  // 公有方法
  foo () {
    //....
  }

  // 假裝是私有方法(其實外部還是可以訪問)
  _bar() {
    //....
  }
}
複製程式碼

原型的屬性

class定義類時,只能在constructor裡定義屬性,在其他位置會報錯。

如果需要在原型上定義方法可以使用:

  1. Fn.prototype.prop = value;
  2. Object.getPrototypeOf()獲取原型,再來擴充套件
  3. Object.assign(Fn.prototype,{在這裡面寫擴充套件的屬性或者方法})

Class 的靜態方法

類相當於例項的原型,所有在類中定義的方法,都會被例項繼承。

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

ES6 明確規定,Class 內部只有靜態方法,沒有靜態屬性。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function

//靜態屬性只能手動設定
class Foo {
}

Foo.prop = 1;
Foo.prop // 1
複製程式碼

get、set

class Fn{
	constructor(){
		this.arr = []
	}
	get bar(){
		return this.arr;
	}
	set bar(value){
		this.arr.push(value)
	}
}


let obj = new Fn();

obj.menu = 1;
obj.menu = 2;

console.log(obj.menu)//[1,2]
console.log(obj.arr)//[1,2]
複製程式碼

繼承

  • 用法
class Fn {
}

class Fn2 extends Fn {
}
複製程式碼
  • 注意

子類必須在constructor方法中呼叫super方法,否則新建例項時會報錯。這是因為子類沒有自己的this物件,而是繼承父類的this物件,然後對其進行加工。如果不呼叫super方法,子類就得不到this物件。

class Point { /* ... */ }

class ColorPoint extends Point {
  constructor() {
    super()//必須呼叫
  }
}

let cp = new ColorPoint(); // ReferenceError
複製程式碼

父類的靜態方法也會被繼承。

Object.getPrototypeOf()

Object.getPrototypeOf方法可以用來從子類上獲取父類。

Object.getPrototypeOf(Fn2) === Fn
// true
複製程式碼

因此,可以使用這個方法判斷,一個類是否繼承了另一個類。

super 關鍵字

super這個關鍵字,既可以當作函式使用,也可以當作物件使用。在這兩種情況下,它的用法完全不同。

第一種情況,super作為函式呼叫時,代表父類的建構函式。ES6 要求,子類的建構函式必須執行一次super函式。

作為函式時,super()只能用在子類的建構函式之中,用在其他地方就會報錯。

class A {}

class B extends A {
  constructor() {
    super();
  }
}
複製程式碼

上面程式碼中,子類B的建構函式之中的super(),代表呼叫父類的建構函式。這是必須的,否則 JavaScript 引擎會報錯。

注意,super雖然代表了父類A的建構函式,但是返回的是子類B的例項,即super內部的this指的是B,因此super()在這裡相當於A.prototype.constructor.call(this)。

第二種情況,super作為物件時,在普通方法中,指向父類的原型物件;在靜態方法中,指向父類。

class A {
  p() {
    return 2;
  }
}

class B extends A {
  constructor() {
    super();
    console.log(super.p()); // 2
  }
}

let b = new B();
複製程式碼

上面程式碼中,子類B當中的super.p(),就是將super當作一個物件使用。這時,super在普通方法之中,指向A.prototype,所以super.p()就相當於A.prototype.p()。

由於this指向子類,所以如果通過super對某個屬性賦值,這時super就是this,賦值的屬性會變成子類例項的屬性。

class A {
  constructor() {
    this.x = 1;
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
    super.x = 3;
    console.log(super.x); // undefined
    console.log(this.x); // 3
  }
}

let b = new B();
複製程式碼

上面程式碼中,super.x賦值為3,這時等同於對this.x賦值為3。而當讀取super.x的時候,讀的是A.prototype.x,所以返回undefined。

第十七章 Module

ES6 的模組自動採用嚴格模式,不管你有沒有在模組頭部加上"use strict";。

export 命令

模組功能主要由兩個命令構成:export和import。

export命令用於規定模組的對外介面。

import命令用於輸入其他模組提供的功能。

一個模組就是一個獨立的檔案。該檔案內部的所有變數,外部無法獲取。如果你希望外部能夠讀取模組內部的某個變數,就必須使用export關鍵字輸出該變數。

export輸出變數的寫法:

// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
複製程式碼

還可以:

// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year};
//跟上面寫法等價,推薦這種寫法。
複製程式碼

export命令除了輸出變數,還可以輸出函式或類(class)。

export function multiply(x, y) {
  return x * y;
};
複製程式碼

通常情況下,export輸出的變數就是本來的名字,但是可以使用as關鍵字重新命名。

function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};
複製程式碼

需要特別注意的是,export命令規定的是對外的介面,必須與模組內部的變數建立一一對應關係。

// 報錯
export 1;

// 報錯
var m = 1;
export m;

//正確寫法
// 寫法一
export var m = 1;

// 寫法二
var m = 1;
export {m};

// 寫法三
var n = 1;
export {n as m};
複製程式碼

同樣的,function和class的輸出,也必須遵守這樣的寫法。

// 報錯
function f() {}
export f;

// 正確
export function f() {};

// 正確
function f() {}
export {f};
複製程式碼

export語句輸出的介面,與其對應的值是動態繫結關係,即通過該介面,可以取到模組內部實時的值。

export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
複製程式碼

上面程式碼輸出變數foo,值為bar,500 毫秒之後變成baz。 export命令可以出現在模組的任何位置,只要處於模組頂層就可以。如果處於塊級作用域內,就會報錯,import命令也是如此。

import 命令

使用export命令定義了模組的對外介面以後,其他 JS 檔案就可以通過import命令載入這個模組。

// main.js
import {firstName, lastName, year} from './profile';

function setName(element) {
  element.textContent = firstName + ' ' + lastName;
}
複製程式碼

上面程式碼的import命令,用於載入profile.js檔案,並從中輸入變數。import命令接受一對大括號,裡面指定要從其他模組匯入的變數名。大括號裡面的變數名,必須與被匯入模組(profile.js)對外介面的名稱相同。

如果想為輸入的變數重新取一個名字,import命令要使用as關鍵字,將輸入的變數重新命名

import { lastName as surname } from './profile';
複製程式碼

import後面的from指定模組檔案的位置,可以是相對路徑,也可以是絕對路徑,.js字尾可以省略。

注意,import命令具有提升效果,會提升到整個模組的頭部,首先執行。

foo();

import { foo } from 'my_module';
//import的執行早於foo的呼叫。這種行為的本質是,import命令是編譯階段執行的,在程式碼執行之前。
複製程式碼

由於import是靜態執行,所以不能使用表示式和變數,這些只有在執行時才能得到結果的語法結構。

// 報錯
import { 'f' + 'oo' } from 'my_module';

// 報錯
let module = 'my_module';
import { foo } from module;

// 報錯
if (x === 1) {
  import { foo } from 'module1';
} else {
  import { foo } from 'module2';
}
複製程式碼
import { foo } from 'my_module';
import { bar } from 'my_module';

// 等同於
import { foo, bar } from 'my_module';
複製程式碼
模組的整體載入

除了指定載入某個輸出值,還可以使用整體載入,即用星號(*)指定一個物件,所有輸出值都載入在這個物件上面。

注意,模組整體載入所在的那個物件,不允許執行時改變。下面的寫法都是不允許的。

import * as circle from './circle';

// 下面兩行都是不允許的
circle.foo = 'hello';
circle.area = function () {};
複製程式碼

export default

使用import命令的時候,使用者需要知道所要載入的變數名或函式名,否則無法載入。

為了給使用者提供方便,讓他們不用閱讀文件就能載入模組,就要用到export default命令,為模組指定預設輸出。

// export-default.js
export default function () {
  console.log('foo');
}
複製程式碼

其他模組載入該模組時,import命令可以為該匿名函式指定任意名字。

// import-default.js
import customName from './export-default';
customName(); // 'foo'
複製程式碼

需要注意的是,這時import命令後面,不使用大括號。 export default命令用在非匿名函式前,也是可以的。

// export-default.js
export default function foo() {
  console.log('foo');
}

// 或者寫成

function foo() {
  console.log('foo');
}

export default foo;
複製程式碼

上面程式碼中,foo函式的函式名foo,在模組外部是無效的。載入的時候,視同匿名函式載入。

下面比較一下預設輸出和正常輸出。

// 第一組
export default function crc32() { // 輸出
  // ...
}

import crc32 from 'crc32'; // 輸入

// 第二組
export function crc32() { // 輸出
  // ...
};

import {crc32} from 'crc32'; // 輸入
複製程式碼

上面程式碼的兩組寫法,第一組是使用export default時,對應的import語句不需要使用大括號;第二組是不使用export default時,對應的import語句需要使用大括號。

export default命令用於指定模組的預設輸出。顯然,一個模組只能有一個預設輸出,因此export default命令只能使用一次。所以,import命令後面才不用加大括號,因為只可能唯一對應export default命令。

本質上,export default就是輸出一個叫做default的變數或方法,然後系統允許你為它取任意名字。所以,下面的寫法是有效的。

// modules.js
function add(x, y) {
  return x * y;
}
export {add as default};
// 等同於
// export default add;

// app.js
import { default as foo } from 'modules';
// 等同於
// import foo from 'modules';
複製程式碼

正是因為export default命令其實只是輸出一個叫做default的變數,所以它後面不能跟變數宣告語句。

// 正確
export var a = 1;

// 正確
var a = 1;
export default a;

// 錯誤
export default var a = 1;
複製程式碼

上面程式碼中,export default a的含義是將變數a的值賦給變數default。所以,最後一種寫法會報錯。

同樣地,因為export default命令的本質是將後面的值,賦給default變數,所以可以直接將一個值寫在export default之後。

// 正確
export default 42;

// 報錯
export 42;
複製程式碼

export 與 import 的複合寫法

如果在一個模組之中,先輸入後輸出同一個模組,import語句可以與export語句寫在一起。

export { foo, bar } from 'my_module';

// 等同於
import { foo, bar } from 'my_module';
export { foo, bar };
複製程式碼

模組的介面改名和整體輸出,也可以採用這種寫法。

// 介面改名
export { foo as myFoo } from 'my_module';

// 整體輸出
export * from 'my_module';
複製程式碼

以上就是我對ES6總結的筆記了, 斷斷續續差不多用了一週的時間,很多程式碼因為我專案中也用不到,都要自己先敲一遍看看用法在總結,在加班上班比較忙, 當然我的筆記是總結的很細的,專案中很多都可以用上面的內容進行優化程式碼, 希望我的筆記對你也能有所幫助, 如果有錯誤的地方請你在評論區指出, 非常感謝, 感覺對你有用的話請關注點贊哈!!!

相關文章