《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 中的原始类型、隐式强制转换、编码类型等几本概念;

系列以下:

若无连接,则是正在学习当中...

相关文章
相关标签/搜索