原文地址:freeCodeCamp(需梯子)
第一次翻譯~ 如有紕漏,請多多指點~
JavaScript 完整手冊
JavaScript 是世界上最流行的程式語言之一,現在也廣泛用於瀏覽器之外的場景中。近幾年,Node.js 的崛起解鎖了長期以來被 Java, Ruby, Python, PHP 等傳統服務端語言統治的後端開發領域。
這本 JavaScript 完整手冊遵循二八定律(the 80/20 rule):在 20% 的時間裡瞭解 80% 的 JavaScript 知識。
快來了解所有你需要知道的 JavaScript 吧!
注意:你可以獲取這篇 JavaScript 指南的 PDF, ePub, Mobi 版本以便在 Kindle 或平板上閱讀。
感謝 xbears 提供的 kindle 電子書格式(中文),提取碼: 2jh9
介紹
JavaScript 是世界上最流行的程式語言之一。自從 20 年前誕生以來,已經走過很長的一段路。作為第一個而且是唯一一個被網頁瀏覽器原生支援的指令碼語言,它被下了死命令。
一開始,它並不像今天這樣強大,主要被用於各種花哨的動畫 ? 和當時被廣泛稱奇的動態 HTML(DHTML)中。伴隨著網頁平臺發展的需要,JavaScript 也有責任變得更好,以滿足世界上最廣泛使用的生態系統之一的需求。平臺本身引入了很多東西,包括瀏覽器 API,同時語言特性也成長了許多。
現在 JavaScript 也可以運算元據庫和開發應用程式,甚至可以開發嵌入式程式,移動 APP,電視應用程式等等。曾經只是在瀏覽器中的小語言,現在也是世界上最流行的語言了~
JavaScript 基本定義
JavaScript 是這樣的程式語言:
- 高階:它允許你更加註重本身的邏輯,忽略當前執行它的機器的詳細資訊。JavaScript 通過垃圾回收器自動管理記憶體,讓你可以更專注程式碼而不是管理記憶體,它也提供了很多建構函式讓你能夠處理強大的變數和物件。
- 動態:和靜態語言在編譯時執行相反,動態語言在執行時才會執行。這有利有弊,JavaScript 給我們提供了強大的功能,比如:動態型別,延遲繫結,反射,函數語言程式設計,物件執行時變更(object runtime alteration),閉包等等。
- 動態型別:變數不用定義型別。你可以為變數重新繫結任何型別,比如給一個已宣告過的字串變數繫結一個整型值。
- 弱型別:與強型別相反,弱型別語言不強制物件的型別。這使得操作更加靈活,但是我們也無法進行型別檢查確保型別安全(TypeScript 和 Flow 旨在改善這個問題)。
- 解釋型:JavaScript 通常被認為是一種解釋型語言,這意味著在程式執行前不需要先編譯,這恰恰與 C 語言,Java 或者是 Go 語言相反。事實上,出於效能考慮瀏覽器會在執行 JavaScript 之前進行編譯,但是這一切都是自然而然發生的,不需要我們進行額外的操作。
- 多範型:JavaScript 不強制使用任何固定的程式設計正規化,不像 Java 強制物件導向程式設計或者是 C 語言強制指令式程式設計。在 JavaScript 中,你可以使用原型和 ES6 中提供的 classes 語法物件導向程式設計,你也可以通過它的頭等函式(first-class functions)編寫函數語言程式設計風格的程式碼,甚至以命令式編寫程式(C-like)。
說明一下,JavaScript 和 Java 無關,這是一個不幸的命名選擇,但是我們不得不接受現實。
JavaScript 版本
讓我介紹一下 ECMAScript。我們有一個專門介紹 ECMAScript 的完整指南,你可以在那裡深入瞭解它,但現在,你只需要知道 ECMAScript(也被稱作 ES)是 JavaScript 標準的名字。JavaScript 是對 ECMAScript 標準的一種實現,這也是為什麼你會聽到 ES6, ES2015, ES2016, ES2017, ES2018 等等。
很久以前,所有瀏覽器中執行的 JavaScript 版本基於 ECMAScipt 3。版本 4 因為特徵蠕動(feature creep)被取消了(他們試圖一次性新增很多特性)。雖然 ES5 是 JavaScript 的一個巨大版本,但是 ES2015(也被稱作 ES6)也是 JavaScript 的重要更新。
從那時起,標準制定委員會決定每年更新一個版本,避免版本迭代間隔太久,也可以加快反饋速度。
現在,最新批准的 JavaScript 版本是 ES2017(譯註:2018/11/10,最新版本是 ES2018)。
ECMASCRIPT
無論何時你閱讀關於 JavaScript 的內容時都無可避免的看到下面這些術語:
- ES3
- ES5
- ES6
- ES7
- ES8
- ES2015
- ES2016
- ES2017
- ECMAScript 2017
- ECMAScript 2016
- ECMAScript 2015
這些都是什麼?他們都指 JavaScript 標準,被稱作 ECMAScript。
ECMAScript 是 JavaScript 所基於的標準,經常被簡稱為 ES。
除了 JavaScript,實現了 ECMAScript 標準的還包括:
- ActionScript(Flash 指令碼語言),自從官方決定從 2020 年起不再維護 Flash 便不再流行。
- JScript(微軟),一開始只有 Netscape 支援 JavaScript,瀏覽器大戰愈演愈烈,微軟便實現了僅 IE 瀏覽器支援的版本。
但是毫無疑問,JavaScript 仍是最普遍的 ES 實現。
為什麼是這個奇怪的名字?Ecma International
是瑞士標準協會,負責制定國際標準。
當 JavaScript 被建立時,它被 Netscape 和 Sun Microsystems 提交到 Ecma,命名為 ECMA-262,也叫做 ECMAScript。根據維基百科上的內容,Netscapte 和 Sun Microsystems(Java 製造商)聯合釋出的新聞稿可能會幫助瞭解如此命名的原因,其中可能包括微軟在委員會裡的法律和品牌問題。
IE9 之後,微軟停止在瀏覽器中將 ES 實現稱為 JScript,開始叫作 JavaScript(至少我不能任何引用)。
所以,從201x年起,唯一一個支援 ECMAScript 標準的流行語言就只有 JavaScript 了。
ECMAScript 最新版本
目前 ECMAScript 版本是 ES2018,即 ES9,2018年六月釋出。
下個版本什麼時候釋出?
通常,JavaScript 會在每年夏天釋出標準版本,所以我們可以在 2019 年夏天見到 ECMAScript 2019(即 ES2019 或者 ES10),但這一切只是猜測。
TC39 是什麼
TC39 是 JavaScript 發展委員會。
TC39 成員涉及 JavaScript 和瀏覽器供應商,包括火狐,谷歌,Facebook,Apple,微軟,英特爾,PayPal,SalesForce 等等。
每一個標準版本的釋出都必須通過不同階段的提案。
ES 版本
我發現 ES 版本有時候通過版本號指代,有時候通過年份,這會讓人困惑。
在 ES2015 之前,ECMAScript 標準通常按照版本號命名,所以 ES5 是2009年更新的 ECMAScript 標準官方命名。
為什麼會這樣?在 ES2015 釋出時,名字從 ES6 變成了 ES2015,但是為時已晚,人們仍然習慣性地稱為 ES6,社群也沒有拋棄這個名字 – 世界人民仍然按照版本號的方式指代 ES 版本。
這個表格可以幫助理清思路:
版本 | 官方名稱 | 釋出日期 |
---|---|---|
ES9 | ES2018 | 2018年6月 |
ES8 | ES2017 | 2017年6月 |
ES7 | ES2016 | 2016年6月 |
ES6 | ES2015 | 2015年6月 |
ES5.1 | ES5.1 | 2011年6月 |
ES5 | ES5 | 2009年12月 |
ES4 | ES4 | 廢棄 |
ES3 | ES3 | 1999年12月 |
ES2 | ES2 | 1998年6月 |
ES1 | ES1 | 1997年6月 |
ES.Next 始終指 JavaScript 未來版本。在撰寫本文時,ES9 已經發布,ES.Next 是 ES10。
ES6 改進
ECMAScript 2015,也即熟知的 ES6, 是 ECMAScript 標準的基礎版本,距離上個版本 ECMAScript 5.1 釋出有四年之久,也是從這個版本開始將版本改為以年命名。所以它不應該叫做 ES6(儘管每個人都這麼叫),而是 ES2015。
ES5 從 1999 年到2009年花費了十年時間完善,儘管對於這個語言來講,它也是一個重要版本,但是太長時間過去了,已經不值得我們討論 ES5 之前的程式碼是如何工作的。
從 ES5.1 到 ES6,JavaScript 語言有了重要的新特性以及在更好的實踐中的關鍵更新。要了解 ES2015 的基本功能,請參閱規範文件的250頁到600頁。
ES2015 中的重要更新包括:
- 箭頭函式
- Promises
- Generators
let
和const
- Classes
- 多行字串
- 模板字串
- 引數預設值
- 擴充套件運算子
- 解構賦值
- 增強的物件字面量
- for..of 迴圈
- Map 和 Set
在這篇指南中,我將在每個章節中專門介紹它們。讓我們開始吧!
箭頭函式
箭頭函式改變了大多數 JavaScript 程式碼的書寫習慣和工作方式。
從視覺上來講,它更簡單了,也是受歡迎的改變,比如:
const foo = function foo() {
// ...
}複製程式碼
變成:
const foo = () =>
{
// ...
}複製程式碼
如果這個函式體只有一行,可以是這樣:
const foo = () =>
doSomething()複製程式碼
如果只有一個引數,可以是這樣:
const foo = param =>
doSomething(param)複製程式碼
和常規函式相比它沒有帶來任何不相容變化,和以前一樣工作。
新的 this 作用域
箭頭函式中的 this
從執行上下文繼承。
在以前的常規函式中,this
通常指最近的函式。然而在箭頭函式中這個問題沒有了,你不再需要重寫一遍 var that = this
。
Promises
Promises 幫我們解決了著名的“回撥地獄”問題,雖然它引入了更復雜的問題(在 ES2017 中可以通過更高階的建構函式 async
解決 )。
在 ES2015 之前,JavaScript 開發者就可以通過使用不同的庫(jQuery,q,deferred.js,vow…)實現類似 Promises 的功能。該標準制定了更通用的方法。
通過使用 promises,你可以重構這個程式碼:
setTimeout(function() {
console.log('I promised to run after 1s') setTimeout(function() {
console.log('I promised to run after 2s')
}, 1000)
}, 1000)複製程式碼
等同於:
const wait = () =>
new Promise((resolve, reject) =>
{
setTimeout(resolve, 1000)
})wait().then(() =>
{
console.log('I promised to run after 1s') return wait()
}).then(() =>
console.log('I promised to run after 2s'))複製程式碼
Generators
生成器是一種特殊的函式,能夠暫停輸出,稍後恢復,而且允許其它程式碼在此期間執行。
程式碼本身決定了它必須等待,以便讓其它程式碼“按照佇列”順序執行,而且保留了“當等待”完成時恢復操作的權利。所有的這些都通過一個簡單的關鍵字 yield
來完成。當一個生成器包含該關鍵字,程式碼將暫停執行。生成器可以包含很多個 yield
關鍵字,因此可以暫停很多次,並且通過 *function
關鍵字標識,不要與 C 語言,C++ 或者 Go 等底層語言的指標反向引用操作符混淆。
在 JavaScript 中,生成器啟用全新的程式設計範例,比如:
- 生成器執行中雙向通訊
- 持久的 while 迴圈,不會凍結程式
這裡有個例子可以解釋生成器是如何工作的:
function *calculator(input) {
var doubleThat = 2 * (yield (input / 2)) var another = yield (doubleThat) return (input * doubleThat * another)
}複製程式碼
初始化:
const calc = calculator(10)複製程式碼
然後開始迭代生成器:
calc.next()複製程式碼
第一次迭代,程式碼返回了 this
物件:
{
done: false value: 5
}複製程式碼
發生了什麼:函式開始執行時,input = 10
作為引數傳入了生成器的建構函式中,直到遇到 yield
,返回了 yeild
的內容:input / 2 = 5
。所以我們得到了一個值為 5,並且告訴我們迭代沒有完成(僅僅是函式暫停了)。
在第二次迭代中,我們傳入 7
:
calc.next(7)複製程式碼
然後我們會得到:
{
done: false value: 14
}複製程式碼
7
是 doubleThat
的值。
注意:你可能會認為
input / 2
是這個引數,但是它僅僅是第一次迭代的返回值,這次我們跳過了這一步,使用新的輸入值7
和 2 相乘。
我們繼續第二次迭代,它返回了 doubleThat
,值為 14
。
下一個,最後一次迭代,我們傳入100:
calc.next(100)複製程式碼
返回:
{
done: true value: 14000
}複製程式碼
整個迭代結束(沒有 yeild
關鍵字了),我們得到了 (input * doubleThat * another)
的值:10 * 14 * 100
。
let 和 const
var
是傳統的函式作用域。
let
是新的宣告變數的方法,擁有塊級作用域。這意味著在 for 迴圈中,if 語句內或者 plain 塊中使用 let
宣告的變數不會“逃出”所在的塊,而 var
變數則會被提升到函式定義。
const
和 like
相似,但是不可更改。
展望 JavaScript 的發展,var
宣告會逐漸消失,只剩下 let
和 const
。
更特別的是,由於不可變的特性,const
在今天已經出人意料的被廣泛使用。
Classes
傳統上,JavaScript 是唯一基於原型繼承的語言。從基於類繼承的語言轉向使用 JavaScript 的程式設計師會覺得困惑,但是 ES2015 引入了 classes,作為 JavaScript 內部實現繼承的語法糖,它改變了我們編寫 JavaScript 程式的方式。
現在,繼承變得非常簡單,和其他物件導向的程式語言類似:
class Person {
constructor(name) {
this.name = name
} hello() {
return 'Hello, I am ' + this.name + '.'
}
}class Actor extends Person {
hello() {
return super.hello() + ' I am an actor.'
}
}var tomCruise = new Actor('Tom Cruise')tomCruise.hello()複製程式碼
上面的程式碼會列印出:“Hello, I am Tom Cruise. I am an actor.”
Classes 沒有顯性的宣告類變數,你必須在建構函式(constructor)中初始化變數。
Constructor
Classes 擁有一個特殊的方法 constructor
,在使用 new
例項化類時被呼叫。
Getters 和 Setters
可以像這樣宣告一個 getter 屬性:
class Person {
get fullName() {
return `${this.firstName
} ${this.lastName
}`
}
}複製程式碼
用同樣的方式宣告 setter 屬性:
class Person {
set age(years) {
this.theAge = years
}
}複製程式碼
模組化
ES2015 之前,至少有三個主要的模組化標準,這分裂了整個社群:
- AMD
- RequireJS
- CommonJS
ES2015 制定了統一的模組化標準。
匯入模組
通過 import ... from ...
匯入模組:
import * from 'mymodule'import React from 'react'import {
React, Component
} from 'react'import React as MyLibrary from 'react'複製程式碼
匯出模組
你可以使用關鍵字 export
將編寫的模組內容匯出到其它模組裡:
export var foo = 2export function bar() {
/* ... */
}複製程式碼
模板字串
模板字串是建立字串的新方法:
const aString = `A string`複製程式碼
使用 ${a_variable
語法可以方便地將表示式的值插到字串裡:
}
const var = 'test'const string = `something ${var
}` //something test複製程式碼
你還可以執行更復雜的表示式,像這樣:
const string = `something ${1 + 2 + 3
}`const string2 = `something ${foo() ? 'x' : 'y'
}`複製程式碼
字串可以是多行的:
const string3 = `Heythisstringis awesome!`複製程式碼
對比一下 ES2015 之前的多行字串的寫法:
var str = 'One\n' +'Two\n' +'Three'複製程式碼
引數預設值
函式現在支援使用預設引數值:
const foo = function(index = 0, testing = true) {
/* ... */
}foo()複製程式碼
擴充套件運算子
你可以通過擴充套件運算子 ...
擴充套件陣列,物件或者是字串。
讓我們用陣列舉個例子:
const a = [1, 2, 3]複製程式碼
你可以這樣建立一個新陣列:
const b = [...a, 4, 5, 6]複製程式碼
你可以複製一個陣列:
const c = [...a]複製程式碼
這對物件同樣奏效,這樣複製一個物件:
const newObj = {
...oldObj
}複製程式碼
對於字串,擴充套件運算子會生成一個對應每個字元的陣列:
const hey = 'hey'const arrayized = [...hey] // ['h', 'e', 'y']複製程式碼
這個運算子非常有用。最重要的就是可以以一種十分簡單的方式為一個函式傳遞陣列形式的引數:
const f = (foo, bar) =>
{
}const a = [1, 2]f(...a)複製程式碼
以前你可以使用 f.apply(null, a)
達到同樣的效果,但是它不是很易讀。
解構賦值
給你一個物件,你可以抽出一些值把他們賦值給別的變數:
const person = {
firstName: 'Tom', lastName: 'Cruise', actor: true, age: 54, //made up
}const {firstName: name, age
} = person複製程式碼
name
和 age
包含這些值。
這個語法也可以用在陣列中:
const a = [1,2,3,4,5][first, second, , , fifth] = a複製程式碼
加強的函式字面量
ES2015 中函式字面量更加強大。
宣告變數的簡單語法
以前:
const something = 'y'const x = {
something: something
}複製程式碼
現在你可以:
const something = 'y'const x = {
something
}複製程式碼
原型
可以像這樣為變數指定原型:
const anObject = {
y: 'y'
}const x = {
__proto__: anObject
}複製程式碼
super()
const anObject = {
y: 'y', test: () =>
'zoo'
}const x = {
__proto__: anObject, test() {
return super.test() + 'x'
}
}x.test() //zoox複製程式碼
動態屬性
const x = {
['a' + '_' + 'b']: 'z'
}x.a_b //z複製程式碼
for-of 迴圈
2009年的 ES5 引入了 forEach()
迴圈。雖然很好,但是不能像 for
那樣中途跳出迴圈。
ES2015 引入了 for-of
迴圈,結合了 forEach
的簡潔和跳出迴圈的能力。
//iterate over the valuefor (const v of ['a', 'b', 'c']) {
console.log(v);
}//get the index as well, using `entries()`for (const [i, v] of ['a', 'b', 'c'].entries()) {
console.log(i, v);
}複製程式碼
Map 和 Set
Map 和 Set(以及各自的弱引用型別 WeakMap 和 WeakSet)是官方實現的兩種非常流行的資料結構(稍後介紹)。
ES2016 改進
ES7,官方稱作 ECMAScript 2016,2016年6月釋出。
和 ES6 相比,ES7 是 JavaScript 的小版本更新,包括兩個功能:
- Array.prototype.includes
- 指數運算子
Array.prototype.includes()
這個功能引入了更易讀的語法來檢查一個陣列是否包括一個元素。
在 ES6 以及更低版本中,檢查一個元素是否在陣列中你不得不使用 indexOf
檢查陣列的索引,如果返回 -1
元素則不在陣列中。
因為 -1
被認為是個真值,所以你無法判斷下面這個例子:
if (![1,2].indexOf(3)) {
console.log('Not found')
}複製程式碼
用 ES7 的新語法可以得到我們預期的結果:
if (![1,2].includes(3)) {
console.log('Not found')
}複製程式碼
指數運算子
指數運算子 **
和 Math.pow()
一致,但是引入的是個語言特性,而不是一個庫函式。
Math.pow(4, 2) === 4 ** 2複製程式碼
這個功能是 Math 密集型 JavaScript 應用很好的補充。**
運算子在很多語言中都已經標準化,比如 Python,Ruby,MATLAB,Lua,Perl 等等。
ES2017 改進
ECMAScript 2017,ECMA-262 標準的第八版(稱作 ES2017 或者 ES8),2017年6月釋出。
和 ES6 相比,ES8 仍然帶來了很多有用的功能:
- String padding
- Object.values
- Object.entries
- Object.getOwnPropertyDescriptors()
- 函式引數尾逗號
- 非同步函式
- Shared memory and atomics
String padding
String padding 的目的是為一個字串新增字元,讓它可以和宣告的長度一致。
ES2017 引入了兩個 String
上的方法:padStart()
和 padEnd()
。
padStart(targetLength [, padString])padEnd(targetLength [, padString])複製程式碼
例子:
padStart() | 輸出 |
---|---|
‘test’.padStart(4) | ‘test’ |
‘test’.padStart(5) | ‘ test’ |
‘test’.padStart(8) | ‘ test’ |
‘test’.padStart(8, ‘abcd’) | ‘abcdtest’ |
padEnd() | 輸出 |
---|---|
‘test’.padEnd(4) | ‘test’ |
‘test’.padEnd(5) | ‘test ‘ |
‘test’.padEnd(8) | ‘test ‘ |
‘test’.padEnd(8, ‘abcd’) | ‘testabcd’ |
Object.values()
這個方法返回一個包含物件自身屬性值的陣列。用法:
const person = {
name: 'Fred', age: 87
}Object.values(person) // ['Fred', 87]複製程式碼
Object.values()
也可以用於陣列:
const people = ['Fred', 'Tony']Object.values(people) // ['Fred', 'Tony']複製程式碼
Object.entries()
這個方法返回一個 [key, value]
形式的包含物件自身所有屬性及值的陣列。用法:
const person = {
name: 'Fred', age: 87
}Object.entries(person) // [['name', 'Fred'], ['age', 87]]複製程式碼
Object.entries()
也可以用於陣列:
const people = ['Fred', 'Tony']Object.entries(people) // [['0', 'Fred'], ['1', 'Tony']]複製程式碼
getOwnPropertyDescriptors()
這個方法返回物件自身的所有描述符(非繼承)。
JavaScript 中的所有物件都有一個屬性集合,每個屬性都有一個描述符。
描述符是屬性的 attribute 集合,它包括下面這些子集:
- value:屬性的值
- writable:為 true 時屬性可以重寫
- get:屬性的 getter 函式,當讀取屬性時被呼叫
- set:屬性的 setter 函式,當設定屬性值時被呼叫
- configurable:如果為 false,屬性不能被刪除,而且不能更改所有的 attribute 值,屬性自身的值除外
- enumerable:為 true 時屬性可列舉
Object.getOwnPropertyDescriptors(obj)
接受物件作為引數,返回一個包含描述符的物件。
這有什麼用?
ES2015 給我們帶來了 Object.assign()
,方便我們複製一個或多個物件自身的可列舉屬性,返回一個新物件。
然而這樣操作有個問題,它無法正確複製沒有預設 attributes 的屬性。舉個例子,如果一個物件只有一個 setter,就不能用 Object.assign()
正確複製它。
const person1 = {
set name(newName) {
console.log(newName)
}
}複製程式碼
這樣不會工作:
const person2 = {
}Object.assign(person2, person1)複製程式碼
但是這樣可以:
const person3 = {
}Object.defineProperties(person3,Object.getOwnPropertyDescriptors(person1))複製程式碼
你可以通過控制檯簡單測試一下:
person1.name = 'x'"x"person2.name = 'x'person3.name = 'x'"x"複製程式碼
person2
沒有 setter,它不能被正確複製。
使用 Object.create()
對物件淺克隆也有同樣的限制。
尾後逗號
這個功能允許在函式宣告和函式呼叫中使用尾後逗號:
const doSomething = (var1, var2,) =>
{
//...
}doSomething('test2', 'test2',)複製程式碼
這個改變將鼓勵開發者停止使用醜陋的“行首逗號”的習慣。
非同步函式
ES2017 引入了非同步函式的概念,它也是這個 ECMAScript 版本里最重要的變化。
非同步函式結合了 promises 和 generators 以減少 promises 帶來的樣板風格和 promises 鏈的“不要打破呼叫鏈”限制。
為什麼他們很有用
它是對 promises 函式的高階抽象。
當 ES2015 裡引入 Promises 的時候,這個功能致力於解決非同步程式碼的問題。但是在 ES2015 到 ES2017 釋出的兩年裡,人們清楚的認識到這不是解決問題的最終方法。
引入 Promises 用來解決著名的回撥地獄問題,但是也帶來了自身的複雜性,加大了語法的複雜程度。他們是很好的開端,在這個基礎上可以讓開發者使用更好的語法:那就是非同步函式。
一個簡短的例子
使用非同步函式的程式碼可以像這樣:
function doSomethingAsync() {
return new Promise((resolve) =>
{
setTimeout(() =>
resolve('I did something'), 3000)
})
}async function doSomething() {
console.log(await doSomethingAsync())
}console.log('Before')doSomething()console.log('After')複製程式碼
上面的程式碼會在瀏覽器控制檯裡列印出:
BeforeAfterI did something //after 3s複製程式碼
串聯多個非同步函式
鏈式呼叫非同步函式很簡單,而且比原始的 promises 更易讀:
function promiseToDoSomething() {
return new Promise((resolve)=>
{
setTimeout(() =>
resolve('I did something'), 10000)
})
}async function watchOverSomeoneDoingSomething() {
const something = await promiseToDoSomething() return something + ' and I watched'
}async function watchOverSomeoneWatchingSomeoneDoingSomething() {
const something = await watchOverSomeoneDoingSomething() return something + ' and I watched as well'
}watchOverSomeoneWatchingSomeoneDoingSomething().then((res) =>
{
console.log(res)
})複製程式碼
共享記憶體和原子
WebWorkers 用來在瀏覽器裡構建多執行緒程式。
他們通過事件(events)提供一種通訊協議。從 ES2017 開始,你可以通過 SharedArrayBuffer
在 web workers 和他們的構造者中間建立一個共享記憶體陣列。
由於我們不知道寫入一個共享記憶體的傳遞部分需要多長事件,因此原子(Atomics)是一種在讀取值時執行操作的方法,並且完成了所有型別的寫入操作。
更多內容可以在這個提案中找到,該提案已經被實現。
ES2018 改進
ES2018 是最新的 ECMAScript 標準。
它引入了什麼新東西呢?
Rest/Spread 屬性
ES6 引入了針對陣列解構的剩餘元素:
const numbers = [1, 2, 3, 4, 5][first, second, ...others] = numbers複製程式碼
和擴充套件元素:
const numbers = [1, 2, 3, 4, 5]const sum = (a, b, c, d, e) =>
a + b + c + d + econst sum = sum(...numbers) // 勘誤:此處不能對 sum 重新賦值,感謝 [森藍情丶](https://juejin.im/user/59c8ea816fb9a00a4746e258)複製程式碼
ES2018 為物件帶來了同樣的功能。
剩餘屬性:
const {
first, second, ...others
} = {
first: 1, second: 2, third: 3, fourth: 4, fifth: 5
}first // 1second // 2others // {
third: 3, fourth: 4, fifth: 5
}複製程式碼
擴充套件屬性允許通過組合在spread運算子之後傳遞的物件的屬性來建立新物件:
const items = {
first, second, ...others
}items //{
first: 1, second: 2, third: 3, fourth: 4, fifth: 5
}複製程式碼
非同步迭代
新的建構函式 for-await-of
允許你使用非同步可迭代物件作為迴圈迭代:
for await (const line of readLines(filePath)) {
console.log(line)
}複製程式碼
因為這裡用了 await
,所以你只能在 async
函式內部使用它,就像一個普通的 await
(參考 async/await)。
Promise.prototype.finally()
當一個 promise 完成(fulfilled),它會一個接一個的呼叫 then()
方法。如果在這個過程中出現了任何錯誤,then()
方法會被跳過,進而執行 catch()
方法。
無論 promise 執行成功還是失敗,finally()
都會執行其中的程式碼:
fetch('file.json') .then(data =>
data.json()) .catch(error =>
console.error(error)) .finally(() =>
console.log('finished'))複製程式碼
正規表示式改進
RegExp 後行斷言:根據前面的內容匹配字串。
這是一個先行斷言:使用 ?=
匹配特定的子字串:
/Roger(?=Waters)//Roger(?= Waters)/.test('Roger is my dog') //false/Roger(?= Waters)/.test('Roger is my dog and Roger Waters is a famous musician') //true複製程式碼
?!
執行相反的操作,匹配字串後面沒有特定的子字串:
/Roger(?!Waters)//Roger(?! Waters)/.test('Roger is my dog') //true/Roger(?! Waters)/.test('Roger Waters is a famous musician') //false複製程式碼
先行斷言使用 ?=
識別符號,它已經可用了。
後行斷言,新功能,使用 ?<
。
=
/(?<
=Roger) Waters//(?<
=Roger) Waters/.test('Pink Waters is my dog') //false/(?<
=Roger) Waters/.test('Roger is my dog and Roger Waters is a famous musician') //true複製程式碼
使用 ?<
否定後行斷言:
!
/(?<
!Roger) Waters//(?<
!Roger) Waters/.test('Pink Waters is my dog') //true/(?<
!Roger) Waters/.test('Roger is my dog and Roger Waters is a famous musician') //false複製程式碼
Unicode 屬性轉義 \p{…
}
和 \P{…
}
}
}
在一個正規表示式裡,你可以使用 \d
匹配任意數字, \s
匹配不包括空格的任意字元,\w
匹配任意字母數字字元等等。
這個新功能通過引入 \p{
和
}\P{
將此概念擴充套件到所有 Unicode 字元。
}
任何 unicode 字元都有一組特定的屬性。舉個例子,Script
決定了語言家族,ASCII
是 ASCII 字串 true 的布林值,等等。你可以把這個特性放到大括號中,正規表示式會檢查是否為真:
/^\p{ASCII
}+$/u.test('abc') //✅/^\p{ASCII
}+$/u.test('ABC@') //✅/^\p{ASCII
}+$/u.test('ABC?') //❌複製程式碼
ASCII_Hex_Digit
是另一個布林屬性,它會檢查字串是否包含有效的十六進位制數字:
/^\p{ASCII_Hex_Digit
}+$/u.test('0123456789ABCDEF') //✅/^\p{ASCII_Hex_Digit
}+$/u.test('h') //❌複製程式碼
還有很多布林屬性,你只用把他們的名字放在大括號中就可以用了,比如:Uppercase
, Lowercase
, White_Space
, Alphabatic
, Emoji
等等:
/^\p{Lowercase
}$/u.test('h') //✅/^\p{Uppercase
}$/u.test('H') //✅/^\p{Emoji
}+$/u.test('H') //❌/^\p{Emoji
}+$/u.test('??') //✅複製程式碼
除了這些二進位制屬性之外,你還可以檢查任意 unicode 字元是否匹配特定的值。在這個例子中,我檢查了字串是希臘語還是拉丁字母:
/^\p{Script=Greek
}+$/u.test('ελληνικά') //✅/^\p{Script=Latin
}+$/u.test('hey') //✅複製程式碼
瞭解更多內容可以閱讀提案。
命名捕獲組
在 ES2018 裡匹配到的組可以繫結一個名字,而不是僅在結果陣列中繫結一個插槽:
const re = /(?<
year>
\d{4
})-(?<
month>
\d{2
})-(?<
day>
\d{2
})/const result = re.exec('2015-01-02')// result.groups.year === '2015';
// result.groups.month === '01';
// result.groups.day === '02';
複製程式碼
正規表示式 ‘s’
s
是單行的縮寫,可以和 .
一起匹配新行,沒有它,點識別符號不會匹配新行:
/hi.welcome/.test('hi\nwelcome') // false/hi.welcome/s.test('hi\nwelcome') // true複製程式碼
程式設計風格
JavaScript 程式設計風格是編寫 JavaScript 時的一系列規範。
程式設計風格是你和你的團隊達成的協議,用來保證專案的一致性。如果你沒有團隊,那麼它就是你自己的協定,應該始終保證你的程式碼符合你的標準。
對程式碼編寫格式有固定的規則有很大的好處,可以使程式碼更易讀和更易管理。
流行的風格指南
在 JavaScript 裡有很多風格指南,其中有兩個最為常見的:
你可以選擇使用其中一種或者制定自己的程式碼風格。
和你的專案保持一致的風格
即使你有自己更喜歡的程式碼風格,在團隊協作中你也要遵守團隊專案風格。
Github 上的每個開源專案都可能會有一系列規則,你參與的另一個專案可能會有完全不同的規則。
Prettier 是格式程式碼的強大工具,你應該試試它。
本指南使用的規則
我們一直使用最新的 ES 版本,在舊版瀏覽器中使用 Babel。
-
縮排:用空格代替 tabs,縮排兩個空格。
-
分號:不要使用分號。
-
每行長度:如果可能,盡力保證每行不超過 80 個字元。
-
行內註釋:在程式碼裡使用行內註釋,只在檔案裡使用塊級註釋。
-
不要有無用程式碼:不要留下舊程式碼,“僅僅以防萬一”它將來還有用。保證只有現在需要的程式碼,版本控制或者你的筆記應用就是為此而生的。
-
只在需要時註釋:不要新增不能幫助理解的註釋。如果程式碼自身有良好的變數、函式命名和 JSDoc 函式註釋,不要新增註釋。
-
變數宣告:避免汙染全域性變數,永遠不要使用
var
。預設使用const
,只在需要重繫結變數時使用let
。 -
常量:用大寫字母宣告所有常量,用
_
分割VARIABLE_NAME
。 -
函式:使用箭頭函式,除非你有使用常規函式的理由,比如在物件方法或者建構函式中,需要確定
this
的指向。用 const 宣告函式,並在可能的情況使用隱式返回。使用巢狀函式隱藏助手函式的多餘程式碼會讓你覺得無拘無束。const test = (a, b) =>
a + bconst another = a =>
a + 2複製程式碼 -
名字:函式命,變數名和方法名以小寫字母開始(除非他們是私有且只讀的)的駝峰命名,只有建構函式和類名應該以大寫字母開始。如果你使用一個有特定規範的框架,根據要求改變你的習慣。檔名應該全部小寫,用
-
分割。 -
特定語句格式和規則:
if
if (condition) {
statements
}if (condition) {
statements
} else {
statements
}if (condition) {
statements
} else if (condition) {
statements
} else {
statements
}複製程式碼for:始終在初始化時快取遍歷物件的長度,不要把它插入到條件語句中。避免使用
for in
表示式,除了和.hasOwnProperty()
配合使用,首選for of
:for (initialization;
condition;
update) {
statements
}複製程式碼while
while (condition) {
statements
}複製程式碼do
do {
statements
} while (condition);
複製程式碼switch
switch (expression) {
case expression: statements default: statements
}複製程式碼try
try {
statements
} catch (variable) {
statements
}try {
statements
} catch (variable) {
statements
} finally {
statements
}複製程式碼 -
空格:機智的通過空格改善程式碼可讀性:在關鍵字和
(
中插入一個空格;在二進位制運算子(+
,-
,/
,*
,&
…)前面和後面插入一個空格;在語句內,每個
&;
後插入一個空格將每個語句分開;在每個,
後插入一個空格。 -
新行:使用新行分隔執行邏輯相關操作的程式碼塊。
-
引號:使用單引號
'
代替雙引號"
。雙引號是 HTML 的標準屬性,所以使用單引號避免在處理 HTML 字串時可能遇到的問題。適當時使用模板字串而不是變數插值。
詞彙結構
現在我們將深入探討JavaScript的構建模組:unicode,分號,空格,大小寫敏感,註釋,字面量,識別符號和保留字。
Unicode
JavaScript 用 Unicode 編寫。這意味著你可以用 Emojis 作為變數名。? ? ? 更重要的是,你可以用任何語言書寫識別符號,比如日文或者中文,相關規則。
分號
JavaScript 語法和 C 語言類似,你可能會在很多示例程式碼裡看到每行程式碼末尾都有分號。
分號不是強制性的,JavaScript 不使用分號沒有任何問題。現在很多開發者,特別是從不需要分號的語言轉過來的那部分開始避免使用分號。
你只需要避免一些奇怪的做法,比如將語句分割成多行:
returnvariable複製程式碼
或者是在一行前以([
或者 (
)開頭。這樣你在 99.9% 的時間裡都是安全的(你的 linter 也會警告你)。
這取決於個人喜好,最近我決定再也不加無用的分號,所以在這篇文章裡你看不到任何分號。
空格
JavaScript 不認為空格有意義。空格和斷行可以憑你的喜好新增,即使理論上是可行的。
在實踐中,你很可能遵守良好的風格和人們習以為常的規則,強制使用 linter 或者 Prettier 這樣的風格工具。
比如,我喜歡縮排兩個字元。
大小寫敏感
JavaScript 是大小寫敏感的。變數 something
和 Somethin
是不同的。這在任何識別符號裡都是一致的。
註釋
在 JavaScript 裡,有兩種註釋方式:
/* *///複製程式碼
第一個可以進行多行註釋並且需要閉合。
第二個會註釋掉當前行位於其右側的所有內容。
字面量和識別符號
我們將原始碼裡的值定義為字面量,例如數字,字串,布林值或者更高階的構造,像物件字面量或者陣列字面量:
5'Test'true['a', 'b']{color: 'red', shape: 'Rectangle'
}複製程式碼
一個識別符號可以用來識別一個變數,一個函式,一個物件。它可以用一個美元符號 $
或者一個下劃線 _
開頭,它也可以包含數字。使用 Unicode,字母可以是任何被允許的字元,比如 emoji :smile:。
TesttestTEST_testTest1$test複製程式碼
美元符號通常被用來區分 DOM 元素。
保留字
你不能使用下列識別符號,因為他們是語言保留字。
breakdoinstanceoftypeofcaseelsenewvarcatchfinallyreturnvoidcontinueforswitchwhiledebuggerfunctionthiswithdefaultifthrowdeleteintryclassenumextendssuperconstexportimportimplementsletprivatepublicinterfacepackageprotectedstaticyield複製程式碼
變數
變數是一個識別符號繫結了一個字面量,所以你可以在後續的程式碼裡引用和使用它。我們將會學習如果在 JavaScript 裡宣告一個變數。
介紹 JavaScript 變數
變數是一個識別符號繫結了一個字面量,所以你可以在後續的程式碼裡引用和使用它。在 JavaScript 裡,變數沒有繫結任何型別。即使你為一個變數繫結了一個特定的型別,你也可以在後面重新繫結其它任何型別,這不會造成型別錯誤或者其它問題。
這也是為什麼有時候提到 JavaScript 會被認為是 “無型別的”。
變數必須在你用到之前宣告。有三種宣告方法:使用 var
, let
或者 const
。這三種方式的不同在於後續你如何和這個變數進行互動。
使用 var
直到 ES2015, var
是定義變數的唯一方法。
var a = 0複製程式碼
如果你忘了加 var
,你將給未宣告的變數繫結值,這個結果可能會有不同:在現代環境裡,開啟嚴格模式,這樣會報錯。在舊環境裡(或者關閉嚴格模式),這樣會初始化一個變數並把它繫結到全域性物件。
如果你宣告變數時沒有初始化,它會獲得一個 undefined
值直到你為它繫結一個值。
var a //typeof a === 'undefined'複製程式碼
你可以重複宣告同一個變數,重寫它的值:
var a = 1var a = 2複製程式碼
你可以一次性宣告多個變數:
var a = 1, b = 2複製程式碼
程式碼的作用域是變數可見的範圍。
在任何函式外部通過 var
初始化的變數會繫結到全域性物件,擁有全域性作用域,在任何位置都可用。在函式內部通過 var
初始化的變數會繫結在這個函式內,僅在函式內部可用,就和函式的引數一樣。
重要的是要了解一個塊(用一對花括號區分)沒有定義一個新的作用域。新作用域只會隨著函式的建立被建立,因為 var
沒有塊級作用域,只有函式作用域。
在函式內部,其中定義的任何變數在函式程式碼裡都是可見的,即使是定義在尾部的變數也會在函式頭部被引入,這是因為 JavaScript 會自動將所有變數移動到頂部(即變數提升)。為了避免造成困擾,始終在函式頭部宣告變數。
使用 let
let
是 ES2015 裡引入的新特性,本質上是擁有塊級作用域的 var
。他的作用域被限制在定義它的塊,語句或者表示式,以及所有包含的內部塊。
現代 JavaScript 開發者可能會只用 let
而完全放棄使用 var
。
如果
let
看起來是個模糊的術語,就看看let color = 'red'
,讓顏色變成紅色,這樣容易明白得多。
在函式外部使用 let
,和 var
相反,沒有建立一個全域性變數。
使用 const
用 var
或者 let
宣告的變數可以在後面進行更改和重繫結。一旦 const
被初始化,它的值將不能被修改,也不能繫結其它的值。
const a = 'test'複製程式碼
我們不能為 a
繫結不同的值。然而,如果 a
是一個提供改變其內容的方法的物件,我們仍然可以改變它。
const
不提供不變性,只能確保不會更改引用。
const
和 let
一樣,提供塊級作用域。
現在 JavaScript 開發者可能會選擇 const
作為宣告將來不需要重新繫結的變數的識別符號。
為什麼?因為我們應該始終使用最簡單的構造變數避免發生錯誤。
型別
有時你會讀到 JS 是無型別的,但是這是不正確的。你可以為一個變數繫結不同的型別是確實存在的,但是 JavaScript 是有型別的。它提供了基本型別和物件型別。
基本型別
原始型別有:
- Numbers
- Strings
- Booleans
還有兩個特別型別:
- null
- undefined
讓我們在下個章節深入瞭解他們。
Numbers
在語言內部,JavaScript 只有一種數字型別:所有數字都是浮點型。
一個數字字面量是原始碼裡表示的數字,amd 取決於它的編寫方式,它可以是整型字面量或者是浮點型字面量。
整型:
1053545767673210xCC //hex複製程式碼
浮點型:
3.14.12345.2e4 //5.2 * 10^4複製程式碼
strings
字串型別是一系列字元。在原始碼裡,它被定義成字串字面量,用單引號或者雙引號包裹。
'A string'"Another string"複製程式碼
字串可以通過反斜槓跨越多行:
"A \string"複製程式碼
字串可以包含轉義序列,在列印字串時解釋,例如 \n 用來換行。當你需要防止字元被誤解為閉合引號時,反斜槓也很有用:
'I\'m a developer'複製程式碼
字串可以用 + 操作符拼接:
"A " + "string"複製程式碼
模板字串
在 ES2015 引入,模板字串允許用更強大的方法定義一個字串字面量。
`a string`複製程式碼
你可以嵌入任何 JavaScript 表示式並替換執行後的結果:
`a string with ${something
}``a string with ${something+somethingElse
}``a string with ${obj.something()
}`複製程式碼
你可以很容易的獲得多行字串:
`a stringwith${something
}`複製程式碼
Booleans
對於布林型,JavaScript 有兩個保留字:true 和 false。大多數比較運算子 ==
===
>
<
等等都返回其中的一個。
if
,while
語句和其它控制結構使用布林值決定程式的流程。
他們不僅接受 true 和 fasle,也會接受 truthy 和 falsy 的值。
Falsy,值被解釋為 false:
0-0NaNundefinednull'' //empty string複製程式碼
其餘的都屬於 truthy。
null
null
是一個特殊值,表示沒有值。這在其它語言裡也是普遍的觀念,比如 nil
或者 Python 裡的 None
。
undefined
undefined
表示變數還沒有初始化,值為空。
函式裡沒有 return
值時就會返回 undefined
。當函式引數沒有被呼叫者賦值時,也是 undefined。
檢測一個值是否為 undefined
,你可以用這個方法:
typeof variable === 'undefined'複製程式碼
物件型別
所有不是基本型別的都是物件型別。
函式,陣列和我們叫做物件的都是物件型別。它們本身是特殊的,但是它們繼承了 objects 的很多特性,比如具有特性和使特性生效的方法。
表示式
表示式是可以執行並解析為值的程式碼單元。JS 中的表示式可以分為幾類。
算術表示式
這個分類下所有表示式對數字進行求值:
1 / 2i++i -= 2i * 2複製程式碼
字串表示式
對字串求值:
'A ' + 'string''A ' += 'string'複製程式碼
原始表示式
這個分類下,是變數引用,字面量和常量:
20.02'something'truefalsethis //the current objectundefinedi //where i is a variable or a constant複製程式碼
也有一些語言關鍵字:
functionclassfunction* //the generator functionyield //the generator pauser/resumeryield* //delegate to another generator or iteratorasync function* //async function expressionawait //async function pause/resume/wait for completion/pattern/i //regex() // grouping複製程式碼
陣列和物件初始表示式
[] //array literal{
} //object literal[1,2,3]{a: 1, b: 2
}{a: {b: 1
}
}複製程式碼
邏輯表示式
邏輯表示式使用邏輯運算子,獲得一個布林值:
a &
&
ba || b!a複製程式碼
Left-hand-side 表示式
new //create an instance of a constructorsuper //calls the parent constructor...obj //expression using the spread operator複製程式碼
屬性訪問表示式
object.property //reference a property (or method) of an objectobject[property]object['property']複製程式碼
物件構造表示式
new object()new a(1)new MyRectangle('name', 2, {a: 4
})複製程式碼
函式定義表示式
function() {
}function(a, b) {
return a * b
}(a, b) =>
a * ba =>
a * 2() =>
{
return 2
}複製程式碼
呼叫表示式
a.x(2)window.resize()複製程式碼
原型繼承
JavaScript 在流行程式語言領域是一個非常獨特的存在,原因在於對原型繼承的使用。讓我們看看這是什麼意思。
大多數面嚮物件語言都是基於類繼承模式,JavaScript 基於原型繼承。
這是什麼意思?
每一個 JavaScript 物件都有一個特性,稱作 prototype
,指向不同的物件。
這個不同的物件就是原型物件。
我們的物件會繼承原型物件的特性和方法。
假設你通過物件字面量語法建立了一個物件:
const car = {
}複製程式碼
或者通過 new Object
建立:
const car = new Object()複製程式碼
無論哪一種方法,car
的原型都是 Object
。
初始化一個陣列,也是一個物件:
const list = []//orconst list = new Array()複製程式碼
這個的原型是 Array
。
你可以通過 __proto__
屬性驗證:
car.__proto__ == Object.prototype //truecar.__proto__ == new Object().__proto__ //truelist.__proto__ == Object.prototype //falselist.__proto__ == Array.prototype //truelist.__proto__ == new Array().__proto__ //true複製程式碼
這裡的
__proto__
屬性不是標準的但是在瀏覽器裡被廣泛實現。一個更可靠的獲得原型的方法是Object.getPrototypeOf(new Object())
。
原型上所有的特性和方法在具有該原型的物件上都是可用的:
Object.prototype
是所有物件的原型。
Array.prototype.__proto__ == Object.prototype複製程式碼
如果你想知道 Object.prototype 的原型是什麼,它沒有原型。這是一片特殊的雪花。❄️
上面的例子是原型鏈的示例。
我可以建立一個擴充套件 Array 和任何例項化物件的物件,這個物件的原型鏈上會有 Array 和 Object,它會繼承所有祖先的特性和方法。
除了用 new
建立一個物件,或者用物件和陣列的字面量語法,你還可以用 Object.create()
例項化一個物件。
第一個引數作為物件的原型:
const car = Object.create({
})const list = Object.create(Array)複製程式碼
你可以用 isPrototypeOf()
方法檢查這個物件的原型:
Array.isPrototypeOf(list) //true複製程式碼
注意,因為你可以這樣例項化陣列:
const list = Object.create(Array.prototype)複製程式碼
因此,Array.isPrototypeOf(list)
等於 false,而 Array.prototype.isPrototypeOf(list)
等於 true。
類
在 2015 年釋出的 ECMAScript 6 (ES6) 標準引入了類。
在那之前,JavaScript 只能通過一個十分奇怪的方法實現繼承。那就是原型繼承,我認為這很神奇,和其它的流行語言都不同。
從 Java 或者 Python 或者其它語言來的人們很難理解原型繼承的複雜性,因此,ECMAScript 委員會決定基於此引入語法糖,類似於其它語言的實現方法。
這很重要:JavaScript 底層仍然和之前一樣,你可以以一種普適的方法訪問原型物件。
定義類
類看起來是這樣:
class Person {
constructor(name) {
this.name = name
} hello() {
return 'Hello, I am ' + this.name + '.'
}
}複製程式碼
類有一個識別符號,可以讓我們通過 new ClassIdentifier()
建立一個新物件。
當這個物件被例項化,會呼叫 constructor
方法,可以傳遞任何引數。
一個類可以有很多方法,在這個例子中 hello
是一個方法,所有從這個類派生的物件都可以呼叫它:
const flavio = new Person('Flavio')flavio.hello()複製程式碼
類繼承
類可以擴充套件另一個類,使用該類初始化的物件繼承這兩個類的所有方法。
如果繼承的類和更高一級的類有同名的方法,則按最近優先的原則呼叫:
class Programmer extends Person {
hello() {
return super.hello() + ' I am a programmer.'
}
}const flavio = new Programmer('Flavio')flavio.hello()複製程式碼
上面的程式會列印出:“Hello, I am Flavio. I am a programmer.”。
類沒有顯式地宣告變數,但是必須要在建構函式中初始化變數。
在類中,你可以通過呼叫 super()
引入父類。
靜態方法
通常來講,方法定義在例項上,而不是類上。
而靜態方法能在類上執行:
class Person {
static genericHello() {
return 'Hello'
}
}Person.genericHello() //Hello複製程式碼
私有方法
JavaScript 沒有內建定義私有方法或者保護方法的手段。有解決方法,但是這裡將不再贅述。
Getters 和 Setters
你可以新增 get
或者 set
字首建立一個 getter 和 setter,用哪一種方法取決於你想做什麼:訪問變數或者修改變數的值。
class Person {
constructor(name) {
this.name = name
} set name(value) {
this.name = value
} get name() {
return this.name
}
}複製程式碼
如果屬性只有一個 getter,這個屬性就不能被設定,所有的修改都會被忽略:
class Person {
constructor(name) {
this.name = name
} get name() {
return this.name
}
}複製程式碼
如果屬性只有一個 setter,你可以隨意修改它的值,但是不能從外部訪問它:
class Person {
constructor(name) {
this.name = name
} set name(value) {
this.name = value
}
}複製程式碼
異常
當程式碼發生意外錯誤時,JavaScript 習慣通過 exceptions 處理錯誤。
建立異常
通過關鍵字 throw
建立異常:
throw value複製程式碼
value
可以是任意的 JavaScript 值,包括字串,數字或者一個物件。
當 JavaScript 程式碼執行到這一行,就會暫停正常的程式流程,控制會跳轉到最近的異常處理器。
處理異常
異常處理器是 try/catch
語句。
try
程式碼塊內丟擲的異常都會在相應的 catch
中處理:
try {
//lines of code
} catch (e) {
}複製程式碼
在這個例子中,e
代表異常值。
你可以新增多個處理器,它們可以捕捉不同的錯誤。
finally
為了完成整個程式碼語句,JavaScript 有另一個語句,finally
,不管程式流程如何,異常是否被處理,是否產生異常,其中的程式碼都會執行:
try {
//lines of code
} catch (e) {
} finally {
}複製程式碼
你也可以在沒有 catch
的情況下使用 finally
,以清除可能在 try
中開啟的任何資源,比如檔案或者網路請求:
try {
//lines of code
} finally {
}複製程式碼
巢狀 try 程式碼塊
try
可以被巢狀使用,異常始終在最近的 catch 塊中被處理:
try {
//lines of code try {
//other lines of code
} finally {
//other lines of code
}
} catch (e) {
}複製程式碼
如果在內部的 try
中發現異常,它會在外部的 catch
中被處理。
分號
JavaScript 分號是可選的,我個人傾向於不寫分號,但是很多人喜歡帶上分號。
分號在 JavaScript 社群中產生了很大的分歧。一些人不論在什麼情況都很喜歡用它,而另一部分則相反。
在用了幾年分號之後,2017 年秋天我決定嘗試避免使用分號,我通過 Prettier 自動移除程式碼中的分號,除非有特定的程式碼構造需要它。
現在我很自然就會避免分號,我認為程式碼看起來更優秀,也更易讀了。
因為 JavaScript 不強制要求使用分號,所以萬事皆有可能。當一個位置需要分號,它會自動新增。
執行此操作的過程成為自動插入分號。
瞭解使用分號的規則非常重要,可以避免編寫與預期行為不一致的錯誤程式碼。
JavaScript 自動插入分號規則
在解釋原始碼期間,發現一下特定情況,JavaScript 直譯器會自動新增分號:
- 當下一行的起始程式碼打斷了當前行(程式碼可能是多行的)
- 當下一行程式碼以
開頭,閉合了當前程式碼塊
} - 當執行到原始碼末尾
- 當前行有
return
語句 - 當前行有
break
語句 - 當前行有
throw
語句 - 當前行有
continue
語句
程式碼行為不一致的例子
基於上面的規則,這有一些其它例子,比如:
const hey = 'hey'const you = 'hey'const heyYou = hey + ' ' + you['h', 'e', 'y'].forEach((letter) =>
console.log(letter))複製程式碼
你會看到錯誤 Uncaught TypeError: Cannot read property 'forEach' of undefined
,因為規則 1
嘗試把程式碼解釋為:
const hey = 'hey';
const you = 'hey';
const heyYou = hey + ' ' + you['h', 'e', 'y'].forEach((letter) =>
console.log(letter))複製程式碼
還有:
(1 + 2).toString() // 3複製程式碼
const a = 1const b = 2const c = a + b(a + b).toString()複製程式碼
相反,上面的程式碼丟擲 TypeError: b is not a function
異常,因為 JavaScript 嘗試把它解釋為:
const a = 1const b = 2const c = a + b(a + b).toString()複製程式碼
另一個關於規則 4 的例子:
(() =>
{
return {
color: 'white'
}
})()複製程式碼
你希望這個立即執行函式可以返回一個包含 color
屬性的物件,但是沒有。它返回的是 undefined
,因為 JavaScript 會在 return
後插入一個分號。
正確的做法是把花括號放在 return
的後面:
(() =>
{
return {
color: 'white'
}
})()複製程式碼
你認為這個程式碼會彈出 0:
1 + 1-1 + 1 === 0 ? alert(0) : alert(2)複製程式碼
但是它顯示的是 2,因為規則 1 把它解釋成:
1 + 1 -1 + 1 === 0 ? alert(0) : alert(2)複製程式碼
小心點。有些人對待分號很偏執,我是無所謂,這個工具既然給了我們不用分號的選項,我們就應該避免使用分號。我不是在推薦什麼,你自己選擇。
我們只需要稍微注意一些特殊情況,即使這些很少會出現在你的程式碼中。
記住這些規則:
- 小心使用
return
語句。如果你想返回一些東西,應該和 return 放在同一行(break
,throw
,continue
同理) - 永遠不要用圓括號開頭,這可能會和上一行連線形成函式呼叫或者陣列元素引用
最後,始終測試你的程式碼,確保它是你想要的。
引號
現在我們將談談 JavaScript 裡的引號和特殊的功能。
JavaScript 允許三種形式的引號:
- 單引號
- 雙引號
- 反引號
前兩種差不多:
const test = 'test'const bike = "bike"複製程式碼
使用其中一種沒有什麼大的差別。唯一的不同在於必須轉移用於分隔字串的引號字元:
const test = 'test'const test = 'te\'st'const test = 'te"st'const test = "te\"st"const test = "te'st"複製程式碼
有很多不同的風格指南,建議始終使用其中一種。
我個人一直都用單引號,只在 HTML 中使用雙引號。
反引號是另一個選項,在 2015 年的 ES6 中引入。
它們都有一個特殊的功能 – 允許多行字串。
多行字串可能是常規字串加上轉義字元:
const multilineString = 'A string\non multiple lines'複製程式碼
使用反引號(鍵盤左上角的數字 1 鍵),你可以不用轉義字元:
const multilineString = `A stringon multiple lines`複製程式碼
不僅如此。你可以用 ${
解析變數:
}
const multilineString = `A stringon ${1+1
} lines`複製程式碼
這也叫做模板字串。
模板字串
ES2015,即ES6引入,模板字串提供了一個新的方式展示字串,一些新的有趣的構造已經被廣泛使用。
與 ES5 及更低版本相比,模板字串作為 ES2015/ES6 的功能允許你用更新穎的方式處理字串。
乍一看,反引號語法和單引號,雙引號相比很簡單:
const a_string = `something`複製程式碼
它們和普通的字串相比提供了更多的功能,特別是:
- 提供了優秀的語法定義多行字串
- 提供了簡單的方式解釋字串中的變數和表示式
- 允許通過模板標籤建立 DSLs
讓我們仔細看看這些功能。
多行字串
在 ES6 之前,建立多行字串需要在每行行尾新增 \
字元:
const string = 'first part \second part'複製程式碼
這樣看是兩行字串,但是渲染時會變成一行:
"first part second part"複製程式碼
為了能夠渲染多行字串,你需要在每行末尾新增 \n
:
const string = 'first line\n \second line'複製程式碼
或者
const string = 'first line\n' + 'second line'複製程式碼
模板字串使建立多行字串變得很簡單:
使用反引號開始多行字串,你只用按下Enter鍵就能建立一個新行,不用什麼特別的字元,像這樣:
const string = `Heythisstringis awesome!`複製程式碼
注意:空格也是有意義的,這樣做:
const string = `First Second`複製程式碼
將會建立一個這樣的字串:
First Second複製程式碼
解決這個問題有一個簡單的辦法:讓第一行是空的,在閉合反引號後面加上 trim()
方法,這樣會忽略掉所有第一個字元前的空格:
const string = `FirstSecond`.trim()複製程式碼
插值
模板字串提供了一個在字串中插入變數和表示式的簡單方法。
你用可以使用 ${...
語法:
}
const var = 'test'const string = `something ${var
}` //something test複製程式碼
在 ${
裡你可以新增任何東西,甚至是表示式:
}
const string = `something ${1 + 2 + 3
}`const string2 = `something ${foo() ? 'x' : 'y'
}`複製程式碼
模板標籤
模板標籤是一個最初聽起來可能不那麼有用的功能,但實際上,它被很多流行庫使用,比如Styled Components、Apollo、GraphQL客戶端/服務端庫,所以理解它的原理也很重要。
在 Styled Components 中,模板標籤被用來定義 CSS:
const Button = styled.button` font-size: 1.5em;
background-color: black;
color: white;
`;
複製程式碼
在 Apollo 中,模板標籤被用來定義 GraphQL 查詢表:
const query = gql` query {
...
}`複製程式碼
這些例子中高亮的 styled button
和 gql
模板標籤都是 函式:
function gql(literals, ...expressions) {
}複製程式碼
這個函式返回一個字串,該字串可以是任何計算結果。
literals
是一個包含被表示式插值標記的模板標籤陣列。
expressions
包含所有的插值。
比如上面的例子:
const string = `something ${1 + 2 + 3
}`複製程式碼
literals
陣列包含兩項。第一個從 something
開始直到遇到第一個插值字串,第二個是空字串,是第一個插值末尾(我們只有一個)和整個字串末尾之間的空格。
一個更復雜的例子是:
const string = `somethinganother ${'x'
}new line ${1 + 2 + 3
}test`複製程式碼
在這個例子中,literals
陣列中第一項是:
`somethinganother `複製程式碼
第二項是:
`new line `複製程式碼
第三項是:
`test`複製程式碼
expressions
是包含 x
和 6
的陣列。
傳遞這些值的函式可以對它們進行任何操作,這也是這個功能的強大之處。
最簡單的例子就是賦值插值字串的功能,通過加入 literals
和 expressions
:
const interpolated = interpolate`I paid ${10
}€`複製程式碼
interpolate
是這樣:
function interpolate(literals, ...expressions) {
let string = `` for (const [i, val] of expressions) {
string += literals[i] + val
} string += literals[literals.length - 1] return string
}複製程式碼
JavaScript 函式
現在我們由面及點地瞭解所有函式功能,幫助你使用它們。
JavaScript 裡的一切都是函式。
函式是一個自包含的程式碼塊,定義一次,可以使用無數次。
函式引數是可選的,只能返回一個值。
JavaScript 裡的函式也是物件,一個特殊的物件:函式物件。它們的超能力在於它們可以被呼叫。
另外,函式被稱為頭等函式(first class functions),因為它們可以繫結值,可以傳遞引數,可以返回一個值。
讓我們先從“舊”的 ES6 之前的語法開始。這是一個函式宣告式:
function dosomething(foo) {
// do something
}複製程式碼
現在,在 ES6/ES2015 裡,屬於常規函式。
函式可以繫結給一個變數(這叫做函式表示式):
const dosomething = function(foo) {
// do something
}複製程式碼
命名函式表示式很簡單,但在堆疊呼叫跟蹤中很好用,當錯誤發生時,它會顯示這個函式的名字:
const dosomething = function dosomething(foo) {
// do something
}複製程式碼
ES6/ES2015 引入了箭頭函式,在作為引數或者回撥的行內函式時很好用:
const dosomething = foo =>
{
//do something
}複製程式碼
箭頭函式和上面其它函式有一個巨大的差異,我們會在後面的話題中深入它。
引數
一個函式可以有一個或多個引數。
const dosomething = () =>
{
//do something
}const dosomethingElse = foo =>
{
//do something
}const dosomethingElseAgain = (foo, bar) =>
{
//do something
}複製程式碼
ES6/ES2015 開始,函式引數可以有預設值:
const dosomething = (foo = 1, bar = 'hey') =>
{
//do something
}複製程式碼
這讓你不用填滿引數也可以呼叫呼叫函式:
dosomething(3)dosomething()複製程式碼
ES2018 可以為引數新增尾逗號,幫助減少移動引數時漏掉逗號的 bug(比如把最後一個引數移到中間):
const dosomething = (foo = 1, bar = 'hey') =>
{
//do something
}dosomething(2, 'ho!')複製程式碼
你可以用陣列包裹所有引數,然後在呼叫時用擴充套件運算子展開:
const dosomething = (foo = 1, bar = 'hey') =>
{
//do something
}const args = [2, 'ho!']dosomething(...args)複製程式碼
記住:函式引數是有序的。使用物件作為引數,可以獲得引數的名字:
const dosomething = ({
foo = 1, bar = 'hey'
}) =>
{
//do something console.log(foo) // 2 console.log(bar) // 'ho!'
}const args = {
foo: 2, bar: 'ho!'
}dosomething(args)複製程式碼
返回值
每個函式都會返回一個值,預設是 undefined
。
所有函式都會在最後一行程式碼執行完後終止,或者執行到 return
關鍵字。當 JavaScript 遇到這個關鍵字時會自動終止函式執行,並把控制權交給呼叫者。
如果你傳遞一個值,這個值就會作為函式的返回值。
const dosomething = () =>
{
return 'test'
}const result = dosomething() // result === 'test'複製程式碼
你只能返回一個值:
為了模擬返回多個值,你可以返回一個物件字面量或一個陣列,呼叫函式時用解構繫結單一值。
使用陣列:
使用物件:
巢狀函式
函式可以定義在另一個函式內部:
const dosomething = () =>
{
const dosomethingelse = () =>
{
} dosomethingelse() return 'test'
}複製程式碼
巢狀的函式在外部函式的作用域裡,不能在除此之外的位置呼叫。
物件方法
作為物件特性時,函式作為方法呼叫:
const car = {
brand: 'Ford', model: 'Fiesta', start: function() {
console.log(`Started`)
}
}car.start()複製程式碼
箭頭函式中的“this”
作為物件方法,箭頭函式和常規函式“this”的指向很重要。看看這個例子:
const car = {
brand: 'Ford', model: 'Fiesta', start: function() {
console.log(`Started ${this.brand
} ${this.model
}`)
}, stop: () =>
{
console.log(`Stopped ${this.brand
} ${this.model
}`)
}
}複製程式碼
這個 stop()
方法不會像你預期的工作。
這是因為兩種函式風格中的 this
不一樣。箭頭函式中的 this
指向封閉的上下文,在這個例子中是 window
物件:
使用 function()
,this
指向宿主物件。
這意味著箭頭函式不適合用於物件方法和建構函式(箭頭建構函式會在呼叫中跑出 TypeError
錯誤)。
IIFE, 立即執行函式表示式
IIFE 在宣告之後會立即執行:
;
(function dosomething() {
console.log('executed')
})()複製程式碼
你可以將結果賦值給變數:
const something = (function dosomething() {
return 'something'
})()複製程式碼
它們非常方便,因為你無需在定義後單獨呼叫該函式。
函式提升
在執行程式碼之前,JavaScript 會根據規則將其重新排序。
比如將函式移動到作用域的頂部。這就是這樣寫合法的原因:
複製程式碼
在底層,JavaScript 把這個函式移動到呼叫語句之前,和其它函式處於同一作用域中:
function dosomething() {
console.log('did something')
}dosomething()複製程式碼
現在,如果你使用命名函式表示式,因為你用了 variables,事情會有所不同。變數被提升了,但是值沒有,也就不是一個函式了。
dosomething()const dosomething = function dosomething() {
console.log('did something')
}複製程式碼
不會工作:
這是因為內部變成了:
const dosomethingdosomething()dosomething = function dosomething() {
console.log('did something')
}複製程式碼
這在 let
宣告,var
宣告也一樣不會工作,但是丟擲的錯誤不同:
這是因為 var
宣告被提升並初始化值為 undefined
,而const
和 let
僅僅只會被提升。
箭頭函式
箭頭函式是 ES6/ES2015 中最重要的改變,現在被廣泛使用。它們和常規函式不同,下面我們看看為什麼。
在上面我已經介紹了箭頭函式,但是它們很重要,所以單獨介紹一下它們。
箭頭函式在 ES6/ES2015 中引入,它們的存在永遠的改變了 JavaScript 程式碼的寫法(和工作)。
在我的觀點中,這個改變很受歡迎,以致於現代程式碼很少用 function
關鍵字。
在視覺上,它是個受歡迎和簡單的變化,讓你可以用更簡單的語法寫一個函式,從:
const myFunction = function foo() {
//...
}複製程式碼
變成:
const myFunction = () =>
{
//...
}複製程式碼
如果函式體只有一個語句,你可以忽略花括號,然後把所有內容寫在一行:
const myFunction = () =>
doSomething()複製程式碼
在圓括號中傳遞引數:
const myFunction = (param1, param2) =>
doSomething(param1, param2)複製程式碼
如果只有一個引數,你可以忽略圓括號:
const myFunction = param =>
doSomething(param)複製程式碼
多謝這個語法,箭頭函式鼓勵使用短函式。
隱性返回
箭頭函式可以隱性返回值:不用使用 return
關鍵字返回。
它在函式體只有一個語句時有效:
const myFunction = () =>
'test'myFunction() //'test'複製程式碼
另一個例子,返回一個物件(記住用圓括號包裹返回值,避免直譯器把它看作函式體):
const myFunction = () =>
({value: 'test'
})myFunction() //{value: 'test'
}複製程式碼
箭頭函式中 this 如何工作
this
是一個很難掌握的理念,上下文造成了它的不同,也受 JavaScript 模式(是否是嚴格模式)的影響。
理清這個概念很重要,因為箭頭函式的表現和常規函式不同。
當定義了物件裡的某一方法,在常規函式裡 this
指向這個物件,所以你可以:
const car = {
model: 'Fiesta', manufacturer: 'Ford', fullName: function() {
return `${this.manufacturer
} ${this.model
}`
}
}複製程式碼
呼叫 car.fullname()
會返回 Ford Fiesta
。
箭頭函式的 this
繼承自執行上下文。箭頭函式根本不會·繫結 this
,所以它的值會在呼叫棧裡查詢,所以在這個程式碼裡 car.fullName()
無意義,然後返回 undefined undefined
:
const car = {
model: 'Fiesta', manufacturer: 'Ford', fullName: () =>
{
return `${this.manufacturer
} ${this.model
}`
}
}複製程式碼
因為此,箭頭函式不適合用於物件方法。
箭頭函式也不能用於初始化物件的建構函式,它會丟擲 TypeError
。
當不需要動態上下文,應該使用常規函式替代。
處理事件時也會有問題。DOM 事件監聽器會設定 this
為目標元素,如果你需要事件處理器的 this
,應該用常規函式:
const link = document.querySelector('#link')link.addEventListener('click', () =>
{
// this === window
})const link = document.querySelector('#link')link.addEventListener('click', function() {
// this === link
})複製程式碼
閉包
這是對閉包話題很友好的介紹,是理解 JavaScript 函式如何工作的關鍵。
如果你寫過 JavaScript 函式,你已經使用了 閉包。這是一個需要理解的關鍵主題,它會影響你所做的事情。當一個函式執行時,它執行在定義它的作用域中,而不是執行它的位置。
作用域基本上是可見的變數合集。函式會記住它的詞法作用域,並且能夠訪問在父作用域中定義的變數。
簡而言之,函式有一整套可以訪問的變數。
我們趕緊通過例子驗證一下:
const bark = dog =>
{
const say = `${dog
} barked!` ;
(() =>
console.log(say))()
}bark(`Roger`)複製程式碼
這個預期一樣列印出:Roger barked!
。
如果你想要返回操作,這樣做:
const prepareBark = dog =>
{
const say = `${dog
} barked!` return () =>
console.log(say)
}const bark = prepareBark(`Roger`)bark()複製程式碼
這個程式碼段會列印出 Roger barked!
。
最後一個例子,讓兩種不同的狗 prepareBark
:
const prepareBark = dog =>
{
const say = `${dog
} barked!` return () =>
{
console.log(say)
}
}const rogerBark = prepareBark(`Roger`)const sydBark = prepareBark(`Syd`)rogerBark()sydBark()複製程式碼
列印:
Roger barked!Syd barked!複製程式碼
正如你所見,變數 say
的結果和函式 prepareBark
返回的是相關的。
第二個呼叫 prepareBark()
時重新定義了新的 say
變數,但是不會影響第一次 prepareBark()
的作用域。
這就是閉包的原理:返回的函式保持作用域裡的初始狀態。
陣列
隨著不斷地發展,JavaScript 陣列有了越來越多的功能,有些時候知道什麼使用什麼方法是很棘手的。本章節旨在解釋截至 2018 年你應該使用什麼。
初始化陣列
const a = []const a = [1, 2, 3]const a = Array.of(1, 2, 3)const a = Array(6).fill(1) //init an array of 6 items of value 1複製程式碼
不要使用舊語法(除了型別陣列):
const a = new Array() //never useconst a = new Array(1, 2, 3) //never use複製程式碼
獲取陣列長度
const l = a.length複製程式碼
通過 every
遍歷陣列
a.every(f)複製程式碼
遍歷 a
直到 f()
返回 false。
通過 some
遍歷陣列
a.some(f)複製程式碼
遍歷 a
直到 f()
返回 true。
遍歷陣列並返回函式結果組成的新陣列
const b = a.map(f)複製程式碼
遍歷 a
,返回每一個 a
元素執行 f()
產生的結果陣列。
過濾陣列
const b = a.filter(f)複製程式碼
遍歷 a
,返回每一個 a
元素執行 f()
都為 true 的新陣列。
Reduce
a.reduce((accumulator, currentValue, currentIndex, array) =>
{
//...
}, initialValue)複製程式碼
reduce()
對陣列中每一項都呼叫回撥函式,並逐步計算計算結果。如果 initaiValue
存在,accumulator
在第一次迭代時等於這個值。
例子:
複製程式碼
foreach
ES6
a.forEach(f)複製程式碼
遍歷 a
執行 f
,不能中途停止。
例子:
a.forEach(v =>
{
console.log(v)
})複製程式碼
for…of
ES6
for (let v of a) {
console.log(v)
}複製程式碼
for
for (let i = 0;
i <
a.length;
i += 1) {
//a[i]
}複製程式碼
遍歷 a
,可以通過 return
或者 break
中止迴圈,通過 continue
跳出迴圈。
@@iterator
ES6
獲取陣列迭代器的值:
const a = [1, 2, 3]let it = a[Symbol.iterator]()console.log(it.next().value) //1console.log(it.next().value) //2console.log(it.next().value) //3複製程式碼
.entries()
返回一個鍵值對的迭代器:
let it = a.entries()console.log(it.next().value) //[0, 1]console.log(it.next().value) //[1, 2]console.log(it.next().value) //[2, 3]複製程式碼
.keys()
返回包含所有鍵名的迭代器:
let it = a.keys()console.log(it.next().value) //0console.log(it.next().value) //1console.log(it.next().value) //2複製程式碼
陣列結束時 .next()
返回 undefined
。你可以通過 it.next()
返回的 value, done
值檢測迭代是否結束。當迭代到最後一個元素時 done
的值始終為 true
。
在陣列末尾追加值
a.push(4)複製程式碼
在陣列開頭新增值
a.unshift(0)a.unshift(-2, -1)複製程式碼
移除陣列中的值
刪除末尾的值
a.pop()複製程式碼
刪除開頭的值
a.shift()複製程式碼
刪除任意位置的值
a.splice(0, 2) // get the first 2 itemsa.splice(3, 2) // get the 2 items starting from index 3複製程式碼
不要使用 remove()
,因為它會留下未定義的值。
移除並插入值
a.splice(2, 3, 2, 'a', 'b') //removes 3 items starting from//index 2, and adds 2 items,// still starting from index 2複製程式碼
合併多個陣列
const a = [1, 2]const b = [3, 4]a.concat(b) // 1, 2, 3, 4複製程式碼
查詢陣列中特定元素
ES5
a.indexOf()複製程式碼
返回匹配到的第一個元素的索引,元素不存在返回 -1。
a.lastIndexOf()複製程式碼
返回匹配到的最後一個元素的索引,元素不存在返回 -1。
ES6
a.find((element, index, array) =>
{
//return true or false
})複製程式碼
返回符合條件的第一個元素,如果不存在返回 undefined。
通常這麼用:
a.find(x =>
x.id === my_id)複製程式碼
上面的例子會返回陣列中 id === my_id
的第一個元素。
findIndex
返回符合條件的第一個元素的索引,如果不存在返回 undefined
:
a.findIndex((element, index, array) =>
{
//return true or false
})複製程式碼
ES7
a.includes(value)複製程式碼
如果 a
包含 value
返回 true。
a.includes(value, i)複製程式碼
如果 a
從位置 i
後包含 value
返回 true。
獲取陣列的一部分
a.slice()複製程式碼
陣列排序
按字母順序排序(按照 ASCII 值 – 0-9A-Za-z
):
const a = [1, 2, 3, 10, 11]a.sort() //1, 10, 11, 2, 3const b = [1, 'a', 'Z', 3, 2, 11]b = a.sort() //1, 11, 2, 3, Z, a複製程式碼
自定義排序
const a = [1, 10, 3, 2, 11]a.sort((a, b) =>
a - b) //1, 2, 3, 10, 11複製程式碼
逆序
a.reverse()複製程式碼
陣列轉字串
a.toString()複製程式碼
返回字串型別的值
a.join()複製程式碼
返回陣列元素拼接的字串。傳遞引數以自定義分隔符:
a.join(',')複製程式碼
複製所有值
const b = Array.from(a)const b = Array.of(...a)複製程式碼
複製部分值
const b = Array.from(a, x =>
x % 2 == 0)複製程式碼
將值複製到本身其它位置
const a = [1, 2, 3, 4]a.copyWithin(0, 2) // [3, 4, 3, 4]const b = [1, 2, 3, 4, 5]b.copyWithin(0, 2) // [3, 4, 5, 4, 5]//0 is where to start copying into,// 2 is where to start copying fromconst c = [1, 2, 3, 4, 5]c.copyWithin(0, 2, 4) // [3, 4, 3, 4, 5]//4 is an end index複製程式碼
迴圈
JavaScript 提供了許多種迴圈方法。這個章節通過小例子和主要屬性講解現代 JavaScript 中的所有迴圈方法。
for
const list = ['a', 'b', 'c']for (let i = 0;
i <
list.length;
i++) {
console.log(list[i]) //value console.log(i) //index
}複製程式碼
- 可以通過
break
中斷for
迴圈 - 可以通過
continue
跳過當前for
迴圈
forEach
ES5 中引入。給你一個陣列,你可以通過 list.forEach()
遍歷它的屬性:
const list = ['a', 'b', 'c']list.forEach((item, index) =>
{
console.log(item) //value console.log(index) //index
})//index is optionallist.forEach(item =>
console.log(item))複製程式碼
不幸的是,你不能中斷這個迴圈。
do…while
const list = ['a', 'b', 'c']let i = 0do {
console.log(list[i]) //value console.log(i) //index i = i + 1
} while (i <
list.length)複製程式碼
可以通過 break
中斷 do...while
迴圈:
do {
if (something) break
} while (true)複製程式碼
可以通過 continue
跳過當前 do...while
迴圈:
do {
if (something) continue //do something else
} while (true)複製程式碼
while
const list = ['a', 'b', 'c']let i = 0while (i <
list.length) {
console.log(list[i]) //value console.log(i) //index i = i + 1
}複製程式碼
可以通過 break
中斷 while
迴圈:
while (true) {
if (something) break
}複製程式碼
可以通過 continue
跳過當前 while
迴圈:
while (true) {
if (something) continue //do something else
}複製程式碼
和 do...while
不同的是 do...while
至少會迴圈一次。
for…in
遍歷物件的所有可迭代屬性名。
for (let property in object) {
console.log(property) //property name console.log(object[property]) //property value
}複製程式碼
for…of
ES2015 中引入了 for...of
迴圈,它結合了 forEach 的易用性和不能中斷的特性:
//iterate over the valuefor (const value of ['a', 'b', 'c']) {
console.log(value) //value
}//get the index as well, using `entries()`for (const [index, value] of ['a', 'b', 'c'].entries()) {
console.log(index) //index console.log(value) //value
}複製程式碼
注意使用 const
。這個迴圈在每次迭代都建立了一個新的作用域,所以我們可以安全的使用它替代 let
。
for…in vs for…of
和 for...in
不同的是:
for...of
迭代屬性值for...in
迭代屬性名
事件
瀏覽器中的 JavaScript 使用事件驅動程式設計模型。萬物始於事件。這個章節介紹了 JavaScript 事件以事件處理器的工作原理。
事件可能是 DOM 載入完成,或者是非同步請求結束,或者是使用者點選了元素或是滾動了頁面,或者是使用者按下鍵盤。
有很多種不同的事件。
事件處理器
你可以通過事件處理器響應所有事件,就是事件發生時呼叫對應函式。
你可以對同一個事件註冊多個處理器,它們都會在事件發生時被呼叫。
JavaScript 提供了三種方法註冊事件處理器:
行內事件處理器
這種方法由於自身限制現在已經很少使用,但在早期的 JavaScript 中是唯一的方法:
<
a href="site.com" onclick="dosomething();
">
A link<
/a>
複製程式碼
DOM 事件處理器
當一個物件只有一個事件處理器時這種方法很常用,在這個例子中沒辦法新增多個處理器:
window.onload = () =>
{
//window loaded
}複製程式碼
在處理 XHR 請求時這也很常見:
const xhr = new XMLHttpRequest()xhr.onreadystatechange = () =>
{
//.. do something
}複製程式碼
你可以通過 if ('onsomething' in window) {
檢查處理器是否已經分配給某個屬性。
}
使用 addEventListener()
這是很現代的方法。這個方法允許我們按需註冊多個事件處理器,你會發現它是最流行的:
window.addEventListener('load', () =>
{
//window loaded
})複製程式碼
注意:IE8 及以下版本不支援這個方法,可以使用
attachEvent()
代替。如果你需要相容舊瀏覽器這很重要。
監聽不同的元素
你可以監聽 window
攔截“全域性”事件,比如鍵盤的使用。你也可以監聽特定元素上發生的事件,比如滑鼠點選了某個按鈕。
這也是為什麼 addEventListener
有時候在 window
上呼叫,有時間在某個 DOM 元素上。
事件物件
事件處理器會獲得一個 Event
物件作為第一個引數:
const link = document.getElementById('my-link')link.addEventListener('click', event =>
{
// link clicked
})複製程式碼
這個物件包含很多有用的屬性和方法,比如:
target
,事件發生的目標 DOM 元素type
,事件型別stopPropagation()
,呼叫以阻止 DOM 事件傳播
(檢視完整清單)
其它屬性提供給特定的事件,Event
只是不同事件的一個介面:
上面的每一個都連結到了 MDN 頁面,你可以在那檢視它們所有的屬性。
舉個例子,當一個鍵盤事件發生時,你可以檢查哪個鍵被按下,通過 key
屬性值得到一個易讀的值(Escape
, Enter
等等):
window.addEventListener('keydown', event =>
{
// key pressed console.log(event.key)
})複製程式碼
在滑鼠事件中我們可以直到哪個按鈕被按下:
const link = document.getElementById('my-link')link.addEventListener('mousedown', event =>
{
// mouse button pressed console.log(event.button) //0=left, 2=right
})複製程式碼
事件冒泡和事件捕捉
事件冒泡和事件捕捉是事件傳播的兩個模型。
假設你的 DOM結構是這樣的:
<
div id="container">
<
button>
Click me<
/button>
<
/div>
複製程式碼
你希望跟蹤使用者什麼時候點選了這個按鈕,你有兩個事件處理器,一個在 button
上,一個在 #container
上。記住,子元素上的點選事件也會傳播到它的父元素上,除非你阻止了事件傳播(稍後詳解)。
這些事件處理器會按照順序呼叫,這個順序通過事件冒泡/事件捕捉模型決定。
冒泡意味著事件從被點選的元素(子元素)一直向上傳播到所有祖先元素,從最近的一個開始。
在我們的例子中,button
上的處理器會在 #container
之前發生。
捕捉恰恰相反:最外部的事件會在特定處理器之前發生,比如 button
。
預設採用事件冒泡模型。
你也可以選擇使用事件捕捉,通過將 addEventListener 的第三個引數設為 true
:
document.getElementById('container').addEventListener( 'click', () =>
{
//window loaded
}, true)複製程式碼
注意:首先執行的是捕捉階段的事件處理器,然後才是冒泡的事件處理器。
這個順序遵循這個原則:DOM 從 Window 物件開始遍歷所有元素,直到找到被點選的物件。執行此操作時,會呼叫任何繫結的事件處理器(捕捉階段)。一旦找到目標元素,它會重複這個過程直到回到 Window 物件,此時呼叫相應的事件處理器(冒泡階段)。
阻止傳播
DOM 元素事件會一直在它的母樹上傳播,除非手動阻止它:
<
html>
<
body>
<
section>
<
a id="my-link" ...>
複製程式碼
a
上的點選事件會傳播到 section
然後是 body
。
你可以呼叫 stopPropagation()
方法阻止事件傳播,一般放在事件處理器的末尾:
const link = document.getElementById('my-link')link.addEventListener('mousedown', event =>
{
// process the event // ... event.stopPropagation()
})複製程式碼
常見事件
這是一個你經常會用到的事件清單。
load
window
和 body
元素的 load
事件在頁面載入完成時觸發。
滑鼠事件
click
事件在滑鼠單擊時觸發。dbclick
事件在雙擊滑鼠時觸發,當然,在這種情況下會先觸發 click
事件。mousedown
,mousemove
和 mouseup
可以和拖動事件結合在一起。小心使用 mousemove
,它會在滑鼠移動過程中觸發很多次(稍後會看到節流)。
鍵盤事件
keydown
事件在按下鍵盤時觸發(並在處於按下狀態時持續觸發)。keyup
事件在鬆開鍵盤時觸發。
滾動
scroll
事件在每一次滾動頁面時觸發。在這個事件處理器內部,你可以通過 window.scrollY
(Y軸)檢視當前滾動位置。
注意這個事件不是一次性的,它會在滾動過程中持續發生,不僅僅是在滾動開始和滾動結束,所以不要在處理事件時進行大量計算和操作 – 使用節流代替。
節流
正如上面提到的,mousemove
和 scroll
都不是一次性事件,它們在操作發生期間持續呼叫事件處理器。這是因為它們需要提供你需要知道的座標。
如果你在這些事件處理器中進行復雜的操作,將會影響效能給你的網頁使用者帶來糟糕的體驗。
像 Lodash 這樣的庫提供了 100 行程式碼實現的節流函式來幫助解決這個問題。一個簡單又容易理解的實現是使用定時器每隔 100ms 快取一次滾動事件:
let cached = nullwindow.addEventListener('scroll', event =>
{
if (!cached) {
setTimeout(() =>
{
//you can access the original event at `cached` cached = null
}, 100)
} cached = event
})複製程式碼
事件迴圈
事件迴圈是 JavaScript 中最重要的內容。
我已經使用 JavaScript 好多年了,但是也沒有完全理解它的工作原理。當然不瞭解這些細枝末節也沒有什麼關係,但通常來講,知道它的工作原理是很有幫助的,你可能也很好奇這個內容。
這個章節致力於解釋 JavaScript 如何是單執行緒工作以及如何處理非同步函式的內在細節。
你的 JavaScript 程式碼執行在單執行緒,同一時間只會發生一件事情。這是一個非常有用的限制,它簡化了很多程式,你不用再為併發問題擔憂。你只需要關注於
如何書寫程式碼,避免造成執行緒堵塞的內容,比如同步網路請求或者無限迴圈。
通常,大多數瀏覽器的每一個瀏覽標籤都有獨立的事件迴圈,以使程式隔離避免有無限迴圈或者繁重處理的頁面阻塞整個瀏覽器。瀏覽器管理多個併發的事件迴圈來解決 API 的呼叫。Web Workers 也執行在自己的事件迴圈裡。
你只需要明白你的程式碼執行在單一事件迴圈,並在寫程式碼時考慮到這一點,避免阻塞它。
阻塞事件迴圈
任何執行時間過長不能將控制權返回給事件迴圈的 JavaScript 程式碼都會阻塞頁面內其它程式碼的執行,甚至阻塞 UI 執行緒,導致使用者不能點選、滾動頁面等等。
大多數 JavaScript 原語是非阻塞的,比如網路請求,Node.js檔案系統操作等等。發生阻塞是意外的,這也是為什麼 JavaScript 基於大量的回撥以及最近的 promises 和 async/await。
呼叫堆疊
呼叫堆疊是 LIFO 佇列(Last In, First Out)。
事件迴圈不斷檢查呼叫堆疊裡是否仍有函式需要執行。於此同時,它將找到的函式加入呼叫堆疊,然後按照順序執行。
你瞭解偵錯程式或者瀏覽器控制檯裡的錯誤堆疊跟蹤資訊嗎?瀏覽器在呼叫堆疊中查詢函式名字,然後標記出當前呼叫由哪個函式觸發:
一個簡單的事件迴圈說明
舉個例子:
const bar = () =>
console.log('bar')const baz = () =>
console.log('baz')const foo = () =>
{
console.log('foo') bar() baz()
}foo()複製程式碼
這個程式碼列印出:
foobarbaz複製程式碼
和預期一樣。
當這個程式碼執行時,最開始 foo()
被呼叫,在 foo()
內部先呼叫 bar()
,然後呼叫 baz()
。
這時我們的呼叫棧看起來就是這樣:
每次迭代的事件迴圈都會檢視呼叫堆疊中是否還有內容,並執行它:
直到整個呼叫堆疊是空的。
函式執行佇列
上面的例子很普通,也沒有什麼特殊之處:JavaScript 發現需要執行的內容然後按照順序執行。
讓我們看看如何延遲函式執行直到清空呼叫棧。
使用 setTimeout(() =>
呼叫一個函式,會在其它函式全部執行完畢那一刻執行這個函式。
{
}, 0)
舉個例子:
const bar = () =>
console.log('bar')const baz = () =>
console.log('baz')const foo = () =>
{
console.log('foo') setTimeout(bar, 0) baz()
}foo()複製程式碼
結果令人驚訝:
foobazbar複製程式碼
當這個程式碼執行時,foo()
先被呼叫。foo()
內部先呼叫 setTimeout,將 bar
作為引數傳入定時器,我們傳入 0 指示它立即執行,然後呼叫 baz()
。
這時呼叫棧時這樣的:
我們的程式中所有函式的執行順序:
為什麼這樣?
訊息佇列
當 setTimeout() 呼叫時,瀏覽器或者 Node.js 開始計時。在這個例子中,我們將 0 作為延時時間,時間一到,回撥函式就會被推入訊息佇列。
訊息佇列也包含使用者發出的點選或者鍵盤事件,或者是在你的程式碼之前已經存在的獲取響應的佇列,或者是像 onLoad
這樣的 DOM 事件。
整個迴圈會優先進行呼叫堆疊,它會先處理呼叫堆疊裡找到的所有內容,一旦沒有內容,它就會在事件佇列裡拾取內容。
我們不必等待像 setTimeout
,fetch等其它自己完成工作的函式,因為它們由瀏覽器提供,具有自己的執行緒。舉個例子,如果你設定一個延時 2 秒的 setTimeout
定時器,你不用等待 2 秒 – 等待在別的地方完成。
ES6 工作佇列(Job Queue)
ECMASciprt 2015 引入了工作佇列概念,用在 Promises(同樣在 ES6/ES2015 中引入)中。這是一種儘快執行非同步函式的方法,而不是放在呼叫堆疊的末尾。
在當前函式結束前完成的 Promises 將會在當前函式之後執行。
我發現在遊樂場坐雲霄飛車可以很好的解釋這個內容:訊息佇列將你放在其它遊客佇列之後,工作佇列是一張快速通行證,能讓你插隊提前坐上雲霄飛車。
例子:
const bar = () =>
console.log('bar')const baz = () =>
console.log('baz')const foo = () =>
{
console.log('foo') setTimeout(bar, 0) new Promise((resolve, reject) =>
resolve('should be right after baz, before bar') ).then(resolve =>
console.log(resolve)) baz()
}foo()複製程式碼
這會列印出:
foobazshould be right after baz, before barbar複製程式碼
這是 Promises(包括基於 promises 的 Async/await)和原生的舊非同步函式 setTimeout()
或者其它平臺 API 之間最大的不同。
非同步程式設計和回撥
JavaScript 預設就是非同步和單執行緒的。這意味著程式碼不能建立新執行緒並且並行執行。讓我們瞭解非同步程式碼是什麼。
程式語言裡的非同步
計算機在設計上是非同步的。
非同步是某些東西可以獨立於主程式流程發生。
在當前的消費計算機中,每一個程式都執行在一個特殊的時間段,然後停止之後讓其它程式開始執行。這一切發生的很快以至於我們無法注意到,我們認為我們的計算機同時執行許多程式,但這是一種誤解(除了多程式機器)。
程式內部使用中斷,一種提交給處理器獲得系統注意的訊號。
我不會深入介紹它,但是要記住這對非同步程式來說很普便,在它們被注意之前停止執行,同時計算機可以執行其它內容。當一個程式等待網路響應時,只有等請求結束才能終止執行。
一般,程式語言是非同步的,其中一些也會提供非同步操作的辦法,這些語言或者庫,C 語言,Java,C#,PHP,Go,Ruby,Swift,Python 預設都是非同步的,其中一些通過使用執行緒處理非同步,產生一個新程式。
JavaScript
JavaScript 預設就是非同步和單執行緒的。這意味著程式碼不能建立新執行緒並且並行執行。
每行程式碼都是一個接著一個執行,舉個例子:
const a = 1const b = 2const c = a * bconsole.log(c)doSomething()複製程式碼
但是 JavaScript 是為了瀏覽器而生的,一開始它的主要工作是響應使用者的操作,比如 onClick
,onMouseOver
,onChnage
,onSubmit
等等。它怎麼能使用非同步程式設計模式?
答案在它的環境中。瀏覽器通過提供一系列可以處理這種功能的 API 解決了這個問題。
尤其最近 Node.js 引入了非阻塞 I/O 環境將這個理念擴充套件到檔案訪問,網路呼叫等等方面。
回撥函式
你無法知道使用者何時要單擊按鈕,因此你要為 click 事件定義事件處理程式。此事件處理程式接受一個函式,該函式將在觸發事件時呼叫:
document.getElementById('button').addEventListener('click', () =>
{
//item clicked
})複製程式碼
這也被叫做回撥函式。
回撥是一個簡單的函式,作為值傳入其它函式,只會在事件發生時被呼叫。我們可以這麼是因為 JavaScript 的頭等函式可以和變數繫結並傳入其它函式(稱作高階函式)。
把你所有的程式碼包裹在 window
物件上的 load
事件監聽器裡是很常見的,這樣程式碼只會在頁面準備好時執行:
window.addEventListener('load', () =>
{
//window loaded //do what you want
})複製程式碼
回撥函式任何地方都會用到,不僅僅是 DOM 事件。
一個常見的例子是使用定時器:
setTimeout(() =>
{
// runs after 2 seconds
}, 2000)複製程式碼
XHR 請求同樣接受一個回撥函式。在這個例子中,將一個函式分配給一個屬性,這樣當特定事件(這裡是請求狀態發生變化)發生時就會呼叫這個函式:
const xhr = new XMLHttpRequest()xhr.onreadystatechange = () =>
{
if (xhr.readyState === 4) {
xhr.status === 200 ? console.log(xhr.responseText) : console.error('error')
}
}xhr.open('GET', 'https://yoursite.com')xhr.send()複製程式碼
處理回撥中的錯誤
你如何處理回撥錯誤?一個常見的策略是採用 Node.js 的方法:回撥函式的第一個引數始終是錯誤物件:錯誤優先回撥。
如果沒有錯誤,這個物件為 null
。如果發生錯誤,它包含描述錯誤和其它資訊的內容。
fs.readFile('/file.json', (err, data) =>
{
if (err !== null) {
//handle error console.log(err) return
} //no errors, process data console.log(data)
})複製程式碼
回撥伴隨的問題
回撥讓簡單程式碼更簡單!
然而每一個回撥函式都會新增一級巢狀,如果你有很多個回撥函式,程式碼會變得十分複雜:
window.addEventListener('load', () =>
{
document.getElementById('button').addEventListener('click', () =>
{
setTimeout(() =>
{
items.forEach(item =>
{
//your code here
})
}, 2000)
})
})複製程式碼
這僅僅是一個簡單的四級程式碼,但是我看到很多層巢狀,這並不有趣。
怎麼解決這個問題?
回撥的替代方案
從 ES6 開始,JavaScript 引入了很多功能讓我們不用回撥就能優雅的寫非同步程式碼:
- Promises(ES6)
- Async/Await(ES8)
Promises
Promises 是 JavaScript 解決非同步程式碼中需要寫太多回撥函式的一種方法。
Promises 通常被定義為一個最終可用的值的代理(a proxy for a value that will eventually become available)。
Promises 是一個解決非同步程式碼的方法,不用在程式碼中寫太多回撥函式。儘管這些年已經變得很流行,也在 ES2015 中被引入併成為標準,在 ES2017 中也被非同步函式(async functions)代替。
promises 工作原理(簡短)
一旦一個 promise 被呼叫,它就會變成 pending 狀態。這意味呼叫者會持續執行,同時等待自身處理結果,然後給呼叫函式一些反饋。
此時,該呼叫函式等待這個 promise 返回 resolved 狀態 或者 rejected 狀態,但是你知道 JavaScript 是非同步的,所以這個函式會在 promise 工作時繼續執行其它程式碼。
哪些 JS API 使用 promises?
除了你自己的程式碼和一些庫之外,現在 Web API 也使用 promises:
- 電池 API
- Fetch API
- Service Workers
在現代 JavaScript 中你不太可能不使用 promises,所以讓我們深挖一下它。
建立 promise
Promise API 暴露出一個 Promise 建構函式,你可以通過 new Promise()
初始化:
let done = trueconst isItDoneYet = new Promise( (resolve, reject) =>
{
if (done) {
const workDone = 'Here is the thing I built' resolve(workDone)
} else {
const why = 'Still working on something else' reject(why)
}
})複製程式碼
你可以看到這個 promise 在全域性常量 done
為 true 時,返回一個已解決的 promise,否則返回一個被拒絕的 promise。
使用 resolve
和 reject
我們可以回傳一個值,在上面的例子中,我們返回了一個字串,也可以返回一個物件。
使用 promise
在上個章節中,我們介紹瞭如何建立一個 promise。
現在讓我們看看如何使用 promise。
const isItDoneYet = new Promise( //...)const checkIfItsDone = () =>
{
isItDoneYet .then((ok) =>
{
console.log(ok)
}) .catch((err) =>
{
console.error(err)
})
}複製程式碼
執行 checkIfItsDone()
會執行 isItDoneYet()
promise 然後等待它 resolve 呼叫 then
回撥,如果發生錯誤,會在 catch
回撥中處理錯誤。
鏈式 promise
promise 可以返回另一個 promise,形成鏈式 promise。
Fecth API(XMLHttpRequest API 上層 API)提供了一個很好的例子。我們可以用它獲取資源並用 promise 鏈式操作資源。
Fetch API 基於 promise 機制,呼叫 fetch()
等同於我們通過 new promise()
定義一個 promise。
鏈式 promise 例子
const status = (response) =>
{
if (response.status >
= 200 &
&
response.status <
300) {
return Promise.resolve(response)
} return Promise.reject(new Error(response.statusText))
}const json = (response) =>
response.json()fetch('/todos.json') .then(status) .then(json) .then((data) =>
{
console.log('Request succeeded with JSON response', data)
}) .catch((error) =>
{
console.log('Request failed', error)
})複製程式碼
在這個例子中,我們呼叫 fetch()
從根域名中的 todos.json
檔案獲取一個 TODO 清單,我們建立了一個 promises 鏈。
執行 fetch()
返回一個包含很多個屬性的響應,我們引用了其中的:
status
,反應 HTTP 狀態的數值statusText
,狀態訊息,請求成功時為OK
response
也有一個 json()
方法,可以將成功獲取的響應內容轉化為 JSON 並作為 promise 返回。
基於此:鏈中第一個 promise 是我們定義的函式 status()
,它用來檢查響應狀態,當結果不在 200 和 299 之間時拒絕這個 promise。這樣會導致 promise 鏈跳過所有連結串列直接進入末尾的 catch()
語句,列印出 Request failed
和錯誤資訊。
如果成功,它會呼叫我們定義的 json()
函式。當成功時,上一個 promise 返回 response
物件,作為第二個 promise 的輸入。
在這個過程中,我們返回處理過的 JSON 資料,所以第三個 promise 直接獲得 JSON 物件:
.then((data) =>
{
console.log('Request succeeded with JSON response', data)
})複製程式碼
在控制檯會列印出這些內容。
處理錯誤
在上面的例子中,promises 鏈後面有一個 catch
塊。當鏈式 promises 有任何錯誤發生,亦或手動返回 rejects,程式控制權會交給錯誤程式碼後距離最近的 catch()
語句。
new Promise((resolve, reject) =>
{
throw new Error('Error')
}) .catch((err) =>
{
console.error(err)
})// ornew Promise((resolve, reject) =>
{
reject('Error')
}) .catch((err) =>
{
console.error(err)
})複製程式碼
級聯錯誤
如果在 catch()
內部又丟擲一個錯誤,你可以新增第二個 catch()
處理它,以此類推。
new Promise((resolve, reject) =>
{
throw new Error('Error')
}) .catch((err) =>
{
throw new Error('Error')
}) .catch((err) =>
{
console.error(err)
})複製程式碼
編排 promises
Promise.all()
如果你需要同時處理多個 promises,Promise.all()
可以幫助你定義一組 promises,等待它們全部完成之後再執行某些操作。比如:
const f1 = fetch('/something.json')const f2 = fetch('/something2.json')Promise.all([f1, f2]).then((res) =>
{
console.log('Array of results', res)
}).catch((err) =>
{
console.error(err)
})複製程式碼
ES2015 的結構語法也允許你這麼做:
Promise.all([f1, f2]).then(([res1, res2]) =>
{
console.log('Results', res1, res2)
})複製程式碼
這不僅限於 fetch
,任何 promise 都可以處理。
Promise.race()
當傳入的 promise 有一個完成 Promise.race()
就開始執行,且只會執行一次附加的回撥函式,傳入首先執行完的 promise 返回的結果。例子:
const first = new Promise((resolve, reject) =>
{
setTimeout(resolve, 500, 'first')
})const second = new Promise((resolve, reject) =>
{
setTimeout(resolve, 100, 'second')
})Promise.race([first, second]).then((result) =>
{
console.log(result) // second
})複製程式碼
Async 和 Await
現在,我們將探討 JavaScript 中更現代的非同步函式方法。JavaScript 從回撥函式發展為 Promise 只用了很短的時間,從 ES2017 開始,async 和 await 讓非同步 JavaScript 變得更加簡單。
非同步函式結合了 promise 和生成器,基本上可以看作是 promises 之上的抽象方法。我再重複一遍:async 和 await 基於 promises。
為什麼引入 async 和 await?
它們減少了 promises 的固定樣板和鏈式 promise “不能切斷鏈式”的限制。
ES2015 引入 promises 時,只是為了解決非同步程式碼的問題,這一點做得很好,但是,ES2015 和 ES2017 之間的這兩年人們發現 promises 並不是最終的解決方案。
引入 Promises 是為了解決著名的回撥地獄問題,但其自身也很複雜,引入了更復雜的語法。
它們是很好的開始,但是應該有更好的語法供開發者使用,所以**非同步函式(async functions)**誕生了。它讓程式碼看起來是同步的,但其實它們是非同步的並不會阻塞後面的程式碼。
工作原理
一個非同步函式返回一個 promise,就像這個例子:
const doSomethingAsync = () =>
{
return new Promise((resolve) =>
{
setTimeout(() =>
resolve('I did something'), 3000)
})
}複製程式碼
你在前面加上 await
然後呼叫這個函式,這樣程式碼會暫停執行直到這個 promise 變成 resolved 或者 rejected。需要注意的是:委託函式必須定義為 async
。例子:
const doSomething = async () =>
{
console.log(await doSomethingAsync())
}複製程式碼
例子
這是一個使用 async/await 非同步執行函式的例子:
const doSomethingAsync = () =>
{
return new Promise((resolve) =>
{
setTimeout(() =>
resolve('I did something'), 3000)
})
}const doSomething = async () =>
{
console.log(await doSomethingAsync())
}console.log('Before')doSomething()console.log('After')複製程式碼
上面的程式碼在瀏覽器控制檯中列印出:
BeforeAfterI did something //after 3s複製程式碼
一切都是 promise
在任何函式前加上 async
字首都使這個函式返回一個 promise。即使它沒有明確這麼做,它也會在內部讓它返回一個 promise。這也是為什麼這個程式碼是合法的:
const aFunction = async () =>
{
return 'test'
}aFunction().then(alert) // This will alert 'test'複製程式碼
它和這個類似:
const aFunction = async () =>
{
return Promise.resolve('test')
}aFunction().then(alert) // This will alert 'test'複製程式碼
程式碼更易讀
你看我們上面的程式碼和原生的 promise 鏈式回撥比起來是多麼的簡單。這僅僅是一個非常簡單的例子,程式碼越複雜,越能凸顯它的優勢。
拿 promises 舉個例子,你需要獲取一個 JSON 資源並且對它進行解析:
const getFirstUserData = () =>
{
return fetch('/users.json') // get users list .then(response =>
response.json()) // parse JSON .then(users =>
users[0]) // pick first user .then(user =>
fetch(`/users/${user.name
}`)) // get user data .then(userResponse =>
response.json()) // parse JSON
}getFirstUserData()複製程式碼
用 async/await 實現同樣的需求:
const getFirstUserData = async () =>
{
const response = await fetch('/users.json') // get users list const users = await response.json() // parse JSON const user = users[0] // pick first user const userResponse = await fetch(`/users/${user.name
}`) // get user data const userData = await user.json() // parse JSON return userData
}getFirstUserData()複製程式碼
串聯多個非同步函式
非同步函式可以很容易的串聯起來,語法也比原生 promise 更易讀:
const promiseToDoSomething = () =>
{
return new Promise(resolve =>
{
setTimeout(() =>
resolve('I did something'), 10000)
})
}const watchOverSomeoneDoingSomething = async () =>
{
const something = await promiseToDoSomething() return something + ' and I watched'
}const watchOverSomeoneWatchingSomeoneDoingSomething = async () =>
{
const something = await watchOverSomeoneDoingSomething() return something + ' and I watched as well'
}watchOverSomeoneWatchingSomeoneDoingSomething().then((res) =>
{
console.log(res)
})複製程式碼
列印出:
I did something and I watched and I watched as well複製程式碼
更容易除錯
除錯 promise 很難,因為偵錯程式不能跳過非同步程式碼。
Async/await 就很簡單了,因為對編譯器來說它就是同步程式碼。
迴圈作用域
對開發者來說,JavaScript 的迴圈作用域可能會讓人感到頭疼。我們將學習迴圈作用域中和 var,let 有關的技巧。
例子:
const operations = []for (var i = 0;
i <
5;
i++) {
operations.push(() =>
{
console.log(i)
})
}for (const operation of operations) {
operation()
}複製程式碼
它遍歷 5 次,每次新增一個函式到 operations 陣列。這個函式能夠列印出迴圈時的索引變數 i
。稍後執行這些函式。
期望的結果是:
01234複製程式碼
但真實結果卻是:
55555複製程式碼
為什麼這樣?原因在於 var
。
由於 var
宣告會被提升,上面的程式碼等同於:
var i;
const operations = []for (i = 0;
i <
5;
i++) {
operations.push(() =>
{
console.log(i)
})
}for (const operation of operations) {
operation()
}複製程式碼
所以,在 for-of 迴圈中,i
始終等於 5。在這個函式中每一次呼叫 i
都等於 5。
所以我們怎麼做能讓它按照我們的需要工作呢?
最簡單的方法是用 let
宣告。在 ES2015 引入,它可以避免 var
宣告帶來一些奇怪的事情。
把迴圈中的 var
改成 let
一切就正常了:
const operations = []for (let i = 0;
i <
5;
i++) {
operations.push(() =>
{
console.log(i)
})
}for (const operation of operations) {
operation()
}複製程式碼
輸出:
01234複製程式碼
這怎麼可能?原因是每一次迴圈迭代 i
都建立了一個新的作用域,每個新增到 operations
陣列的函式都獲取當時的 i
副本。
另一個解決這個問題的辦法在 ES6 之前很常用,使用立即執行函式表示式(IIFE)。
在這個問題中,你可以包裹整個函式並繫結 i
。這樣每次都建立了一個立即執行的函式,並返回了一個新函式,所以我們可以稍後執行:
const operations = []for (var i = 0;
i <
5;
i++) {
operations.push(((j) =>
{
return () =>
console.log(j)
})(i))
}for (const operation of operations) {
operation()
}複製程式碼
定時器
寫 JavaScript 程式碼時,你可能想要延時執行某個函式。本節我們將討論如何使用 setTimeout 和 setInterval 安排將來的函式。
setTimeout()
寫 JavaScript 程式碼時,你可能想要延時執行某個函式。這個工作交給 setTimeout
。你可以指定一個延時執行的函式和需要的延時時間,以毫秒計:
setTimeout(() =>
{
// runs after 2 seconds
}, 2000)setTimeout(() =>
{
// runs after 50 milliseconds
}, 50)複製程式碼
這個語法定義了一個新函式。你可以在任何位置呼叫它,你也可以傳遞一個已經存在的函式名,也可以設定一些引數:
const myFunction = (firstParam, secondParam) =>
{
// do something
}// runs after 2 secondssetTimeout(myFunction, 2000, firstParam, secondParam)複製程式碼
setTimeout
返回一個定時器識別符號。通常不會用到它,但是你可以儲存這個 id,在需要刪除這個定時函式時清空它:
const id = setTimeout(() =>
{
// should run after 2 seconds
}, 2000)// I changed my mindclearTimeout(id)複製程式碼
零延時
如果你指定延時時間為 0
,這個回撥函式將會盡可能快的執行,但是必須等當前函式執行完畢:
setTimeout(() =>
{
console.log('after ')
}, 0)console.log(' before ')複製程式碼
會列印出 before after
。
通過對排程程式中的函式進行排序,這對於避免在密集型任務上阻塞 CPU 並在執行繁重計算時讓其他函式能夠執行特別有用。
一些瀏覽器(IE 和 Edge)實現了
setImmediate()
方法達到上述目的,但是沒有成為標準,不能在其它瀏覽器中使用。但在 Node.js 可用。
setInterval()
setInterval
和 setTimeout
類似,區別在於:和只執行一次回撥函式不同,它能夠在指定的特定時間間隔(以毫秒為單位)一直執行它:
setInterval(() =>
{
// runs every 2 seconds
}, 2000)複製程式碼
上面的函式每隔兩秒執行一次,除非你用 clearInterval
停止它,傳入 setInterval
返回的間隔 id:
const id = setInterval(() =>
{
// runs every 2 seconds
}, 2000)clearInterval(id)複製程式碼
在 setInterval 回撥函式中呼叫 clearInterval
很常見,這讓它自己決定是否需要繼續執行。例子中的程式碼會一直執行,除非 App.somethingIWait 的值等於 arrived
:
const interval = setInterval(() =>
{
if (App.somethingIWait === 'arrived') {
clearInterval(interval) return
} // otherwise do things
}, 100)複製程式碼
遞迴 setTimeout
setInterval
每隔 n 毫秒執行一次函式,不會考慮函式是否執行完畢。如果函式執行都花費相同的時間,這沒有任何問題:
但存在執行時間不一致的可能,比如網路條件導致的:
還有執行時間與下一個重合:
為了避免這個情況,你可以使用遞迴 setTImeout,在回撥函式結束後呼叫:
const myFunction = () =>
{
// do something setTimeout(myFunction, 1000)
}setTimeout( myFunction()
}, 1000)複製程式碼
實現這種情況:
setTimeout
和 setInterval
在 Node.js 的 Timers 模組裡可用。
Node.js 也提供了 setImmediate()
,和使用 setTimeout(() =>
效果一樣,主要用在 Node.js 的事件迴圈。
{
}, 0)
This
this
的值取決於在哪裡使用它。不瞭解這個小細節會讓人頭大,所以花五分鐘時間學習一下它吧。
嚴格模式的 this
嚴格模式,在物件外的 this
始終是 undefined
。
注意我提到了嚴格模式。如果嚴格模式沒有開啟(預設關閉,除非你在檔案頭部顯式新增 use strict
),即草率模式( sloppy mode),沒有特別說明,下面提到的 this
都指向全域性物件。在瀏覽器環境裡是 window
物件。
方法裡的 this
方法是附加在物件裡的函式。
你可以看到不同的格式,比如:
const car = {
maker: 'Ford', model: 'Fiesta', drive() {
console.log(`Driving a ${this.maker
} ${this.model
} car!`)
}
}car.drive()//Driving a Ford Fiesta car!複製程式碼
在這個例子中,使用了常規函式,this
自動繫結了這個物件。
注意:上面的方法宣告和 drive: function() {..
相同,只是更短:
}
const car = {
maker: 'Ford', model: 'Fiesta', drive: function() {
console.log(`Driving a ${this.maker
} ${this.model
} car!`)
}
}複製程式碼
在這個例子裡也一樣:
const car = {
maker: 'Ford', model: 'Fiesta'
}car.drive = function() {
console.log(`Driving a ${this.maker
} ${this.model
} car!`)
}car.drive()//Driving a Ford Fiesta car!複製程式碼
箭頭函式與此不同,因為它屬於詞法繫結:
const car = {
maker: 'Ford', model: 'Fiesta', drive: () =>
{
console.log(`Driving a ${this.maker
} ${this.model
} car!`)
}
}car.drive()//Driving a undefined undefined car!複製程式碼
繫結箭頭函式
你不能像給普通函式那樣給箭頭函式繫結值。這是因為它們的原理不同,this
是詞法繫結,這意味著它的值來自定義它們的上下文。
顯式傳遞 this 指向的物件
JavaScript 提供了一種對映 this
和物件的方法。
在函式宣告階段使用 bind()
:
const car = {
maker: 'Ford', model: 'Fiesta'
}const drive = function() {
console.log(`Driving a ${this.maker
} ${this.model
} car!`)
}.bind(car)drive()//Driving a Ford Fiesta car!複製程式碼
你也可以重新對映一個已經存在的物件作為 this
值:
const car = {
maker: 'Ford', model: 'Fiesta', drive() {
console.log(`Driving a ${this.maker
} ${this.model
} car!`)
}
}const anotherCar = {
maker: 'Audi', model: 'A4'
}car.drive.bind(anotherCar)()//Driving a Audi A4 car!複製程式碼
在函式呼叫階段使用 call()
和 apply()
:
const car = {
maker: 'Ford', model: 'Fiesta'
}const drive = function(kmh) {
console.log(`Driving a ${this.maker
} ${this.model
} car at ${kmh
} km/h!`)
}drive.call(car, 100)//Driving a Ford Fiesta car at 100 km/h!drive.apply(car, [100])//Driving a Ford Fiesta car at 100 km/h!複製程式碼
傳入 call()
或者 apply()
的第一個引數始終繫結 this
。call() 和 apply() 不同之處在於 apply() 傳入的作為函式引數的引數是一個陣列,而 call() 可以接受多個引數。
瀏覽器事件處理中的特殊情況
在事件處理器的回撥中。this
指向收到事件的 HTML 元素:
document.querySelector('#button').addEventListener('click', function(e) {
console.log(this) //HTMLElement
})複製程式碼
你可以這樣繫結:
document.querySelector('#button').addEventListener( 'click', function(e) {
console.log(this) //Window if global, or your context
}.bind(this))複製程式碼
嚴格模式
嚴格模式是 ES5 的功能,它使 JavaScript 表現得更好 – 開啟嚴格模式可以改變 JavaScript 語言的語義。知道嚴格模式和一般模式,也經常被稱作草率模式,在 JavaScript 程式碼的不同十分重要。
嚴格模式主要移除了 ES3 中存在的功能,從 ES5 開始棄用這些功能(考慮到向後相容的需要沒有被刪除)。
如何開啟嚴格模式
嚴格模式是可選的。伴隨著不相容的變化,我們不能簡單的改變語言的預設行為,這會破壞大量的 JavaScript 程式碼,而且 JavaScript 花費了巨大的努力確保 1996 年的程式碼在今天仍然能夠生效。這也是它能成功的關鍵。
所以在我們需要開啟嚴格模式的時候,有了 use strict
指令。你可以把它放在檔案的開頭,把它應用到整個檔案:
'use strict'const name = 'Flavio'const hello = () =>
'hey'//...複製程式碼
你也可以在獨立的函式中啟動嚴格模式,只需要把 use strict
放在函式體開始的位置:
function hello() { 'use strict' return 'hey'
}複製程式碼
這對操作那些你沒有時間測試或者沒有信心在整個檔案開啟嚴格模式的歷史遺留程式碼很有用。
嚴格模式的變化
意外的全域性變數
如果你把值繫結在未宣告的變數上,JavaScript 預設會在全域性物件上建立這個變數:
;
(function() {
variable = 'hey'
})()(() =>
{
name = 'Flavio'
})()variable //'hey'name //'Flavio'複製程式碼
開啟嚴格模式,這樣做就會丟擲錯誤:
;
(function() { 'use strict' variable = 'hey'
})()(() =>
{
'use strict' myname = 'Flavio'
})()複製程式碼
分配錯誤
JavaScript 默默處理了一些轉換錯誤。
在嚴格模式,這些錯誤會被展示出來:
const undefined = 1(() =>
{
'use strict' undefined = 1
})()複製程式碼
Infinity,NaN,eval
,arguments
等等同樣如此。
在 JavaScript 中,你可以定義一個不可寫的物件屬性,比如:
const car = {
}Object.defineProperty(car, 'color', {
value: 'blue', writable: false
})複製程式碼
在嚴格模式下,你不能覆蓋這個值,但在草率模式下可以這麼做:
和 getters 的原理一樣:
複製程式碼
草率模式下可以擴充套件一個不可擴充套件物件:
const car = {
color: 'blue'
}Object.preventExtensions(car)car.model = 'Fiesta'( //ok () =>
{
'use strict' car.owner = 'Flavio' //TypeError: Cannot add property owner, object is not extensible
})()複製程式碼
而且可以設定原始值的屬性,沒有任何錯誤提示,但是也不生效:
true.false = ''( //'' 1).name = 'xxx' //'xxx'var test = 'test' //undefinedtest.testing = true //truetest.testing //undefined複製程式碼
嚴格模式下這些都不被允許:
;
(() =>
{
'use strict' true.false = ''( //TypeError: Cannot create property 'false' on boolean 'true' 1 ).name = 'xxx' //TypeError: Cannot create property 'name' on number '1' 'test'.testing = true //TypeError: Cannot create property 'testing' on string 'test'
})()複製程式碼
刪除錯誤
草率模式下,如果你嘗試刪除不能刪除的屬性值,JavaScript 只會返回 false,但在嚴格模式,它會丟擲 TypeError:
delete Object.prototype( //false () =>
{
'use strict' delete Object.prototype //TypeError: Cannot delete property 'prototype' of function Object() {
[native code]
}
})()複製程式碼
同名函式引數
在一般函式中,可能有衝突的引數名:
(function(a, a, b) {
console.log(a, b)
})(1, 2, 3)//2 3(function(a, a, b) { 'use strict' console.log(a, b)
})(1, 2, 3)//Uncaught SyntaxError: Duplicate parameter name not allowed in this context複製程式碼
在這個例子中,箭頭函式始終丟擲 SyntaxError
:
((a, a, b) =>
{
console.log(a, b)
})(1, 2, 3)//Uncaught SyntaxError: Duplicate parameter name not allowed in this context複製程式碼
八進位制
八進位制語法在嚴格模式下是禁用的。預設情況下,在數字前加上相容八進位制格式的 0
可以把它解釋為八進位制數字(有時看起來很困惑):
(() =>
{
console.log(010)
})()//8(() =>
{
'use strict' console.log(010)
})()//Uncaught SyntaxError: Octal literals are not allowed in strict mode.複製程式碼
你仍然可以通過 0oXX
語法在嚴格模式下使用八進位制數字:
;
(() =>
{
'use strict' console.log(0o10)
})()//8複製程式碼
移除了 with
嚴格模式不能使用 with
關鍵字,移除了一些邊界情況,以使編譯器層面可以更好的優化。
立即執行函式(IIFE)
立即執行函式在它們被建立的時候就會立即執行。
立即執行函式非常有用,因為它們不會汙染全域性變數,而且能夠隔離變數宣告。
立即執行函式的語法是這樣的:
;
(function() {
/* */
})()複製程式碼
立即執行函式也可以使用箭頭函式:
;
(() =>
{
/* */
})()複製程式碼
基本上,我們把函式定義在圓括號內,然後再後面加上一對 ()
執行這個函式:(/* function */)()
。
這些括號實際上是使我們的函式在內部被視為表示式的原因,否則,函式宣告將是無效的,因為我們沒有指定任何名稱:
函式宣告需要一個名字,而函式表示式不需要。
你也可以把呼叫括號放在表示式括號裡面,這是一樣的,僅僅是寫法不同:
(function() {
/* */
}())(() =>
{
/* */
}())複製程式碼
使用一元運算子
在使用 IIFE 時使用任意一元運算子很奇怪,但是在實際運用中卻非常有用:
;
-(function() {
/* */
})()+(function() {
/* */
})()~(function() {
/* */
})()!(function() {
/* */
})()複製程式碼
(箭頭函式上無效)
命名的 IIFE
IIFE 也可以命名常規函式(不是箭頭函式)。這不會導致函式“洩露”到全域性作用域,而且在它執行之後也不能再次呼叫:
;
(function doSomething() {
/* */
})()複製程式碼
IIFE 前的分號
你會看到:
;
(function() {
/* */
})()複製程式碼
這是為了防止把兩個檔案拼合在一起時出現問題。因為 JavaScript 不強制使用分號,你可能會在最後一行中使用某些語句連線一個檔案,從而導致語法錯誤。
這個問題可以通過一種“聰明”的程式碼打包工具解決,比如webpack。
數學運算子
對任何程式語言執行數學運算都是很常見的事情。JavaScript 提供了幾個操作符來幫助我們處理數字。
算術運算子
加(+)
const three = 1 + 2const four = three + 1複製程式碼
+
運算子也會拼接字串,所以注意:
const three = 1 + 2three + 1 // 4'three' + 1 // three1複製程式碼
減(-)
const two = 4 - 2複製程式碼
除(/)
返回第一個數字和第二個數字之間商:
const result = 20 / 5 //result === 4const result = 20 / 7 //result === 2.857142857142857複製程式碼
如果除以 0,JavaScript 不會丟擲任何錯誤,而是返回 Infinity
(如果是負值返回 -Infinity
)。
1 / 0 //Infinity-1 / 0 //-Infinity複製程式碼
取餘(%)
取餘在很多情況下都很有用:
const result = 20 % 5 //result === 0const result = 20 % 7 //result === 6複製程式碼
對 0 取餘始終是 NaN
,意思是“不是一個數字”:
1 % 0 //NaN-1 % 0 //NaN複製程式碼
乘(*)
1 * 2 //2-1 * 2 //-2複製程式碼
求冪(**)
將第一個運算元乘第二個運算元次數:
1 ** 2 //12 ** 1 //22 ** 2 //42 ** 8 //2568 ** 2 //64複製程式碼
一元運算子
遞增(++)
遞增數字。這是一個一元運算子,如果把它放在數字前面,返回自增後的值。如果把它放在數字後面,先返回初始值,然後遞增。
let x = 0x++ //0x //1++x //2複製程式碼
遞減(–)
和遞增操作符相似,不過它遞減值。
let x = 0x-- //0x //-1--x //-2複製程式碼
一元減(-)
返回負值
let x = 2-x //-2x //2複製程式碼
一元加(+)
如果目標不是數字,它會嘗試轉化它。否則什麼也不做。
let x = 2+x //2x = '2'+x //2x = '2a'+x //NaN複製程式碼
快速賦值
常規的賦值操作符 =
對所有的數學運算子都有一個快捷方式讓你能組合賦值,將第一個運算元和第二個運算元的結果賦值給第一個運算元。
它們是:
+=
:加法賦值-=
:除法賦值*=
:乘法賦值/=
:除法賦值%=
:取餘賦值**=
:求冪賦值
例子:
const a = 0a += 5 //a === 5a -= 2 //a === 3a *= 2 //a === 6a /= 2 //a === 3a %= 2 //a === 1複製程式碼
優先順序
每個複雜的語句都會引入優先問題。看這個:
const a = 1 * 2 + 5 / 2 % 2複製程式碼
結果等於 2.5。但是為什麼呢?哪個運算先執行,哪個後執行?
有些運算子的優先順序比其它的高。其優先順序遵循以下規則:
-
+
++
--
一元運算子,遞增,遞減*
/
%
乘法/除法+
-
加法/減法=
+=
-=
*=
/=
%=
**=
賦值運算
同級運算子(比如 +
和 -
)按照順序執行。
根據上面的順序,我們可以計算這個式子:
const a = 1 * 2 + 5 / 2 % 2const a = 1 * 2 + 5 / 2 % 2const a = 2 + 2.5 % 2const a = 2 + 0.5const a = 2.5複製程式碼
Math 物件
Math 物件包含很多數學相關的工具。讓我們看看都有什麼。
常量
函式
所有函式方法都是靜態的,Math 不能被繼承。
Math.abs()
返回數字的絕對值
Math.abs(2.5) //2.5Math.abs(-2.5) //2.5複製程式碼
Math.acos()
返回反餘弦值,引數必須在 -1 到 1 之間。
Math.acos(0.8) //0.6435011087932843複製程式碼
Math.asin()
返回反正弦值,引數必須在 -1 到 1 之間。
Math.asin(0.8) //0.9272952180016123複製程式碼
Math.atan()
返回反正切值
Math.atan(30) //1.5374753309166493複製程式碼
Math.atan2()
返回其引數商的反正切值
Math.atan2(30, 20) //0.982793723247329複製程式碼
Math.ceil()
向上取整
Math.ceil(2.5) //3Math.ceil(2) //2Math.ceil(2.1) //3Math.ceil(2.99999) //3複製程式碼
Math.cos()
用弧度表示角度的餘弦值
Math.cos(0) //1Math.cos(Math.PI) //-1複製程式碼
Math.exp()
返回 Math.E 的引數次方
Math.exp(1) //2.718281828459045Math.exp(2) //7.38905609893065Math.exp(5) //148.4131591025766複製程式碼
Math.floor()
向下取整
Math.ceil(2.5) //2Math.ceil(2) //2Math.ceil(2.1) //2Math.ceil(2.99999) //2複製程式碼
Math.log()
返回基數 e 的自然對數
Math.log(10) //2.302585092994046Math.log(Math.E) //1複製程式碼
Math.max()
返回傳入的一系列數字中的最大值
Math.max(1,2,3,4,5) //5Math.max(1) //1複製程式碼
Math.min()
返回傳入的一系列數字中的最小值
Math.max(1,2,3,4,5) //1Math.max(1) //1複製程式碼
Math.pow()
返回第一個引數的第二引數次方
Math.pow(1, 2) //1Math.pow(2, 1) //2Math.pow(2, 2) //4Math.pow(2, 4) //16複製程式碼
Math.random()
返回 0.0 到 1.0 之間的偽隨機值
Math.random() //0.9318168241227056Math.random() //0.35268950194094395複製程式碼
Math.round()
四捨五入
Math.round(1.2) //1Math.round(1.6) //2複製程式碼
Math.sin()
用弧度計算角度的正弦值
Math.sin(0) //0Math.sin(Math.PI) //1.2246467991473532e-16)複製程式碼
Math.sqrt()
開方
Math.sqrt(4) //2Math.sqrt(16) //4Math.sqrt(5) //2.23606797749979複製程式碼
Math.tan()
用弧度計算角度的正切值
Math.tan(0) //0Math.tan(Math.PI) //-1.2246467991473532e-16複製程式碼
ES 模組
ES 模組是用於處理模組的 ECMAScript 標準。Node.js 長期以來一直使用 CommonJS,然而瀏覽器還沒有模組系統。每個主要決策(如模組系統)必須首先先由 ECMAScript 標準化,然後由瀏覽器實現。
這個標準化過程在 ES6 中完成,與此同時,瀏覽器開始逐步實現這個標準,盡力使工作方式保持一致。現在 ES 模組被 Chrome,Safari,Edge 和 Firefox(從 60 版本開始)支援。
模組很酷,它們允許你封裝任意功能,然後作為庫暴露給其它 JavaScript 檔案。
ES 模組語法
匯入一個模組可以用:
import package from 'module-name'複製程式碼
然而 CommonJS 使用:
const package = require('module-name')複製程式碼
一個模組是一個通過 export
匯出一個或多個值(物件,函式或變數)的 JavaScript 檔案。舉個例子,這個模組匯出了一個大寫字串的函式:
uppercase.js
export default str =>
str.toUpperCase()複製程式碼
在這個例子中,模組定義了一個唯一的,預設匯出(default export),所以它可以是一個匿名函式。否則它需要一個名字來區分其它的匯出。現在,其它任何的 JavaScript 模組都可以通過匯入 uppercase.js 匯入這個函式。
一個 HTML 頁面可以通過 <
標籤上特殊的
script>type="module"
屬性新增一個模組:
<
script type="module" src="index.js">
<
/script>
複製程式碼
注意:模組匯入行為和
defer
指令碼載入類似。檢視使用延遲和非同步高效載入 JavaScript。
重要的是任何有 type="module"
的模組都是以嚴格模式載入的。
在這個例子中,uppercase.js
模組定義了一個預設匯出,所以我們可以匯入這個模組,還能夠給它繫結一個我們喜歡的名字:
import toUpperCase from './uppercase.js'複製程式碼
然後我們可以使用它:
toUpperCase('test') //'TEST'複製程式碼
你也可以使用絕對路徑匯入模組,以便引用其它域名中定義的模組:
import toUpperCase from 'https://flavio-es-modules-example.glitch.me/uppercase.js'複製程式碼
這種語法也是有效的:
import {
foo
} from '/uppercase.js'import {
foo
} from '../uppercase.js'複製程式碼
這種不行:
import {
foo
} from 'uppercase.js'import {
foo
} from 'utils/uppercase.js'複製程式碼
路徑必須是絕對路徑,或在名字前加上 ./
或者 /
。
其它匯入/匯出方法
我們已經知道這個例子:
export default str =>
str.toUpperCase()複製程式碼
這建立了一個預設的匯出。然而在一個檔案中你可以匯出多個內容,比如:
const a = 1const b = 2const c = 3export {
a, b, c
}複製程式碼
另一個模組可以匯入所有:
import * from 'module'複製程式碼
你可以只選擇匯入部分內容,使用解構繫結:
import {
a
} from 'module'import {
a, b
} from 'module'複製程式碼
你也可以匯入預設匯出,然後通過名字匯入沒有預設匯出的內容,就像常見的匯入 React:
import React, {
Component
} from 'react'複製程式碼
在這裡檢視 ES 模組的示例。
跨域資源共享 CORS
通過 CORS 獲取模組,意味著如果你從其它域名引入指令碼,他們必須有一個有效的 CORS 頭允許跨網頁載入(像 Access-Control-Allow-Origin
)。
不支援模組的瀏覽器怎麼辦?
結合使用 type=module
和 nomodule
:
<
script type="module" src="module.js">
<
/script>
<
script nomodule src="fallback.js">
<
/script>
複製程式碼
總結
ES 模組是現代瀏覽器引入的最重要的功能。它們是 ES6 的一部分,但實現它們的過程很漫長。
現在我們可以使用它們!但是我們也必須知道多個模組會影響頁面的效能,因為這是瀏覽器必須執行的一步。
即使 ES 模組已經在瀏覽器端登陸,Webpack 也將扮演重要的角色,但是直接用語言構建這樣的功能對於統一模組在客戶端和 Node.js 上的工作方式來說任務繁重。
CommonJS
CommonJS 模組規範是 Node.js 中的模組標準。
瀏覽器端的 JavaScript 採用 ES 模組。
它們讓你能夠建立易於分割且可複用的程式碼片段,每個片段都可以獨立測試。
龐大的 npm 生態環境就建立在 CommonJS 格式之上。
匯入模組的語法如下:
const package = require('module-name')複製程式碼
在 CommonJS 中,模組同步載入,按照 JavaScript 執行時找到的順序執行。這個系統原本為伺服器端 JavaScript 而生,不相容客戶端(這也是為什麼引入 ES 模組)。
JavaScript 檔案是匯出一個或多個其中定義的符號的模組,它們可以是變數,函式,物件:
uppercase.js
exports.uppercase = str =>
str.toUpperCase()複製程式碼
任何 JavaScript 檔案都可以匯入並且使用這個模組:
const uppercaseModule = require('uppercase.js')uppercaseModule.uppercase('test')複製程式碼
在 Glitch 上的簡單例子。
你可以匯出多個值:
exports.a = 1exports.b = 2exports.c = 3複製程式碼
通過解構賦值單獨匯入每一個值:
const {
a, b, c
} = require('./uppercase.js')複製程式碼
或者只匯出一個值:
//file.jsmodule.exports = value複製程式碼
然後匯入:
const value = require('./file.js')複製程式碼
詞彙表
最後,一些前端開發的術語可能和你理解的不同。
非同步
非同步程式碼是當你啟動某個內容,可以先忘掉它,無需刻意等待,當結果準備就緒你會自動得到它。典型的例子就是 AJAX 呼叫,可能會佔用很多秒,與此同時你可以完成其它事情,當獲取響應,回撥函式會被呼叫。Promises 和 async/await 是處理非同步程式碼更現代的方法。
塊級作用域
塊級作用域裡任何定義在塊內的變數對整個塊可見,可以在內部訪問,不能在塊外部訪問。
回撥
回撥是一個事件發生時呼叫的函式。元素繫結的點選事件在使用者點選了元素時呼叫。一個 fetch 請求的回撥在資源下載完成後呼叫。
宣告
宣告方法是你說明細節,告訴機器你需要做什麼。React 被認為是宣告式的,理由是更注重抽象推理而不是直接編輯 DOM。每個高階程式語言都比低階程式語言,如 Assembler,更具宣告性。JavaScript 比 C 語言更加宣告性,HTML 也是宣告性的。
回退
回退用來在使用者無法訪問特定功能時提供更好的體驗。比如瀏覽器關閉了 JavaScript,使用者應該得到一個原生版本的 HTML 頁面。或者遇到瀏覽器沒有實現的 API,你應該有一個可靠的辦法避免嚴重影響使用者體驗。
函式作用域
函式作用域裡任何定義在函式內的變數對整個函式可見,可以在函式內部訪問,不能在函式外部訪問。
一致性
一個變數的值在建立之後不能改變,我們就稱這變數是不變的。可變變數是可變的。這在陣列和物件裡都一樣。
詞彙範圍
詞彙範圍是一個特定作用域,父函式的變數可以在內部函式使用。內部函式的作用域也包括父函式的作用域。
Polyfill
Polyfill 用來為舊瀏覽器提供它沒有原生支援的現代瀏覽器具有的新功能。Polyfill 是一種特殊的 shim。
純粹函式(pure function)
純粹函式是一個沒有副作用(不會修改外部資源),而且引數決定輸出的函式。你可以呼叫函式一百萬次,每次都用相同的引數,輸出始終保持一致。
重新賦值
var
和 let
允許你無數次地為變數重新賦值。const
宣告瞭一個不可變的字串,整型,布林值,物件(但是你仍然可以通過提供的方法修改它)。
範圍
範圍是一系列變數對於程式可見的部分。
作用域
作用域是程式語言裡定義的決定變數值的一系列規則。
Shim
Shim 包含了許多功能或者 API。它通常用於抽象內容,預填充引數或為不支援某些功能的瀏覽器新增 polyfill。你可以把它看作相容層。
副作用
副作用是一個函式與其它函式或者物件進行互動。與網路、檔案系統或者是 UI 互動都有副作用。
狀態(state)
談到元件時,不得不提到狀態。一個元件管理資料是有狀態的,否則是無狀態的。
有狀態(stateful)
一個有狀態的元件,函式或者類會自己管理自己的狀態(資料)。它可以儲存一個陣列,一個計時器或者其它的東西。
無狀態(stateless)
一個無狀態的元件,函式或者類也被稱作 dumb,因為它無法使用自己的資料做出決定,所以它的輸出或者展示完成基於它的引數。這意味著純粹函式無狀態的。
嚴格模式
嚴格模式是 ECMAScipt 5.1 的新功能,它會導致 JavaScript 執行時捕捉更多的錯誤,但是它可以通過拒絕未宣告的變數和衝突的物件屬性等其他容易被忽視的問題來幫助你改進 JavaScript 程式碼。建議:使用嚴格模式。另一個“草率模式”看名字就知道不是什麼好東西。
Tree Shaking
Tree Shaking 意味著從你打包傳送給使用者的程式碼中刪除“死程式碼”。如果你在很重要語句中新增了從來不會用到的程式碼,它不會傳送給你的應用使用者,以此減少檔案體積和載入時間。
感謝閱讀!
注意:你可以獲取這篇 JavaScript 指南的 PDF, ePub, Mobi 版本以便在 Kindle 或平板上閱讀。
感謝 xbears 提供的 kindle 電子書格式(中文),提取碼: 2jh9
百度盤:MD 提取碼: hzav