《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 并发模型的共享方面

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

系列如下:

  • 《Effective JS》的 68 条准则「1 至 7 条」- 大大读的书
  • 《Effective JS》的 68 条准则「8 至 17 条」- 大大读的书
  • 《Effective JS》的 68 条准则「18 至 29 条」- 大大读的书
  • 《Effective JS》的 68 条准则「30 至 42 条」- 大大读的书
  • 《Effective JS》的 68 条准则「43 至 52 条」- 大大读的书
  • 《Effective JS》的 68 条准则「53 至 60 条」- 大大读的书
  • 《Effective JS》的 68 条准则「61 至 68 条」- 大大读的书

若无链接,则是正在学习当中…

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享