《Effective JS》的 68 條準則「一至七條」

原由

在以前某次尤大大作直播的講演中,回答了哪些前端書籍是值得被閱讀的,其中一本即是《Effective JavaScript》,因而開始閱讀學習,以自身閱讀和理解,着重記錄內容精華部分以及對內容進行排版,便於往後自身回顧學習以及你們交流學習。前端

因內容居多,分爲每一個章節來進行編寫文章,每章節的準條多少不一,故每篇學習筆記的文章以章節爲準。程序員

適合碎片化閱讀,精簡閱讀的小友們。爭取讓小友們看完系列 === 看整本書的 85+%。正則表達式

前言

內容總覽

  • 第一章讓初學者快速熟悉 JavaScript,瞭解 JavaScript 中的原始類型、隱式強制轉換、編碼類型等幾本概念;
  • 第二章着重講解了有關 JavaScript 的變量做用域的建議,不只介紹了怎麼作,還介紹了操做背後的緣由,幫助讀者加深理解;
  • 第三章和第四章的主題涵蓋函數、對象及原型三大方面,這但是 JavaScript 區別於其餘語言的核心;
  • 第五章闡述了數組和字典這兩種容易混淆的經常使用類型及具體使用時的建議,避免陷入一些陷阱;
  • 第六章講述了庫和 API 設計;
  • 第七章講述了並行編程,這是晉升爲 JavaScript 專家的必經之路

JavaScript 與 ECMAScript

澄清一下 JavaScript 和 ECMAScript 的術語。總所周知,當人們提起 ECMAScript時,一般是指由 Ecma 國際標準化組織制定的 「理想語言」。算法

而 JavaScript 這個名字意味着來自語言自己的全部事物,例如某個供應商特定的 JavaScript 引擎、DOM、BOM 等。編程

爲了保持清晰度和一致性,在本書中,我將只使用 ECMAScript 來談論官方標準,其餘狀況,將使用 JavaScript 指代語言。數組

關於 Web

避開 Web 來談 JavaScript 是很難的,不過本書是關於 JavaScript 而非 Web 的編程,故本書重點是 JavaScript 語言的語法、語義和語用,而不是 Web 平臺的 API 和技術。瀏覽器

關於併發

JavaScript 一個新奇的方面是在併發環境中其行爲是徹底不明朗的。所以,只是從技術角度介紹一些非官方的 JavaScript 特性,但實際上,全部主流的 JavaScript 引擎都有一個共同的併發模型。安全

將來版本的 ECMAScript 標準可能會正式標準化這些 JavaScript 併發模型的共享方面markdown

第 1 章「讓本身習慣 JavaScript」

JavaScript 語言提供爲數很少的核心概念,所以顯得如此的平易近人,可是精通這門語言須要更多的時間,須要更深刻地理解它的語義、特性以及最有效的習慣用法。網絡

本書每一個章節都涵蓋了高效 JavaScript 編程的不一樣主題。第 1 章主要講述一些最基本的主題

第 1 條:瞭解你使用的 JavaScript 版本

版本問題

因爲 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 的哪些版本。
  • 確保你使用的任何 JavaScript 的特性對於應用程序將要運行的全部環境都是支持的。
  • 老是在執行嚴格模式檢查的環境中測試嚴格代碼。
  • 小心接連那些在不一樣嚴格模式下有不一樣預期的腳本。

第 2 條:理解 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
複製代碼

以上表達式進行的實際步驟爲

  1. JavaScript 將雙精度浮點數的數字 8 和 1 轉換爲 32 位整數。
    1. 整數 8 表示 32 位二進制序列:00000000 00000000 00000000 00001000,也可使用 (8).toString(2) // "1000" 方法進行查看
    2. 整數 1 表示 32 位二進制序列:00000000 00000000 00000000 00000001
  2. 使用整數位模式,進行位算術運算符操做
    1. 也就是按位或運算表達式合併兩個比特序列,結果爲:00000000 00000000 00000000 00001001
  3. 最後將結果轉換爲標準的 JavaScript 浮點數。

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
複製代碼

解決

  • 一個有效的解決方法是儘量地採用整數值運算,就不會有舍入偏差。
    • 但仍是要小心 JavaScript 全部的計算只適用於 -2^53 ~ 2^53

我的的解決方案

  • 將浮點數模擬爲字符串,利用字符串來進行實際運算過程。
  • 將小數轉爲整數再進行計算
    • 變爲字符串
    • 利用 .split(.) 分割字符串
    • 根據小數的個數,找到最大指數 baseNum
    • 最後獲得結果 (num1 * baseNum + num2 * baseNum) / baseNum;)

總結

  • JavaScript 的數字都是雙精度的浮點數。
  • JavaScript 中的整數僅僅是雙精度浮點數的一個子集,而不是一個單獨的數據類型。
  • 位運算符將數字視爲 32 位的有符號整數。
  • 小心浮點運算中的精度陷阱

第 3 條:小心隱式的強制轉換

JavaScript 隱式的強制轉換

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
複製代碼

NaN

強制轉換也會隱藏錯誤。結果爲 null 的變量在算術運算中不會致使失敗,而是被隱式地轉換爲 0.

一個未定義的變量將被轉換爲特殊的浮點數值 NaN,這些強制轉換不是當即拋出一個異常,而是繼續運算,每每致使一些不可預測的結果。而測試 NaN 值也是異常困難,由於兩個緣由

  1. JavaScript 遵循了 IEEE 浮點數標準使人頭疼的要求 - NaN 不等於其自己

    const x = NaN
    x === NaN		// false
    複製代碼
  2. 標準庫函數 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
複製代碼

當一個對象同時包含 toStringvalueOf 方法時,運算符 + 應該調用哪一個方法並不明顯。所以,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 個假值:false0-0NaN""nullundefined,其餘全部的值都爲真值。

檢查參數是否爲 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 進行比價而不是使用真實運算。

第 4 條:原始類型優於封裝對象

封裝對象

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 對象,更新第一個封裝對象並不會形成持久的影響。

這也常常形成錯誤給一個原始值設置屬性,而程序默認行爲,致使一些難以發現的錯誤並難以診斷。

總結

  • 當作相等比較時,原始類型的封裝對象與其原始值行爲不同。
  • 獲取和設置原始類型值的屬性會隱式地建立封裝對象。

第 5 條:避免對混合類型使用 == 運算符

== 運算符

先看一個例子,你認爲返回的結果是什麼?

"1.0e0" == { valueOf: function() { return true } }
複製代碼

像第 3 條描述的隱式強制轉換同樣,在比較以前它們都會被轉換爲數字。最終結果與 1 == 1 是等價的

  • 字符串 "1.0e0" 被解析爲 1
  • 匿名對象也經過調用其自身的 valueOd 方法獲得結果 true,而後再轉換爲數字,獲得 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()) {
  // ...
}
複製代碼

與 === 運算符區別

當兩個屬性屬於同一類型時,===== 運算符的行爲是沒有區別的。但最好使用嚴格相等運算符,來準確比較數據的內容和類型,而非僅僅看數據的內容。

轉換規則

== 運算符強制轉換的規則並不明顯,但這些規則具備對稱性。

轉換規則一般都試圖產生數字,但它們處理對象時會變得難以捉摸。會將對象試圖轉換爲原始值來進行判斷,能夠經過調用對象的 valueOftoString 方法而實現。而使人值得注意的是,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 將原始類型轉換爲數字

總結

  • 當參數類型不一樣時,== 運算符應用了一套難以理解的隱式強制轉換規則。
  • 使用 === 運算符,使讀者不須要涉及任何的隱式強制轉換就能明白你的比較運算。
  • 當比較不一樣類型的值時,使用你本身的顯示強制轉換使程序的行爲更清晰。

第 6 條:瞭解分號插入的侷限

分號插入

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 語法限制產生式

JavaScript 語法限制產生式**不容許在兩個字符之間出現換行,所以會強制地插入分號。**以下例子:

return 
{ };
// 將被解析爲 3 條單獨的語句。
return;
{ };
;
複製代碼

換句話說,return 關鍵字後的換行會強制自動地插入分號,該代碼例子被解析爲不帶參數的 return 語句,後接一個空的代碼塊和一個空語句。

除了 return 的語法限制生成式,還有如下其餘的 JavaScript 語句限制生產式。

  • throw 語句
  • 帶有顯示標籤的 breakcontinue 語句
  • 後置自增或自減運算符

第三條規則

分號不會做爲分隔符在 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); }	// 正確
複製代碼

總結

  • 分號僅在 } 標記以前、一個或多個換行以後和程序輸入的結尾被插入。
  • 分號僅在緊接着的標記不能被解析的時候推導分號。
  • 在以 ([+-/ 字符開頭的語句前毫不能省略分號。
  • 當腳本鏈接的時候,在腳本之間顯式地插入分號。
  • returnthrowbreakcontinue++-- 地參數以前覺不能換行。
  • 分號不能做爲 for 循環的頭部或空語句的分隔符而被推導出。

第 7 條:視字符串爲 16 位的代碼單元序列

Unicode 概念

Unicode 概念是爲世界上全部的文字系統的每一個字符單位分配了一個惟一的整數,該整數介於 0 和 1114 111 之間,在 Unicode 術語中稱爲代碼點

Unicode 與其餘字符編碼幾乎沒有任何不一樣(例如,ASCII)。但不一樣點是,ASCII 將每一個索引映射爲惟一的二進制表示,Unicode 容許多個不一樣二進制編碼的代碼點。不一樣的編碼在存儲的字符串數量和操做速度之間進行權衡(也就是時間與空間的權衡)。目前由多種 Unicode 的編碼標準,最流行的幾個是:UTF-8UTF-16UTF-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)

JavaScript 代碼單元

因代碼點的範圍擴展,UCS-2 就變的過期,所以UTF-16 採用代理對錶示附加的代碼點,一對 16 位的代碼單元共同編碼一個等於或大於 2^16 的代碼點

例如分配給高音譜號的音樂符號 𝄞 的代碼點爲 U+1D11E(代碼點數 119 070 的 Unicode 的慣用 16 進制寫法),UTF-16 經過合併兩個代碼單元 0xd8340xddle 選擇的位來對這個代碼點進行解碼。

"𝄞".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 操做函數:sendcodeURIdecodeURIencodeURIComponentdecodeURIComponent

故每當一個 JavaScript 環境提供一個庫操做字符串(例如操做一個 Web 頁面的內容或者執行關於字符串的 I/O 操做),你都須要查閱這些庫文檔,看它們如何處理 Unicode 代碼點的整個範圍。

  • 瞭解 Unicode 概念。
  • 理解代碼點和代碼單元。
  • JavaScript 字符串由 16 位的代碼單元組成,而不是由 Unicode 代碼點組成。
  • JavaScript 使用兩個代碼單元表示 2^16 及其以上的 Unicode 代碼點。這兩個代碼單元被稱爲代理對。
  • 代理對甩開了字符串元素計數lengthcharAtcharCodeAt方法以及正則表達式模式(例如 .)受到了影響。
  • 使用第三方的庫編寫可識別代碼點的字符串操做。
  • 每當你使用一個含有字符串操做的庫時,你都須要查閱該庫文檔,看它如何處理代碼點的整個範圍。

後言

以上爲 第一章內容 學習了 1~7 條規則 着重於熟悉 JavaScript,瞭解 JavaScript 中的原始類型、隱式強制轉換、編碼類型等幾本概念;

系列以下:

若無連接,則是正在學習當中...

相關文章
相關標籤/搜索