前言
這是我第三次翻開紅寶書也就是《 JavaScript 高階程式設計第三版》,不得不說,雖然書有一些年份,很多知識點也不適合現代的前端開發,但是對於想要掌握 JavaScript 基礎的前端新手,亦或是像我一樣想找回曾經遺忘在記憶角落的那些碎片知識,這本書依舊非常的適合,不愧被成為 "JavaScript 聖經"
本文是讀書筆記,之所以又一次選擇讀這本書還有一個理由,之前都是記的紙質筆記,這次想把它作為電子版,也算是對之前知識的整理
本文篇幅較長,目的是作為我的電子版學習筆記,我會盡可能去其糟粕,取其精華,同時我會新增一些書上未記載但很重要的知識點補充
let's go
JavaScript 簡介
一個完整的 JavaScript 由 3 個部分組成,核心(ECMAScript 語法),DOM,BOM,後兩者目前已經是可選項了,或者可以抽象為宿主,因為 JS 已經不僅限執行於瀏覽器
在 HTML 中使用 JavaScript
在瀏覽器中使用 JS 可以通過 script
標籤來執行 JS 檔案,進一步可以分為 3 種方式,內嵌 JS 程式碼,通過 src 指向本地 JS 檔案,通過 src 指向某個靜態伺服器的 JS 檔案(域名),推薦的是使用 src 的形式,相比於內嵌可以利用快取提高頁面載入速度和解析 DOM 的速度,並且,因為 JS 和 HTML 解耦了可維護性更強
當 script 標籤是 src 形式的外部指令碼,中可以設定 defer
,async
屬性,前者可以讓頁面解析完畢後再執行指令碼,後者則是非同步下載指令碼並執行,同時會非同步的執行 JS 程式碼,這 2 個屬性都是為了解決瀏覽器必須要等到 script 標籤中的 JS 程式碼下載並執行後才會解析之後的元素從而導致的白屏時間久的問題
<script src="xxx" async></script>
複製程式碼
JavaScript 基本概念
識別符號
識別符號指的是變數,函式,屬性的名字,主流的名字以駝峰命名為主,或者 $, _
第一個字元不能是數字(但從第二個字元開始就是合法的)
// illegal
let 123hello = '123'
// legitimate
let $123hello = '123'
let helloWorld = '123'
let hello123World = '123'
let _$hello = '123'
複製程式碼
資料型別
截至今日,JavaScript 有 7 種簡單資料型別,1種複雜資料型別
簡單資料型別:
- Undefined
- Null
- Boolean
- Number
- String
- Symbol
- BigInt (ES10 草案)
複雜資料型別:
- Object
Function 是 Object 的子類,即繼承於 Object
Undefined 型別
Undefined 型別只有一個值,即 undefined,它和 not defined 很容易混淆,它們的異同在於
- 使用 typeof 操作符都會返回 'undefined'
- 使用 undefined 變數是安全的,使用 not defined 的變數會丟擲錯誤
let foo
console.log(typeof foo) // 'undefined'
console.log(typeof bar) // 'undefined'
console.log(foo) // undefined
console.log(bar) // Uncaught ReferenceError: bar is not defined
複製程式碼
Null 型別
Null 型別也只有一個值,即 null,null 表示一個空物件指標,如果使用 typeof 操作符,返回的型別是 'object',但這只是語言上的 BUG,目前幾乎不可能修復,如果使用 instanceof 操作符判斷是否是 Object 的例項,會返回 false,證明 null 和 Object 並沒有什麼關係
console.log(typeof null) // 'object'
console.log(null instanceof Object) // false
複製程式碼
undefined 值是派生自 null 值的,所以它們寬鬆相等
console.log(undefined == null) // true
複製程式碼
Number 型別
JS 的 Number 型別使用 IEEE754 格式來表示整數和浮點數值,它會導致一些小問題,例如 JS 的 0.1 其實並不是真正的 0.1,它的二進位制為 0.001100110011...,無限迴圈(小數十進位制轉二進位制的規則是乘 2 取整),內部是這樣儲存的
可以通過 Number 函式,將傳入的引數轉為相應的 Number 型別(注意隱式轉換的坑)
console.log(Number('123')) // 123
console.log(Number(null)) // 0
console.log(Number(undefined)) // NaN
console.log(Number('false')) // NaN
console.log(Number(true)) // 1
複製程式碼
NaN 屬於 Number 型別,且 NaN 不等於自身,可以通過 window 物件的 isNaN
來判斷引數是否是 NaN,但是它有個缺陷在於會先將引數轉為 Number 型別(同樣是隱式轉換),所以會出現 isNaN('foo')
返回 true 的情況,ES6 的 Number.isNaN
彌補了這一個缺陷,它會返回 false,證明 'foo' 字串並不是 NaN
console.log(NaN === NaN) // false
console.log(isNaN(NaN)) // true
console.log(isNaN('foo')) // true 但這是不合理的,因為 'foo' 並不是 NaN
console.log(Number.isNaN('foo')) // false
複製程式碼
window.isNaN 是用來判斷引數是不是一個數字,Number.isNaN 是用來判斷引數是不是 NaN
parseInt 和 Number 函式的區別在於,前者是逐個字元解析引數,而後者是直接轉換
console.log(parseInt('123.456')) // 123
console.log(parseInt('123foo')) // 123
console.log(Number('123foo')) // NaN
複製程式碼
parseInt 會逐個解析引數 '123foo',當遇到非數字字元或者小數點則停止(這裡是字串 f),會返回之前轉換成功的數字,而 Number 則是將整個引數轉為數字
(值得一提的是 parseFloat 遇到非數字字元或者第二個小數點,會返回之前轉換成功的數字)
String
ECMAScript 中的字串是一旦建立,它們的值就不可改變,如果需要改變某個變數儲存的字串,需要銷燬原來的字串,再用另一個新值字串填充該變數
Object
DOM 和 BOM 物件都是由宿主提供的宿主物件,這裡的宿主即瀏覽器,換句話非瀏覽器環境可能會沒有瀏覽器上的一些全域性變數和方法,例如 node 中就沒有 alert 方法
操作符
一元操作符
只能操作一個值的操作符叫做一元操作符,後置遞增/遞減操作符與前置遞增/遞減有一個重要的區別,後置是在包含它們的語句被求值之後執行的
let num1 = 2
let num2 = 20
let num3 = --num1 + num2 // 21
let num4 = num1 + num2 // 21
複製程式碼
let num1 = 2
let num2 = 20
let num3 = num1-- + num2 // 22
let num4 = num1 + num2 // 21
複製程式碼
前者先讓 num1 減1,再執行和 num2 累加,後者是先和 num2 累加,再讓 num 減1 ,另外一元操作符會先嚐試將變數轉換為數字
布林操作符
邏輯與和邏輯非這兩個操作符都是短路操作,即第一個運算元能決定結果,就不會對第二個運算元求值
let num = 0
true || num++
console.log(num) //0
複製程式碼
以下常用的邏輯與判斷結果
第一個運算元 | 操作符 | 第二個運算元 | 結果 |
---|---|---|---|
null | && | 任何 | 第一個運算元 |
undefined | && | 任何 | 第一個運算元 |
NaN | && | 任何 | 第一個運算元 |
false | && | 任何 | 第一個運算元 |
"" | && | 任何 | 第一個運算元 |
0 | && | 任何 | 第一個運算元 |
物件 | && | 任何 | 第二個運算元 |
true | && | 任何 | 第二個運算元 |
當第一個引數是假值時,邏輯與返回第一個運算元,反之返回第二個運算元
以下是所有假值的列表:false,null,undefined,0,NaN,""
邏輯或與邏輯與相反,以下常用的邏輯或與判斷結果
第一個運算元 | 操作符 | 第二個運算元 | 結果 |
---|---|---|---|
null | || | 任何 | 第二個運算元 |
undefined | || | 任何 | 第二個運算元 |
NaN | || | 任何 | 第二個運算元 |
false | || | 任何 | 第二個運算元 |
"" | || | 任何 | 第二個運算元 |
0 | || | 任何 | 第二個運算元 |
物件 | || | 任何 | 第一個運算元 |
true | || | 任何 | 第一個運算元 |
當第一個引數是假值時,邏輯或返回第二個運算元,反之返回第一個運算元
加性操作符
在 ECMAScript 中,加性操作符有一些特殊的行為,這裡分為運算元中有字串和沒有字串的情況
有字串一律視為字串拼接,如果其中一個是字串,另一個不是字串,則會將它轉為字串再拼接,接著會遇到兩種情況
- 第二個運算元是物件,則會呼叫 [[toPrimitive]] 將其轉為原始值,如果原始值是字串那仍會執行字串拼接
- 運算元不是物件,則直接視為字串拼接
console.log("123" + 123) // "123123"
console.log('123' + NaN) // "123NaN"
console.log("123" + {}) // "123[object Object]"
console.log("123" + undefined) // "123undefined"
複製程式碼
如果兩個運算元都不是字串,又會有兩種情況
- 運算元是物件,則會呼叫 [[toPrimitive]] 將其轉為原始值,如果原始值是字串那仍會執行字串拼接
- 運算元不是物件,則會轉為 Number 型別再計算
值得一提的是,涉及到 NaN 的四則運算最終結果都是 NaN(另一個運算元為字串仍視為字串拼接)
console.log(123 + true) // 124
console.log(123 + undefined) // NaN 因為 undefined 被轉為 NaN
console.log(NaN + {}) // "NaN[object Object]" 含有物件會轉為原始值,因為是字串所以視為拼接
複製程式碼
關係操作符
和加性操作符一樣,JS 中的關係操作符(>,<,>=,<=)也會有一些反常的行為
- 兩個運算元都是數值,則執行數值比較(如果其中一個是 NaN,則始終返回 false)
- 兩個運算元都是字串,逐個比較字串的編碼值
- 其中一個運算元是物件,則呼叫 [[toPrimitive]] 轉為原始值,按照之前規則比較
- 其中一個運算元是布林值,會轉為 Number 型別,再執行比較
對於第二條,舉個例子
console.log('abc' < 'abd') // true
複製程式碼
內部是這麼判斷的,由於兩個都是字串,先判斷字串的第一位,發現都是 "a",接著比較第二個,發現也是相同的,接著比較第三個,由於 "c" 的編碼比 "d" 小(前者是 99 後者是 100),所以字串 abc "小於" 字串 abd
相等操作符
相等操作符和加性,關係操作符一樣師承一脈,也有很多奇怪的特點,以至於十幾年後的今天還被人詬病,先看一下網上的一些例子
undefined==null //true
[]==[] //false
[]==![] //true
{}==!{} //false
![]=={} //false
[]==!{} //true
[1,2]==![1] //false
複製程式碼
具體我不想展開講,英語不錯的朋友可以直接檢視規範,我說一下個人的記憶技巧
- 如果型別相同,直接判斷是否相等,不同型別才會發生隱式轉換
- 涉及到物件,則會呼叫 [[toPrimitive]]
- NaN 和任何都不想等,包括自身
- 等式兩邊都會盡可能轉為 Number 型別,如果在轉為數字的途中,已經是同一型別則不會進一步轉換
- null 和 undefined 有些特殊行為,首先它們兩個是寬鬆相等(==),但不是嚴格相等(===),除此之外任何值都不會和 null / undefined 寬鬆/嚴格相等
綜合來說,為了避免隱式轉換的坑,儘量使用嚴格相等(===)
for 語句
for 語句其實是 while 語句衍變而來的,for 語句包含 3 個表示式,通過分號分隔,第一個表示式一般為宣告或者賦值語句,第二個表示式為迴圈終止條件,第三個語句為一次迴圈後執行的表示式
let i = 0
for (;;i++){
//...
}
複製程式碼
上述程式碼會陷入死迴圈,讓瀏覽器崩潰,原因是第二個表示式沒有設定,會被視為始終為 true,即永遠不會退出迴圈,並且每次迴圈變數 i 都會 +1,同時沒有初始語句,程式碼本身無任何意義,只是說明 for 迴圈的 3 個表示式都是可選的
如果按照執行順序來給 for 語句的執行順序進行排序的話,是這樣的
for (/* 1 */let i = 0;/* 2 */i < 10;/* 3 */i++) {
/* 4 */ console.log(i)
}
複製程式碼
順序為 1 -> 2 -> 4 -> 3 -> 4 -> 3 -> 4 -> ... -> 退出
for in 語句
for in 語句會返回物件的屬性,返回的順序可能會因瀏覽器而異,因為沒有規範,所以不要依賴它返回的順序,而 Reflect.ownKeys ,Object.getOwnPropertyNames,Object.getOwnPropertySymbols 是由 ES6 規範 [[OwnPropertyKeys]] 演算法定義的,其內容如下
- 首先順序返回整數的屬性(陣列的屬性)
- 依次按照建立順序返回字串屬性
- 最後返回所有符號屬性
label 語句
使用 label 語句可以為 for 語句新增標籤的功能,當 for 語句內部通過 break,continue 語句退出時,可以額外指定標籤名來退出到更外層的迴圈,這會用在多層 for 迴圈中
let num = 0
outer: for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
if (i === 5 && j === 5) {
continue outer
}
num++
}
}
console.log(num) // 95
複製程式碼
當 i 和 j 都是 5 的時候,會跳過 5 次遍歷(55,56,57,58,59),最終結果為 95,即迴圈執行了 95 次
switch 語句
在 switch 語句中,如果每個條件不寫 break 關鍵字退出判斷的話,會發生條件穿透
let i = 25
switch (i) {
case 25:
console.log('25')
case 35:
console.log('35')
break;
default:
console.log('default')
}
// "25"
// "35"
複製程式碼
i 滿足第一個 case,所以列印了字串 25,但是由於沒有 break,會無視第二個判斷條件直接執行第二個 case 的語句,如果第二個條件也沒有 break 還會繼續穿透到 default 中
switch 語句中 case 的判斷條件是嚴格相等,字串 10 不等於數字 10
函式
在 ES6 以前,函式的引數會被儲存在一個叫 arguments
的物件中在函式執行的時候被建立,它是一個類陣列,它有 length 屬性代表引數個數,這裡的引數個數是執行函式時傳入的引數個數,而不是函式定義的引數個數
function func(a,b,c) {
console.log(arguments)
}
func(1,2) // Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ, length:2]
複製程式碼
即使定義了 3 個引數, arguments 反映的只是函式執行時候的引數個數,另外 arguments 還有一些比較特殊的特性,非嚴格模式下它和函式執行時的引數會建立一個連結,當引數被修改時會反映到 arguments 上,反之同理
function func(a,b,c) {
console.log(arguments)
a = 123
console.log(arguments)
}
func(1,2)
// Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ, length:2]
// Arguments(2) [123, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ, length:2]
複製程式碼
function func(a,b,c) {
console.log(a)
arguments[0] = 123
console.log(a)
}
func(1,2)
// 1
// 123
複製程式碼
而嚴格模式不會建立這種連結,兩者完全分離,雖然 ES6 仍可以使用 arguments,但是它已經被廢棄,推薦使用剩餘運算子(...)
函式的引數是按值傳遞,不是按引用傳遞,即如果引數是一個物件,則在函式內部,通過形參修改這個物件,會反映到所有指向這個引數的變數
let obj = {}
function func(o) {
o.a = '123'
}
console.log(obj) // {}
func(obj)
console.log(obj) // {a:"123"}
複製程式碼
由於按值傳遞,所以這裡變數 obj 和形參 o 都指向同一個堆記憶體的物件,在 func 內部通過形參 o 往這個物件中新增了 a 屬性,會同時反映到變數 obj
未完待續
參考資料
《JavaScript 高階程式設計第三版》
《你不知道的JavaScript》