JavaScript 從 0 到 1 入門手冊(2020版)

wangsys發表於2021-09-09

前言

在學習 JavaScript 的過程中,我們通常會把 JavaScript 分為以下三個部分

  1. JavaScript核心語言(The Core (ECMAScript))
  2. 文件物件模型(The Document Object Model (DOM))
  3. 瀏覽器物件模型(The Browser Object Model (BOM))

本手冊將把重點放到 JavaScript 語言本身身上,也就是第一部分 JavaScript 核心語言(The Core (ECMAScript)

如果你把 JavaScript 選為你的第一門語言,那麼希望本手冊能夠幫助到你儘快掌握它。

如果你已經在日常開發中使用 JavaScript 了,那麼本手冊的內容也是一個很好的複習。

現在,讓我們從 JavaScript 的歷史開始。

瞭解歷史

在正式開始學習前,我們先來簡短地看一下 JavaScript 的歷史,這有助於第一次接觸 JavaScript 的人更好地學習。

誕生

JavaScript 建立於 1995 年。由當時 Netscape (網景) 公司的 Brendan Eich (布蘭登·艾克) 負責開發,最初命名為 Mocha,後來改名為 LiveScript,最後才重新命名為 JavaScript

在建立的初期,JavaScript 並沒有對應的標準(沒有統一的語法或功能),這造成了瀏覽器指令碼編寫的困難,所以當時業界都希望可以把 JavaScript 語言標準化

標準化

1997 年,JavaScript 1.1 版本被提交到了 ECMA(歐洲計算機制造商協會)ECMA 把它分配給了 TC39 技術委員會,希望可以制定出一個標準、通用、跨平臺且和各大瀏覽器廠商都無關的標準。

TC39 技術委員會在當時包括了 NetscapeSunMicrosoftBorland 等佔據市場主流的技術公司,幾個月的技術會議後,最終釋出了 ECMA-262

ECMA-262 這份標準定義了一種新的指令碼語言,名稱就是我們現在聽到的 ECMAScript,至於為什麼叫 ECMAScript 而不是就叫 JavaScript,這有可能是因為當時一些相關的法律或者品牌的原因所造成的。

版本名稱的變化

JavaScript 其實是 ECMAScript 標準的實現,這就是為什麼您會聽到有關 ES6ES2015ES2016ES2017ES2018ES2019ES2020 等的原因,ES 指的就是 ECMAScript

如果你看到我上面羅列的一些版本名稱,你會奇怪有一個 ES6,而其他都是 ES + 年份,這其實是因為這個更改的時間有點晚。

當時 ES5 是 2009 年時釋出 ECMAScript 規範的名稱,這導致 ES5 的下一個版本被人們稱為 ES6,而且官方決定宣佈使用 ES2015 而不是 ES6 的時間點其實很晚,這也就導致了目前在社群會有 ES6ES7ES8ES9ES10 這些版本名稱的出現,但是其實 ES + 年份才是目前的官方名稱。

其他

除了 JavaScript 外,其實也有其他語言也實現了 ECMAScript,比如 ActionScript,它是 Flash 的指令碼語言,但是目前已經逐漸消失了。

現在,支援 ECMAScript 規範的語言就是 JavaScript

其中,ES5 從 2009 年到現在,已經過去超過 10 年,雖然 ES5 在 JavaScript 的歷史中是一個非常重要的版本,但是 ES5 的很多知識已經不值得再投入過多時間。

現在,我們應該轉向 ES2015(ES6) 或之後的版本進行學習。

語法概述

大小寫

JavaScript 是區分大小寫的,不管是變數、函式名還是運算子都區分大小寫,比如變數名 Apple 和 apple 就代表了兩個變數。

識別符號的合法性

我們上面提到的變數 apple,其實就是程式中的識別符號

識別符號是用於描述程式中變數函式屬性引數等的名稱。

在 JavaScript 中,一個合法的識別符號規則如下:

  • 第一個字元必須是字母下劃線( _ )美元符號( $ )
  • 其他所有字元可以是字母下劃線( _ )美元符號( $ )數字
  • 不能是 JavaScript 中的關鍵字保留字

另外,對於識別符號,有三點需要提到:

  1. 識別符號如果包含 兩個或兩個以上 的單詞時,那麼應該採用 駝峰式 的寫法,第一個單詞的首字母小寫,後面的單詞首字母大寫,比如 myApple。
  2. 美元符號通常在引用 DOM 元素時被使用,又或者被一些常用的庫所使用(比如 jQuery),所以平時我們命名變數也很少使用美元符號( $ )
  3. 我們很少會去背關鍵字保留字,一般都是學習中或使用中慢慢熟悉。

註釋

JavaScript 採用 C語言 風格的註釋,即使用 //單行註釋,和使用 多行註釋

如:

// 單行註釋

/*
多
行註釋
*/

分號

分號(;) 用於結束一條語句,而一些足夠聰明的直譯器可以識別一條語句什麼時候結束。

這就導致了目前的一個爭議,一些開發人員建議始終使用分號,而另一些開發人員認為不需要使用分號

這裡的重點是,保持統一的做法

如果使用分號就都使用,不要在一個專案中,一個地方使用分號,另一個地方不使用。

以下兩條語句的區別只在於程式碼風格的不同:

const hello = 1;
let world = 2

風格可以討論,但不需要一個定論。

值和型別

JavaScript 有很多種型別,但是目前還沒到展開的時候,我們現在需要了解的是每一個都有對應的型別

比如 100 是一個,而數字是這個型別。又比如 apple 是一個,而字串是這個型別

通常,我們會說,字串 apple,數字 100。

而當我們需要使用這些的時候,我們需要把它們儲存到變數中,而每個變數都有一個變數名(也就是識別符號)來標識它,這樣我們就可以透過變數名來找到我們要使用的了。

變數

首先,變數是什麼?

對於初學者來說,一個易於理解的例子就是,變數是程式中的一個盒子,並且盒子上貼有唯一的標籤(變數名),而盒子裡面的內容,就是我們的

另外,可以想象盒子的大小或者形狀則是我們的型別

我們將使用兩個關鍵字去宣告變數,一個是 const(常量),另一個是 let(變數)

const foo = 1
let bar = 2

const

const(常量) 宣告瞭該變數不能為這個變數重新分配一個值了。

例如,以下操作將會報錯:

const foo = 1
foo = 2

JavaScript中的常量和一些程式語言中的常量是有區別的。

對於 const,我們並沒有說不能改變它的值,而是說不能為這個變數重新賦值。

那麼,如果我們使用 const 宣告一個物件,其實是可以修改這個物件的屬性的。

這一點,我們將在學習物件中討論。

關於常量,我們還需要提到關於命名。

現實中,對於常量的命名,我們有一個常規做法就是都大寫,比如上面的例子,我們將命名為 FOO

但是也有另外一種情況,就是我們的常量的值並非是提前知道的,而是需要在執行期間才能獲得,那麼,此時的常量的命名仍然使用駝峰式

以下 const 變數名命名的例子:

const APPLE_COLOR_RED = "#F00"
const codeExcuTime = /* 值將來自程式碼執行後 */

let

let 宣告的變數可以重新分配一個值。
例如,以下操作將會成功:

let bar = 2
bar = 3

var

var 是 ES6 之前用於宣告變數關鍵字

下面是一個例子

var foo = 2

var 類似我們的 let,這裡不會進入過多的討論,現實中,我們應該避免使用 var

建議

總的來說,我們透過宣告變數來存放我們的資料。

對於現在,我們應該只使用兩個關鍵字來宣告變數,letconst

其中的不同是,如果是使用 const 宣告變數,那就表示我們不希望這個變數的值被再次賦值。

注意,JavaScript 中,const 雖然表示常量,但並沒有表示不能修改它的值。

我們也提到的另外一個關鍵字 var,但我認為應該在你需要的時候再花時間去了解它。

專案中,我們應該避免使用 var,更多考慮使用的是 const,然後才是 let

型別

從一般的程式設計概念來看,變數的型別定義了可以存放到這個變數中的,以及可以對這個值所進行的操作

比如一個變數存放的值是數字型別,那麼這個變數可以執行加減乘除操作。

通常,我們會把型別劃分為兩類:

  • Primitive Types(原始型別)
  • Object Types (物件型別)

原始型別

JavaScript 中,原始型別包括了 NumberBigIntStringBooleanSymbol

另外,我們把 NullUndefined 這兩個特殊的型別也劃分到原始型別中,所以總共有 7原始型別

物件型別

物件型別,也就是 Object 型別

可以說 JavaScript 中,除去原始型別就是物件型別了。
物件型別涉及到 properties(屬性)methods(方法),我們將會在物件的知識點中討論。

在各大經典的 JavaScript 教程中都使用了原始型別物件型別來對 JavaScript 中的型別進行劃分。

但是在規範(ECAMScript2020)中並沒有這種劃分。只是直接列出了 8 種型別,也就是我們提到的 UndefinedNullBooleanStringSymbolNumberBigIntObject

其中 SymbolBigInt 算是後加進來的,所以在一些舊的教程中,你可能看到的是 6 種,而不是 8 種。

表示式

表示式概述

表示式總是和語句產生關聯,這也在社群中造成了一些爭議,但從掌握一門程式語言的過程來看,我認為區分表示式語句還是有必要的,這對我們學習和理解函數語言程式設計也有幫助。

表示式和語句的區別

語句其實是由關鍵字組成的一條命令,用於告訴 JavaScript 做什麼,比如我們常會說的匯入某某庫,這就需要我們寫語句來告訴 JavaScript了。

以下是一個語句的另外一個例子,用於告訴 JavaScript,我們宣告瞭一個變數,並把一個值儲存到該變數中:

let foo = 101

表示式可以看做就是值,以下就是一些表示式

101
1.38
'apple'
true
false

我們可以看到,表示式其實是表達一個,這就導致了表示式通常被放到等號的右邊。

語句通常都會涉及到關鍵字,比如迴圈語句,用於告訴 JavaScript 這裡需要重複執行。

在我們後面接觸的語句越來越多的時候,就可以更好地理解了。

表示式的分類

透過表示式,我們會得到一個,這裡如果細分,又可以分為算術表示式,這將得到一個數字,比如:

1 + 2
i++
i * 100

又或者字串表示式邏輯表示式,比如:

// 結果是 hello world!
’hello' + ' ' + 'world!'
// 結果是 true 或者 false
isCar && isHouse

除此之外,還可以是我們後面學到的函式物件陣列等,後面的章節將會介紹它們。

運算子

運算子的其實是 Operator 的翻譯。

前面我們提到,型別定義了可以對值所進行的操作

所以,通常的翻譯是運算子,而運算子,我覺得更多是數學上的概念,可能會更通俗一些。

但某些操作使用運算子可能會更好理解,所以,有時候也會使用運算子

賦值運算子

我們已經見過不少運算子了,第一個要正式介紹的運算子我們也已經見過,就是等號( = )

等號其實是賦值運算子,用於分配

另一種說法是初始化

以下就是一個初始化變數的例子:

// 把 score 初始化為 100
let score = 100

算術運算子

最常見的算術運算子當然就是 加( + )減( - )乘( * )除( / ) 了。

除此之外,還有取餘(%)求冪()** 。

以下是 例子:

let foo = 1 + 2
foo = 1 - 2
foo = 1 * 2
foo = 1 / 2
foo = 2 % 3
foo = 1 ** 2

Infinity 和 NaN

InfinityNaN 其實 JavaScript 全域性物件中的屬性屬性的值就是他們本身。

我們還沒有聊得到物件,但是這裡由於算術表示式會導致這兩個值出現,所以我們有必要了解一下。

Infinity

除法中,如果除以零,JavaScript 給出的結果是Infinity(正無窮大),或者是 -Infinity(負無窮大),而不是報錯。

let foo = 1 / 0 // Infinity
foo = -1 / 0 // -Infinity

另外,Infinity 和數學中的無窮大概念很類似,比如任何正數乘以 Infinity 會得到 正Infinity(負數則得負 Infinity,任何正數值除以 Infinity,則得到正 0。

NaN

NaN(Not-A-Number) 表示不是一個數字。

我們都知道 0 不能做除數,那麼如果這麼做,JavaScript 會告訴你得到的不是一個數字,也就是 NaN

如下是一個例子:

let foo = 1 % 0 // NaN
foo = -1 % 0 // NaN

通常 NaN 的出現,其實就和它的含義一樣,透過計算,得到了一個不能表示為數字的值。

比如運算子中兩個變數的型別不同,又或者其中一個變數的值已經為 NaN 了.
如:

'a' / 1 // NaN
1 + ('a' / 1) // NaN

對於運算中何時會出現 Infinity,何時會出現 NaN,在 ECMA 的規範中羅列了很多情況,剛開始學習的時候我們並沒有必要每一條都找出來,只需要知道這兩個值得含義就好了。

下圖是 ECMA2020 中關於僅關於除法操作出現的情況,大家感受一下:
圖片描述

運算子的優先順序

我們都知道在數學中,加減乘除運算是有優先順序的,通常口訣就是先乘除,後加減

在 JavaScript 中也是一樣的,運算子優先順序決定了運算執行的先後順序

比如:

1 + 2 * 3 // 1 + 6 結果為 7

所有運算子都有優先順序,有人彙總成了一個表格,但是我們並不需要去背這個。

因為我們有一個處理優先順序的超強辦法,就是使用小括號(或者說是圓括號)

下面是一個例子:

(1 + 2) * 3 // 3 * 3 結果為 9

小括號優先順序最高的,並且本身沒有關聯性

關聯性指的是,如果大家的優先順序相同,那麼我們應該怎麼執行。

一般來說,關聯性分為左關聯(從左到右執行)右關聯(從右到左執行)

比如我們的加減乘除就是左關聯的,那麼 1 + 2 + 3,會優先考慮計算 1 加 2,然後再加 3 了。

右關聯的一個例子就是我們的賦值運算子

在一條賦值語句中,JavaScript 會把等號右邊的表示式算出來後再賦值給等號左邊的變數中:

let foo = 1 + 2 * 3 // 把7算出來後,再儲存到 foo 變數中

比較運算子

第三個要了解的運算子是比較運算子

比較過後,我們會得到一個布林型別的值,總共就兩個,分別是 truefalse

常見的比較運算子如下:

  • < (小於)
  • <= (小於或等於)
  • >(大於)
  • >=(大於或等於)
  • === (是否相等)
  • !== (是否不相等)

下面是例子:

let foo = 1
foo >= 1 // true

關於相等不相等,其實還有另外的運算子,==!=。對於初學者來說,我們應該選擇使用 === 來進行檢查是否相等,使用 !== 來檢查是否不相等

這也是現實程式設計中的常規選擇。

有了比較運算子之後,我們就可以討論開始條件語句了。

條件語句

通常,程式碼會從上到下一行接一行地被執行。

但是有時候,我們需要根據條件來選擇執行不同的程式碼。

為此,我們使用 if 語句。

if 語句實在是太常用了,以至於幾乎所有的程式語言都會提供,所以當你掌握一種程式語言的 if 語句後,其他語言的完全不在話下。

三種 if 語句

一般來說,和其他程式語言一樣,JavaScript 也提供了 if 語句的三種語法:

// 如果條件為真(true),則執行大括號裡面的語句。
if (condition) {
  statements
}
/**
 * 如果條件為真(true),則執行大括號裡面的語句。
 * 否則執行else語句塊的內容。
 */
if (condition) {
  statements
}
/**
 * 如果條件1為真(true),則執行大括號裡面的語句。
 * 否則透過 else if 語句判斷條件2,為真(true)則執行大括號裡面的語句。
 * 如果前面的if和elseif(一個或多個)都不為真(true),則執行else語句塊的內容。
 */
 if (condition_1) {
   statements
 } else if (condition_2) {
   statements
 } else {
   statements
 }

如果你有學過其他程式語言的 if語句 的話,那麼上面就是 JavaScript 中的 if語句 的用法,複習一下之後,你就可以進入下一節的內容了。

我們來一個個看,首先是一個第一個簡單的例子:

// 永遠被執行
if (true) {
  const score = 100;
}
// 永遠不被執行I 
if (false) {
  const socre = 0;
}

接著我們可以為 if語句 提供 else,作為當 if 為假的時候的選擇:

// 如果是真,score賦值為100,否則賦值為0
if (true) {
  const score = 100;
} else {
  const score = 0;
}

最後,我們的 else 其實也是可以新增語句的,所以,我們可以巢狀一個 if/esle 語句,這樣,我們就可以對多個條件進行判斷:

if (score === 0) {
  // ......
} else if ( foo === true) {
  // ......
} else {
  // ......
}

注意,一個 if 語句中,else if 可以寫多個,但 ifelse 只能寫一個。

語句塊和作用域

下面是 if語句 的一個簡單例子:

if (true) {
  // ......
}

現在,我們來關注花括號花括號代表了語句塊,也就是我們說的 block

語句塊表面上看,就是可以讓我們把一些語句放在一起。

當然,一條語句也可以放到花括號中,變成語句塊

但是,使用花括號對我們的語句進行分組並不是最需要理解的概念。

最重要的是,語句塊定義了一個新的範圍,我們稱之為作用域(scope)

作用域 通俗的理解就是程式碼的可見範圍

if (true) {
    let score = 1
}
/**
  * 當我們直接使用score的時候,將報錯
  * 報錯:ReferenceError: score is not defined
  * console.log() 用於向控制檯列印內容。
  */
console.log(score)

上面的例子中,if語句塊 中定義了的變數 score。

但在語句塊外面想使用時,卻告訴我們這個變數沒有定義的。

因為語句塊劃分了可見範圍,語句塊裡面和外面,就好像兩個世界一樣。

示例中 if 語句的語句塊所形成的作用域,我們也稱為塊級作用域,而在 ES2015,也就是 ES6 之前,並不存在塊級作用域

ES5 中的 var 沒有塊級作用域,是我們放棄使用 var 的重要原因。

陣列

透過前面型別的學習,我們知道目前 JavaScript 中只有 8 種資料型別,其中並沒有包括陣列。

也就是說陣列本身不是一種型別。

陣列的作用是可以儲存多個值,更官方的說法是,陣列是元素集合

元素,簡單來說就是值,反向地說,元素是組成了某個東西的一些東西。

之前我們對 8 種型別進行了劃分,提到除了原始型別之外,就都是物件型別

那麼,陣列其實就是物件了。

初始化

首先我們看看如何初始化陣列一個空陣列

const foo = []
const bar = Array()

上面是兩種初始化空陣列的方法。

第一種是直接使用了中括號語法。

第二種其實是使用了Array函式

我們還沒學到函式,目前先知道這使用了函式就可以了。

我們再看看如何初始化一個有元素的陣列:

const foo = [1, 2, 3]
const bar = Array().of(1, 2, 3)

陣列其實可以包含不同型別的值,甚至可以包括另外一個陣列:

const foo = [1, ’apple', [100]]

現實中,除非有很好的理由,否則一個陣列應該只放一種型別的元素。

至於多維陣列,等需要的時候再去了解吧。

訪問元素

要訪問陣列中元素,我們需要透過陣列中的索引

const foo = [1, 2, 3] 
foo[0] // 取得1

索引就是陣列中元素的唯一編號,所以透過索引就找到對應的元素了。

我們可以看出,陣列的索引其實是從 0 開始,程式設計的世界大都如此。

陣列的操作

因為陣列不在 7 個原始型別之中,所以我們知道陣列其實是物件,也就是說陣列本身應該有一些行為(函式)。

如果完全沒有物件或者函式的概念,那麼可以先學完相關的知識後再回到這裡。

函式很多,希望本手冊隨著更新會越來越全。

快速填充陣列

使用 fill 可以為我快速初始化一個陣列:

// 初始化一個長度為3的陣列,裡面的元素都設定為100
const foo = Array(3).fill(100)

獲取陣列長度

要知道陣列的長度,我們可以使用 length 屬性:

const foo = [0, 1, 3]
foo.length // 3

增添元素

我們可以使用 push() 函式,該方法在陣列的尾部新增元素:

let foo = [1, 2]
foo.push(3) // 1, 2, 3

而使用unshift()方法則在頭部新增元素:

let foo = [1, 2]
foo.unshift(3) // 3, 1, 2

取出元素

和新增類似,我們可以使用 pop()shift() 函式。

pop() 用於取出陣列尾部的元素:

let foo = [1, 2, 3]
foo.pop() // foo變成1, 2

shift() 用於取出陣列頭部的元素:

let foo = [1, 2, 3]
foo.shift() // foo變成2, 3

刪除元素

刪除元素,我們可以使用 delete。

let foo = [1, 2, 3]
delete foo[1] // 刪除索引為1的元素
console.log(foo)  // [1, ,3]
console.log(foo.length) // 3

注意,使用delete刪除陣列中的元素,只是移除了元素的值。

元素原本的位置仍被佔據著,陣列的長度不會變。

拼接陣列

拼接陣列,我們可以使用 concat() 函式:

const foo = [1, 2]
const bar = [3, 4]
const baz = foo.concat(bar) // [1, 2, 3, 4]

除此之外,使用 擴充套件運算子 也是可以的:

const foo = [1, 2]
const bar = [3, 4]
const baz = [...a, ...b] // [1, 2, 3, 4]

查詢元素

查詢元素,我們使用 find() 函式:

let foo = [
  {id: 1, score: 80},
  {id: 2, score: 90},
  {id: 3, score: 100}
];

let bar = foo.find(item => item.id == 1);

console.log(bar.score); // 80
console.log(bar); // { id: 1, score: 80 }

相比起來,find 的用法稍微複雜,但是對於儲存物件的陣列來說,非常有用。

在上面的例子中,我們透過傳遞一個只有一個引數的函式 item => item.id == 1 來使用 find,這也是現實中使用較多的用法,其他引數的一般很少使用。

另外就是如果想找出陣列中元素的索引,我們只需要把 find 改為 findIndex 就可以了。

字串

字串也就是 String,在程式語言中通常佔據重要地位。

多個字元就組成了字串,更官方準確的說法就是字元序列

宣告字串

在 JavaScript 中,定義字串,使用單引號雙引號都可以:

'I am string.'
"I am string too."

一般來說,一個專案除了 JavaScript 檔案外,還有 HTML 檔案,所以更多建議 JavaScript 中使用單引號,而在 HTML 中使用雙引號

字串的操作

既然字串是一個字元序列,那麼自然就會有相關操作了。

首先看看如何初始化一個字串變數:

const foo = 'dog'

獲取字串長度

使用 length 屬性檢視字串的長度:

const foo = 'dog'
console.log(foo.length) // 3

字串可以看做是一個字元陣列,所以注意,字串的索引也是從 0 開始的。

字串的連線

連線兩個字串,我們可以直接使用 “+” 號:

const foo = 'Hello' + ' World!'
console.log(foo) // Hello World!

變數也是可以的:

const foo = 'Hello'
const bar = foo + ' World!'
console.log(bar) // Hello World!

定義多行字串

有時候,我們希望做一些模擬資料,需要定義多行字串,我們可以使用反引號:

const string = `Hello
this 
 is string
World`

迴圈

迴圈是所有程式語言中必須要有的語句之一。

透過迴圈,我們可以重複執行一段程式碼。

通常來說,whilefor 是主要的內容。

JavaScript 中有很多種迴圈的方法,我們這裡介紹三種:

  • while
  • for
  • for…of

while

while 應該是最簡單的一種了:

while (condition) {
  statement
}

其中 condition條件表示式,也就是它需要一個布林值,真(true)或假(false)。

如果是真,就執行語句塊裡的語句,每執行完一次語句塊,就重新執行一次條件表示式,看一下值是真還是假。

如果是真,就繼續執行語句塊,如果是假,迴圈就停止了。

使用 while 語句,重要的是需要新增能控制條件表示式在某次迴圈得到一個假的值,不然迴圈一直下去,無法停止的迴圈,我們稱為死迴圈

除此之外,對於第一個介紹的迴圈語句,我還想結束一些其他的概念,以便更好地學習後面的 for 迴圈。

下面是另外一個例子:

const foo = ['a', 'b', 'c']
let i = 0
while (i < foo.length) {
  console.log(foo[i]) // 將列印 a, b, c
  console.log(i) // 將列印 0, 1, 2
  i = i + 1 // 每次迴圈都需要遞增 1
}

首先,while 語句後面的小括號的內容是條件表示式

條件判斷為真的時候就執行後面語句塊的內容。

也就是說當透過計算, i < foo.lenght 得到的值是 true 的時候,後面的語句塊就會被執行。

在迴圈語句中,語句塊也叫 迴圈體

迴圈語句中還有一個迭代的概念,通常,迴圈體 被執行一次,我們也叫 一次迭代,上面的例子就是 三次迭代 了。

如果例子中沒有 i = i + 1 這條語句的話,那麼理論上說,迴圈體會一直被執行。

另外,只要最終可以被 JavaScript 轉化為一個布林值的表示式,就都可以放到迴圈條件中,而不僅僅是大小運算子比較的條件表示式。

for

當使用 for 關鍵字的時候,我們一般會告訴 for 三件事情:

  • 初始化變數是什麼
  • 迴圈條件是什麼
  • 每次迴圈後需要執行什麼

下面是一個例子

for (let i = 0; i < 3; i++) { // 結果為 0、1、2
  console.log(i);
}

上面的例子中,初始化變數是 i,迴圈條件是 i < 3,每次迴圈過後執行 i++

++ 是自增運算子,簡單來說就是讓自己加1。

由於每次迴圈後,i 都會加1,那麼,i 將會在某次迴圈過後,它的值將不是小於3的了。

此時,迴圈終止。

如果你是第一次接觸 for 迴圈語句,那麼還需要知道的是,三個表示式都是可選,極端的情況是都不寫(但是分號不可省略)。

另外,在不同的程式語言,或者不同的教程中,對 for 語句中的三個表示式有不一樣的稱呼。

比如 ECMAScript2020 文件,因為是標準,所以簡單明瞭,對這三個表示式都稱呼為表示式,然後使用 opt 代表都是可選的,大概像下面這樣:

for(Expression(opt); Expression(opt); Expression(opt) {
  Statement
}

而在 MDN 的文件中,使用的是 initializationconditionfinal-expression,然後使用中括號代表都是可選的,大概像下面這樣:

for ([initialization]; [condition]; [final-expression]) {
  statement
}
   

其實除了標準是簡單粗暴地稱為表示式外,其他教程幾乎都會對 for 迴圈裡的三個表示式採用一個不同的稱呼。

這都是為了讓讀者更好地明白 for 語句是怎麼用的。

我們這裡把三個表示式,從左到右編號,就是表示式1,表示式2,表示式3:

for (表示式1; 表示式2; 表示式3)
   statement

我們要知道 1,2,3 各自什麼時候執行。

其中,記住表示式1只執行一次,然後執行表示式2。

如果表示式2判斷為真,那麼進入迴圈體,迴圈體執行完後,執行表示式3。

這裡,其實就是不斷迴圈了,表示式2開始,然後迴圈體,然後表示式3,之後回到表示式2,然後迴圈體,然後表示式3,之後回到表示式2,如此迴圈執行。

直到有一次,表示式2判斷為假了,整個迴圈也就結束了。

for…of

for…ofES2105,也就是 ES6 中才出現,當我們想檢查物件的屬性時,使用它更加方便,特別是有 key-value 這種資料的時候(比如屬性用作“鍵”),需要檢查其中的任何鍵是否為某值的情況時,還是推薦用for … in。

下面是一個簡單的例子:

const hi = ['h', 'i']

for (const value of hi) {
  console.log(value) // hi
}

上面的例子中,我們只是遍歷看看,並不改動,所以使用了 const 來宣告語句塊的變數。

接下來我們,將會進入函式的學習。

函式

終於來到函式了,可以說,函式是 JavaScript 中最重要同時也最有意思的部分。

那麼,什麼是函式?

在大部分程式語言中,一個函式通常就是一個可以被重複呼叫的語句塊

當然,在 JavaScript 中會有不一樣的概念。

JavaScript 中,函式其實是物件,如果你有物件的概念,那麼現在就可以知道,函式的名稱其實是指向函式物件的一個指標。

同時,函式自己是一個物件,這說明函式本身具有對應的屬性和方法。

ok,對函式的解釋到這裡就可以了。

如果你一頭霧水,那麼沒關係。

因為,對於初學者來說,學習函式就從函式是一個可以被重複呼叫的語句塊 這個概念開始就很好。

讓我們從如何宣告和呼叫一個簡單的函式開始。

函式的宣告和呼叫

讓我們看一下,我們如何宣告一個函式:

function sayHello() {
    console.log('Hello!')
}

其中,function 是關鍵字,用於宣告函式,接著是函式名 sayHello

函式名後面是圓括號,圓括號裡的內容,我們稱呼為形式引數(通常簡稱形參)。

形參如果出現多個多個,我們使用逗號分隔。

在有多個形參的時候,我們稱為形參列表

最後的花括號,我們稱之為函式體

目前,我們知道一個函式包括了以下4個內容:

  1. function關鍵字
  2. 函式名
  3. 形參列表
  4. 函式體

接下來,我們來看另外一個函式:

function sum(num1, num2) {
    return num1 + num2;
}

上面的函式所描述的功能是兩數相加。

我們會發現其中多出了一個關鍵字 return

return 的作用是返回一個值(簡稱返回值)。

在 JavaScript 中,每個函式其實都會返回一個值,預設情況下,返回 undefined,及時沒有寫 return語句也是一樣。

上面的例子中,我們使用 return 語句來明確我們的返回值是兩個形參之和。

我們目前只是宣告瞭一個函式。

如果不呼叫它們,那麼它們並不會自己執行。

要呼叫一個函式很簡單,只需要使用函式名和告訴它形參是什麼就可以了。

下面是對上面兩個函式進行呼叫的例子:

sayHello() // Hello!
console.log(sum(1, 2)) // 呼叫,並把函式返回的結果列印出來

函式表示式

讓我們再看一下實現兩數相加的函式:

function sum(num1, num2) {
    return num1 + num2;
}

上面的函式其實等同於我們使用一個函式表示式,就像下面這樣:

let sum = function(num1, num2) {
    return num1 + num2;
}

如果你忘了什麼是表示式,那麼建議你回看前面關於表示式的內容。

現在,我們可以像之前那樣呼叫這個函式:

console.log(sum(1, 2)) // 呼叫,並把函式返回的結果列印出來

在 JavaScript 中,function關鍵字函式名形參列表函式體這些就構成了我們的函式。

這種函式通常也被我們稱呼為普通函式,或者常規函式。

箭頭函式

箭頭函式在ES6中出現,目前已經變得非常常用了。

有時候,箭頭函式和我們上面剛提到的函式表示式很類似。

下面是一個比較,大家感受一下:

// 函式表示式
let sum1 = function(num1, num2) {
    return num1 + num2;
}
// 箭頭函式
let sum2 = (num1, num2) => {
    return num1 + num2
}
console.log(sum1(1, 2)) // 列印 3
console.log(sum2(1, 2)) // 列印 3

從外表看,箭頭函式會更簡單,它不僅省略了 function 關鍵字,還省略了函式名

比如普通函式:

function sayHello() {
  console.log('Hello!')
}

箭頭函式是這樣:

() => {
  console.log('Hello!')
}

當我們想像普通函式一樣呼叫箭頭函式的時候,我們會發現,由於省略了函式名,我們沒辦法呼叫。

所以,我們需要將它賦值給一個變數。

let sayHello = () => {
    console.log('Hello!')
}

這樣,我們就可以透過變數名來呼叫這個箭頭函式了:

sayHello()

如果我們只有一條語句,我甚至可以省略花括號:

let sayHello = () => console.log('Hello!')
sayHello() // 將列印 Hello!

可以看到,對於只有一行程式碼的函式來說,箭頭函式展示了它的便捷性。

當然,如果函式體內有多行語句,我們還是要顯示地使用花括號和 return 語句。

讓我們看看兩數之和的箭頭函式是怎麼樣的:

let sum = (num1, num2) => num1 + num2
console.log(sum(1, 2)) // 列印 3

我們可以看到,箭頭函式允許我們省略 return 關鍵字。

如果我們只有一個引數,我們還可以省略圓括號:

const getClassName = className => console.log(className)
getClassName('三年二班') // 列印 三年二班

總的來說,箭頭函式的要點是:

  1. 使用函式表示式的方式,但是省略 function 關鍵字和函式名
  2. 形參列表和函式體之間使用 =>
  3. 當形參列表只有一個引數的時候,我們可以省略圓括號
  4. 當函式體內只有一條語句的時候,我們可以省略花括號,或者 return 關鍵字。

更多的用法,我們可以在需要的時候再瞭解。

剛開始接觸箭頭函式的時候並不容易,但是多嘗試幾次,習慣之後就會好很多了。

物件

物件是我們的 8 八大型別之一,在 JavaScript 之中,如果不是原始型別,那麼就是物件型別了。

物件的建立

我們先來看看如何建立一個物件:

const foo = {

}

上面建立物件的方式是目前 JavaScript 裡面最好的特性之一。

這種方式我們稱呼為字面量方式。

直接使用花括號,就好像我們使用單引號雙引號宣告一個字串一樣。

只不過,花括號宣告的是物件,而單引號雙引號宣告的是字串

除了花括號外,還有以下兩種方式:

一種是使用物件的建構函式,語法是 new Object

const foo = new Object()

其中,new 是我們的關鍵字。

new 可以用於呼叫物件的建構函式,並以此來初始化物件。

第三種是使用 Object.create()

const foo = Object.create()

儘管有多種建立物件的方法,但是現實中,我們都傾向於使用字面量的方式來建立物件,它更加地視覺化,以及使用了更少的程式碼。

所以,請使用字面量的方式來建立物件。

物件的屬性

物件裡面通常有屬性方法

我們先來看看如何宣告物件的屬性

下面我們以字面量的方式建立一個物件

let person = {
    name: "Jack",
    "age": 18,
    2: true,
    "likes dogs": true,
}

上面的例子,演示了物件中屬性的寫法。

首先是屬性名,後面跟上冒號,之後是屬性的值。

多個屬性之間使用逗號分隔。

最後一個屬性可以沒有逗號,因為在一些老的瀏覽器中,有逗號會報錯。

但是現代的瀏覽器都支援最後一個屬性可以寫逗號,所以現在建議最後的屬性也加逗號,方便他人也方便自己。

另外,屬性名會被自動轉換為字串,所以可以加雙引號也可以不加。

但是如果屬性名是多個單詞的就必須要加上雙引號了。

物件的方法

物件中的方法就是我們之前學習的函式,只不過和物件聯絡起來的時候,我們把函式稱呼為方法

下面是一個例子:

let person = {
    name: "Jack",
    age: 18,
    sayHello: function() {
      console.log('Hello!')
    },
}

挺熟悉的是嗎,我們也是寫了一個屬性,只不過屬性值是一個沒有函式名的函式而已。

我們為 sayHello 屬性分配了一個函式,這種情況下,我們就把函式稱呼為方法

還記得,我們學過箭頭函式嗎?

箭頭函式也是可以的:

let person = {
    name: "Hello",
    age: 100,
    sayHello: () => console.log('Hello!'),
}

訪問物件的屬性和方法

我們為物件設定了屬性和方法後,怎麼呼叫呢?

和大多數物件導向語言一樣,JavaScript 也是使用點運算子來訪問物件的屬性和方法。

另外,JavaScript 還提供了方括號來訪問屬性和方法。

下面是一些例子:

let person = {
    name: "Jack",
    "age": 18,
    2: true,
    "likes dogs": true,
    sayHello: () => console.log('Hello!'),
}
console.log(person.name) // Jack
console.log(person.age) // 18
console.log(person[2]) // true
console.log(person["likes dogs"]) // true
person.sayHello()

一般而言,使用點運算子和使用中括號在功能上講,並沒有什麼區別。

但是有時候,我們必須使用中括號,如上的 2likes dogs

因為點操作無法使用,原因是點運算子的操作物件需要符合我們識別符號的命名規則。

如果大家忘記了合法識別符號的那些規則的話,建議回看前面的內容了。

重點是第一個字元必須是字母美元符號($)短下劃線(_)

有了點運算子和中括號的概念後,我們可以介紹使用 new 關鍵字來建立物件了:

let person = new Object();
person.name = "Jack"
person.age = 18
person[2] = true
person["like dogs"] = true

這裡,我們使用了點運算子來設定我們的屬性,但是像數字和多詞屬性名就沒辦法使用點運算子了。我們需要使用中括號。

再次說明,我們更喜歡使用字面量的方式來建立物件。

物件中的箭頭函式和普通函式

注意,我們示例中雖然使用了箭頭函式作為物件的方法,但有時候我們必須選擇普通函式來作為物件的方法。

這是因為在物件中,箭頭函式和物件之間根本沒有繫結,這導致在箭頭函式中的 this 關鍵字是無效的。

下面是一個例子:

let person = {
    name: "Jack",
    sayHello: function() {
        console.log(`Hello, ${this.name}`)
    },
    anotherHello: () => {
        console.log(`Hello, ${this.name}`)
    }
}
person.sayHello() // Hello, Jack
person.anotherHello() // Hello, undefined

這是箭頭函式和普通函式之間的一個區別。

如果我們理解了物件導向,那麼,我們很難想象只有物件,而沒有類的世界。

因為這樣的世界裡所有的物件都無跡可尋。

那麼類到底是什麼呢?

在物件導向的世界裡,對於類的解釋,流傳著這麼一句話:類是物件的藍圖。

也就是說,只要我們規劃好了類,我們就可以根據類建立出無數個我們想要的物件。

我們也可以把類理解為,一種自定義型別。

我們自己定義了一種型別,然後建立很多相同型別的物件供我們使用。

類的定義、屬性和方法

那麼如何定義一個類呢?

和大多數面向程式語言一樣,在 JavaScript 中我們也是使用 class 關鍵字和花括號來定義類。

下面是一個例子:

class Person {}

我們雖然定義了一個類,但是,在 JavaScript 中,類其實只是一種特殊的函式。

所以,和函式類似,除了函式表示式外,我們還有類表示式。

下面是一個例子:

const Person = class {}

如果你觀察,你會發現 的名稱,我們使用大寫字母開頭。

如有兩個或兩個以上的單詞,仍然使用駝峰式,也就是每個單詞的首字母仍然需要大寫。

下面我們來定義一個包含屬性的類:

class Person {
  name
}

接著我們可以透過 new 關鍵字初始化一個該類的物件:

const jack = new Person()

和我們之前介紹的物件類似,我們可以使用點運算子來訪問其屬性:

jack.name = 'Jack'

我們是可以直接在類中初始化屬性的:

class Person {
  name = 'Jack'
}

和物件類似,類中除了可以定義屬性外,還可以定義方法:

class Person {
  name = 'Jack'
  
  sayHello() {
  	console.log('Hello!')
  }
}

構造方法和 this

類中除了普通的方法外,還有幾種特殊的方法,其中一個就是構造方法

構造方法通常用於在我們構造一個物件的同時初始化屬性。

構造方法也叫做構造器,在 javascript 中透過使用 constructor() 建立:

class Person {
    constructor(name) {
        this.name = name
    }
    sayHello() {
        console.log(this.name)
    }
}

上面的例子中,我們使用 constructor() 來初始化屬性,除了使用了形參列表外,我們還是用了另外一個關鍵字 this

對於 this,我們可以認為,誰呼叫,this 就是誰。

比如 我們建立了 jack 物件,然後 jack 物件呼叫了 sayHello() 方法,那麼 this 指的就是 jack 物件。

下面是構造方法來初始化 Person 類,並呼叫其方法的例子:

const jack = new Person('Jack')
jack.sayHello() // Jack

靜態方法

有時候,我們可能希望不建立物件,就直接呼叫類中的方法。

透過使用關鍵字 static,我們可以這樣做。

使用 static 宣告的方法也叫靜態方法

下面是一個例子:

class Person {
  static sayHello() {
    console.log('Hello!')
  }
}

類中的方法被 static 修飾後,可以使用類名直接呼叫:

Person.sayHello()

get/set方法

類中除了普通方法靜態方法構造方法外,還有 get 方法和 set 方法。

下面是 get/set 方法的一個簡單例子:

class Person {
  constructor(name) {
    this.name = name; // 將呼叫set方法來設定name屬性
  }
  
  get name() {
    return this._name;
  }
  
  set name(newName) {
    this._name = newName
  }
}
let jack = new Person();
jack.name = 'Jack'
console.log(jack.name) // Jack

總結

一個類中可以包含屬性普通方法構造方法get/set 方法,當然,這些都是可選的,就像我們一開始定義一個空的類一樣。

繼承

在 JavaScript 中,類的繼承就是一個類對另外一個類進行擴充套件。

要使用這種功能,我們需要使用到關鍵字 extends

下面是一個例子:

class Animal {
    eat() {
        console.log('eat......')
    }
}

class Cat extends Animal {
    super()
    miao() {
        console.log('miao, miao......')
    }
}

我們建立的貓類擴充套件了動物類。

我們把貓類稱為動物類的子類,而動物類就是貓類的父類

現在,由於貓類繼承(擴充套件)動物類,所以也有動物類的行為。

如下,我們可以呼叫父類的 eat() 方法:

const cat = new Cat()
cat.eat()
cat.miao()

同時,在子類可以透過 super 來呼叫父類

class Cat extends Animal {
    miao() {
        super.eat()
        console.log('miao, miao......')
    }
}
const cat = new Cat()
cat.miao()

上面程式將列印:eat…miao,miao…。

非同步程式設計和回撥

同步和非同步

同步和非同步其實是計算機世界裡的一個基礎概念。

同步可以簡單地理解為在完成一條計算機指令之前,下一條指令不會被執行。

同步的一個問題其實是會造成程式執行被中斷,從而進入等待。

比如我們瀏覽器需要呼叫網頁裡 JavaScript 檔案中的一個函式,我們需要先下載這個檔案。

否則我們瀏覽器將提示找不到這個函式。

但在下載這個檔案期間,網頁中的其他程式碼應該可以繼續載入執行,而不是被下載這個 JavaScript 檔案的行為所中斷,造成使用者的等待。

JavaScript 使用 回撥 來解決這個問題。

回撥的使用

介紹回撥通常都是以一個簡單的計時器開始。

以下是一個例子:

setTimeout(() => {
    console.log('2 秒後...')
  }, 2000)

setTimeout() 函式里有兩個引數,一個是函式,另一個數字(單位是毫秒)。

語句 console.log(‘2 秒後…’) 將在兩秒後被呼叫。

setTimeout() 並非是 JavaScript 核心語言的一部分,它並沒有包括在 ECMAScript2020 手冊中。

setTimeout() 一般由瀏覽器或者常見的 Node.js 環境所提供,所以在這裡,2秒並不一定和你係統上的2秒相同,它取決於當時的執行環境。

我們再執行如下程式碼:

console.log('我還沒來')
setTimeout(() => {
    console.log('2 秒後...')
  }, 2000)
console.log('我來了')

我們看到的結果可能是這樣的:

我還沒來
我來了
2 秒後...

回撥的模式在處理網路、事件或者瀏覽器中的DOM的時候,都非常常見。

callback 的使用

處理回撥我們通常會定義一個 callback 引數:

const foo = callback => {
  callback()
}

callback 通常是一個函式,下面是我們之前計算兩數之和的一個例子,我們修改為使用回撥函式:

function sum(num1, num2, callback) {
    setTimeout(() => callback(num1 + num2), 2000)
}
sum(1, 2, (num3) => console.log(`It's ${num3}`))

上面的程式將在2秒後輸出 It’s 3

處理回撥的成功和失敗

在回撥的模式中,其實是一個等待處理的過程,那麼,處理的結果就有可能出現失敗。

以下是回撥時,處理成功和失敗的例子:

function sum(num1, num2, success, failure) {
    setTimeout(() => {
        try {
            if (0 === num2) {
                throw '除數不能為0'
            }
            success(num1 / num2)
        } catch (e) {
            failure(e);
        }
    }, 1000)
}
const successCallback = (num3) => console.log(`成功: ${num3}`)
const failureCallback = (e) => console.log(`失敗: ${e}`)

sum(6, 2, successCallback, failureCallback)
sum(6, 0, successCallback, failureCallback)

輸出的結果如下:

成功: 3
失敗: 除數不能為0

上面處理回撥成功和失敗的方式已經比較舊了,也不太推薦使用。

回撥地獄

不太推薦使用上一節中處理回撥的原因之一就是:在我們的回撥需要依賴另一個回撥時,就形成了巢狀的回撥。

這將使我們的程式碼出現著名的回撥地獄

我們看看把上面兩數之和的例子變成回撥地獄是什麼樣子:

function sum(num1, num2, success, failure) {
    setTimeout(() => {
        try {
            if (0 === num2) {
                throw '除數不能為0'
            }
            success(num1 / num2)
        } catch (e) {
            failure(e);
        }
    }, 1000)
}
// 沒有巢狀回撥的時候
// const successCallback = (num3) => console.log(`成功: ${num3}`)
// 回撥地獄開始
const successCallback = (num3) => {
    sum(8, 4,  () => {
        sum(10, 5, () => {
            sum(12, 6, (anotherNum) => console.log(`Success: ${anotherNum}`))
        })
    });
}
const failureCallback = (e) => console.log(`失敗: ${e}`)
sum(6, 2, successCallback, failureCallback)

我們輸入 6 和 2 ,但最終,透過回撥地獄,我們得到的是:

Success: 2

一旦程式碼中出現了 回撥地獄 ,這份程式碼將非常難以維護。

為了解決回撥地獄的問題, ES2015 也就是 ES6 推出了 Promises ,並且推出後,採用率就非常高,可以說成為了 JavaScript 中處理非同步程式碼的標準。

Promises

promise 概述

promises 是處理非同步的另外一種方式。

在 JavaScript 中,promises 較早期的使用應該是在 jQuery 或者 Dojo 的一些 API 中。

promises 規範有很多版本的實現,它在 2010 年開始慢慢普及。

對 JavaScript 來說,直到 ECMA2015 也就是 ES6 中才被正式引入。

現在幾乎所有的現代瀏覽器都完全地支援了 Promises

promise 涉及了一些新的概念,就像我們學習物件和類一樣。

promise 沒有在我們的原始型別中,所以其實它是物件,更準確地說是一種引用型別。

建立 promise 物件

要建立一個 promise 其實和我們建立一個物件很相識:

let foo = new Promise(() => {});
// 普通函式的寫法
// let foo = new Promise(function () {});

promise 中的狀態

在使用 promise 之前,我們首先要了解 promise 物件中的一個屬性叫做 state

state 用於描述 promise 中的三種狀態:

  • pending:這是一種初始狀態,不是成功也不是失敗。
  • fulfilled:呼叫成功時的狀態。
  • rejected:呼叫失敗時的狀態。

promise 的狀態指明瞭 promise 是否開始執行了。

pending表示執行還沒有開始或者還在執行中。

fulfilled 表示執行成功,沒有出現異常。

rejected 表示沒有執行成功,出現了異常。

其實狀態在程式碼中是不會直接使用的,所以無需太在意狀態的名稱。

重點是知道有這三種狀態。

在promise的狀態中,還有一個概念是需要我們知道。

這個概念叫 settled(不變的)

settled 表示的是 fulfilledrejected 這兩個中狀態中的任一種。

promise 物件的狀態,從 pending 轉為 settled 後,這個 promise 物件的狀態就不會再改變了。

promise 中的 resolve() 和 reject()

我們知道了 promise 有三種狀態,但是 promise 的這三種狀態其實是私有的,並沒有公開讓我們訪問。

這樣做的原因是,為了防止使用 promise 的時候直接根據狀態來對物件進行同步程式設計。

雖然我們無法直接訪問這三種狀態,但 promise 封裝了一個非同步函式,稱之為 executor,透過它,我們可以對 promise 的狀態進行遷移。

JavaScript 自身為我們的 executor 提供了兩個可選引數,一個是 resolve,另一個是 reject

所以,建立一個帶有引數的引數的 executor 的例子如下:

let promise = new Promise((resolve, reject) => {
  // Do executor things
})

console.log(promise) // 將列印:Promise { <pending> }

上面的例子中,用於初始化 promise 物件的引數是一個函式,這個函式就是我們的 executor

如果要進行狀態的轉換,我們需要使用 JavaScript 為我們提供的兩個回撥:

  • resolve(value):成功且帶有結果value
  • reject(evalue):失敗且出現error

這個兩個回到都可以在 executor 直接使用。

現在,讓我們討論一下狀態是怎麼變化的。

首先,executor 一開始處於 pending 狀態,此時的返回值是 undefined

然後可以透過 resolve(value),將狀態更改為 fulfilled,此時返回值是 value

而透過 reject(error),則將狀態更改為 rejected,此時返回值是 error

下面,我們來看一個 resolve(value) 的例子:

let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve('ok'), 1000)
})

下面是一個 reject(error) 的例子:

let promise = new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error('foo')), 1000)
})

通常 executor 會使用非同步執行,所以我們例子中用了 setTimeout(),但是其實並非必須的,直接寫上執行的程式碼也可以:

let promise = new Promise((resolve, reject) => {
    resolve("ok")
})

另外,promise 的狀態被轉換後是無法再次修改的,也就是說一個 promise 一次只能改變一種狀態:

let promise = new Promise((resolve, reject) => {
    resolve() // Promise <resolved>
    reject() // 將不會產生效果效果
})

promise 中的 .then、.catch 和 .finally

executor 用於執行我們的內容,那麼我們如何處理執行後的結果呢?

為此,我們可以使用 promise 提供的 .then.catch.finally 方法。

.then

.then 方法中,有兩個引數,兩個引數都是函式。

一個用於接收 resolved 的結果,一個用於接收 rejected 的結果。

假設這兩個方法為 onFulfilled 和 onRejected,則 .then 的例子如下:

promise.then(
  onFulfilled, 
  onRejected
);

我們可以選擇只使用其中一個作為 .then 方法的引數,比如我們只對成功的結果感興趣::

// 初始化一個 promise 物件
let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve('ok'), 1000)
})
// 使用 .then 處理結果
promise.then(function (result) {console.log(result)})
// 在只有一行語句的時候,如果你掌握了箭頭函式,選擇使用箭頭函式會更好
// promise.then(result => console.log(result))

上面的例子在一秒鐘後,輸出了 ok

其實 resolve 方法的引數並沒有什麼特別的規定。

但一般我們會把要傳給回撥函式的引數放進去,而 then 方法可以接收到這個引數值。

所以這裡我們輸出了 ok

.catch

.catch 其實只是 promise.then(undefined, onRejected) 的別名而已,使用 .catch 就相當於我們沒有定義 .then 中的第一個引數。

下面是一個例子:

// 初始化一個 promise 物件
let promise = new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error('foo')), 1000)
})
// 使用 .catch 處理結果
promise.catch(error => console.error(error))
// 上面的.catch等同於下面的.then
// promise.then(null, error => console.error(error))

.finally

許多程式語言都會有 finally 子句,JavaScript也一樣。

.finally 通常用於進行一些收尾的工作,比如回收資源,停止載入指示符等等。

.finally 是無論結果如何,都需要執行的語句,以下是一個例子:

let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve('ok'), 1000)
})
promise.finally(() => console.log('All Done!'))

async 和 await

asyncawait 其實是 promise 的一種抽象或者說是 JavaScript 提供給我們的特殊語法,它們都是關鍵字,這使得在 JavaScript 進行非同步程式設計更加優雅。

async

async 關鍵字可以用在函式宣告、函式表示式、箭頭函式和物件的方法中。

下面是一個例子:

async function foo() {
  return 'Hello'
}

這樣就可以了!

接下來,我們就可以像使用 promise 一樣地使用這個函式。

測試一下:

async function foo() {
    return 'Hello'
}
foo().then(result => console.log(result))

await

await 關鍵字的作用是讓我們等待非同步的動作完成並返回結果。

下面是一個例子:

async function foo() {
    let promise = new Promise((resolve, reject) => {
        setTimeout(() => resolve("ok"), 1000)
    })
    console.log(await promise) // 等 promise resolve 才執行
}
foo()

使用 await 需要注意的是,await 不能再普通函式中使用,只能在被 async 宣告的函式中使用。

我們可以透過一個匿名的 async 函式來使用它,如:

(async () => {
  let foo = await fetch('test.json');
})

終章

非常感謝您閱讀這本手冊。

如無意外,本手冊仍會更新。

希望本手冊能幫助大家在學習 JavaScript 的道路上一路前行。

文曉歡歡。

2020.03.15

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3244/viewspace-2825074/,如需轉載,請註明出處,否則將追究法律責任。

相關文章