在以前某次尤大大作直播的講演中,回答了哪些前端書籍是值得被閱讀的,其中一本即是《Effective JavaScript》,因而開始閱讀學習,以自身閱讀和理解,着重記錄內容精華部分以及對內容進行排版,便於往後自身回顧學習以及你們交流學習。前端
因內容居多,分爲每一個章節來進行編寫文章,每章節的準條多少不一,故每篇學習筆記的文章以章節爲準。程序員
適合碎片化閱讀,精簡閱讀的小友們。爭取讓小友們看完系列 === 看整本書的 85+%。正則表達式
澄清一下 JavaScript 和 ECMAScript 的術語。總所周知,當人們提起 ECMAScript時,一般是指由 Ecma 國際標準化組織制定的 「理想語言」。算法
而 JavaScript 這個名字意味着來自語言自己的全部事物,例如某個供應商特定的 JavaScript 引擎、DOM、BOM 等。編程
爲了保持清晰度和一致性,在本書中,我將只使用 ECMAScript 來談論官方標準,其餘狀況,將使用 JavaScript 指代語言。數組
避開 Web 來談 JavaScript 是很難的,不過本書是關於 JavaScript 而非 Web 的編程,故本書重點是 JavaScript 語言的語法、語義和語用,而不是 Web 平臺的 API 和技術。瀏覽器
JavaScript 一個新奇的方面是在併發環境中其行爲是徹底不明朗的。所以,只是從技術角度介紹一些非官方的 JavaScript 特性,但實際上,全部主流的 JavaScript 引擎都有一個共同的併發模型。安全
將來版本的 ECMAScript 標準可能會正式標準化這些 JavaScript 併發模型的共享方面markdown
JavaScript 語言提供爲數很少的核心概念,所以顯得如此的平易近人,可是精通這門語言須要更多的時間,須要更深刻地理解它的語義、特性以及最有效的習慣用法。網絡
本書每一個章節都涵蓋了高效 JavaScript 編程的不一樣主題。第 1 章主要講述一些最基本的主題
因爲 JavaScript 歷史悠久且實現多樣化,所以咱們很難肯定哪些特性在哪些平臺上是可用的。而 Web 瀏覽器,它並不支持讓程序員指定某個 JavaScript 的版原本執行代碼,最終用戶可能使用不一樣 Web 瀏覽器的不一樣版本。
好比應用程序在本身的計算機活着測試環境上運行良好,但部署到不一樣的產品環境中時卻沒法運行。例如 const 關鍵字在支持非標準特性的 JavaScript 引擎上測試時運行良好,但將部署到不識別 const 關鍵字的 Web 瀏覽器就會出現語法錯誤等等。
ES5 引入另外一種版本控制的考量 —— 嚴格模式。此特性容許選擇在受限制的 JavaScript 版本中禁止使用一些 JavaScript 語言中問題較多或易於出錯的特性。在程序中啓用嚴格模式的方式是在程序的最開始增長一個特定的字符串字面量 "use strict"
"use strict"
指令只有在腳本或函數的頂部才能生效,若在開發中使用多個獨立的文件,而一個文件是嚴格模式下,另外一個是非嚴格模式下,部署到產品環境時卻須要鏈接成一個單一文件。
// file1.js
"use strict"
function f() {
// ...
}
// file2.js
function g() {
var grauments = []
}
複製代碼
若是以 file1.js 文件開始,那麼鏈接後的代碼運行於嚴格模式下
// file1.js
"use strict"
function f() {
//...
}
// file2.js
function g() {
var arguments = [] // error: redefinition of arguments
}
複製代碼
若是以 file2.js 文件開始,那麼鏈接後的代碼運行於非嚴格模式下
// file2.js
function g() {
var arguments = []
}
// file1.js
"use strict"
function f() {
// ...
}
複製代碼
在本身的項目中能夠堅持只使用 「嚴格模式」 或只使用 「非嚴格模式」 的策略,但若是你要編寫健壯的代碼應對各類各樣的代碼連接,如下有兩個可選方案。
第一個解決方案是不要將進行嚴格模式檢查的文件和不進行嚴格模式檢查的文件鏈接起來。
第二個解決方案是經過將其自身包裹在當即調用的函數表達式中的方式鏈接多個文件。(第 13 條將對當即調用的函數表達式進行深刻的講解)
將每一個文件的內容包裹在一個當即調用的函數中,即便在不一樣的模式下,它們都將被獨立地解決實行,例子以下:
// 當即調用的函數表達式中
(function () {
// file1.js
"use strict";
function f() {
// ...
}
// ...
})()
(function () {
// file2.js
function g() {
var arguments = []
}
})()
複製代碼
因爲每一個文件的內容被放置在一個單獨的做用域中,因此用不用嚴格模式指令隻影響本文件的內容。
但這種方式會致使這些文件的內容不會在全局做用域內解釋。
所以若是爲了達到更爲廣泛的兼容性,爲將來新版本的 Javascript 更好地作鋪墊,以及消除代碼運行的一些不安全之處,保證代碼運行的安全等等。建議在必要時刻使用嚴格模式下編寫代碼
JavaScript 只有一種數字類型 Number
。
typeof 17; // "number"
typeof 98.6; // "number"
typeof -2.1; // "number"
複製代碼
事實上,JavaScript 中全部的數字都是雙精度浮點數,它能完美地表示高達 53 位精度的整數(JavaScript 正是如此隱式轉換爲整數)。所以,儘管 JavaScript 中缺乏明顯的整數類型,可是徹底能夠進行整數運算。
0.1 * 1.9 // 0.19
-99 + 100 // 1
21 - 12.3 // 8.7
2.5 / 5 // 0.5
21 % 8 // 5
複製代碼
在進行位算術運算符時,JavaScript 不會直接將操做數做爲浮點數進行運算,而是會將其隱式轉換爲 32 位整數後進行運算。
8 | 1 // 9
複製代碼
以上表達式進行的實際步驟爲
(8).toString(2) // "1000"
方法進行查看JavaScript 中數字是以浮點數存儲的,必須將其轉換爲整數,而後再轉換回浮點數。然而某些狀況下,算術表達式甚至變量只能使用整數參與運算,優化編譯器有時候能夠推斷出這些情形而在內部將數字以整數的鵝方式存儲以免多餘的轉換。
雙精度浮點數也只能表示一組有限的數字,當執行一系列的運算,隨着舍入偏差的積累,運算結果會愈來愈不精確。
例如,實數知足結合律,這意味着,對於任意的實數 x, y, z,老是知足(x+y)+z = x+(y+z)
。然而對於浮點數來講,卻不老是這樣:
(0.1 + 0.2) + 0.3 // 0.6000000 000000001
0.1 + (0.2 + 0.3) // 0.6
複製代碼
-2^53 ~ 2^53
我的的解決方案
(num1 * baseNum + num2 * baseNum) / baseNum;)
JavaScript 對類型錯誤出奇寬容,在靜態類型語言中,含有不一樣類型運算的表達式 3 + true;
是不會被容許運行。然而 JavaScript 卻會順利地產生結果 4.
在 JavaScript 中也有極少數的狀況,提供錯誤的類型會產生一個即時錯誤。
// 調用一個非函數對象
"hello"(1) // error: not a function
// 試圖選擇 null 屬性
null.x; // error: cannot rend property 'x' of null
複製代碼
算術運算符 -
、*
、/
和 %
在計算以前都會嘗試將其參數轉換爲數字。如運算符 +
既重載了數字相加、又重載了字符串鏈接操做。
2 + 3; // 5
"hello" + "world"; // "hello world"
複製代碼
因爲加法運算是自左結合(即左結合律),所以有以下等式。
1 + 2 + "3" // "33"
// 等於
(1 + 2) + "3" // "33"
1 + "2" + 3 // "123"
// 等於
(1 + "2") + 3 // "123"
複製代碼
位運算符不只會將操做數轉換爲數字,並且還會將操做數轉換爲 32 位整數。
~
、&
、^
和 |
<<
、>>
、>>>
和 <<<
這些強制轉換十分方便。例如,來自用戶輸入、文本文件或者網絡流的字符串都將被自動轉換。
"17" * 3; // 51
"8" | "1"; // 9
複製代碼
強制轉換也會隱藏錯誤。結果爲 null
的變量在算術運算中不會致使失敗,而是被隱式地轉換爲 0.
一個未定義的變量將被轉換爲特殊的浮點數值 NaN
,這些強制轉換不是當即拋出一個異常,而是繼續運算,每每致使一些不可預測的結果。而測試 NaN 值也是異常困難,由於兩個緣由
JavaScript 遵循了 IEEE 浮點數標準使人頭疼的要求 - NaN 不等於其自己
const x = NaN
x === NaN // false
複製代碼
標準庫函數 isNaN
也不是很可靠。
isNaN(NaN) // true
isNaN("foo") // true
isNaN(undefined) // true
isNaN({}) // true
isNaN({ valueOd: "foo" }) // true
複製代碼
幸運的是有一個既簡單又可靠的習慣用法來測試 NaN
function isReallyNaN(x) {
return x !== x
}
複製代碼
對象經過隱式地調用其自身的 toString
方法轉換爲字符串。
Match.toString() // "[object Math]"
JSON.toString() // "[object JSON]"
複製代碼
相似地,對象也能夠經過其 valueOf
方法轉換爲數字。經過其方法能夠來控制對象的類型轉換。
"J" + { toString: function() { return "S" } } // JS
2 * { valueOf: function() { return 3 } } // 6
複製代碼
當一個對象同時包含 toString
和 valueOf
方法時,運算符 +
應該調用哪一個方法並不明顯。所以,JavaScript經過盲目地選擇 valueOf
方法而不是 toString
方法來解決這種含糊地狀況。
const obj = {
toString: function() {
return "[object MyObject]"
},
valueOf: function() {
return 17
}
}
"object:" + obj // "object: 17"
複製代碼
所以,無論是對象的的鏈接仍是對象的相加,重載的運算符 +
老是一致的行爲 - 相同數字的字符串或數值表示。
通常狀況下,字符串的強制轉換遠比數字的強制轉換更常見、更有用。最好避免使用 valueOf
方法,除非對象的確是一個數字的抽象,而且 obj.toString() 能產生一個 obj.valueOf() 的字符串表示。
真值運算 if
、||
和 &&
等運算符邏輯上須要布爾值做爲操做參數,但之際上能夠接受任何值。
JavaScript 中有 7 個假值:false
、0
、-0
、NaN
、""
、null
和 undefined
,其餘全部的值都爲真值。
檢查參數是否爲 undefined
更爲嚴格的方式是使用 typeof
function point(x, y) {
if (typeof x === "undefined") {
x = 320
}
if (typeof y === "undefined") {
y = 240
}
return {x: x, y: y}
}
// 此方法能夠判斷比較 0 和 undefind
point() // {x: 320, y: 240}
point(0, 0) // {x: 0, y: 0}
複製代碼
另外一種方法是與 undefined 直接比較 if (x === undefined) {...}
+
是進行加法運算仍是字符串鏈接操做取決於其參數類型。valueOf
方法的對象應該實現 toString
方法,返回一個 valueOf
方法產生的數字的字符串表示。typeof
或者與 undefined
進行比價而不是使用真實運算。JavaScript 標準庫中提供了構造函數來封裝原始值的類型。如能夠建立一個 String 對象,該對象封裝了一個字符串值。
const s = new String("hello")
// 也能夠將其與另外一個值鏈接建立字符串
s + "world" // "hello world"
複製代碼
可是不一樣於原始的字符串,String 對象是一個真正的對象
typeof "hello" // "string"
typeof s // "object"
複製代碼
這意味着不能使用內置的操做符來比較兩個大相徑庭的 String 對象的內容,每一個 String 對象都是一個單獨的對象,不論內容是否一致,其老是隻等於自身。
const s1 = new String("hello")
const s2 = new String("hello")
s1 == s2 // false
s1 === s2 // false
複製代碼
由於原始值不是一個對象,因此不能對原始值設置屬性,但能對封裝對象設置屬性。
const sObj = new String("hello")
const s = "hello"
sObj.prop = "world"
s.prop = "world"
sObj.prop // "world"
s.prop // undefined
複製代碼
封裝對象存在的理由,也就是它們的做用是構造函數上的實用方法。
而當咱們對原始值提取屬性或進行方法調用時,JavaScript 會內置隱式轉換爲對應的對象類型封裝。例如,String 的原型對象有一個 toUpperCase
方法,能夠將字符串轉換爲大寫,那麼能夠對原始字符串調用這個方法。
"hello".toUpperCase() // "HELLO"
複製代碼
每次隱式封裝都會產生一個新的 String 對象,更新第一個封裝對象並不會形成持久的影響。
這也常常形成錯誤給一個原始值設置屬性,而程序默認行爲,致使一些難以發現的錯誤並難以診斷。
先看一個例子,你認爲返回的結果是什麼?
"1.0e0" == { valueOf: function() { return true } }
複製代碼
像第 3 條描述的隱式強制轉換同樣,在比較以前它們都會被轉換爲數字。最終結果與 1 == 1
是等價的
所以,咱們很容易使用這些強制轉換完成一些工做。例如,從一個 Web 表單讀取一個字段並與一個數字進行比較
const today = new Date()
if (form.month.value == (today.getMonth() + 1) && form.day.value == today.getDate()) {
// ...
}
// 是與下列隱式轉換爲數字等價的
const today = new Date()
if (+form.month.value == (today.getMonth() + 1) && +form.day.value == today.getDate()) {
// ...
}
複製代碼
當兩個屬性屬於同一類型時,==
和 ===
運算符的行爲是沒有區別的。但最好使用嚴格相等運算符,來準確比較數據的內容和類型,而非僅僅看數據的內容。
== 運算符強制轉換的規則並不明顯,但這些規則具備對稱性。
轉換規則一般都試圖產生數字,但它們處理對象時會變得難以捉摸。會將對象試圖轉換爲原始值來進行判斷,能夠經過調用對象的 valueOf
和 toString
方法而實現。而使人值得注意的是,Date
對象以相反的順序嘗試調用這兩個方法。
咱們在第 3 條提到了,JavaScript 默認先調用 valueOf 再調用 toString 來轉換爲原始值
參數類型1 | 參數類型2 | 強制轉換 |
---|---|---|
null | undefined | 不轉換,老是返回 true |
null 或 undefined | 其餘任何非 null 或 undefined 的類型 | 不轉換,老是返回 false |
原始類型:string、number、boolean 或 Symbol | Date 對象 | 將原始類型轉換爲數字;將 Date 對象轉換爲原始類型(優先嚐試 toString 方法,再嘗試 toString 方法) |
原始類型:string、number、boolean 或 Symbol | 非 Date 對象 | 將原始類型轉換爲數字;將非 Date 對象轉換爲原始類型(優先嚐試 valueOf 方法,再嘗試 toString 方法) |
原始類型:string、number、boolean 或 Symbol | 原始類型:string、number 或 boolean | 將原始類型轉換爲數字 |
JavaScript 的自動分號插入技術是一種程序解析技術。能推斷出某些上下文中省略的分號,而後有效地自動地將分號「插入」到程序中,ECMAScript 標準也指定了分號機制,所以可選分號能夠在不一樣的 JavaScript 引擎之間移植。
分號插入在解析時有其陷阱,JavaScript 語法對其也有額外的限制。所以咱們需瞭解學會分號插入的三條規則,便能從刪除沒必要要的分號痛苦中解脫出來。
分號僅在 } 標記以前、一個或多個換行以後和程序輸入的結尾被插入。
也就是說,只能在一個代碼塊、一行或一段程序結束的地方省略分號,不能在連續的語句中省略分號。
function area(r) { r = +r; return Math.PI * r * r }
複製代碼
function area(r) { r = +r return Match.PI * r * r } // error
複製代碼
分號僅在隨後的輸入標記不能解析時插入。
也就是說,分號插入是一種錯誤矯正機制。咱們老是要注意下一條語句的開始,從而發現可否合法地省略分號。
有 5 個明確有問題的字符須要密切注意:(
、[
、+
、-
和 /
。這些依賴於具體上下文,且都能做爲一個表達運算符或上一條語句的前綴。以下例子:
()
a = b
(f());
// 等價於
a = b(f());
// 將被解析爲兩條獨立語句
a = b
f()
複製代碼
[]
a = b
["r", "g", "b"].forEach(function (key) {
background[key] = foreground[key] / 2;
})
// 等價於
a = b["r", "g", "b"].forEach(function (key) {
background[key] = foreground[key] / 2;
})
// 將被解析爲兩條獨立語句,一條賦值,一條數組 forEach 方法
a = b
["r", "g", "b"].forEach(function (key) {
background[key] = foreground[key] / 2;
})
複製代碼
+
和 -
a = b
+c;
// 等價於
a = b + c;
// 將被解析爲兩條獨立語句,一條賦值,一條轉爲正整數。
a = b
+c
// - 如上
複製代碼
/
:有特殊意義於正則表達式標記的開始字符
a = b
/Error/ i.test(str) && fail();
// 等價於
a = b / Error / i.test(str) && fail(); // '/' 將會被解析爲除法運算符
// 將被解析爲兩條獨立語句
a = b
/Error/ i.test(str) && fail()
複製代碼
省略分號可能致使腳本鏈接問題,若每一個文件可能由大量的函數調用表達式組成。當每一個文件做爲一個單獨的程序加載時,分號能自動地插入到末尾,將函數調用轉變爲一條語句。
// file1.js
(function() {
// ...
})()
// file2.js
(function() {
// ...
})()
複製代碼
但當咱們使用多個文件做爲程序加載文件時,若咱們省略了分號,結果將被解析爲一條單獨的語句。
(function() {
// ...
})()(function() {
// ...
})()
複製代碼
咱們能夠防護性地在每一個文件前綴一個額外的分號以保護腳本免受粗心鏈接的影響。也就是說,若是文件最開始的語句以上述全部 5 個字符問題開頭,則需作出如下解決方法。
// file1.js
;(function() {
// ...
})()
// file2.js
;(function() {
// ...
})()
複製代碼
總的以上來講,省略語句的分號不只須要小心當前文件的下一個標記(字符問題),並且還須要小心腳本鏈接後可能出現語句以後的任一標記。
JavaScript 語法限制產生式**不容許在兩個字符之間出現換行,所以會強制地插入分號。**以下例子:
return
{ };
// 將被解析爲 3 條單獨的語句。
return;
{ };
;
複製代碼
換句話說,return
關鍵字後的換行會強制自動地插入分號,該代碼例子被解析爲不帶參數的 return 語句,後接一個空的代碼塊和一個空語句。
除了
return
的語法限制生成式,還有如下其餘的 JavaScript 語句限制生產式。
throw
語句- 帶有顯示標籤的
break
或continue
語句- 後置自增或自減運算符
分號不會做爲分隔符在
for
循環空語句的頭部或空循環體的while
循環中被自動插入。
意味着你須要在 for
循環頭部顯示地包含分號。
// 在 for 循環頭部中,以換行代替分號,將致使解析錯誤。
for (let i = 0, total = 1 // parse error
i < n
i++) {
total *= 1
}
複製代碼
在空循環體的 while
循環一樣也須要顯示分號。
function infiniteLoop() { while (true) } // parse error
function infiniteLoop() { while (true); } // 正確
複製代碼
(
、[
、+
、-
或 /
字符開頭的語句前毫不能省略分號。return
、throw
、break
、continue
、++
或 --
地參數以前覺不能換行。Unicode 概念是爲世界上全部的文字系統的每一個字符單位分配了一個惟一的整數,該整數介於 0 和 1114 111 之間,在 Unicode 術語中稱爲代碼點
。
Unicode 與其餘字符編碼幾乎沒有任何不一樣(例如,ASCII)。但不一樣點是,ASCII 將每一個索引映射爲惟一的二進制表示,Unicode 容許多個不一樣二進制編碼的代碼點。不一樣的編碼在存儲的字符串數量和操做速度之間進行權衡(也就是時間與空間的權衡)。目前由多種 Unicode 的編碼標準,最流行的幾個是:UTF-8
、UTF-16
和 UTF-32
。
Unicode 的設計師根據歷史的數據,錯誤估算了代碼點的容量範圍。起初產生了 UCS-2
其爲 16 位代碼的原始標準,也就是 Unicode 具備 2^16 個代碼點。因爲每一個代碼點能夠容納一個 16 位的數字,當代碼點與其編碼元素一對一地映射起來,這稱爲一個代碼單元。
其結果是當時許多平臺都採用 16 位編碼的字符串。如 Java,而 JavaScript 也緊隨其後,因此 JavaScript 字符串的每一個元素都是一個 16 位的值。
現在 Unicode 也擴大其最初的範圍,標準從當時的 2^16 擴展到了超過 2^20 的代碼點,新增長的範圍被組織爲 17 個大小爲 2^16 代碼點的字範圍。第一個子範圍稱爲基本多文種平面,包含最初的 2^16 個代碼點,餘下的 16 個範圍稱爲輔助平面。
![image-20210621102240846](/Users/Mr-luo/Library/Application Support/typora-user-images/image-20210621102240846.png)
因代碼點的範圍擴展,UCS-2 就變的過期,所以UTF-16
採用代理對錶示附加的代碼點,一對 16 位的代碼單元共同編碼一個等於或大於 2^16 的代碼點。
例如分配給高音譜號的音樂符號 𝄞
的代碼點爲 U+1D11E(代碼點數 119 070 的 Unicode 的慣用 16 進制寫法),UTF-16 經過合併兩個代碼單元 0xd834
和 0xddle
選擇的位來對這個代碼點進行解碼。
"𝄞".charCodeAt(0); // 56606(0xd834)
"𝄞".charCodeAt(1); // 56606(0xdd1e)
'\ud834\udd1e' // "𝄞"
複製代碼
JavaScript 已經採用了 16 位的字符串元素,字符串屬性和方法(如 length、charAt 和 charCodeAt)都是基於代碼單元層級,而不是代碼點層級。因此簡單來講,一個 JavaScript 字符串的元素是一個 16 位的代碼單元。
![image-20210621124352078](/Users/Mr-luo/Library/Application Support/typora-user-images/image-20210621124352078.png)
JavaScript 引擎能夠在內部優化字符串內容的存儲,但考慮到字符串的屬性和方法,字符串表現得像 UTF-16 的代碼單元序列。
也就是說雖然事實上
𝄞
只有一個代碼點,但由於是基於代碼單元層級,故.length
顯示爲代碼單元的個數 2。
"𝄞".length // 2
"a".length // 1
複製代碼
提取該字符串的某個字符的方法 `` 獲得的是代碼單元,而不是代碼點。
"𝄞 ".charCodeAt(0); // 56606(0xd834)
"𝄞 ".charCodeAt(1); // 56606(0xdd1e)
"𝄞 ".charAt(1) === " " // false,表示第二個代碼單元不是空字符
"𝄞 ".charAt(2) === " " // true
'\ud834\udd1e' // "𝄞"
複製代碼
正則表達式也工做於代碼單元層級,因單字符模式 .
匹配一個單一的代碼單元。
/^.$/.test("𝄞"); // false
/^..$/.test("𝄞") // true
複製代碼
這意味着若是需操做代碼點,應用程序不能信賴字符串方法、長度值、索引查找或者許多正則表達模式。若是但願使用除 BMP 以外的代碼點,那麼求助於一些支持代碼點的庫是個好主意。
雖然 JavaScript 內置的字符串數據類型工做於代碼單元層級,但這並不能阻止一些 API 意識到代碼點和代理對。例如 URI 操做函數:sendcodeURI
、decodeURI
、encodeURIComponent
和 decodeURIComponent
。
故每當一個 JavaScript 環境提供一個庫操做字符串(例如操做一個 Web 頁面的內容或者執行關於字符串的 I/O 操做),你都須要查閱這些庫文檔,看它們如何處理 Unicode 代碼點的整個範圍。
字符串元素計數
,length
、charAt
、charCodeAt
方法以及正則表達式模式(例如 .
)受到了影響。以上爲 第一章內容 學習了 1~7 條規則 着重於熟悉 JavaScript,瞭解 JavaScript 中的原始類型、隱式強制轉換、編碼類型等幾本概念;
系列以下:
- 《Effective JS》的 68 條準則「一至七條」
- 《Effective JS》的 68 條準則「八至十七條」
- 《Effective JS》的 68 條準則「十八至二十九條」
- 《Effective JS》的 68 條準則「三十至四十二條」
- 《Effective JS》的 68 條準則「四十三至五十二條」
- 《Effective JS》的 68 條準則「五十三至六十條」
- 《Effective JS》的 68 條準則「六十一至六十八條」
若無連接,則是正在學習當中...