摘自 現代 JavaScript 教程;總結自己覺得重要/疏忽/未知的部分,閒來無事時看看,抓耳撓腮時看看。長篇預警!
ECMA-262 規範
最權威的信息來源(語言細節),每年都會發佈一個新版本的規範
🚩最新的規範草案請見 https://tc39.es/ecma262/
🚩最新最前沿的功能,包括“即將納入規範的”(所謂的 “stage 3”),請看這裏的提案 https://github.com/tc39/proposals
現代模式,"use strict"
- 新的特性被加入,舊的功能也沒有改變 這麼做有利於兼容舊代碼,
- 但缺點是 JavaScript 創造者的任何錯誤或不完善的決定也將永遠被保留在 JavaScript 語言中
- 這種情況一直持續到 2009 年 ECMAScript 5 (ES5) 的出現
- ES5 規範增加了新的語言特性並且修改了一些已經存在的特性
- 為了保證舊的功能能夠使用,大部分的修改是默認不生效的
- 【需要一個特殊的指令 —— "use strict" 來明確地激活這些特性】
🚩位置和時機
- 腳本文件的頂部
- 函數體的開頭
- “classes” 和 “modules”自動啓用`'use strict'`
---
- 沒有辦法取消 `'use strict'`
- console控制枱`'use strict'; <Shift+Enter 換行>`
大寫形式的常數
一個普遍的做法是將常量用作別名,以便記住那些在執行之前就已知的難以記住的值
🚩使用大寫字母和下劃線來命名這些常量
例如,讓我們以所謂的“web”(十六進制)格式為顏色聲明常量:
const COLOR_RED = "#F00";
const COLOR_GREEN = "#0F0";
const COLOR_BLUE = "#00F";
const COLOR_ORANGE = "#FF7F00";
// ……當我們需要選擇一個顏色
let color = COLOR_ORANGE;
alert(color); // #FF7F00
好處:
- `COLOR_ORANGE` 比 `"#FF7F00"` 更容易記憶
- 比起 `COLOR_ORANGE` 而言,`"#FF7F00"` 更容易輸錯
- 閲讀代碼時,`COLOR_ORANGE` 比 `#FF7F00` 更易懂
🚩什麼時候該為常量使用大寫命名,什麼時候進行常規命名?讓我們弄清楚一點
- 作為一個“常數”,意味着值永遠不變
- **但是有些常量在執行之前就已知了(比如紅色的十六進制值),還有些在執行期間被“計算”出來,但初始賦值之後就不會改變**
例如:
const pageLoadTime = /* 網頁加載所需的時間 */;
// **`pageLoadTime` 的值在頁面加載之前是未知的,所以採用常規命名,但是它仍然是個常量,因為賦值之後不會改變**
// 換句話説,**大寫命名的常量僅用作“硬編碼(hard-coded)”值的別名**。**當值在執行之前或在被寫入代碼的時候,就知道值是什麼了**。
數據類型
在 JavaScript 中有 8 種基本的數據類型(譯註:**7 種原始類型(基本數據類型)和 1 種引用類型(複雜數據類型)**)
- `number`
- `bigint`
- `string`
- `boolean`
- `null`
- `undefined`
- `symbol`
- `object`
🚩可以通過 typeof 運算符查看存儲在變量中的數據類型
- 兩種形式:`typeof x` 或者 `typeof(x)`
- 以字符串的形式返回類型名稱,例如 `"string"`
- `typeof null` 會返回 `"object"` —— 這是 JavaScript 編程語言的一個錯誤,實際上它並不是一個 object
- `typeof alert` 的結果是 `"function"`。在 JavaScript 語言中沒有一個特別的 `“function”` 類型。函數隸屬於 object 類型。但是 `typeof` 會對函數區分對待,並返回 `"function"`。這也是來自於 JavaScript 語言早期的問題。*從技術上講,這種行為是不正確的,但在實際編程中卻非常方便。*
Number 類型
number 類型代表整數和浮點數;除了常規的數字,還包括所謂的“特殊數值(“special numeric values”)”也屬於這種類型:Infinity、-Infinity 和 NaN。
- `alert( 1 / 0 ); // Infinity`
- `alert( "not a number" / 2 + 5 ); // NaN` **NaN 是粘性的。任何對 NaN 的進一步操作都會返回 NaN**
常用的類型轉換:轉換為 string 類型、轉換為 number 類型和轉換為 boolean 類型
🚩字符串轉換
- 轉換髮生在輸出內容的時候
- 或通過 String(value) 進行顯式轉換
🚩數字型轉換
- 轉換髮生在進行算術操作時
- 或通過 Number(value) 進行顯式轉換
- 規則:
- Number(undefined);// NaN
- Number(null);// 0
- Number(true);// 1
- Number(false);// 0
- Number(str);// 原樣讀取str字符串,忽略兩端空白,空字符串轉換為0,出錯則為NaN
🚩布爾型轉換
- 轉換髮生在進行邏輯操作時
- 可以通過 Boolean(value) 進行顯式轉換
- Boolean(0);//false
- Boolean(null);//false
- Boolean(undefined);//false
- Boolean(NaN);//false
- Boolean("");//false
- Boolean(" ");//true
- Boolean("0");//true
自增/自減
**所有的運算符都有返回值**,自增/自減也不例外
- 前置形式返回一個新的值
- 後置返回原來的值(做加法/減法之前的值)
賦值 = 返回一個值
在 JavaScript 中,大多數運算符都會返回一個值
- 這對於 + 和 - 來説是顯而易見的
- 但對於 = 來説也是如此
🚩語句 x = value 將值 value 寫入 x 然後返回 x。
let a = 1;
let b = 2;
let c = 3 - (a = b + 1);
alert( a ); // 3
alert( c ); // 0
// 上面這個例子,(a = b + 1) 的結果是賦給 a 的值(也就是 3)。然後該值被用於進一步的運算。
// 有時會在 JavaScript 庫中看到它。不過,請不要寫這樣的代碼。這樣的技巧絕對不會使代碼變得更清晰或可讀。
在比較字符串的大小時,JavaScript 會使用“字典(dictionary)”或“詞典(lexicographical)”順序進行判定
換言之,字符串是按字符(母)逐個進行比較的。
例如:
alert( 'Z' > 'A' ); // true
alert( 'Glow' > 'Glee' ); // true
alert( 'Bee' > 'Be' ); // true
🚩字符串的比較算法非常簡單
- 首先比較兩個字符串的首位字符大小
- 如果一方字符較大(或較小),則該字符串大於(或小於)另一個字符串。算法結束。
- 否則,如果兩個字符串的首位字符相等,則繼續取出兩個字符串各自的後一位字符進行比較
- 重複上述步驟進行比較,直到比較完成某字符串的所有字符為止
- 如果兩個字符串的字符同時用完,那麼則判定它們相等,否則未結束(還有未比較的字符)的字符串更大
🚩非真正的字典順序,而是 Unicode 編碼順序
這是因為在 JavaScript 使用的內部編碼表中(Unicode),小寫字母的字符索引值更大
值的比較
避免問題:
- 除了嚴格相等 === 外,其他但凡是有 undefined/null 參與的比較,我們都需要格外小心
- 除非你非常清楚自己在做什麼,否則永遠不要使用 >= > < <= 去比較一個可能為 null/undefined 的變量
- 對於取值可能是 null/undefined 的變量,請按需要分別檢查它的取值情況
邏輯 或 運算符
''一個或運算 || 的鏈,將返回第一個真值,如果不存在真值,就返回該鏈的最後一個值''
返回的值是操作數的初始形式,不會做布爾轉換
🚩與“純粹的、傳統的、僅僅處理布爾值的或運算”相比,這個規則就引起了一些很有趣的用法
一,''獲取變量列表或者表達式中的第一個真值''
// 例如,有變量 firstName、lastName 和 nickName,都是可選的(即可以是 undefined,也可以是假值)。
// 用或運算 || 來選擇有數據的那一個,並顯示出來(如果沒有設置,則用 "Anonymous"):
let firstName = "";
let lastName = "";
let nickName = "SuperCoder";
alert( firstName || lastName || nickName || "Anonymous"); // SuperCoder
// 如果所有變量的值都為假,結果就是 "Anonymous"。
二,''短路求值(Short-circuit evaluation)''
// 或運算符 || 的另一個用途是所謂的“短路求值”。
// 這指的是,|| 對其參數進行處理,直到達到第一個真值,然後立即返回該值,而無需處理其他參數。
//如果操作數不僅僅是一個值,而是一個有副作用的表達式,例如變量賦值或函數調用,那麼這一特性的重要性就變得顯而易見了。
//在下面這個例子中,只會打印第二條信息:
true || alert("not printed");
false || alert("printed");
// 在第一行中,或運算符 || 在遇到 true 時立即停止運算,所以 alert 沒有運行。
// 有時,人們利用這個特性,只在左側的條件為假時才執行命令。
邏輯 與 運算符
與運算返回第一個假值,如果沒有假值就返回最後一個值
返回的值是操作數的初始形式,**不會做布爾轉換**
一,''獲取變量列表或者表達式中的第一個假值''
alert( 1 && 2 && null && 3 ); // null
二,''短路求值(Short-circuit evaluation):如果所有的值都是真值,最後一個值將會被返回''
alert( 1 && 2 && 3 ); // 3,最後一個值
與運算 && 的優先級比或運算 || 要高
邏輯 非 運算符
邏輯非運算符接受一個參數,並按如下運作:
- 將操作數轉化為布爾類型:true/false。
- **返回相反的值**
**兩個非運算 !! 有時候用來將某個值轉化為布爾類型**
空值合併運算符(nullish coalescing operator) ??
- 將值既不是 null 也不是 undefined 的表達式**定義為**“已定義的(defined)
a ?? b 的結果是:
- 如果 a 是已定義的,則結果為 a
- 如果 a 不是已定義的,則結果為 b
- 換句話説,如果第一個參數不是 null/undefined,則 ?? 返回第一個參數。否則,返回第二個參數
result = a ?? b
// 等價於
result = (a !== null && a !== undefined) ? a : b;
🚩場景:
// 1. 為可能是未定義的變量提供一個默認值
let user;
alert(user ?? "Anonymous"); // Anonymous
// 2. 可以使用 ?? 序列從一系列的值中選擇出第一個非 null/undefined 的值
let firstName = null;
let lastName = null;
let nickName = "Supercoder";
// 顯示第一個已定義的值:
alert(firstName ?? lastName ?? nickName ?? "Anonymous"); // Supercoder
🚩 || 和 ?? 之間重要的區別是:
- || 返回第一個 真 值
- ?? 返回第一個 已定義的 值
switch 類型很關鍵
🚩嚴格相等
// 被比較的值必須是相同的類型才能進行匹配
let arg = prompt("Enter a value?")
switch (arg) {
case '0':
case '1':
alert( 'One or zero' );
break;
case '2':
alert( 'Two' );
break;
case 3:
alert( 'Never executes!' );
break;
default:
alert( 'An unknown value' )
}
// 輸入 3,因為 prompt 的結果是字符串類型的 "3",不嚴格相等 === 於數字類型的 3,所以 case 3 不會執行!因此 case 3 部分是一段無效代碼。所以會執行 default 分支。
函數 return 返回值
1.**空值的 return 或沒有 return 的函數返回值為 undefined**
2.**不要在 return 與返回值之間添加新行**
對於 return 的長表達式,可能你會很想將其放在單獨一行
如下所示:
return
(some + long + expression + or + whatever * f(a) + f(b))
但這不行,因為 JavaScript 默認會在 return 之後加上分號。上面這段代碼和下面這段代碼運行流程相同:
return;
(some + long + expression + or + whatever * f(a) + f(b))
因此,實際上它的返回值變成了空值
函數表達式末尾會有個分號?
🚩為什麼函數表達式結尾有一個分號 ; 而函數聲明沒有?
function sayHi() {
// ...
}
let sayHi = function() {
// ...
};
答案很簡單
- 在代碼塊的結尾不需要加分號 ;
- if { ... }
- for { }
- function f { }
- 等語法結構後面都不用加
- 函數表達式是在語句內部的:
- `let sayHi = ...;`
- 作為一個值,它不是代碼塊而是一個賦值語句
- 不管值是什麼,都建議在語句末尾添加分號 ;
- 所以這裏的分號與函數表達式本身沒有任何關係,它只是用於終止語句
Babel :Transpiler And Polyfill
- 當使用語言的一些現代特性時,一些引擎可能無法支持這樣的代碼
- 正如上所述,並不是所有功能在任何地方都有實現
- 這就是 Babel 來拯救的東西
🚩Babel 是一個 transpiler,它將現代的 JavaScript 代碼轉化為以前的標準形式。
實際上,Babel 包含了兩部分
1.第一,用於重寫代碼的 transpiler 程序
- 開發者在自己的電腦上運行它,它以之前的語言標準對代碼進行重寫
- 然後將代碼傳到面向用户的網站
- 像 [webpack](http://webpack.github.io/) 這樣的現代項目構建系統,提供了在每次代碼改變時自動運行 transpiler 的方法,因此很容易集成在開發過程中
2.第二,polyfill
- 新的語言特性可能不僅包括語法結構,還包括新的內建函數
- Transpiler 會重寫代碼,將語法結構轉換為舊的結構
- 但是對於新的內建函數,需要我們去實現
- JavaScript 是一個高度動態化的語言,腳本可以添加/修改任何函數,從而使它們的行為符合現代標準
- 更新/添加新函數的腳本稱為 “polyfill”,它“填補”了缺口,並添加了缺少的實現
🚩兩個有意思的 polyfills
- [core js](https://github.com/zloirock/core-js) 支持很多,允許只包含需要的功能
- [polyfill.io](http://polyfill.io/) 根據功能和用户的瀏覽器,為腳本提供 polyfill 的服務
🚩transpiler 和 polyfill 是必要的
如果要使用現代語言功能,transpiler 和 polyfill 是必要的
尾隨(trailing)或懸掛(hanging)逗號
列表中的最後一個屬性應以逗號結尾:
let user = {
name: "John",
age: 30,
}
- 列表中的最後一個屬性應以逗號結尾,叫做尾隨(trailing)或懸掛(hanging)逗號
- 這樣便於添加、刪除和移動屬性,因為所有的行都是相似的
方括號訪問屬性的靈活性
🚩對於多詞屬性,點操作就不能用了:
// 這將提示有語法錯誤
user.likes birds = true
// JavaScript 理解不了。它認為我們在處理 user.likes,然後在遇到意外的 birds 時給出了語法錯誤。
- 點符號要求 key 是''有效的變量標識符''。這意味着:''不包含空格,不以數字開頭,也不包含特殊字符(允許使用 $ 和 _)''。
- 另一種方法,就是使用方括號,可用於任何字符串
let user = {};
// 設置
user["likes birds"] = true;
// 讀取
alert(user["likes birds"]); // true
// 刪除
delete user["likes birds"];
// 請注意方括號中的字符串要放在引號中,單引號或雙引號都可以
🚩方括號同樣''提供了一種可以通過任意表達式來獲取屬性名的方法'' —— 跟語義上的字符串不同 —— 比如像類似於下面的變量:
let key = "likes birds";
// 跟 user["likes birds"] = true; 一樣
user[key] = true;
🚩''變量 key 可以是程序運行時計算得到的,也可以是根據用户的輸入得到的。然後可以用它來訪問屬性。這給了我們很大的靈活性。''
例如:
let user = {
name: "John",
age: 30
};
let key = prompt("What do you want to know about the user?", "name");
// 訪問變量
alert( user[key] ); // John(如果輸入 "name")
點符號不能以類似的方式使用:
let user = {
name: "John",
age: 30
};
let key = "name";
alert( user.key ) // undefined
計算屬性 && 方括號訪問屬性
🚩在對象字面量中,使用方括號
let fruit = prompt("Which fruit to buy?", "apple");
let bag = {
[fruit]: 5, // 屬性名是從 fruit 變量中得到的
};
alert( bag.apple ); // 5 如果 fruit="apple"
🚩本質上,這跟下面的語法效果相同:
let fruit = prompt("Which fruit to buy?", "apple");
let bag = {};
// 從 fruit 變量中獲取值
bag[fruit] = 5;
屬性命名沒有限制
變量名不能是編程語言的某個保留字,如 “for”、“let”、“return” 等……
🚩但''對象的屬性名並不受此限制'':
// 這些屬性都沒問題
let obj = {
for: 1,
let: 2,
return: 3
};
alert( obj.for + obj.let + obj.return ); // 6
🚩簡而言之,屬性命名沒有限制。
- ''屬性名可以是任何字符串或者 symbol(一種特殊的標誌符類型,將在後面介紹)''
- ''其他類型會被自動地轉換為字符串''
🚩陷阱:
名為 `__proto__ `的屬性。不能將它設置為一個''非對象''的值
let obj = {};
obj.__proto__ = 5; // 分配一個數字
alert(obj.__proto__); // [object Object] — 值為對象,與預期結果不同
對象有順序嗎?
對象有順序嗎?換句話説,如果我們遍歷一個對象,我們獲取屬性的順序是和屬性添加時的順序相同嗎?這靠譜嗎?
- 簡短的回答是:“有特別的順序”:''整數屬性會被進行排序,其他屬性則按照創建的順序顯示''
> 這裏的“整數屬性”指的是一個可以在不做任何更改的情況下與一個整數進行相互轉換的字符串
🚩整數屬性:
let codes = {
"49": "Germany",
"41": "Switzerland",
"44": "Great Britain",
// ..,
"1": "USA"
};
for(let code in codes) {
alert(code); // 1, 41, 44, 49
}
🚩非整數屬性,按照創建時的順序來排序:
let user = {
name: "John",
surname: "Smith"
};
user.age = 25; // 增加一個
// 非整數屬性是按照創建的順序來排列的
for (let prop in user) {
alert( prop ); // name, surname, age
}
🚩利用非整數屬性名來欺騙程序:
let codes = {
"+49": "Germany",
"+41": "Switzerland",
"+44": "Great Britain",
// ..,
"+1": "USA"
};
for (let code in codes) {
alert( +code ); // 49, 41, 44, 1
}
垃圾回收
JavaScript內存管理得重要概念-可達性(Reachability)
- ''可達值:以某種方式可訪問或可用的值''
- ''根(roots)'':固有的可達值的基本集合(這些值明顯不能被釋放):
* 當前函數的局部變量和參數
* 嵌套調用時,當前調用鏈上所有函數的變量與參數
* 全局變量
* 還有一些內部的
''被引用與可訪問(從一個根)不同'':一組相互連接的對象可能整體都不可達
https://zh.javascript.info/ga...
this
💡this 的值是在代碼運行時計算出來的,它取決於代碼上下文
* 如果你經常使用其他的編程語言,那麼你可能已經習慣了“綁定 this”的概念,即在對象中定義的方法總是有指向該對象的 this
* 在 JavaScript 中,this 是“自由”的,它的值是在調用時計算出來的,它的值並不取決於方法聲明的位置,而是取決於在“點符號前”的是什麼對象
* 在運行時對 this 求值的這個概念既有優點也有缺點
- 一方面,函數可以被重用於不同的對象
- 另一方面,更大的靈活性造成了更大的出錯的可能
如果 `obj.f()` 被調用了,則 `this` 在 `f` 函數調用期間是 `obj`
new操作符
🚩new
當一個函數被使用 new 操作符執行時,它按照以下步驟:
- 一個新的空對象被創建並分配給 this
- 函數體執行.通常它會修改 this,為其添加新的屬性
- 返回 this 的值
// `new User(...)` 做的就是類似的事情
function User(name) {
// this = {};(隱式創建)
// 添加屬性到 this
this.name = name;
this.isAdmin = false;
// return this;(隱式返回)
}
🚩 return
- 通常,構造器沒有 return 語句,任務是將所有必要的東西寫入 this,並自動轉換為結果
- 但是,如果這有一個 return 語句,那麼規則就簡單了:
* ''如果 return 返回的是一個對象,則返回這個對象,而不是 this''
* ''如果 return 返回的是一個原始類型,則忽略''
🚩思考題:是否可以創建像 new A() == new B() 這樣的函數 A 和 B?
function A() { ... }
function B() { ... }
let a = new A;
let b = new B;
alert( a == b ); // true
這是構造器的主要目的 —— 實現''可重用的對象創建''代碼
new.target
在一個函數內部,可以使用 new.target 屬性來檢查/判斷該函數是被
- 通過 new 調用的“構造器模式”
- 還是沒被通過 new 調用的“常規模式”
function User() {
alert(new.target);
}
// 不帶 "new":
User(); // undefined
// 帶 "new":
new User(); // function User { ... }
function User(name) {
if (!new.target) { // 如果你沒有通過 new 運行我
return new User(name); // ……我會給你添加 new
}
this.name = name;
}
let john = User("John"); // 將調用重定向到新用户
alert(john.name); // John
?.可選鏈
🚩不存在的屬性”的問題
// 獲取 user.address.street,而該用户恰好沒提供地址信息,會收到一個錯誤:
let user = {}; // 一個沒有 "address" 屬性的 user 對象
alert(user.address.street); // Error!
這是預期的結果
- JavaScript 的工作原理就是這樣的,但是在很多實際場景中,我們''更希望得到的是 undefined 而不是一個錯誤''
可能最先想到的方案是在訪問該值的屬性之前,使用 if 或條件運算符 ? 對該值進行檢查,像這樣:
let user = {};
alert(user.address ? user.address.street : undefined);
……但是不夠優雅,💡''對於嵌套層次更深的屬性就會出現更多次這樣的重複,這就是問題了''
// 例如,讓我們嘗試獲取 user.address.street.name。既需要檢查 user.address,又需要檢查 user.address.street:
let user = {}; // user 沒有 address 屬性
alert(user.address ? user.address.street ? user.address.street.name : null : null);
這樣就''太扯淡了'',並且這可能導致寫出來的代碼很難讓別人理解
更好的實現方式,就是💡使用 && 運算符:
let user = {}; // user 沒有 address 屬性
alert( user.address && user.address.street && user.address.street.name ); // undefined(不報錯
但仍然不夠優雅
🚩可選鏈
''如果可選鏈 ?. 前面的部分是 undefined 或者 null,它會停止運算並返回該部分''
🚩不要過度使用可選鏈
''應該只將 ?. 使用在一些東西`可以不存在(null/undefined)`的地方''
- 例如,如果根據的代碼邏輯,user 對象必須存在,但 address 是可選的,那麼我們應該這樣寫 user.address?.street,而不是這樣 user?.address?.street
- 所以,如果 user 恰巧因為失誤變為 undefined,我們會看到一個編程錯誤並修復它。否則,代碼中的錯誤在不恰當的地方被消除了,這會導致調試更加困難
🚩短路效應
如果 ?. 左邊部分不存在,就會立即停止運算(“短路效應”)
🚩 其它變體:?.(),?.[]
- 可選鏈 ?. 不是一個運算符,而是一個特殊的語法結構
- 它還可以與函數和方括號一起使用
Symbol
🚩Symbol 值表示唯一的標識符
// id1 id2 是 symbol 的一個實例化對象, 描述都為"id"
let id1 = Symbol("id");
let id2 = Symbol("id");
// 描述相同的 Symbol —— 它們不相等
alert(id1 == id2); // false
alert(id1); // 類型錯誤:無法將 Symbol 值自動轉換為字符串。
alert(id1.toString()); // 通過 toString 顯示轉化,現在它有效了
alert(id.description);// 或者獲取 symbol.description 屬性,只顯示描述(description)
🚩“隱藏”屬性
- ''Symbol 允許創建對象的“隱藏”屬性
- 代碼的任何其他部分都不能意外訪問或重寫這些屬性''
例如,💡''如果使用的是屬於第三方代碼的 user 對象,我們想要給它們添加一些標識符''
// 屬於另一個代碼
let user = {
name: "John"
};
// 使用 Symbol("id") 作為鍵
let id = Symbol("id");
user[id] = 1;
// 使用 Symbol 作為鍵來訪問數據
alert( user[id] );
📌使用 Symbol("id") 作為鍵,比起用字符串 "id" 來有什麼好處呢?
- 因為 user 對象屬於其他的代碼,那些代碼也會使用這個對象,所以不應該在它上面直接添加任何字段,這樣很不安全
- 但是添加的 Symbol 屬性不會被意外訪問到,''第三方代碼根本不會看到它'',所以使用 Symbol 基本上不會有問題
- 另外,假設另一個腳本希望在 user 中有自己的標識符,以實現自己的目的
- 這可能是另一個 JavaScript 庫,因此腳本之間完全不瞭解彼此
- 然後該腳本可以創建自己的 Symbol("id")
像這樣:
// ...
let id = Symbol("id");
user[id] = "Their id value";
- 我們的標識符和它們的標識符之間不會有衝突
- 因為 Symbol 總是不同的,即使它們有相同的名字
- ……但如果我們處於同樣的目的,使用字符串 "id" 而不是用 symbol,那麼 就會 ''出現衝突''
例如
let user = { name: "John" };
// 我們的腳本使用了 "id" 屬性。
user.id = "Our id value";
// ……另一個腳本也想將 "id" 用於它的目的……
user.id = "Their id value"
// 砰!無意中被另一個腳本重寫了 id!
🚩 跳過
''Symbol 屬性不參與 for..in 循環''
let id = Symbol("id");
let user = {
name: "John",
age: 30,
[id]: 123
};
for (let key in user) alert(key); // name, age (no symbols)
// 使用 Symbol 任務直接訪問
alert( "Direct: " + user[id] );
- ''Object.keys(xxx)也會忽略'',“隱藏符號屬性”原則的一部分
- 相反,''Object.assign 會同時複製字符串和 symbol 屬性'',
這裏並不矛盾,就是這樣設計的
- 這裏的想法是當克隆或者合併一個 object 時,通常希望'' 所有 ''屬性被複制(包括像 id 這樣的 Symbol)
🚩全局 symbol
`有時想要名字相同的 Symbol 具有相同的實體`
例如,應用程序的不同部分想要訪問的 Symbol "id" 指的是完全相同的屬性。為了實現這一點,可以創建一個 ''全局 Symbol 註冊表''。
要從註冊表中讀取(不存在則創建)Symbol,請使用 Symbol.for(key):
// 從全局註冊表中讀取
let id = Symbol.for("id"); // 如果該 Symbol 不存在,則創建它
// 再次讀取(可能是在代碼中的另一個位置)
let idAgain = Symbol.for("id");
// 相同的 Symbol
alert( id === idAgain ); // true
Symbol 不是 100% 隱藏的
- 內置方法 Object.getOwnPropertySymbols(obj) 允許獲取所有的 Symbol
- 還有一個名為 Reflect.ownKeys(obj) 的方法可以返回一個對象的 所有 鍵,包括 Symbol
所以它們並不是真正的隱藏
使用兩個點來調用一個方法
alert( 123456..toString(36) ); // 2n9c
🚩如果想直接在一個數字上調用一個方法,比如上面例子中的 toString,那麼需要在它後面放置兩個點 ..
如果放置一個點:`123456.toString(36)`,那麼就會出現一個 error,因為 JavaScript 語法隱含了第一個點之後的部分為小數部分
如果再放一個點,那麼 JavaScript 就知道小數部分為空,現在使用該方法
也可以寫成 `(123456).toString(36)`
為什麼 0.1 + 0.2 不等於 0.3?
alert( 0.1 + 0.2 == 0.3 ); // false
alert( 0.1 + 0.2 ); // 0.30000000000000004
- 在十進制數字系統中,可以保證以 10 的整數次冪作為除數能夠正常工作,但是以 3 作為除數則不能(1/3 * 3 = 1 1/3 = 0.3333... 無限循環)
- 也是同樣的原因,在二進制數字系統中,可以保證以 2 的整數次冪作為除數時能夠正常工作,但 1/10 就變成了一個無限循環的二進制小數
- 使用二進制數字系統無法 精確 存儲 0.1 或 0.2,就像沒有辦法將三分之一存儲為十進制小數一樣
- IEEE-754 數字格式通過將數字舍入到最接近的可能數字來解決此問題,這些舍入規則通常不允許看到的“極小的精度損失”,但是它確實存在。
- 不僅僅是 JavaScript
許多其他編程語言也存在同樣的問題
- PHP,Java,C,Perl,Ruby 給出的也是完全相同的結果,因為它們基於的是相同的數字格式
- 有時候我們可以嘗試完全避免小數
- 例如,正在創建一個電子購物網站,那麼可以用角而不是元來存儲價格。但是,如果要打 30% 的折扣呢?
- ''實際上,完全避免小數處理幾乎是不可能的。只需要在必要時剪掉其“尾巴”來對其進行舍入即可''
兩個零
數字內部表示的另一個有趣結果是存在兩個零:
- 0
- -0
這是因為在存儲時,使用一位來存儲符號,因此對於包括零在內的任何數字,可以設置這一位或者不設置
在大多數情況下,這種區別並不明顯,因為運算符將它們視為相同的值
isFinite 和 isNaN
兩個特殊的數值
- Infinity(和 -Infinity)是一個特殊的數值,比任何數值都大(小)
- NaN 代表一個 error
🚩 isNaN
isNaN(value) 將其參數轉換為數字,然後測試它是否為 NaN
alert( isNaN(NaN) ); // true
alert( isNaN("str") ); // true
但是需要這個函數嗎?不能只使用 `=== NaN` 比較嗎?
- 不好意思,這不行
- `值 “NaN” 是獨一無二的,它不等於任何東西,包括它自身`
alert( NaN === NaN ); // false
🚩 isFinite:
isFinite(value) 將其參數轉換為數字,如果是常規數字,則返回 true,而不是 NaN/Infinity/-Infinity
alert( isFinite("15") ); // true
alert( isFinite("str") ); // false,因為是一個特殊的值:NaN
alert( isFinite(Infinity) ); // false,因為是一個特殊的值:Infinity
有時 isFinite 被用於驗證字符串值是否為常規數字
let num = +prompt("Enter a number", '');
// 結果會是 true,除非你輸入的是 Infinity、-Infinity 或不是數字
alert( isFinite(num) );
Object.is
有一個特殊的內建方法 Object.is,它類似於 === 一樣對值進行比較,但它對於`兩種邊緣情況`更可靠:
* 它適用於 `NaN`:`Object.is(NaN,NaN)=== true`,這是件好事。
* 值 `0` 和 `-0` 是不同的:`Object.is(0,-0)=== false`,從技術上講這是對的,因為在內部,數字的符號位可能會不同,即使其他所有位均為零。
- ''在所有其他情況下,Object.is(a,b) 與 a === b 相同。''
- 這種比較方式經常被用在 JavaScript 規範中
- 當內部算法需要比較兩個值是否完全相同時,它使用 Object.is(內部稱為 SameValue)
- https://tc39.es/ecma262/#sec-samevalue
在所有數字函數中,空字符串或僅有空格的字符串均被視為 0
isFinite('');// true
isFinite(' ');// true
Number(''); // 0
Number(' ');//0
+'';//0
+' ';//0
隨機數
🚩從 min 到 max 的隨機數
// 將區間 0…1 中的所有值“映射”為範圍在 min 到 max 中的值
// 1.將 0…1 的隨機數乘以 max-min,則隨機數的範圍將從 0…1 增加到 0..max-min
// 2.將隨機數與 min 相加,則隨機數的範圍將為 min 到 max
function random(min, max) {
return min + Math.random() * (max - min);
}
alert( random(1, 5) );
alert( random(1, 5) );
alert( random(1, 5) );
🚩從 min 到 max 的隨機整數
👉''錯誤的方案''
function randomInteger(min, max) {
let rand = min + Math.random() * (max - min);
return Math.round(rand);
}
alert( randomInteger(1, 3) );
獲得邊緣值 min 和 max 的概率比其他值''低兩倍'';💡因為 Math.round() 從範圍 1..3 中獲得隨機數,並按如下所示進行四捨五入:
values from 1 ... to 1.4999999999 become 1
values from 1.5 ... to 2.4999999999 become 2
values from 2.5 ... to 2.9999999999 become 3
👉''正確的解決方案''
方法一:調整取值範圍的邊界
//為了確保相同的取值範圍,我們可以生成從 0.5 到 3.5 的值,從而將所需的概率添加到取值範圍的邊界
function randomInteger(min, max) {
// 現在範圍是從 (min-0.5) 到 (max+0.5)
let rand = min - 0.5 + Math.random() * (max - min + 1);
return Math.round(rand);
}
alert( randomInteger(1, 3) );
方法二:使用 `Math.floor`
function randomInteger(min, max) {
// here rand is from min to (max+1)
let rand = min + Math.random() * (max + 1 - min);
return Math.floor(rand);
}
alert( randomInteger(1, 3) );
間隔都以這種方式映射
values from 1 ... to 1.9999999999 become 1
values from 2 ... to 2.9999999999 become 2
values from 3 ... to 3.9999999999 become 3
Unicode 規範化形式
http://www.unicode.org/report...
http://www.unicode.org/
代理對
所有常用的字符都是一個 2 字節的代碼
大多數歐洲語言,數字甚至大多數象形文字中的字母都有 2 字節的表示形式
但 2 字節只允許 65536 個組合,這對於表示每個可能的符號是不夠的
所以稀有的符號被稱為“''代理對''”的一對 2 字節的符號編碼
這些符號的''長度是 2'':
alert( '𝒳'.length ); // 2,大寫數學符號 X
alert( '😂'.length ); // 2,笑哭表情
alert( '𩷶'.length ); // 2,罕見的中國象形文字
`String.fromCharCode` 和 `str.charCodeAt`
與
`String.fromCodePoint` 和 `str.codePointAt`,差不多
但是不適用於''代理對''
https://developer.mozilla.org...
JavaScript-Array
''JavaScript 中的數組
- 既可以用作隊列,
- 也可以用作棧''
''允許從首端/末端來添加/刪除元素''
- 這在計算機科學中,允許這樣的操作的數據結構被稱為 雙端隊列(deque)
- https://en.wikipedia.org/wiki/Double-ended_queue
JavaScript-數組誤用的幾種方式
🚩數組誤用的幾種方式
// 1. 添加一個非數字的屬性,比如 :
arr.test = 5。
// 2. 製造空洞,比如:添加 arr[0],然後添加 arr[1000] (它們中間什麼都沒有)。
arr[0] = 'first';
arr[1000] = 'last';
// 3. 以倒序填充數組,比如 arr[1000],arr[999] 等等。
arr[1000] = 1000;
arr[999] = 999;
JavaScript中數組是一種特殊的【對象】
- 請將數組視為作用於 ''有序數據'' 的特殊結構
- ''數組在 JavaScript 引擎內部是經過特殊調整的,使得更好地作用於連續的有序數據,所以請以正確的方式使用數組''
- 如果需要任意鍵值,那很有可能實際上需要的是常規對象 {}
JavaScript - 關於 Array 的 “length”
一,當修改數組的時候,length 屬性會自動更新
let fruits = [];
fruits[123] = "Apple";
alert( fruits.length ); // 124
二,數組的length 屬性是可寫的
// 如果手動增加它,則不會發生任何有趣的事兒。但是如果減少它,數組就會被截斷。該過程是不可逆的,下面是例子:
let arr = [1, 2, 3, 4, 5];
arr.length = 2; // 截斷到只剩 2 個元素
alert( arr ); // [1, 2]
arr.length = 5; // 又把 length 加回來
alert( arr[3] ); // undefined:被截斷的那些數值並沒有回來
📌所以,''清空數組最簡單的方法就是:arr.length = 0''
thisArg
🚩users.filter(user => army.canJoin(user)) 替換為users.filter(army.canJoin, army)
的區別?
用 users.filter(user => army.canJoin(user)) 替換對 users.filter(army.canJoin, army) 的調用
- 前者的使用頻率更高
- 因為對於大多數人來説,它更容易理解
顯式調用迭代器
let str = "Hello";
// 和 for..of 做相同的事
// for (let char of str) alert(char);
let iterator = str[Symbol.iterator]();
while (true) {
let result = iterator.next();
if (result.done) break;
alert(result.value); // 一個接一個地輸出字符
}
Symbol.iterator
let range = {
from: 1,
to: 5
};
希望 for..of 這樣運行:
for(let num of range) ... num=1,2,3,4,5
註釋的 range 的完整實現
let range = {
from: 1,
to: 5
};
// 1. for..of 調用首先會調用這個:
range[Symbol.iterator] = function() {
// ……它返回迭代器對象(iterator object):
// 2. 接下來,for..of 僅與此迭代器一起工作,要求它提供下一個值
return {
current: this.from,
last: this.to,
// 3. next() 在 for..of 的每一輪循環迭代中被調用
next() {
// 4. 它將會返回 {done:.., value :...} 格式的對象
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
};
// 現在它可以運行了!
for (let num of range) {
alert(num); // 1, 然後是 2, 3, 4, 5
}
🚩''注意可迭代對象的核心功能:關注點分離''
- range 自身沒有 next() 方法
- 相反,是通過調用 range[Symbol.iterator]() 創建了另一個對象,即所謂的“迭代器”對象,並且它的 next 會為迭代生成值。
''迭代器對象和與其進行迭代的對象是分開的''
🚩可迭代對象必須實現 Symbol.iterator 方法
* `obj[Symbol.iterator]()` 的結果被稱為 `迭代器(iterator)`。由它處理進一步的迭代過程。
*一個迭代器必須有 `next()` 方法,它返回一個 `{done: Boolean, value: any}` 對象,這裏 `done:true` 表明迭代結束,否則 value 就是下一個值
`Symbol.iterator `方法會被 `for..of `自動調用
內置的可迭代對象例如字符串和數組,都實現了 `Symbol.iterator`
可迭代(iterable)和類數組(array-like)
🚩可迭代(iterable)和類數組(array-like)
Iterable 是實現了 Symbol.iterator 方法的對象
Array-like 是有索引和 length 屬性的對象(所以它們看起來很像數組)
一個可迭代對象也許不是類數組對象。反之亦然,類數組對象可能不可迭代
🚩如果有一個這樣的對象,並想像數組那樣操作它?
有一個全局方法 `Array.from` 可以接受一個`可迭代或類數組的值`,並從中獲取一個“真正的”數組
然後就可以對其調用數組方法了
`Array.from(obj[, mapFn, thisArg])` 將`可迭代對象`或`類數組對象` obj 轉化為`真正的數組 Array`
Map可以使用對象作為鍵
let john = { name: "John" };
// 存儲每個用户的來訪次數
let visitsCountMap = new Map();
// john 是 Map 中的鍵
visitsCountMap.set(john, 123);
alert( visitsCountMap.get(john) ); // 123
- ''使用對象作為鍵是 Map 最值得注意和重要的功能之一''
- 對於字符串鍵,Object(普通對象)也能正常使用,但對於對象鍵則不行
let john = { name: "John" };
let visitsCountObj = {}; // 嘗試使用對象
visitsCountObj[john] = 123; // 嘗試將 john 對象作為鍵
// 是寫成了這樣!
alert( visitsCountObj["[object Object]"] ); // 123
// 因為 `visitsCountObj` 是一個對象,它會將所有的鍵如 john 轉換為字符串,所以得到字符串鍵 `"[object Object]"`
Map 是怎麼比較鍵的?
* Map 使用 SameValueZero 算法來比較鍵是否相等
* 它和嚴格等於 === 差不多,但區別是
- NaN 被看成是等於 NaN所以 NaN 也可以被用作鍵
- 0 不等於 -0
這個算法不能被改變或者自定義
https://tc39.github.io/ecma26...
Map 鏈式調用
每一次 map.set 調用都會返回 map 本身,所以可以進行“鏈式”調用:
map.set('1', 'str1')
.set(1, 'num1')
.set(true, 'bool1');
Map 迭代
可以使用以下三個方法:
一,遍歷所有的鍵
map.keys() —— 遍歷並返回所有的鍵(returns an iterable for keys)
二,遍歷所有的值
map.values() —— 遍歷並返回所有的值(returns an iterable for values)
三,遍歷所有的實體
map.entries() —— 遍歷並返回所有的實體(returns an iterable for entries)
[key, value],for..of 在默認情況下使用的就是這個
Map 使用插入順序
* 迭代的順序與插入值的順序相同
* 與普通的 Object 不同,Map 保留了此順序'
Map 有內置的 forEach 方法
與 Array 類似
let recipeMap = new Map([
['cucumber', 500],
['tomatoes', 350],
['onion', 50]
]);
// 對每個鍵值對 (key, value) 運行 forEach 函數
recipeMap.forEach( (value, key, map) => {
alert(`${key}: ${value}`); // cucumber: 500 etc
});
Object.entries:從對象創建 Map
如果想從一個已有的普通對象(plain object)來創建一個 Map
那麼可以使用內建方法 `Object.entries(obj)`
''該方法返回對象的鍵/值對數組,該數組格式完全按照 Map 所需的格式''
let obj = {
name: "John",
age: 30
};
let map = new Map(Object.entries(obj));
alert( map.get('name') ); // John
* `Object.entries` 返回`鍵/值對數組:[ ["name","John"], ["age", 30] ]`
* 這就是 Map 所需要的格式
Object.fromEntries:從 Map 創建對象
`Object.fromEntries` 方法的作用和`Object.entries(obj)`的使用是相反的
給定一個具有 [key, value] 鍵值對的數組,它會根據給定數組創建一個對象
let prices = Object.fromEntries([
['banana', 1],
['orange', 2],
['meat', 4]
]);
// 現在 prices = { banana: 1, orange: 2, meat: 4 }
alert(prices.orange); // 2
Set 迭代(iteration)
可以使用
- `for..of`
- 或 `forEach`
來遍歷 `Set`
一,使用 `for..of`
let set = new Set(["oranges", "apples", "bananas"]);
for (let value of set) {
alert(value);
}
二,使用 `forEach` 來遍歷
let set = new Set(["oranges", "apples", "bananas"]);
// 於數組 forEach 類似 ,與 Map forEach 相同:
set.forEach((value, valueAgain, set) => {
alert(value);
});
forEach 的回調函數有三個參數:
- 一個 value
- 然後是 同一個值 valueAgain
- 最後是目標對象
沒錯,同一個值在參數裏出現了兩次
* 👍forEach 的回調函數有三個參數,是為了與 Map 兼容 - 底層實現是一致的
🚩Map 中用於迭代的方法在 Set 中也同樣支持:
一,`set.keys()`
set.keys() —— 遍歷並返回所有的值(returns an iterable object for values)
二,`set.values()`
set.values() —— 與 set.keys() 作用相同,這是為了兼容 Map
三,`set.entries()`
set.entries() —— 遍歷並返回所有的實體(returns an iterable object for entries)[value, value],它的存在也是為了兼容 Map
`''Set 和 Map 是兼容的''`
WeakMap 和 Map 的區別
一,WeakMap 的鍵必須是對象,不能是原始值
let weakMap = new WeakMap();
let obj = {};
weakMap.set(obj, "ok"); // 正常工作(以對象作為鍵)
// 不能使用字符串作為鍵
weakMap.set("test", "Whoops"); // Error,因為 "test" 不是一個對象
二,如果在 WeakMap 中使用一個對象作為鍵,並且沒有其他對這個對象的引用 —— 該對象將會被從內存(和Map)中''自動清除''
let john = { name: "John" };
let weakMap = new WeakMap();
weakMap.set(john, "...");
john = null; // 覆蓋引用
// john 被從內存中刪除了!
🚩JavaScript 引擎在值可訪問(並可能被使用)時將其存儲在內存中
let john = { name: "John" };
// 該對象能被訪問,john 是它的引用
// 覆蓋引用
john = null;
// 該對象將會被從內存中清除
通常,當對象、數組這類數據結構在內存中時,它們的子元素,如對象的屬性、數組的元素都是可以訪問的
例如,如果把一個對象放入到數組中,那麼只要這個數組存在,那麼這個對象也就存在,即使沒有其他對該對象的引用
就像這樣:
let john = { name: "John" };
let array = [ john ];
john = null; // 覆蓋引用
// 前面由 john 所引用的那個對象被存儲在了 array 中
// 所以它不會被垃圾回收機制回收
💡類似的,如果使用對象作為常規 Map 的鍵,那麼當 Map 存在時,該對象也將存在;它會佔用內存,並且應該不會被(垃圾回收機制)回收
例如:
let john = { name: "John" };
let map = new Map();
map.set(john, "...");
john = null; // 覆蓋引用
// john 被存儲在了 map 中,
// 我們可以使用 map.keys() 來獲取它
👍WeakMap 在這方面有着根本上的不同;它不會阻止垃圾回收機制對作為鍵的對象(key object)的回收
三,WeakMap 不支持''迭代''以及 `keys()`,`values()` 和 `entries()` 方法;所以沒有辦法獲取 WeakMap 的所有鍵或值
WeakMap 只有以下的方法:
- weakMap.get(key)
- weakMap.set(key, value)
- weakMap.delete(key)
- weakMap.has(key)
從技術上講,WeakMap 的當前元素的數量是[未知的]
- JavaScript 引擎可能清理了其中的垃圾
- 可能沒清理
- 也可能清理了一部分
因此,暫不支持訪問 WeakMap 的所有鍵/值的方法
WeakMap 使用場景:額外的數據
假如正在處理一個“屬於”另一個代碼的一個對象,也可能是第三方庫,並想存儲一些與之相關的數據,那麼這些數據就應該與這個對象【共存亡】
—— 這時候 WeakMap 正是我們所需要的利器👍
- 將這些數據放到 WeakMap 中,並使用該對象作為這些數據的鍵,那麼當該對象被垃圾回收機制回收後,這些數據也會被自動清除
weakMap.set(john, "secret documents");
// 如果 john 消失,secret documents 將會被自動清除
🚩例如,有用於處理用户訪問計數的代碼
收集到的信息被存儲在 map 中:
- 一個用户對象作為鍵,其訪問次數為值
- 當一個用户離開時(該用户對象將被垃圾回收機制回收),這時我們就不再需要他的訪問次數了
使用 Map 的計數函數的例子:
// 📁 visitsCount.js
let visitsCountMap = new Map(); // map: user => visits count
// 遞增用户來訪次數
function countUser(user) {
let count = visitsCountMap.get(user) || 0;
visitsCountMap.set(user, count + 1);
}
/** 其他部分的代碼,可能是使用它的其它代碼 **/
// 📁 main.js
let john = { name: "John" };
countUser(john); // count his visits
// 不久之後,john 離開了
john = null;
/**
現在 john 這個對象應該被垃圾回收,但他仍在內存中,因為它是 visitsCountMap 中的一個鍵
當移除用户時,需要清理 visitsCountMap,否則它將在內存中無限增大。在複雜的架構中,這種清理會成為一項繁重的任務
**/
📌可以通過使用 WeakMap 來避免這樣的問題:
// 📁 visitsCount.js
let visitsCountMap = new WeakMap(); // weakmap: user => visits count
// 遞增用户來訪次數
function countUser(user) {
let count = visitsCountMap.get(user) || 0;
visitsCountMap.set(user, count + 1);
}
/**
現在不需要去清理 visitsCountMap 了。當 john 對象變成不可訪問時,即便它是 WeakMap 裏的一個鍵,它也會連同它作為 WeakMap 裏的鍵所對應的信息一同被從內存中刪除
**/
WeakMap 使用場景:緩存
當一個函數的結果需要被記住(“緩存”)
這樣在後續的對同一個對象的調用時,就可以重用這個被緩存的結果
🚩使用 Map 來存儲結果
/ 📁 cache.js
let cache = new Map();
// 計算並記住結果
function process(obj) {
if (!cache.has(obj)) {
let result = /* calculations of the result for */ obj;
cache.set(obj, result);
}
return cache.get(obj);
}
// 現在我們在其它文件中使用 process()
// 📁 main.js
let obj = {/* 假設我們有個對象 */};
let result1 = process(obj); // 計算完成
// ……稍後,來自代碼的另外一個地方……
let result2 = process(obj); // 取自緩存的被記憶的結果
// ……稍後,我們不再需要這個對象時:
obj = null;
alert(cache.size); // 1(啊!該對象依然在 cache 中,並佔據着內存!)
/**
對於多次調用同一個對象,它只需在第一次調用時計算出結果,之後的調用可以直接從 cache 中獲取。這樣做的缺點是,當不再需要這個對象的時候需要清理 cache
**/
📌用 WeakMap 替代 Map,這個問題便會消失:當對象被垃圾回收時,對應的緩存的結果也會被自動地從內存中清除
// 📁 cache.js
let cache = new WeakMap();
// 計算並記結果
function process(obj) {
if (!cache.has(obj)) {
let result = /* calculate the result for */ obj;
cache.set(obj, result);
}
return cache.get(obj);
}
// 📁 main.js
let obj = {/* some object */};
let result1 = process(obj);
let result2 = process(obj);
// ……稍後,我們不再需要這個對象時:
obj = null;
// 無法獲取 cache.size,因為它是一個 WeakMap,
// 要麼是 0,或即將變為 0
// 當 obj 被垃圾回收,緩存的數據也會被清除
WeakSet 和 Set 的區別
一,只能向 WeakSet 添加對象(而不能是原始值)
二,對象只有在其它某個(些)地方能被訪問的時候,才能留在 set 中
三,跟WeakMap類似 支持 `add`,`has` 和 `delete` 方法,但不支持 `size` 和 `keys()`,並且不可`迭代`
.keys(),.values(),*entries()
`*.keys(),*.values(),*entries()` 對於 `Array` `Map` `Set`是通用的,對於普通對象有所不同
Array Map Set |
Object |
|
|---|---|---|
| 調用語法 | arr.keys() map.keys() set.keys() |
Object.keys(obj),而不是 obj.keys() |
| 返回值 | 可迭代項 | ''“真正的”數組'' |
🚩兩個重要的區別:
一,對於對象使用的調用語法是 `Object.keys(obj)`,而不是 `obj.keys()`
主要原因是''靈活性''
- ''在 JavaScript 中,對象是所有複雜結構的基礎''
- 因此,可能有一個自己創建的對象,比如 data,並實現了它自己的 data.values() 方法
- 同時,依然可以對它調用 Object.values(data) 方法
二,`Object.*` 方法返回的是''“真正的”數組''對象,而不只是一個可迭代項
這主要是歷史原因
🚩會忽略 symbol 屬性
`*.keys(),*.values(),*entries()`會忽略 `symbol` 屬性
就像 `for..in` 循環一樣,這些方法會忽略使用 `Symbol(...)` 作為鍵的屬性
🚩但是,如果也想要 Symbol 類型的鍵,那麼這兒有一個單獨的方法
Object.getOwnPropertySymbols
- https://developer.mozilla.org/zh/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertySymbols
- 它會返回一個只包含 Symbol 類型的鍵的數組
另外,還有一種方法 Reflect.ownKeys(obj)
- https://developer.mozilla.org/zh/docs/Web/JavaScript/Reference/Global_Objects/Reflect/ownKeys
- 它會返回 所有 鍵
數組解構
🚩解構數組的完整語法:
let [item1 = default, item2, ...rest] = array
數組是一個存儲數據的''有序''集合,`因此解構特徵和數據順序相關`
🚩一,“解構”並不意味着“破壞”
let [firstName, surname] = "Ilya Kantor".split(' ');
// 這種語法叫做“解構賦值”,因為它通過將結構中的各元素複製到變量中來達到“解構”的目的。但數組本身是沒有被修改的。
// 這只是下面這些代碼的更精簡的寫法而已:
// let [firstName, surname] = arr;
let firstName = arr[0];
let surname = arr[1];
🚩二,忽略使用逗號的元素
// 數組中不想要的元素也可以通過添加額外的逗號來把它丟棄
// 不需要第二個元素
let [firstName, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
alert( title ); // Consul
🚩三,等號右側可以是任何可迭代對象
let [a, b, c] = "abc"; // ["a", "b", "c"]
let [one, two, three] = new Set([1, 2, 3]);
🚩四,賦值給等號左側的任何內容
// 可以在等號左側使用任何“可以被賦值的”東西。
// 例如,一個對象的屬性:
let user = {};
[user.name, user.surname] = "Ilya Kantor".split(' ');
🚩五,與 .entries() 方法進行循環操作
let user = {
name: "John",
age: 30
};
// 循環遍歷鍵—值對
for (let [key, value] of Object.entries(user)) {
alert(`${key}:${value}`); // name:John, then age:30
}
對於 map 對象也類似:
let user = new Map();
user.set("name", "John");
user.set("age", "30");
for (let [key, value] of user) {
alert(`${key}:${value}`); // name:John, then age:30
}
🚩六,交換變量的典型技巧
let guest = "Jane";
let admin = "Pete";
// 交換值:讓 guest=Pete, admin=Jane
[guest, admin] = [admin, guest];
alert(`${guest} ${admin}`); // Pete Jane(成功交換!)
🚩七,剩餘的 …
let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
alert(name1); // Julius
alert(name2); // Caesar
// 請注意,`rest` 的類型是數組
alert(rest[0]); // Consul
alert(rest[1]); // of the Roman Republic
alert(rest.length); // 2
🚩八,默認值
let [firstName, surname] = [];
alert(firstName); // undefined
alert(surname); // undefined
// 默認值
let [name = "Guest", surname = "Anonymous"] = ["Julius"];
alert(name); // Julius(來自數組的值)
alert(surname); // Anonymous(默認值被使用了)
對象解構
🚩解構對象的完整語法:
let {prop : varName = default, ...rest} = object
對象是''通過鍵來存儲數據項的單個實體'',`因此結構特徵和鍵相關`
🚩一,等號左側包含被解構對象相應屬性的一個“模式(pattern)”
// 在簡單的情況下,等號左側的就是 被解構對象 中的變量名列表
let options = {
title: "Menu",
width: 100,
height: 200
};
let {title, width, height} = options;
alert(title); // Menu
alert(width); // 100
alert(height); // 200
🚩二,剩餘模式(pattern)…
let options = {
title: "Menu",
height: 200,
width: 100
};
// title = 名為 title 的屬性
// rest = 存有剩餘屬性的對象
let {title, ...rest} = options;
// 現在 title="Menu", rest={height: 200, width: 100}
alert(rest.height); // 200
alert(rest.width); // 100
🚩三,嵌套解構
可以在等號左側使用更復雜的模式(pattern)來提取更深層的數據
let options = {
size: {
width: 100,
height: 200
},
items: ["Cake", "Donut"],
extra: true
};
// 為了清晰起見,解構賦值語句被寫成多行的形式
let {
size: { // 把 size 賦值到這裏
width,
height
},
items: [item1, item2], // 把 items 賦值到這裏
title = "Menu" // 在對象中不存在(使用默認值)
} = options;
alert(title); // Menu
alert(width); // 100
alert(height); // 200
alert(item1); // Cake
alert(item2); // Donut
🚩''四,智能函數參數''
有時,一個函數可能有很多參數,其中大部分的參數都是可選的
// 實現這種函數的一個很不好的寫法
function showMenu(title = "Untitled", width = 200, height = 100, items = []) {
// ...
}
// 缺點一:參數的順序
// 缺點二:可讀性會變得很差
📌''可以把所有參數當作一個對象來傳遞,然後函數馬上把這個對象解構成多個變量''
// 傳遞一個對象給函數
let options = {
title: "My menu",
items: ["Item1", "Item2"]
};
// ……然後函數馬上把對象展開成變量
function showMenu({title = "Untitled", width = 200, height = 100, items = []}) {
// title, items – 提取於 options,
// width, height – 使用默認值
alert( `${title} ${width} ${height}` ); // My Menu 200 100
alert( items ); // Item1, Item2
}
showMenu(options);
如果想讓所有的參數都使用默認值,那應該傳遞一個空對象:
showMenu({}); // 不錯,所有值都取默認值
showMenu(); // 這樣會導致錯誤
📌可以通過指定空對象 {} 為整個參數對象的默認值來解決這個問題:
function showMenu({ title = "Menu", width = 100, height = 200 } = {}) {
alert( `${title} ${width} ${height}` );
}
showMenu(); // Menu 100 200
🚩''五,不使用 let 時的陷阱''
以下代碼無法正常運行:
let title, width, height;
// 這一行發生了錯誤
{title, width, height} = {title: "Menu", width: 200, height: 100};
''問題在於 JavaScript 把主代碼流(即不在其他表達式中)的 {...} 當做一個代碼塊。這樣的代碼塊可以用於對語句分組,如下所示:''
{
// 一個代碼塊
let message = "Hello";
// ...
alert( message );
}
''為了告訴 JavaScript 這不是一個代碼塊,可以把整個賦值表達式用括號 (...) 包起來''
let title, width, height;
// 現在就可以了
({title, width, height} = {title: "Menu", width: 200, height: 100});
alert( title ); // Menu
日期和時間
https://zh.javascript.info/date
JSON 方法,toJSON
JSON (JavaScript Object Notation) 是一種數據格式(表示值和對象的通用格式),具有自己的獨立標準和大多數編程語言的庫
[[ RFC 4627 標準中有對其的描述|http://tools.ietf.org/html/rf...]]
JSON 支持
- Objects { ... }
- Arrays [ ... ]
- Primitives:
- strings
- numbers
- boolean (true/false)
- null
* JSON 是語言無關的純數據規範
* 因此一些特定於 JavaScript 的對象屬性會被 JSON.stringify 跳過
- 函數屬性(方法)
- Symbol 類型的屬性
- 存儲 undefined 的屬性
''重要的限制:不得有循環引用''
例如:
let user = {
sayHi() { // 被忽略
alert("Hello");
},
[Symbol("id")]: 123, // 被忽略
something: undefined // 被忽略
};
alert( JSON.stringify(user) ); // {}(空對象)
🚩如何解決?(自定義轉換)
JavaScript 提供序列化(serialize)成 JSON 的方法 JSON.stringify 和解析 JSON 的方法 JSON.parse
這兩種方法都支持用於智能讀/寫的轉換函數
`JSON.stringify(student)` 得到的 json 字符串是一個被稱為'' JSON 編碼(JSON-encoded'' 或 或 ''字符串化(stringified)'' 或 ''編組化(marshalled)'' 的對象''序列化(serialized)''
🚩JSON.stringify 的完整語法:
let json = JSON.stringify(value[, replacer, space])
🚩JSON.parse的完整語法:
let value = JSON.parse(str, [reviver]);
如果一個對象具有 toJSON,那麼它會被 JSON.stringify 調用
Spread 語法
其實 `任何可迭代對象都可以`
🚩Spread 語法內部使用了迭代器來收集元素,與 for..of 的方式相同
let str = "Hello";
alert( [...str] ); // H,e,l,l,o
// 對於一個字符串,for..of 會逐個返回該字符串中的字符,...str 也同理會得到 "H","e","l","l","o" 這樣的結果。隨後,字符列表被傳遞給數組初始化器 [...str]
// 還可以使用 Array.from 來實現,運行結果與 [...str] 相同
let str = "Hello";
// Array.from 將可迭代對象轉換為數組
alert( Array.from(str) ); // H,e,l,l,o
🚩不過 Array.from(obj) 和 [...obj] 存在一個細微的差別
- Array.from 適用於類數組對象也適用於可迭代對象
- Spread 語法只適用於可迭代對象
Spread 語法 Array.from(obj) 的差別
Array.from 適用於類數組對象也適用於可迭代對象
Spread 語法只適用於可迭代對象
支持傳入任意數量參數的內建函數
- Math.max(arg1, arg2, ..., argN) —— 返回入參中的最大值
- Object.assign(dest, src1, ..., srcN) —— 依次將屬性從 src1..N 複製到 dest
closure
閉包是指使用一個''特殊的屬性'' ''[[Environment]]'' 來''記錄函數自身的創建時的環境''的''函數''
- ''特殊的屬性'' ''[[Environment]]''
- ''記錄函數自身的創建時的環境''
- ''函數''
https://zh.javascript.info/cl...
IIFE(immediately-invoked function expressions,IIFE)
// 創建 IIFE 的方法
(function() {
alert("Parentheses around the function");
})();
(function() {
alert("Parentheses around the whole thing");
}());
!function() {
alert("Bitwise NOT operator starts the expression");
}();
+function() {
alert("Unary plus starts the expression");
}();
void function() {
alert("Unary plus starts the expression");
}();
"new Function"
🚩語法:
let func = new Function ([arg1, arg2, ...argN], functionBody);
🚩場景:
使用 new Function 創建函數的應用場景非常特殊
比如在複雜的 Web 應用程序中,需要從服務器獲取代碼或者動態地從模板編譯函數時才會使用
特殊:
如果使用 `new Function` 創建一個函數,那麼該函數的 [[Environment]] 並不指向當前的詞法環境,而是指向全局環境
這一點區別於[[closure]]
,因此,''此類函數無法訪問外部(outer)變量,只能訪問全局變量''
function getFunc() {
let value = "test";
let func = new Function('alert(value)');
return func;
}
getFunc()(); // error: value is not defined
常規行為進行比較:
function getFunc() {
let value = "test";
let func = function() { alert(value); };
return func;
}
getFunc()(); // "test",從 getFunc 的詞法環境中獲取的
📌''這一點實在實際中卻非常實用'':
在將 JavaScript 發佈到生產環境之前,需要使用 壓縮程序(minifier) 對其進行壓縮(刪除多餘的註釋和空格等壓縮代碼 —— 更重要的是,將局部變量命名為較短的變量)
如果使 new Function 可以訪問自身函數以外的變量,它也很有可能無法找到重命名的 userName,這是因為新函數的創建發生在代碼壓縮以後,變量名已經被替換了
調度:setTimeout 和 setInterval
🚩語法:
let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)
let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...)
🚩垃圾回收和 setInterval/setTimeout 回調(callback)
// 當一個函數傳入 setInterval/setTimeout 時,將為其創建一個內部引用,並保存在調度程序中。這樣,即使這個函數沒有其他引用,也能防止垃圾回收器(GC)將其回收
// 在調度程序調用這個函數之前,這個函數將一直存在於內存中
setTimeout(function() {...}, 100);
// 一個副作用:
// 如果函數引用了外部變量(譯註:閉包),那麼只要這個函數還存在,外部變量也會隨之存在。它們可能比函數本身佔用更多的內存
// 💡因此,當不再需要調度函數時,最好通過''定時器標識符(timer identifier)'取消它,即使這是個(佔用內存)很小的函數。
🚩嵌套的 setTimeout
嵌套的 setTimeout 能夠精確地設置兩次執行之間的延時,而 setInterval 卻不能
// setInterval
let i = 1;
setInterval(function() {
// 使用 setInterval 時,func 函數的實際調用間隔要比代碼中設定的時間間隔要短!
func(i++); // 這也是正常的,因為 func 的執行所花費的時間“消耗”了一部分間隔時間
}, 100);
//setTimeout
let i = 1;
setTimeout(function run() {
func(i++);
setTimeout(run, 100);
}, 100);
🚩零延時實際上不為零(在瀏覽器中)
特殊的用法:
setTimeout(func)
setTimeout(func, 0)
在瀏覽器環境下,嵌套定時器的運行頻率是受限制的。根據 HTML5 標準 所講:“''經過 5 重嵌套定時器之後,時間間隔被強制設定為至少 4 毫秒''”
let start = Date.now();
let times = [];
setTimeout(function run() {
times.push(Date.now() - start); // 保存前一個調用的延時
if (start + 100 < Date.now()) alert(times); // 100 毫秒之後,顯示延時信息
else setTimeout(run); // 否則重新調度
});
歷史遺留:
這個限制來自“遠古時代”,並且許多腳本都依賴於此,所以這個機制也就存在至今
對於服務端的 JavaScript,就沒有這個限制,並且還有其他調度即時異步任務的方式。例如 Node.js 的 setImmediate。因此,這個提醒只是針對瀏覽器環境的
屬性標誌和屬性描述符
🚩屬性標誌 :對象(存儲屬性(properties), 鍵值對)還有三個特殊的特性(attributes)(除了value)
- writable — 如果為 true,則值可以被修改,否則它是隻可讀的
- enumerable — 如果為 true,則會被在循環中列出,否則不會被列出
- configurable — 如果為 true,則此特性可以被刪除,這些屬性也可以被修改,否則不可以
🚩查詢屬性描述符對象(屬性的完整信息),使用Object.getOwnPropertyDescriptor
- 屬性描述符對象:它包含值和所有的屬性標誌
語法:
let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
例如:
let user = {
name: "John"
};
let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
alert( JSON.stringify(descriptor, null, 2 ) );
/* 屬性描述符:
{
"value": "John",
"writable": true,
"enumerable": true,
"configurable": true
}
*/
🚩修改屬性標誌,使用 Object.defineProperty
語法:
Object.defineProperty(obj, propertyName, descriptor)
例如:
let user = {};
Object.defineProperty(user, "name", {
value: "John"
});
let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
alert( JSON.stringify(descriptor, null, 2 ) );
/*
{
"value": "John",
"writable": false,
"enumerable": false,
"configurable": false
}
*/
不可配置性(configurable)對 defineProperty 施加了一些限制:
- 不能修改 configurable 標誌
- 不能修改 enumerable 標誌
- 不能將 writable: false 修改為 true(反過來則可以)
- 不能修改訪問者屬性的 get/set(但是如果沒有可以分配它們)
''"configurable: false" 的用途是防止更改和刪除屬性標誌,但是允許更改對象的值''
例如:
let user = {
name: "John"
};
Object.defineProperty(user, "name", {
configurable: false
});
user.name = "Pete"; // 正常工作
delete user.name; // Error
🚩多個屬性接口 Object.defineProperties和Object.getOwnPropertyDescriptors
// 一起使用可以用作克隆對象的標誌屬性
let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));
和for..in的區別:
- for..in 會忽略 symbol 類型的屬性
- Object.getOwnPropertyDescriptors 返回包含 symbol 類型的屬性在內的 所有 屬性描述符
🚩屬性描述符在''單個屬性''的級別上工作,還有一些限制訪問 ''整個對象''的方法
- Object.preventExtensions(obj)
- Object.seal(obj)
- Object.freeze(obj)
...
屬性的 getter 和 setter
🚩兩種種類型的對象屬性:
- 數據屬性
- 訪問器屬性(accessor properties): 本質上是用於獲取和設置值的函數,但從外部代碼來看就像常規屬性
🚩訪問器屬性由 “getter” 和 “setter” 方法表示,在對象字面量中,它們用 get 和 set 表示
- 從外表看,訪問器屬性看起來就像一個普通屬性
- 這就是訪問器屬性的設計思想:不以函數的方式調用,obj.xxx正常讀取 (getter 在幕後運行)
let obj = {
get propName() {
// 當讀取 obj.propName 時,getter 起作用
},
set propName(value) {
// 當執行 obj.propName = value 操作時,setter 起作用
}
};
🚩訪問器屬性的描述符與數據屬性的不同
對於訪問器屬性
- 沒有 value 和 writable
- get 一個沒有參數的函數,在讀取屬性時工作
- set 帶有一個參數的函數,在設置屬性時工作
- enumerable —— 與數據屬性的相同
- configurable —— 與數據屬性的相同
🚩一個屬性要麼是訪問器(具有 get/set 方法),要麼是數據屬性(具有 value),但不能兩者都是
// 在同一個描述符中同時提供 get 和 value,則會出現錯誤
// Error: Invalid property descriptor.
Object.defineProperty({}, 'prop', {
get() {
return 1
},
value: 2
});
🚩訪問器的一大用途
允許隨時通過使用 getter 和 setter 『替換』“正常的”數據屬性,來控制和調整這些屬性的行為
例如:
// 始使用數據屬性 name 和 age 來實現 user 對象
function User(name, age) {
this.name = name;
this.age = age;
}
let john = new User("John", 25);
alert( john.age ); // 25
// ...
// ……但遲早,情況可能會發生變化,可能會決定存儲 birthday,而不是 age,因為它更精確,更方便
function User(name, birthday) {
this.name = name;
this.birthday = birthday;
}
let john = new User("John", new Date(1992, 6, 1));
// ...
// 💡現在應該如何處理仍使用 age 屬性的舊代碼呢?
// 可以嘗試找到所有這些地方並修改它們,但這會花費很多時間
// 而且如果其他很多人都在使用該代碼,那麼可能很難完成所有修改
// ...
// 為 age 添加一個 getter 來解決這個問題
function User(name, birthday) {
this.name = name;
this.birthday = birthday;
// 年齡是根據當前日期和生日計算得出的
Object.defineProperty(this, "age", {
get() {
let todayYear = new Date().getFullYear();
return todayYear - this.birthday.getFullYear();
}
});
}
let john = new User("John", new Date(1992, 6, 1));
alert( john.birthday ); // birthday 是可訪問的
alert( john.age ); // ……age 也是可訪問的
原型繼承(Prototypal inheritance)
原型繼承 是JavaScript語言特性之一 能 實現 【代碼重用】
🚩[[Prototype]]
* 在 JavaScript 中,【對象】有一個特殊的隱藏屬性 [[Prototype]](如規範中所命名的)
- [[Prototype]]要麼為 null
- [[Prototype]]要麼就是對【另一個對象的引用】(該對象被稱為“原型”)
* 當從 object 中讀取一個缺失的屬性時,JavaScript 會自動從原型中獲取該屬性
- 在編程中,這種行為被稱為“原型繼承”
🚩__proto__ 和 [[Prototype]]
* 屬性 [[Prototype]] 是內部的而且是隱藏的,但是有很多設置它的方式(其中之一就是使用特殊的名字 __proto__)
- 引用不能形成閉環。如果試圖在一個閉環中分配 __proto__,JavaScript 會拋出錯誤
- __proto__ 與內部的 [[Prototype]] 不一樣:__proto__ 是 [[Prototype]] 的 getter/setter
- 現代編程語言建議使用函數 Object.getPrototypeOf/Object.setPrototypeOf 來取代 __proto__ 去 get/set 原型
- 根據規範,__proto__ 必須僅受瀏覽器環境的支持。但實際上,包括服務端在內的所有環境都支持它
🚩for..in循環
* for..in 循環也會迭代繼承的屬性
* 幾乎所有其他鍵/值獲取方法都忽略繼承的屬性。例如 Object.keys 和 Object.values 等
F.prototype
* JavaScript中可以使用諸如 new F() 這樣的構造函數來創建一個新對象
- 如果 F.prototype 是一個對象,那麼 new 操作符會使用它為新對象設置 [[Prototype]]
注意:這裏的 F.prototype 指的是 F 的一個名為 "prototype" 的常規屬性
例如:
let animal = {
eats: true
};
function Rabbit(name) {
this.name = name;
}
// 設置 Rabbit.prototype = animal 的字面意思是:“當創建了一個 new Rabbit 時,把它的 [[Prototype]] 賦值為 animal”
Rabbit.prototype = animal;
let rabbit = new Rabbit("White Rabbit"); // rabbit.__proto__ == animal
alert( rabbit.eats ); // true
🚩F.prototype 僅用在 new F 時
* F.prototype 屬性僅在 new F 被調用時使用,它為新對象的 [[Prototype]] 賦值
- 如果在創建之後,F.prototype 屬性有了變化(F.prototype = <another object>),那麼通過 new F 創建的新對象也將隨之擁有新的對象作為 [[Prototype]],但已經存在的對象將保持舊有的值
🚩每個【函數】都有 "prototype" 屬性,即使沒有提供它
- 默認的 "prototype" 是一個只有屬性 constructor 的對象,屬性 constructor 指向函數自身
function Rabbit() {}
// by default:
// Rabbit.prototype = { constructor: Rabbit }
alert( Rabbit.prototype.constructor == Rabbit ); // true
* 可以使用 constructor 屬性來創建一個新對象,該對象使用與現有對象相同的構造器
- 當有一個對象,但不知道它使用了哪個構造器(例如它來自第三方庫),並且需要創建另一個類似的對象時,用這種方法就很方便
例如
function Rabbit(name) {
this.name = name;
alert(name);
}
let rabbit = new Rabbit("White Rabbit");
let rabbit2 = new rabbit.constructor("Black Rabbit");
* F.prototype 的值要麼是一個對象,要麼就是 null:其他值都不起作用
- "prototype" 屬性僅在設置了一個構造函數(constructor function),並通過 new 調用時,才具有這種特殊的影響
例如
// 在常規對象上,prototype 沒什麼特別的
let user = {
name: "John",
prototype: "Bla-bla" // 這裏只是普通的屬性
};
* 默認情況下,【所有函數】都有 F.prototype = {constructor:F}
- 所以可以通過訪問它的 "constructor" 屬性來獲取一個對象的構造器
原生的原型
* 所有的內建對象都遵循相同的模式(pattern)
- 方法都存儲在 prototype 中(Array.prototype、Object.prototype、Date.prototype 等)
- 對象本身只存儲數據(數組元素、對象屬性、日期)
* 原始數據類型也將方法存儲在包裝器對象的 prototype 中:Number.prototype、String.prototype 和 Boolean.prototype
* 只有 undefined 和 null 沒有包裝器對象
* 內建原型可以被修改或被用新的方法填充
- 但是不建議更改它們
- 唯一允許的情況可能是,當添加一個還沒有被 JavaScript 引擎支持,但已經被加入 JavaScript 規範的新標準時,才可能允許這樣做
原型簡史
- 有多少種處理 [[Prototype]] 的方式,答案是有很多!
- 很多種方法做的都是同一件事兒!
🚩為什麼會出現這種情況?這是歷史原因!
* 構造函數的 "prototype" 屬性自古以來就起作用
* 之後,在 2012 年,Object.create 出現在標準中
- 它提供了使用給定原型創建對象的能力
- 但沒有提供 get/set 它的能力
- 因此,許多瀏覽器廠商實現了非標準的 __proto__ 訪問器,該訪問器允許用户隨時 get/set 原型
* 之後,在 2015 年,Object.setPrototypeOf 和 Object.getPrototypeOf 被加入到標準中
- 執行與 __proto__ 相同的功能
- 由於 __proto__ 實際上已經在所有地方都得到了實現,但它已過時,所以被加入到該標準的附件 B 中,即:在非瀏覽器環境下,它的支持是可選的
🚩為什麼將 proto 替換成函數 getPrototypeOf/setPrototypeOf?
__proto__ 是 [[Prototype]] 的 getter/setter,就像其他方法一樣,【它位於 Object.prototype】
🚩如果速度很重要,就請不要修改已存在的對象的 [[Prototype]]
- 從技術上來講,可以在任何時候 get/set [[Prototype]]。但是通常只在創建對象的時候設置它一次,自那之後不再修改
- 並且,JavaScript 引擎對此進行了高度優化。用 Object.setPrototypeOf 或 obj.__proto__= “即時”更改原型是一個非常緩慢的操作,因為它破壞了對象屬性訪問操作的內部優化
- 因此,除非你知道自己在做什麼,或者 JavaScript 的執行速度對你來説完全不重要,否則請避免使用它
🚩Object.create(null)
語法:
Object.create(proto, [descriptors]) // 利用給定的 proto 作為 [[Prototype]](可以是 null)和可選的屬性描述來創建一個空對象
通過 Object.create(null) 來創建沒有原型的對象。這樣的對象被用作 “pure dictionaries” / “very plain” 對象
* 如果要將一個用户生成的鍵放入一個對象,那麼內建的 __proto__ getter/setter 是不安全的
- 因為用户可能會輸入 "__proto__" 作為鍵,這會導致一個 error,雖然希望這個問題不會造成什麼大影響,但通常會造成不可預料的後果
- 因此,可以使用 Object.create(null) 創建一個沒有 __proto__ 的 “very plain” 對象
- 或者對此類場景堅持使用 Map 對象
- 此外,Object.create 提供了一種簡單的方式來淺拷貝一個對象的所有描述符
let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
Class 基本語法
🚩基本的類語法看起來像這樣:
class MyClass {
prop = value; // 屬性; class 字段 prop 會在在每個獨立對象中被設好,而不是設在 Myclass.prototype
prop = () => { // 屬性; class 字段 prop 更優雅的綁定方法
// ...
}
constructor(...) { // 構造器
// ...
}
method(...) {} // method
get something(...) {} // getter 方法
set something(...) {} // setter 方法
[Symbol.iterator]() {} // 有計算名稱(computed name)的方法(此處為 symbol)
// ...
}
- MyClass 是一個函數(提供作為 constructor 的那個)
- methods、getters 和 settors 都被寫入了 MyClass.prototype
- prop 每個實例都有一份
🚩什麼是 class?在 JavaScript 中,類是一種函數
很好的詮釋:
class User {
constructor(name) { this.name = name; }
sayHi() { alert(this.name); }
}
// class 是一個函數
alert(typeof User); // function
// ...或者,更確切地説,是 constructor 方法
alert(User === User.prototype.constructor); // true
// 方法在 User.prototype 中,例如:
alert(User.prototype.sayHi); // alert(this.name);
// 在原型中實際上有兩個方法
alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi
🚩class 不僅僅是語法糖!
1. 通過 class 創建的函數具有特殊的內部屬性標記 [[IsClassConstructor]]: true;編程語言會在許多地方檢查該屬性
例如
// class 必須使用 new 來調用
class User {
constructor() {}
}
alert(typeof User); // function
User(); // Error: Class constructor User cannot be invoked without 'new'
2.大多數 JavaScript 引擎中的類構造器的字符串表示形式都以 “class…” 開頭
js中
class User {
constructor() {}
}
alert(User); // class User { ... }
3.類方法不可枚舉; 類定義將 "prototype" 中的所有方法的 enumerable 標誌設置為 false
如果對一個對象調用 for..in 方法,通常不希望 用 class 方法出現
4 類總是使用 use strict。 在類構造中的所有代碼都將自動進入嚴格模式
🚩類表達式
- 像函數一樣,類可以在另外一個表達式中被定義,被傳遞,被返回,被賦值等
匿名類表達式(類似匿名函數):
let User = class {
sayHi() {
alert("Hello");
}
};
“命名類表達式(Named Class Expression)”(類似於命名函數表達式(Named Function Expressions):
// “命名類表達式(Named Class Expression)”
// (規範中沒有這樣的術語,但是它和命名函數表達式類似)
let User = class MyClass {
sayHi() {
alert(MyClass); // MyClass 這個名字僅在類內部可見
}
};
new User().sayHi(); // 正常運行,顯示 MyClass 中定義的內容
alert(MyClass); // error,MyClass 在外部不可見;名字僅在類內部可見
類繼承
🚩擴展一個類:class Child extends Parent
* 在內部,關鍵字 extends 使用了很好的舊的原型機制進行工作
- 它將 Child.prototype.[[Prototype]] 設置為 Parent.prototype
在 extends 後允許任意表達式:
function f(phrase) {
return class {
sayHi() { alert(phrase); }
};
}
class User extends f("Hello") {}
new User().sayHi(); // Hello
// 這對於高級編程模式,例如當根據許多條件使用函數生成類,並繼承它們時來説可能很有用
🚩重寫一個方法
* 默認情況下,所有未在 class child 中指定的方法均從 class Parent 中直接獲取
- 有時不希望完全替換父類的方法,而是希望在父類方法的基礎上進行調整或擴展其功能
Class 為此提供了 "super" 關鍵字:
- 執行 super.method(...) 來調用一個父類方法
- 執行 super(...) 來調用一個父類 constructor(只能在子類的 constructor 中)
補充:
- 箭頭函數沒有 super 和 this
🚩重寫一個 constructor
根據 規範,如果一個類擴展了另一個類並且沒有 constructor,那麼將生成下面這樣的 constructor:
class Child extends Parent {
// 為沒有自己的 constructor 的擴展類生成的
constructor(...args) {
super(...args);
}
}
''繼承類的 constructor 必須調用 super(...),並且 (!) 一定要在使用 this 之前調用''
💡為什麼呢?
* 在 JavaScript 中,繼承類(所謂的“派生構造器”,英文為 “derived constructor”)的構造函數與其他函數之間是有區別的
- 派生構造器具有特殊的內部屬性 [[ConstructorKind]]:"derived";這是一個特殊的內部標籤
該標籤會影【響它的 new 行為】:
- 當通過 new 執行一個常規函數時,它將創建一個空對象,並將這個空對象賦值給 this
- 但是,當繼承的 constructor 執行時,它不會執行此操作;它期望父類的 constructor 來完成這項工作
* 因此,派生的 constructor 必須調用 super 才能執行其父類(base)的 constructor,否則 this 指向的那個對象將不會被創建
😨重寫類字段: 一個棘手的注意要點;可以重寫方法,也可以重寫字段:
class Animal {
name = 'animal';
constructor() {
alert(this.name); // (*)
}
}
class Rabbit extends Animal {
name = 'rabbit';
}
new Animal(); // animal
new Rabbit(); // animal
// 兩種情況下:new Animal() 和 new Rabbit(),在 (*) 行的 alert 都打印了 animal
// 有點懵逼,用方法來進行比較:
class Animal {
showName() { // 而不是 this.name = 'animal'
alert('animal');
}
constructor() {
this.showName(); // 而不是 alert(this.name);
}
}
class Rabbit extends Animal {
showName() {
alert('rabbit');
}
}
new Animal(); // animal
new Rabbit(); // rabbit
// 請注意:這時的輸出是不同的
// 這才是本來所期待的結果。當父類構造器在派生的類中被調用時,它會使用被重寫的方法;……但對於類字段並非如此。正如前文所述,父類構造器總是使用父類的字段
為什麼會有這樣的區別呢?
原因在於類字段初始化的順序:
- 對於基類(還未繼承任何東西的那種),在構造函數調用前初始化
- 對於派生類,在 super() 後立刻初始化
''這種字段與方法之間微妙的區別只特定於 JavaScript;這種行為僅在一個被重寫的字段被父類構造器使用時才會顯現出來;可以通過使用方法或者 getter/setter 替代類字段,來修復這個問題''
🚩深入地研究 super [[HomeObject]]
- 當一個對象方法執行時,它會將當前對象作為 this
- 隨後如果調用 super.method(),那麼引擎需要從當前對象的原型中獲取 method
😨super怎麼做到的?看似容易,其實並不簡單!
使用普通對象演示一下:
let animal = {
name: "Animal",
eat() {
alert(`${this.name} eats.`);
}
};
let rabbit = {
__proto__: animal,
name: "Rabbit",
eat() {
// 這就是 super.eat() 可以大概工作的方式
this.__proto__.eat.call(this); // (*)
}
};
rabbit.eat(); // Rabbit eats.
''this.__proto__.eat() 將在原型的上下文中執行 eat,而非當前對象''
let animal = {
name: "Animal",
eat() {
alert(`${this.name} eats.`);
}
};
let rabbit = {
__proto__: animal,
eat() {
// ...bounce around rabbit-style and call parent (animal) method
this.__proto__.eat.call(this); // (*)
}
};
let longEar = {
__proto__: rabbit,
eat() {
// ...do something with long ears and call parent (rabbit) method
this.__proto__.eat.call(this); // (**)
}
};
longEar.eat(); // Error: Maximum call stack size exceeded
- 在 (*) 和 (**) 這兩行中,this 的值都是當前對象(longEar);這是至關重要的一點:所有的對象方法都將當前對象作為 this,而非原型或其他什麼東西
- 因此,在 (*) 和 (**) 這兩行中,this.__proto__ 的值是完全相同的:都是 rabbit。它們倆都調用的是 rabbit.eat,它們在不停地循環調用自己,而不是在原型鏈上向上尋找方法
// 1.在 longEar.eat() 中,(**) 這一行調用 rabbit.eat 併為其提供 this=longEar
// 在 longEar.eat() 中我們有 this = longEar
this.__proto__.eat.call(this) // (**)
// 變成了
longEar.__proto__.eat.call(this)
// 也就是
rabbit.eat.call(this);
// 2.之後在 rabbit.eat 的 (*) 行中,希望將函數調用在原型鏈上向更高層傳遞,但是 this=longEar,所以 this.__proto__.eat 又是 rabbit.eat!
// 在 rabbit.eat() 中我們依然有 this = longEar
this.__proto__.eat.call(this) // (*)
// 變成了
longEar.__proto__.eat.call(this)
// 或(再一次)
rabbit.eat.call(this);
//3. ……所以 rabbit.eat 在不停地循環調用自己,因此它無法進一步地提升
😭這個問題沒法僅僅通過使用 this 來解決!!!
🚩為了提供解決方法,JavaScript 為函數添加了一個特殊的內部屬性:[[HomeObject]]
- 當一個函數被定義為類或者對象方法時,它的 [[HomeObject]] 屬性就成為了該對象
- 然後 super 使用它來解析(resolve)父原型及其方法
看它是怎麼工作的(對於普通對象)
let animal = {
name: "Animal",
eat() { // animal.eat.[[HomeObject]] == animal
alert(`${this.name} eats.`);
}
};
let rabbit = {
__proto__: animal,
name: "Rabbit",
eat() { // rabbit.eat.[[HomeObject]] == rabbit
super.eat();
}
};
let longEar = {
__proto__: rabbit,
name: "Long Ear",
eat() { // longEar.eat.[[HomeObject]] == longEar
super.eat();
}
};
// 正確執行
longEar.eat(); // Long Ear eats.
🚩方法並不是“自由”的
* 函數通常都是“自由”的,並沒有綁定到 JavaScript 中的對象。正因如此,它們可以在對象之間複製,並用另外一個 this 調用它。
- [[HomeObject]] 的存在違反了上述原則,因為方法記住了它們的對象
- [[HomeObject]] 不能被更改,所以這個綁定是永久的
- 在 JavaScript 語言中 [[HomeObject]] 僅被用於 super;所以,如果一個方法不使用 super,那麼仍然可以視它為自由的並且可在對象之間複製;但是用了 super 再這樣做可能就會出錯
錯誤示範
let animal = {
sayHi() {
alert(`I'm an animal`);
}
};
// rabbit 繼承自 animal
let rabbit = {
__proto__: animal,
sayHi() {
super.sayHi();
}
};
let plant = {
sayHi() {
alert("I'm a plant");
}
};
// tree 繼承自 plant
let tree = {
__proto__: plant,
sayHi: rabbit.sayHi // (*)
};
tree.sayHi(); // I'm an animal (?!?)
🚩方法,不是函數屬性
- [[HomeObject]] 是為類和普通對象中的方法定義的。但是對於對象而言,方法必須確切指定為 method(),而不是 "method: function()"
- 這個差別對開發者來説可能不重要,但是對 JavaScript 來説卻非常重要
錯誤示範
let animal = {
eat: function() { // 這裏是故意這樣寫的,而不是 eat() {...
// ...
}
};
let rabbit = {
__proto__: animal,
eat: function() {
super.eat();
}
};
rabbit.eat(); // 錯誤調用 super(因為這裏沒有 [[HomeObject]])
靜態屬性和靜態方法
🚩靜態方法
把一個方法賦值給類的函數本身,而不是賦給它的 "prototype"
這樣的方法被稱為 靜態的(static)
class User {
static staticMethod() {
alert(this === User);
}
}
User.staticMethod(); // true
// 和作為屬性賦值的作用相同
class User { }
User.staticMethod = function() {
alert(this === User);
};
User.staticMethod(); // true
靜態方法被用於實現屬於整個類的功能;它與具體的類實例無關
🚩靜態屬性
靜態屬性類似靜態方法
class Article {
static publisher = "Levi Ding";
}
alert( Article.publisher ); // Levi Ding
// 等同於直接給 Article 賦值:
Article.publisher = "Levi Ding";
靜態屬性被用於想要存儲類級別的數據時,而不是綁定到實例
🚩繼承靜態屬性和方法
- 靜態屬性和方法是可被繼承的
- 繼承對常規方法和靜態方法都有效
class Animal {
static planet = "Earth";
constructor(name, speed) {
this.speed = speed;
this.name = name;
}
run(speed = 0) {
this.speed += speed;
alert(`${this.name} runs with speed ${this.speed}.`);
}
static compare(animalA, animalB) {
return animalA.speed - animalB.speed;
}
}
// 繼承於 Animal
class Rabbit extends Animal {
hide() {
alert(`${this.name} hides!`);
}
}
let rabbits = [
new Rabbit("White Rabbit", 10),
new Rabbit("Black Rabbit", 5)
];
rabbits.sort(Rabbit.compare);
rabbits[0].run(); // Black Rabbit runs with speed 5.
alert(Rabbit.planet); // Earth
它是如何工作的?再次,使用原型😱。extends 讓 Rabbit 的 [[Prototype]] 指向了 Animal
Rabbit extends Animal 創建了兩個 [[Prototype]] 引用:
- 1. Rabbit 函數原型繼承自 Animal 函數
- 2. Rabbit.prototype 原型繼承自 Animal.prototype
校驗
class Animal {}
class Rabbit extends Animal {}
// 對於靜態的
alert(Rabbit.__proto__ === Animal); // true
// 對於常規方法
alert(Rabbit.prototype.__proto__ === Animal.prototype); // true
私有的和受保護的屬性和方法
🚩就面向對象編程(OOP)而言,內部接口與外部接口的劃分被稱為 [[封裝|https://en.wikipedia.org/wiki...(computer_programming)]]
封裝具有以下優點:
1. 保護用户,使他們不會誤傷自己
如果一個 class 的使用者想要改變那些本不打算被從外部更改的東西 —— 後果是不可預測的
2. 可支持性
如果嚴格界定內部接口,那麼這個 class 的開發人員可以自由地更改其內部屬性和方法,甚至無需通知用户
對於用户來説,當新版本問世時,應用的內部可能被進行了全面檢修,但如果外部接口相同,則仍然很容易升級
3. 隱藏複雜性
當實施細節被隱藏,並提供了簡單且有據可查的外部接口時,總是很方便的
🚩為了隱藏內部接口,JavaScript使用受保護的或私有的屬性
- 受保護的字段以 _ 開頭;這是一個眾所周知的約定,不是在語言級別強制執行的;程序員應該只通過它的類和從它繼承的類中訪問以 _ 開頭的字段
- 私有字段以 # 開頭;JavaScript 確保我們只能從類的內部訪問它們
受保護
class CoffeeMachine {
_waterAmount = 0;
set waterAmount(value) {
if (value < 0) throw new Error("Negative water");
this._waterAmount = value;
}
get waterAmount() {
return this._waterAmount;
}
constructor(power) {
this._power = power;
}
}
// 創建咖啡機
let coffeeMachine = new CoffeeMachine(100);
// 加水
coffeeMachine.waterAmount = -10; // Error: Negative water
// 受保護的屬性通常以下劃線 _ 作為前綴;一個眾所周知的約定,即不應該從外部訪問此類型的屬性和方法
// 也可使用 get.../set... 函數
class CoffeeMachine {
_waterAmount = 0;
setWaterAmount(value) {
if (value < 0) throw new Error("Negative water");
this._waterAmount = value;
}
getWaterAmount() {
return this._waterAmount;
}
}
new CoffeeMachine().setWaterAmount(100);
// 函數更靈活(可以接受多個參數); ,get/set 語法更短
只讀
class CoffeeMachine {
// ...
constructor(power) {
this._power = power;
}
get power() {
return this._power;
}
}
// 創建咖啡機
let coffeeMachine = new CoffeeMachine(100);
alert(`Power is: ${coffeeMachine.power}W`); // 功率是:100W
coffeeMachine.power = 25; // Error(沒有 setter)
// 只能被在創建時進行設置,之後不再被修改;只需要設置 getter,而不設置 setter
擴展內建類
🚩內建的類,例如 Array,Map 等也都是可以擴展的(extendable)
// 給 PowerArray 新增了一個方法(可以增加更多)
class PowerArray extends Array {
isEmpty() {
return this.length === 0;
}
}
let arr = new PowerArray(1, 2, 5, 10, 50);
alert(arr.isEmpty()); // false
let filteredArr = arr.filter(item => item >= 10);
alert(filteredArr); // 10, 50
alert(filteredArr.isEmpty()); // false
// 💡注意一個非常有趣的事兒!
// 內建的方法例如 filter,map 等 — 返回的正是子類 PowerArray 的新對象;它們內部使用了對象的 constructor 屬性來實現這一功能
// arr.constructor === PowerArray
🚩如果希望像 map 或 filter 這樣的內建方法返回常規數組,可以在 Symbol.species 中返回 Array
class PowerArray extends Array {
isEmpty() {
return this.length === 0;
}
// 內建方法將使用這個作為 constructor
static get [Symbol.species]() {
return Array;
}
}
let arr = new PowerArray(1, 2, 5, 10, 50);
alert(arr.isEmpty()); // false
// filter 使用 arr.constructor[Symbol.species] 作為 constructor 創建新數組
let filteredArr = arr.filter(item => item >= 10);
// filteredArr 不是 PowerArray,而是 Array
alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function
其他集合的工作方式類似;例如 Map 和 Set 的工作方式類似。它們也使用 Symbol.species;
🚩內建類沒有靜態方法繼承
- 內建對象有它們自己的靜態方法,例如 Object.keys,Array.isArray 等
- 原生的類互相擴展,例如,Array 擴展自 Object
- 通常,當一個類擴展另一個類時,靜態方法和非靜態方法都會被繼承
- 但內建類卻是一個例外,它們相互間【不繼承靜態方法】
類型檢查方法
🚩類型檢查方法
- typeof 原始數據類型;返回string
- {}.toString 原始數據類型,內建對象,包含 Symbol.toStringTag 屬性的對象;返回string
- instanceof 對象;返回true/false
🚩instanceof 操作符
語法:
obj instanceof Class
- 通常,instanceof 在檢查中會將原型鏈考慮在內
- 此外,還可以在靜態方法 Symbol.hasInstance 中設置自定義邏輯
🚩obj instanceof Class 算法的執行過程大致如下
// 1.如果這兒有靜態方法 Symbol.hasInstance,那就直接調用這個方法
// 設置 instanceOf 檢查
// 並假設具有 canEat 屬性的都是 animal
class Animal {
static [Symbol.hasInstance](obj) {
if (obj.canEat) return true;
}
}
let obj = { canEat: true };
alert(obj instanceof Animal); // true:Animal[Symbol.hasInstance](obj) 被調用
// 2. 大多數 class 沒有 Symbol.hasInstance。在這種情況下,標準的邏輯是:使用 obj instanceOf Class 檢查 Class.prototype 是否等於 obj 的原型鏈中的原型之一
obj.__proto__ === Class.prototype?
obj.__proto__.__proto__ === Class.prototype?
obj.__proto__.__proto__.__proto__ === Class.prototype?
...
// 如果任意一個的答案為 true,則返回 true
// 否則,如果我們已經檢查到了原型鏈的尾端,則返回 false
🚩 objA.isPrototypeOf(objB)
- 如果 objA 處在 objB 的原型鏈中,則返回 true
- 可以將 obj instanceof Class 檢查改為 Class.prototype.isPrototypeOf(obj)
🚩福利:使用 Object.prototype.toString 方法來揭示類型
可以將Object.prototype.toString 方法作為 typeof 的增強版或者 instanceof 的替代方法來使用
按照 [[規範 |https://tc39.github.io/ecma26...]]所講,內建的 toString 方法可以被從對象中提取出來,並在任何其他值的上下文中執行。其結果取決於該值
// 方便起見,將 toString 方法複製到一個變量中
let objectToString = Object.prototype.toString;
// 它是什麼類型的?
let arr = [];
alert( objectToString.call(arr) ); // [object Array]
// 💡其結果取決於該值
// 對於 number 類型,結果是 [object Number]
// 對於 boolean 類型,結果是 [object Boolean]
// 對於 null:[object Null]
// 對於 undefined:[object Undefined]
// 對於數組:[object Array]
// ……等(可自定義)
// 💡Symbol.toStringTag
// 可以使用特殊的對象屬性 Symbol.toStringTag 自定義對象的 toString 方法的行為
let user = {
[Symbol.toStringTag]: "User"
};
alert( {}.toString.call(user) ); // [object User]
Mixin 模式
* Mixin — 是一個通用的面向對象編程術語:一個包含其他類的方法的類
* 一些其它編程語言允許多重繼承。JavaScript 不支持多重繼承,但是可以通過將方法拷貝到原型中來實現 mixin
🚩EventMixin : 可以使用 mixin 作為一種通過添加多種行為來擴充類的方法 例如:事件處理
let eventMixin = {
/**
* 訂閲事件,用法:
* menu.on('select', function(item) { ... }
*/
on(eventName, handler) {
if (!this._eventHandlers) this._eventHandlers = {};
if (!this._eventHandlers[eventName]) {
this._eventHandlers[eventName] = [];
}
this._eventHandlers[eventName].push(handler);
},
/**
* 取消訂閲,用法:
* menu.off('select', handler)
*/
off(eventName, handler) {
let handlers = this._eventHandlers?.[eventName];
if (!handlers) return;
for (let i = 0; i < handlers.length; i++) {
if (handlers[i] === handler) {
handlers.splice(i--, 1);
}
}
},
/**
* 生成具有給定名稱和數據的事件
* this.trigger('select', data1, data2);
*/
trigger(eventName, ...args) {
if (!this._eventHandlers?.[eventName]) {
return; // 該事件名稱沒有對應的事件處理程序(handler)
}
// 調用事件處理程序(handler)
this._eventHandlers[eventName].forEach(handler => handler.apply(this, args));
}
};
用法:
// 創建一個 class
class Menu {
choose(value) {
this.trigger("select", value);
}
}
// 添加帶有事件相關方法的 mixin
Object.assign(Menu.prototype, eventMixin);
let menu = new Menu();
// 添加一個事件處理程序(handler),在被選擇時被調用:
menu.on("select", value => alert(`Value selected: ${value}`));
// 觸發事件 => 運行上述的事件處理程序(handler)並顯示:
// 被選中的值:123
menu.choose("123");
錯誤處理,"try..catch"
- 通常,如果發生錯誤,腳本就會“死亡”(立即停止),並在控制枱將錯誤打印出來。
- 但是有一種語法結構 try..catch,它可以“捕獲(catch)”錯誤,因此腳本可以執行更合理的操作,而不是死掉
🚩語法
try {
// 執行此處代碼
} catch(err) {
// 如果發生錯誤,跳轉至此處
// err 是一個 error 對象
} finally {
// 無論怎樣都會在 try/catch 之後執行
}
- 可能會沒有 catch 部分或者沒有 finally,所以 try..catch 或 try..finally 都是可用的
Error 對象包含下列屬性:
- message — 人類可讀的 error 信息
- name — 具有 error 名稱的字符串(Error 構造器的名稱)
- stack(沒有標準,但得到了很好的支持)— Error 發生時的調用棧
- 如果不需要 error 對象,可以通過使用 catch { 而不是 catch(err) { 來省略它
- 可以使用 throw 操作符來生成自定義的 error。從技術上講,throw 的參數可以是任何東西,但通常是繼承自內建的 Error 類的 error 對象
🚩try..catch 僅對運行時的 error 有效
// 在“計劃的(scheduled)”代碼中發生異常,則 try..catch 不會捕獲到異常,例如在 setTimeout 中
try {
setTimeout(function() {
noSuchVariable; // 腳本將在這裏停止運行,函數本身要稍後才執行,這時引擎已經離開了 try..catch 結構
}, 1000);
} catch (e) {
alert( "won't work" );
}
// 為了捕獲到計劃的(scheduled)函數中的異常,那麼 try..catch 必須在這個函數內
setTimeout(function() {
try {
noSuchVariable; // try..catch 處理 error 了!
} catch {
alert( "error is caught here!" );
}
}, 1000);
🚩變量和 try..catch..finally 中的局部變量
- 如果使用 let 在 try 塊中聲明變量,那麼該變量將只在 try 塊中可見
🚩finally 和 return
- finally 子句適用於 try..catch 的 任何 出口,包括顯式的 return
function func() {
try {
return 1;
} catch (e) {
/* ... */
} finally {
alert( 'finally' );// finally 會在控制轉向外部代碼前被執行
}
}
alert( func() ); // 先執行 finally 中的 alert,然後執行這個 alert
🚩再次拋出(rethrowing)是一種錯誤處理的重要模式:catch 塊通常期望並知道如何處理特定的 error 類型,因此它應該再次拋出它不知道的 error
// catch 應該只處理它知道的 error,並“拋出”所有其他 error
// “再次拋出(rethrowing)”技術可以被更詳細地解釋為:
// 1.Catch 捕獲所有 error
// 2.在 catch(err) {...} 塊中,對 error 對象 err 進行分析
// 3.如果不知道如何處理它,那就 throw err
// ...
// 在下面的代碼中,使用“再次拋出”,以達到在 catch 中只處理 SyntaxError 的目的:
let json = '{ "age": 30 }'; // 不完整的數據
try {
let user = JSON.parse(json);
if (!user.name) {
throw new SyntaxError("Incomplete data: no name");
}
blabla(); // 預料之外的 error
alert( user.name );
} catch(e) {
if (e instanceof SyntaxError) {// 可以使用 instanceof 操作符判斷錯誤類型;還可以從 err.name 屬性中獲取錯誤的類名,所有原生的錯誤都有這個屬性;另一種方式是讀取 err.constructor.name
alert( "JSON Error: " + e.message );
} else {
throw e; // 再次拋出 (*)
}
}
🚩全局 catch:即使我們沒有 try..catch,大多數執行環境也允許我們設置“全局”錯誤處理程序來捕獲“掉出(fall out)”的 error。在瀏覽器中,就是 window.onerror
// 如果在 try..catch 結構外有一個致命的 error,然後腳本死亡了!有什麼辦法可以用來應對這種情況嗎?可能想要記錄這個 error,並向用户顯示某些內容(通常用户看不到錯誤信息)等
// 規範中沒有相關內容,但是代碼的執行環境一般會提供這種機制,因為它確實很有用。例如,Node.JS 有 process.on("uncaughtException")。在瀏覽器中,可以將將一個函數賦值給特殊的 window.onerror 屬性,該函數將在發生未捕獲的 error 時執行
window.onerror = function(message, url, line, col, error) {
// ...
};
- 全局錯誤處理程序 window.onerror 的作用通常不是恢復腳本的執行 — 如果發生編程錯誤,那這幾乎是不可能的,它的作用是將錯誤信息發送給開發者
- 異常監控:有針對這種情況提供錯誤日誌的 Web 服務,例如 https://errorception.com 或 http://www.muscula.com
回調
🚩 異步 行為(action):現在開始執行的行為,但它們會在稍後完成;例如,setTimeout 函數就是一個這樣的函數;例如加載腳本和模塊
實際中的異步行為的示例:
/**
* 使用給定的 src 加載腳本
* @param src
**/
function loadScript(src) {
// 創建一個 <script> 標籤,並將其附加到頁面
// 這將使得具有給定 src 的腳本開始加載,並在加載完成後運行
let script = document.createElement('script');
script.src = src;
document.head.append(script);
}
可以像這樣使用這個函數:
// 在給定路徑下加載並執行腳本
loadScript('/my/script.js');
// loadScript 下面的代碼
// 不會等到腳本加載完成才執行
// ...
// 💡腳本是“異步”調用的,因為它從現在開始加載,但是在這個加載函數執行完成後才運行。如果在 loadScript(…) 下面有任何其他代碼,它們不會等到腳本加載完成才執行
假設需要在新腳本加載後立即使用它,這將不會有效:
loadScript('/my/script.js'); // 這個腳本有 "function newFunction() {…}"
newFunction(); // 沒有這個函數!
😭到目前為止,loadScript 函數並沒有提供跟蹤加載完成的方法。腳本加載並最終運行,僅此而已。但是希望瞭解腳本何時加載完成,以使用其中的新函數和變量
💡添加一個 callback 函數作為 loadScript 的第二個參數,該函數應在腳本加載完成時執行:
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}
loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => {
// 在腳本加載完成後,回調函數才會執行
alert(`Cool, the script ${script.src} is loaded`);
alert( _ ); // 所加載的腳本中聲明的函數
});
''''這就是被稱為“基於回調”的異步編程風格'''':異步執行某項功能的函數應該提供一個 callback 參數用於在相應事件完成時調用
🚩回調地獄
如何依次加載兩個腳本:第一個,然後是第二個?第三個?
loadScript('/my/script.js', function(script) {
loadScript('/my/script2.js', function(script) {
loadScript('/my/script3.js', function(script) {
// ...加載完所有腳本後繼續
});
});
});
加入處理 Error:
loadScript('1.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...加載完所有腳本後繼續 (*)
}
});
}
});
}
});
這就是著名的“''回調地獄''”或“厄運金字塔”
💡可以通過使每個行為都成為一個獨立的函數來嘗試減輕這種問題
loadScript('1.js', step1);
function step1(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', step2);
}
}
function step2(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', step3);
}
}
function step3(error, script) {
if (error) {
handleError(error);
} else {
// ...加載完所有腳本後繼續 (*)
}
}
優缺點
- 沒有深層的嵌套,獨立為頂層函數
- 可讀性差
- 沒有重用
最好的方法之一就是 “''promise''”
Promise
🚩語法
let promise = new Promise(function(resolve, reject) {
// executor
// 當 promise 被構造完成時,executor自動執行此函數
// executor 通常是異步任務
// ...
})
// handler
.then((result)=>{
// ...
},(error)=>{
// ...
});
1.當 new Promise 被創建,executor 被自動且立即調用
2.由 new Promise 構造器返回的 promise 對象具有以下【內部屬性】
- state — 最初是 "pending",然後在 resolve 被調用時變為 "fulfilled",或者在 reject 被調用時變為 "rejected"
- result — 最初是 undefined,然後在 resolve(value) 被調用時變為 value,或者在 reject(error) 被調用時變為 error
3.與最初的 “pending” promise 相反,一個 resolved 或 rejected 的 promise 都會被稱為 “settled”
4.executor 只能調用一個 resolve 或一個 reject;任何狀態的更改都是最終的(不可逆)
🚩立即resolve/reject的Promise
// executor 通常是異步執行某些操作,並在一段時間後調用 resolve/reject,但這不是必須的;還可以立即調用 resolve 或 reject
// 💡當開始做一個任務時,但隨後看到一切都已經完成並已被緩存時,可能就會發生這種情況。這挺好😀
let promise = new Promise(function(resolve, reject) {
// 不花時間去做這項工作
resolve(123); // 立即給出結果:123
});
🚩示例:加載腳本的 loadScript 函數
基於回調函數的變體版本:
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));
document.head.append(script);
}
// 用法:
loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => {
// 在腳本加載完成後,回調函數才會執行
alert(`${script.src} is loaded!`);
alert( _ ); // 所加載的腳本中聲明的函數
});
基於Promise重寫的版本:
function loadScript(src) {
return new Promise(function(resolve, reject) {
let script = document.createElement('script');
script.src = src;
script.onload = () => resolve(script);
script.onerror = () => reject(new Error(`Script load error for ${src}`));
document.head.append(script);
});
}
// 用法:
let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");
promise.then(
script => alert(`${script.src} is loaded!`),
error => alert(`Error: ${error.message}`)
);
promise.then(script => alert('Another handler...'));
Promise 鏈
🚩Promise 鏈:回憶回調中,何依次加載兩個腳本:第一個,然後是第二個?第三個?
// 💡Promise 提供了一些方案來做到這一點:Promise 鏈
// like this
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000); // (*)
}).then(function(result) { // (**)
alert(result); // 1
return result * 2;
}).then(function(result) { // (***)
alert(result); // 2
return result * 2;
}).then(function(result) {
alert(result); // 4
return result * 2;
});
// 📌為什麼可以?因為對 promise.then 的調用會返回了一個 promise,所以我們可以在其之上調用下一個 .then
// 當處理程序(handler)返回一個值時,它將成為該 promise 的 result,所以將使用它調用下一個 .then
// 💣''新手常犯的一個經典錯誤:從技術上講,我們也可以將多個 .then 添加到一個 promise 上。但這並不是 promise 鏈(chaining)''
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
// 💡這裏所做的只是一個 promise 的幾個處理程序(handler)。它們不會相互傳遞 result;相反,它們之間彼此獨立運行處理任務
🚩返回 promise
- .then(handler) 中所使用的處理程序(handler)可以創建並返回一個 promise
- 在這種情況下,其他的處理程序(handler)將【等待它 settled 後再獲得其結果(result)】
示例:promise 化的 loadScript
loadScript("/article/promise-chaining/one.js")
.then(script => loadScript("/article/promise-chaining/two.js"))
.then(script => loadScript("/article/promise-chaining/three.js"))
.then(script => {
// 腳本加載完成,我們可以在這兒使用腳本中聲明的函數
one();
two();
three();
});
// 💡注意:這兒每個 loadScript 調用都返回一個 promise,並且在它 resolve 時下一個 .then 開始運行。然後,它啓動下一個腳本的加載。所以,腳本是一個接一個地加載的
// 💡並且代碼仍然是“扁平”的 — 它向下增長,而不是向右
// ...
// 從技術上講,可以向每個 loadScript 直接添加 .then,就像這樣:
loadScript("/article/promise-chaining/one.js").then(script1 => {
loadScript("/article/promise-chaining/two.js").then(script2 => {
loadScript("/article/promise-chaining/three.js").then(script3 => {
// 此函數可以訪問變量 script1,script2 和 script3
one();
two();
three();
});
});
});
// 💡這段代碼做了相同的事兒:按順序加載 3 個腳本。但它是“向右增長”的。所以會有和使用回調函數一樣的問題
// 👍剛開始使用 promise 的人可能不知道 promise 鏈,所以他們就這樣寫了。通常,鏈式是首選
🚩Thenables
- 確切地説,處理程序(handler)返回的不完全是一個 promise,而是返回的被稱為 “thenable” 對象 — 一個具有方法 .then 的任意對象
- thenable對象會被當做一個 promise 來對待
- 這個想法是,第三方庫可以實現自己的“promise 兼容(promise-compatible)”對象;它們可以具有擴展的方法集,但也與原生的 promise 兼容,因為它們實現了 .then 方法
- 這個特性允許將自定義的對象與 promise 鏈集成在一起,而不必繼承自 Promise
示例:
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
alert(resolve); // function() { native code }
// 1 秒後使用 this.num*2 進行 resolve
setTimeout(() => resolve(this.num * 2), 1000); // (**)
}
}
new Promise(resolve => resolve(1))
.then(result => {
return new Thenable(result); // (*)
})
.then(alert); // 1000ms 後顯示 2
🚩作為一個好的做法:異步行為應該始終返回一個 promise
- 這樣就可以使得之後計劃後續的行為成為可能
- 即使現在不打算對鏈進行擴展,但之後可能會需要
示例:
function loadJson(url) {
return fetch(url)
.then(response => response.json());
}
function loadGithubUser(name) {
return fetch(`https://api.github.com/users/${name}`)
.then(response => response.json());
}
function showAvatar(githubUser) {
return new Promise(function(resolve, reject) {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
});
}
// 使用它們:
loadJson('/article/promise-chaining/user.json')
.then(user => loadGithubUser(user.name))
.then(showAvatar)
.then(githubUser => alert(`Finished showing ${githubUser.name}`));
// ...
使用 promise 進行錯誤處理
🚩Promise 鏈在錯誤(error)處理
- 當一個 promise 被 reject 時,控制權將移交至最近的 rejection 處理程序(handler);這在實際開發中非常方便
- .catch 不必是立即的;它可能在一個或多個 .then 之後出現
示例:
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(response => response.json())
.then(githubUser => new Promise((resolve, reject) => {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
}))
.catch(error => alert(error.message));
🚩隱式 try…catch
- Promise 的執行者(executor)和 promise 的處理程序(handler)周圍有一個“隱式的 try..catch”
- 如果發生異常,它(譯註:指異常)就會被捕獲,並被視為 rejection 進行處理
示例:
// excutor 中
new Promise((resolve, reject) => {
throw new Error("Whoops!");
}).catch(alert); // Error: Whoops!
// 等同於
new Promise((resolve, reject) => {
reject(new Error("Whoops!"));
}).catch(alert); // Error: Whoops!
// ...
// handler 中
new Promise((resolve, reject) => {
resolve("ok");
}).then((result) => {
throw new Error("Whoops!"); // reject 這個 promise
}).catch(alert); // Error: Whoops!
🚩再次拋出(Rethrowing)
- 如果在 .catch 中 throw,那麼控制權就會被移交到下一個最近的 error 處理程序(handler)。如果處理該 error 並正常完成,那麼它將繼續到最近的成功的 .then 處理程序(handler)
// 執行流:catch -> then
new Promise((resolve, reject) => {
throw new Error("Whoops!");
}).catch(function(error) {
alert("The error is handled, continue normally");
}).then(() => alert("Next successful handler runs"));
// 執行流:catch -> catch
new Promise((resolve, reject) => {
throw new Error("Whoops!");
}).catch(function(error) { // (*)
if (error instanceof URIError) {
// 處理它
} else {
alert("Can't handle such error");
throw error; // 再次拋出此 error 或另外一個 error,執行將跳轉至下一個 catch
}
}).then(function() {
/* 不在這裏運行 */
}).catch(error => { // (**)
alert(`The unknown error has occurred: ${error}`);
// 不會返回任何內容 => 執行正常進行
});
🚩未處理的 rejection
new Promise(function() {
noSuchFunction(); // 這裏出現 error(沒有這個函數)
})
.then(() => {
// 一個或多個成功的 promise 處理程序(handler)
}); // 尾端沒有 .catch!
// ...
// 當一個 error 沒有被處理會發生什麼?
// 💡如果出現 error,promise 的狀態將變為 “rejected”,然後執行應該跳轉至最近的 rejection 處理程序(handler)。但是上面這個例子中並沒有這樣的處理程序(handler)。因此 error 會“卡住(stuck)”。沒有代碼來處理它
// 在實際開發中,就像代碼中常規的未處理的 error 一樣,這意味着某些東西出了問題
// 當發生一個常規的錯誤(error)並且未被 try..catch 捕獲時會發生什麼?腳本死了,並在控制枱(console)中留下了一個信息。對於在 promise 中未被處理的 rejection,也會發生類似的事兒
JavaScript 引擎會跟蹤此類 rejection,在這種情況下會生成一個全局的 error
- 在瀏覽器中,可以使用 unhandledrejection 事件來捕獲這類 error
window.addEventListener('unhandledrejection', function(event) {
// 這個事件對象有兩個特殊的屬性:
alert(event.promise); // [object Promise] - 生成該全局 error 的 promise
alert(event.reason); // Error: Whoops! - 未處理的 error 對象
});
new Promise(function() {
throw new Error("Whoops!");
}); // 沒有用來處理 error 的 catch
Promise API
在 Promise 類中,有 5 種靜態方法
- Promise.all([iterable])
- Promise.allSettled([iterable])
- Promise.race([iterable])
- Promise.resolve()
- Promise.reject()
🚩Promise.all
語法
// 接受一個 promise 數組(可以是任何可迭代的)作為參數並返回一個新的 promise
let promise = Promise.all([iterable]);
注意
- 並行執行多個 promise,當所有給定的 promise 都被 成功 時,新的 promise 才會 resolve,並且其結果數組將成為新的 promise 的結果
- 結果數組中元素的順序與其在源 promise 中的順序相同(即使第一個 promise 花費了最長的時間)
- 如果任意一個 promise 被 reject,由 Promise.all 返回的 promise 就會立即 reject,並且帶有的就是這個 error
🚩如果出現 error,其他 promise 將被忽略
- 如果其中一個 promise 被 reject,Promise.all 就會立即被 reject,完全忽略列表中其他的 promise。它們的結果也被忽略
- 例如,如果有多個同時進行的 fetch 調用,其中一個失敗,其他的 fetch 操作仍然會繼續執行,但是 Promise.all 將不會再關心(watch)它們。它們可能會 settle,但是它們的結果將被忽略
- Promise.all 沒有采取任何措施來取消它們,因為 promise 中沒有“取消”的概念
🚩Promise.all(iterable) 允許在 iterable 中使用 non-promise 的“常規”值
// romise.all(...) 接受含有 promise 項的可迭代對象(大多數情況下是數組)作為參數。但是,如果這些對象中的任何一個不是 promise,那麼它將被“按原樣”傳遞給結果數組
Promise.all([
new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000)
}),
2,
3
]).then(alert); // 1, 2, 3
🚩Promise.allSettled
Promise.allSettled 等待所有的 promise 都被 settle,無論結果如何,結果數組具有:
- {status:"fulfilled", value:result} 對於成功的響應
- {status:"rejected", reason:error} 對於 error
Polyfill
if (!Promise.allSettled) {
const rejectHandler = reason => ({ status: 'rejected', reason });
const resolveHandler = value => ({ status: 'fulfilled', value });
Promise.allSettled = function (promises) {
const convertedPromises = promises.map(p => Promise.resolve(p).then(resolveHandler, rejectHandler));
return Promise.all(convertedPromises);
};
}
🚩Promise.race
- 只等待第一個 settled 的 promise 並獲取其結果(或 error)
示例
Promise.race([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1
🚩Promise.resolve/reject
語法
// 結果 value 創建一個 resolved 的 promise
Promise.resolve(value)
// 等同於
let promise = new Promise(resolve => resolve(value));
//...
// Promise.reject() 類似
- 當一個函數被期望返回一個 promise 時,這個方法用於兼容性
- 💡這裏的兼容性是指,直接從緩存中獲取了當前操作的結果 value,但是期望返回的是一個 promise,所以可以使用 Promise.resolve(value) 將 value “封裝”進 promise,以滿足期望返回一個 promise 的這個需求
示例:
let cache = new Map();
function loadCached(url) {
if (cache.has(url)) {
return Promise.resolve(cache.get(url)); // (*)
}
return fetch(url)
.then(response => response.text())
.then(text => {
cache.set(url,text);
return text;
});
}
// 💡可以使用 loadCached(url).then(…),因為該函數保證了會返回一個 promise。可以放心地在 loadCached 後面使用 .then。這就是 (*) 行中 Promise.resolve 的目的
Promisification
- “Promisification” 指將一個接受回調的函數轉換為一個返回 promise 的函數
- 由於許多函數和庫都是基於回調的,所以將基於回調的函數和庫 promisify 是有意義的
示例:
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));
document.head.append(script);
}
// 用法:
// loadScript('path/script.js', (err, script) => {...})
// ...
// promisify
let loadScriptPromise = function(src) {
return new Promise((resolve, reject) => {
loadScript(src, (err, script) => {
if (err) reject(err);
else resolve(script);
});
});
};
// 用法:
// loadScriptPromise('path/script.js').then(...)
新的函數是對原始的 loadScript 函數的包裝,在實際開發中,可能需要 promisify 很多函數
🚩promisify
function promisify(f) {
return function (...args) { // 返回一個包裝函數(wrapper-function) (*)
return new Promise((resolve, reject) => {
function callback(err, result) { // 對 f 的自定義的回調 (**)
if (err) {
reject(err);
} else {
resolve(result);
}
}
args.push(callback); // 將自定義的回調附加到 f 參數(arguments)的末尾
f.call(this, ...args); // 調用原始的函數
});
};
}
// 用法:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);
🚩promisification 函數的模塊(module)
- https://github.com/digitaldesignlabs/es6-promisify
- 在 Node.js 中,有一個內建的 promisify 函數 util.promisify
🚩Promisification場景
- Promisification 不是回調的完全替代
- 請記住,一個 promise 可能只有一個結果,但從技術上講,一個回調可能被調用很多次
- 因此,promisification 僅適用於調用一次回調的函數。進一步的調用將被忽略
微任務(Microtask)
- Promise 處理始終是異步的,因此,.then/catch/finally 處理程序(handler)總是在當前代碼完成後才會被調用
- 所有 promise 行為都會通過內部的 “promise jobs” 隊列,也被稱為“微任務隊列”(ES8 術語
- 如果需要確保一段代碼在 .then/catch/finally 之後被執行,可以將它添加到鏈式調用的 .then 中
async/await
- async/await 是以更舒適的方式使用 promise 的一種特殊語法,同時它也非常易於理解和使用
- async/await兩個關鍵字一起提供了一個很好的用來編寫異步代碼的框架,這種代碼易於閲讀也易於編寫
- 有了 async/await 之後,就幾乎不需要使用 promise.then/catch,但是不要忘了它們是基於 promise 的,因為有些時候(例如在最外層作用域)不得不使用這些方法
🚩 async 有兩個作用
1.讓這個函數總是返回一個 promise;其他值將自動被包裝在一個 resolved 的 promise 中
2.允許在該函數內使用 await
🚩await
Promise 前的關鍵字 await 使 JavaScript 引擎等待該 promise settle,然後:
- 如果有 error,就會拋出異常 — 就像那裏調用了 throw error 一樣
- 否則,就返回結果
示例
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});
let result = await promise; // 等待,直到 promise resolve (*)
alert(result); // "done!"
}
f();
// 💡(*) 那一行:await 實際上會暫停函數的執行,直到 promise 狀態變為 settled,然後以 promise 的結果繼續執行
🚩await 不能在頂層代碼運行
// 用在頂層代碼中會報語法錯誤
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
// ...
// 但可以將其包裹在一個匿名 async 函數中,如下所示:
(async () => {
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
...
})();
🚩await 接受 “thenables”
// 💡像 promise.then 那樣,await 允許使用 thenable 對象(那些具有可調用的 then 方法的對象)。這裏的想法是,第三方對象可能不是一個 promise,但卻是 promise 兼容的:如果這些對象支持 .then,那麼就可以對它們使用 await
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
alert(resolve);
// 1000ms 後使用 this.num*2 進行 resolve
setTimeout(() => resolve(this.num * 2), 1000); // (*)
}
}
async function f() {
// 等待 1 秒,之後 result 變為 2
let result = await new Thenable(1);
alert(result);
}
f();
// 如果 await 接收了一個非 promise 的但是提供了 .then 方法的對象,它就會調用這個 .then 方法,並將內建的函數 resolve 和 reject 作為參數傳入(就像它對待一個常規的 Promise executor 時一樣)。然後 await 等待直到這兩個函數中的某個被調用(在上面這個例子中發生在 (*) 行),然後使用得到的結果繼續執行後續任務
🚩Error 處理
// 如果一個 promise 正常 resolve,await promise 返回的就是其結果
// 但是如果 promise 被 reject,它將 throw 這個 error,就像在這一行有一個 throw 語句那樣
async function f() {
await Promise.reject(new Error("Whoops!"));
}
// 等同於
async function f() {
throw new Error("Whoops!");
}
// 👍因此,可以用 try..catch 來捕獲上面提到的那個 error,與常規的 throw 使用的是一樣的方式
async function f() {
try {
let response = await fetch('http://no-such-url');
} catch(err) {
alert(err); // TypeError: failed to fetch
}
}
f();
// ...
// 👌如果沒有 try..catch,那麼由異步函數 f() 的調用生成的 promise 將變為 rejected;可以在函數調用後面添加 .catch 來處理這個 error:
async function f() {
let response = await fetch('http://no-such-url');
}
// f() 變成了一個 rejected 的 promise
f().catch(alert); // TypeError: failed to fetch // (*)
Generator
- 常規函數只會返回一個單一值(或者不返回任何值)
- 而 Generator 可以按需一個接一個地返回(“yield”)多個值
- 可與 iterable 完美配合使用,從而可以輕鬆地創建數據流
🚩Generator 函數
generator 的主要方法就是 next()
- 當被調用時,執行直到最近的 yield <value> 語句(value 可以被省略,默認為 undefined)
- 然後函數執行暫停,並將產出的(yielded)值返回到外部代碼
示例:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
let generator = generateSequence();
let one = generator.next();
alert(JSON.stringify(one)); // {value: 1, done: false}
let two = generator.next();
alert(JSON.stringify(two)); // {value: 2, done: false}
let three = generator.next();
alert(JSON.stringify(three)); // {value: 3, done: true}
next() 的結果始終是一個具有兩個屬性的對象:
- value: 產出的(yielded)的值
- done: 如果 generator 函數已執行完成則為 true,否則為 false
🚩Generator 是可迭代的
- generator 具有 next() 方法, 因此generator 是 可迭代(iterable)的
- 因此可以使用 iterator 的所有相關功能,例如:spread 語法 ...
💡next() 是 iterator 的必要方法(可以使用 for..of 循環遍歷)
示例:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
let generator = generateSequence();
for(let value of generator) {
alert(value); // 1,然後是 2
}
🚩使用 generator 進行迭代
// 👉非generator 函數實現 Symbol.iterator
let range = {
from: 1,
to: 5,
// for..of range 在一開始就調用一次這個方法
[Symbol.iterator]() {
// ...它返回 iterator object:
// 後續的操作中,for..of 將只針對這個對象,並使用 next() 向它請求下一個值
return {
current: this.from,
last: this.to,
// for..of 循環在每次迭代時都會調用 next()
next() {
// 它應該以對象 {done:.., value :...} 的形式返回值
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}
};
// 迭代整個 range 對象,返回從 `range.from` 到 `range.to` 範圍的所有數字
alert([...range]); // 1,2,3,4,5
// ...
// 👉generator 函數實現 Symbol.iterator
let range = {
from: 1,
to: 5,
*[Symbol.iterator]() { // [Symbol.iterator]: function*() 的簡寫形式
for(let value = this.from; value <= this.to; value++) {
yield value;
}
}
};
alert( [...range] ); // 1,2,3,4,5
🚩Generator 組合(composition)
- 將一個 generator 流插入到另一個 generator 流的自然的方式
示例:成一個更復雜的序列:首先是數字 0..9(字符代碼為 48…57),接下來是大寫字母 A..Z(字符代碼為 65…90),接下來是小寫字母 a...z(字符代碼為 97…122)
// 👉generator composition
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* generatePasswordCodes() {
// 0..9
yield* generateSequence(48, 57);
// A..Z
yield* generateSequence(65, 90);
// a..z
yield* generateSequence(97, 122);
}
let str = '';
for(let code of generatePasswordCodes()) {
str += String.fromCharCode(code);
}
alert(str); // 0..9A..Za..z
// 👉等同於
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* generateAlphaNum() {
// yield* generateSequence(48, 57);
for (let i = 48; i <= 57; i++) yield i;
// yield* generateSequence(65, 90);
for (let i = 65; i <= 90; i++) yield i;
// yield* generateSequence(97, 122);
for (let i = 97; i <= 122; i++) yield i;
}
let str = '';
for(let code of generateAlphaNum()) {
str += String.fromCharCode(code);
}
alert(str); // 0..9A..Za..z
🚩“yield” 是一條雙向路
yield 不僅可以向外返回結果,而且還可以將外部的值傳遞到 generator 內
示例:
function* gen() {
// 向外部代碼傳遞一個問題並等待答案
let result = yield "2 + 2 = ?"; // (*)
alert(result);
}
let generator = gen();
let question = generator.next().value; // <-- yield 返回的 value
generator.next(4); // --> 將結果傳遞到 generator 中
// 1.第一次調用 generator.next() 應該是不帶參數的(如果帶參數,那麼該參數會被忽略)。它開始執行並返回第一個 yield "2 + 2 = ?" 的結果。此時,generator 執行暫停,而停留在 (*) 行上
// 然後,yield 的結果進入調用代碼中的 question 變量
// 在 generator.next(4),generator 恢復執行,並獲得了 4 作為結果:let result = 4
異步迭代 和 異步generator
🚩異步可迭代對象
// 👉可迭代的 range 的一個實現
let range = {
from: 1,
to: 5,
[Symbol.iterator]() { // 在 for..of 循環開始時被調用一次
return {
current: this.from,
last: this.to,
next() { // 每次迭代時都會被調用,來獲取下一個值
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}
};
for(let value of range) {
alert(value); // 1,然後 2,然後 3,然後 4,然後 5
}
// 👉異步可迭代的 range 的一個實現
let range = {
from: 1,
to: 5,
[Symbol.asyncIterator]() { // (1)
return {
current: this.from,
last: this.to,
async next() { // (2)
// 注意:可以在 async next 內部使用 "await"
await new Promise(resolve => setTimeout(resolve, 1000)); // (3)
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}
};
(async () => {
for await (let value of range) { // (4)
alert(value); // 1,2,3,4,5
}
})()
// 💡
// 使一個對象可以異步迭代,它必須具有方法 【Symbol.asyncIterator 】(1)
// 這個方法必須返回一個帶有 next() 方法的對象,next() 方法會【返回一個 promise】 (2)
// 這個 next() 方法可以不是 async 的,它可以是一個返回值是一個 promise 的常規的方法,但是使用 async 關鍵字可以允許在方法內部使用 await,所以會更加方便
// 使用【 for await(let value of range) 】(4) 來進行迭代,也就是在 for 後面添加 await。它會調用一次 range[Symbol.asyncIterator]() 方法一次,然後調用它的 next() 方法獲取值
🚩異步可迭代對象 Spread 語法 ... 無法異步工作
- 這很正常,因為它期望找到 Symbol.iterator,而不是 Symbol.asyncIterator
- for..of 的情況和這個一樣:沒有 await 關鍵字時,則期望找到的是 Symbol.iterator
🚩異步generator
// 👉可迭代的 range 的 generate 的 一個實現
let range = {
from: 1,
to: 5,
*[Symbol.iterator]() { // [Symbol.iterator]: function*() 的一種簡寫
for(let value = this.from; value <= this.to; value++) {
yield value;
}
}
};
for(let value of range) {
alert(value); // 1,然後 2,然後 3,然後 4,然後 5
}
// 👉可迭代的 range 的 generate異步 的 一個實現
async function* generateSequence(start, end) {
for (let i = start; i <= end; i++) {
// 哇,可以使用 await 了!
await new Promise(resolve => setTimeout(resolve, 1000));
yield i;
}
}
(async () => {
let generator = generateSequence(1, 5);
for await (let value of generator) {
alert(value); // 1,然後 2,然後 3,然後 4,然後 5(在每個 alert 之間有延遲)
}
})();
// 💡
// 在一個常規的 generator 中,使用 result = generator.next() 來獲得值
// 但在一個異步 generator 中,應該添加 await 關鍵字,像這樣:
result = await generator.next(); // result = {value: ..., done: true/false}
// 💡 這就是為什麼異步 generator 可以與 for await...of 一起工作
模塊 (Module)
🚩起源
- 隨着項目越來越大,需要將其拆成多個文件,即模塊(module)(包含特定目的的類或者函數庫)
很長一段時間,JavaScript 都沒有語言級(language-level)的模塊語法,但隨着腳本越來越複雜,因此社區發明了許多種方法來將代碼組織到模塊中,使用特殊的庫按需加載模塊
- AMD
- https://en.wikipedia.org/wiki/Asynchronous_module_definition
- 最初由 require.js 庫實現
- CommonJS
- http://wiki.commonjs.org/wiki/Modules/1.1
- 為 Node.js 服務器創建的模塊系統
- UMD
- https://github.com/umdjs/umd
- 與 AMD 和 CommonJS 都兼容
- ''`語言級的模塊系統在 2015 年的時候出現在了標準(ES6)中`''
🚩模塊的核心概念
一個模塊就是一個文件,覽器需要使用 <script type="module">
與常規腳本相比(<script src="xx">),擁有 type="module" 標識的腳本有一些特定於瀏覽器的差異:
- 默認是延遲解析的(deferred)
- Async 可用於內聯腳本(對於非模塊腳本,async 特性(attribute)僅適用於外部腳本(異步腳本會在準備好後立即運行,獨立於其他腳本或 HTML 文檔),對於模塊腳本,也適用於內聯腳本)
- 從另一個源(域/協議/端口)加載外部腳本,需要 CORS header
- 重複的外部腳本會被忽略
模塊具有自己的本地頂級作用域,並可以通過 import/export 交換功能
- “this” 是 undefined
模塊始終使用 use strict
模塊代碼只執行一次。導出僅創建一次,然後會在導入之間共享
在生產環境中,出於性能和其他原因,開發者經常使用諸如 Webpack 之類的打包工具將模塊打包到一起
導出和導入
- 把 import/export 語句放在腳本的頂部或底部,都沒關係
- 在實際開發中,導入通常位於文件的開頭,但是這只是為了更加方便
🚩 export 導出類型
// 💡在聲明一個 class/function/… 之前:
export [default] class/function/variable ...
// 💡獨立的導出:
export {x [as y], ...}.
// 💡重新導出:
export {x [as y], ...} from "module"
export * from "module"(不會重新導出默認的導出)。
export {default [as y]} from "module"(重新導出默認的導出)
🚩 import 導入類型
// 💡模塊中命名的導出:
import {x [as y], ...} from "module"
// 💡默認的導出:
import x from "module"
import {default as x} from "module"
// 💡所有:
import * as obj from "module"
// 💡導入模塊(它的代碼,並運行),但不要將其賦值給變量:
import "module"
動態導入
🚩 靜態導入/導出
// 💡模塊路徑必須是原始類型字符串,不能是函數調用
import ... from getModuleName(); // Error, only from "string" is allowed
// 💡無法根據條件
if(...) {
import ...; // Error, not allowed!
}
// 💡無法在運行時導入
{
import ...; // Error, we can't put import in any block
}
export / import 語法嚴格且簡單:只提供結構主幹
- 便於分析代碼結構
- 可以收集模塊
- 可以使用特殊工具將收集的模塊打包到一個文件中
- 可以刪除未使用的導出(“tree-shaken”)
🚩import() 表達式
import(module)
// 返回一個 promise,該 promise resolve 為一個包含其所有導出的模塊對象
let modulePath = prompt("Which module to load?");
import(modulePath)
.then(obj => <module object>)
.catch(err => <loading error, e.g. if no such module>)
// or
let module = await import(modulePath)
let xx = module .default;// 可以使用模塊對象的 default 屬性
動態導入在常規腳本中工作時,不需要 script type="module".
😱儘管 import() 看起來像一個函數調用,但它只是一種特殊語法,只是恰好使用了括號(類似於 super())
😱因此,不能將 import 複製到一個變量中,或者對其使用 call/apply
😱因為它不是一個函數
Eval:執行代碼字符串
let result = eval(code);
內建函數 eval 允許執行一個代碼字符串
🚩eval 的結果是最後一條語句的結果
let value = eval('let i = 0; ++i');
alert(value); // 1
🚩eval 內的代碼在當前詞法環境(lexical environment)中執行,因此它能訪問外部變量
let a = 1;
function f() {
let a = 2;
eval('alert(a)'); // 2
}
f();
// 嚴格模式下,eval 有屬於自己的詞法環境,如果不啓用嚴格模式,eval 沒有屬於自己的詞法環境
eval("let x = 5; function f() {}");
alert(typeof x); // undefined(沒有這個變量)
// 函數 f 也不可從外部進行訪問
柯里化(Currying)
柯里化是一種函數的轉換:
- 指將一個函數從可調用的 f(a, b, c) 轉換為可調用的 f(a)(b)(c)
- 柯里化不會調用函數,只是對函數進行轉換
🚩創建一個輔助函數 curry(f)
// curry(f) 執行柯里化轉換
function curry(f) {
return function(a) {
return function(b) {
return f(a, b);
};
};
}
// 用法
function sum(a, b) {
return a + b;
}
let curriedSum = curry(sum);
alert( curriedSum(1)(2) ); // 3
// 💡實現非常簡單:只有兩個包裝器(wrapper)
🚩柯里化?目的是什麼?
輕鬆地生成偏函數
🚩高級柯里化實現
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
// 如果傳入的 args 長度與原始函數所定義的(func.length)相同或者更長,那麼只需要將調用傳遞給它即可
return func.apply(this, args);
} else {
// 否則,獲取一個偏函數,func 還沒有被調用;返回另一個包裝器 ,它將重新應用 curried,將之前傳入的參數與新的參數一起傳入;在一個新的調用中,再次,將獲得一個新的偏函數(如果參數不足的話),或者最終的結果
return function(...args2) {
return curried.apply(this, args.concat(args2));
}
}
};
}
// 使用
function sum(a, b, c) {
return a + b + c;
}
let curriedSum = curry(sum);
alert( curriedSum(1, 2, 3) ); // 6,仍然可以被正常調用
alert( curriedSum(1)(2,3) ); // 6,對第一個參數的柯里化
alert( curriedSum(1)(2)(3) ); // 6,全柯里化
柯里化要求函數具有固定數量的參數,f(...args),不能以這種方式進行柯里化
Reference Type
Reference Type 是語言內部的一個類型
- 在obj.method()中 . 返回的準確來説不是屬性的值 而是一個特殊的 “Reference Type” 值 其中儲存着屬性的值和它的來源對象
- 這是為了【隨後】的方法調用 () 獲取來源對象,然後將 this 設為它
- 對於所有其它操作,Reference Type 會自動變成屬性的值
BigInt
// 創建 bigint 的方式有兩種:在一個整數字面量後面加 n 或者調用 BigInt 函數,該函數從字符串、數字等中生成 bigint
const bigint = 1234567890123456789012345678901234567890n;
const sameBigint = BigInt("1234567890123456789012345678901234567890");
const bigintFromNumber = BigInt(10); // 與 10n 相同
- 不可以把 bigint 和常規數字類型混合使用,應該顯式地轉換
- 對 bigint 和 number 類型的數字進行比較沒有問題
- == 比較時相等,但在進行 ===(嚴格相等)比較時不相等
- 除法 向下整除
- BigInt 不支持一元加法
- bigint 0n 為假,其他值為 true
🚩Polyfill
https://github.com/GoogleChro...