三刷紅寶書之 JavaScript 基本概念

前言

這是我第三次翻開紅寶書也就是《 JavaScript 高級程序設計第三版》,不得不說,雖然書有一些年份,不少知識點也不適合現代的前端開發,可是對於想要掌握 JavaScript 基礎的前端新手,亦或是像我同樣想找回曾經遺忘在記憶角落的那些碎片知識,這本書依舊很是的適合,不愧被成爲 "JavaScript 聖經"javascript

本文是讀書筆記,之因此又一次選擇讀這本書還有一個理由,以前都是記的紙質筆記,此次想把它做爲電子版,也算是對以前知識的整理html

本文篇幅較長,目的是做爲個人電子版學習筆記,我會盡量去其糟粕,取其精華,同時我會添加一些書上未記載但很重要的知識點補充前端

let's gojava

JavaScript 簡介

一個完整的 JavaScript 由 3 個部分組成,核心(ECMAScript 語法),DOM,BOM,後二者目前已是可選項了,或者能夠抽象爲宿主,由於 JS 已經不只限運行於瀏覽器node

在 HTML 中使用 JavaScript

在瀏覽器中使用 JS 能夠經過 script 標籤來執行 JS 文件,進一步能夠分爲 3 種方式,內嵌 JS 代碼,經過 src 指向本地 JS 文件,經過 src 指向某個靜態服務器的 JS 文件(域名),推薦的是使用 src 的形式,相比於內嵌能夠利用緩存提升頁面加載速度和解析 DOM 的速度,而且,由於 JS 和 HTML 解耦了可維護性更強git

當 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》

ECMA-262

MDN

相關文章
相關標籤/搜索