exposir / TIL

📝 Today I Learned
https://exposir.github.io/blog/
4 stars 0 forks source link

《Javascript 悟道》读书笔记.md #51

Closed exposir closed 2 years ago

exposir commented 3 years ago

JavaScript 悟道

道格拉斯·克罗克福德 111 个笔记

◆ 第 0 章 导读 ○ ○ ○ ○ ○

事实上,真正的“精通”应该体现在代码的可读性、可维护性以及是否无错上。如果你做到了这几点,那就真的可以炫耀了。做一个谦逊的程序员吧。吾日三省吾身:自身可乎?工作可乎?可有提升乎?经验之谈,为炫技而过分使用各种特性,只会适得其反。

我推荐你阅读 ECMAScript 规范。它虽然读起来可能有些晦涩,但好在是免费的。

当下和未来的互联网需要下一代的编程范式,它应当是全局分布式的、安全的和事件化编程的。遗憾的是,当下包括 JavaScript 在内的几乎所有编程语言依旧停留在旧的范式中,即本地化的、不安全的和顺序化编程的。我把 JavaScript 看作一门过渡的语言。在 JavaScript 中使用最佳实践可以很好地为我们未来理解新的编程范式做好准备

对陌生事物产生的奇怪感觉并不能证明它是错的。

◆ 第 1 章 命名 ○ ○ ○ ○ ●

编程时,我们应该努力使用顾名思义、一目了然的名称。

因为 JavaScript 没有私有属性,所以通常只能将对应的公有属性名或者全局变量名加上下划线前缀或下划线后缀来从语义上表示其为私有。

美元符号则通常是被一些代码生成器、转义器和宏处理器加到变量里的,以此来保证生成的变量名不会与人工编写的代码冲突。

JavaScript 中的所有名字都应该以小写字母开头,这一切都拜 JavaScript 中的 new 运算符所赐。

所有

的构造函数都应该以大写字母开头,而其他任何名字都应该以小写字母开头。

我其实还有一个诀窍:从不用 new。

保留字其实是 20 世纪五六十年代计算机内

存空间有限的另一个遗留产物,因为保留字的设计可以给编译器节约少许字节。

◆ 第 2 章 数值 ○ ○ ○ ● ○

我真心建议你不要拿零做除数,也永远不要使用 Object.is()。

NaN 是 Not a Number 的缩写。你说怪不怪?虽然它的含义是“不是一个数”,但是 typeof 对它的结果又告诉大家 NaN 是一个数("number")。

最让人困惑的是,NaN 居然不等于它自己!这是 IEEE 754 的糟粕,JavaScript 却将其照搬了过来,没有做任何处理。

Number.MAX_SAFE_INTEGER 的值为 9007199254740991,约为 9000 万亿,表示最大安全整数。在最大安全整数和最小安全整数之间的整数统称为安全整数。这就是 JavaScript 不需要整数类型的原因,毕竟光 number 类型就足以表示到 Number.MAX_SAFE_INTEGER 的整数了,相当于有 54 位有符号整数类型。

也就是说,在 JavaScript 中,只有在所有的运算因子、运算结果以及中间结果都是安全整数的情况下,才能进行精确的整数运算,才适用于加法结合律和乘法分配律。一旦有一项的值不是安全整数,事情就会变得不那么可控。例如,当我们计算一堆数的和时,相加的顺序会影响结果。举个例子,((0.1 + 0.2) + 0.3)的结果比(0.1 + (0.2 + 0.3))的结果大。我们可以通过 Number.isSafeInteger(number)函数来判断一个数是否是安全整数。如果是,它会返回 true。

所有的数值类型都继承自 Number.prototype 对象。该对象包含一系列方法,但我觉得这些方法都没什么用。

Math 对象包含一系列本该在 Number 中的重要函数。

Math.floor 返回的是一个恰比传入参数小的整数,而 Math.trunc 返回的则是恰比传入参数更接近 0 的整数。它们在正数上的结果是一样的,但在负数上的结果存在差异。

Math.min 的返回值是传入的一系列参数中最小的数,而 Math.max 则返回最大的数。

我衷心希望下一门取代 JavaScript 的语言一定要有精确的小数类型。然而就算有那么一天,这门语言还是无法精确地表示实数——没有一个有限的系统可以做到。有限的系统只能表示人类生活中的日常数字,即十进制数组成的值。

一旦你的程序开始游离于安全整数范围之外,带有小数点(.)或者使用十进制指数(e)科学计数法的数就不再精确了。两个大小差不多的数相加通常会比两个大小悬殊的数相加产生更小的误差。这就是为什么部分求和会比单独求和更精确。

◆ 第 3 章 高精度整数 ○ ○ ○ ● ●

ECMAScript 中的 BigInt 类型其实也只是 54 位有符号整数类型。

◆ 第 6 章 布尔类型 ○ ○ ● ● ○

布尔(boolean)类型是以英国数学家乔治·布尔(George Boole)命名的,他发明了代数逻辑系统。克劳德·香农将布尔乔治·布尔的系统应用在了数字电路的设计上,所以我们称计算机电路为逻辑电路。

当两个值都为字符串或者都为数值的时候,

<、<=、>和>=的结果都是准确的。不过在其他情况下,这些比较大多是无意义的。JavaScript 并不会阻止你比较不同的类型,这些情况需要你自行规避。所以要尽可能避免在不同类型之间进行比较。

答应我,永远不要用这两个运算符;答应我,务必使用===和!==。

布尔式犯蠢类型

这些幻假的值虽然表面上看起来像 false,但实际上大多是装出来的。幻真的值也一样。这些犯蠢的类型是设计上的缺陷,但这并不能全怪 JavaScript。JavaScript 沿用的是 C 语言的习惯。

C 语言本身是一门类型不足的语言。0、FALSE、NULL、字符串结束符,还有一些类似东

西的值其实都一样。所以在 if 语句的条件判断位,C 语言其实判断的是表达式结果是否为 0。C 语言程序员有一个流派,就是利用这个“特性”让条件判断尽可能简洁。

理论上,一个条件判断的结果只应为 true 或 false,其余的值都应该在编译时就抛错。

对于 NaN 而言,唯一有意义的运算就是 Number.isNaN(NaN)。除此之外,不要在任何场景用 NaN。

◆ 第 7 章 数组 ○ ○ ● ● ●

其实,JavaScript 的数组几乎就是对象,它们仅有四处不同。

数组有一个神奇的 length 属性。该属性

并不是指数组中元素的数量,而是指数组元素的最高序数加 1。

数组对象都继承自 Array.prototype,该原型比 Object.prototype 多了一些更实用的函数。

JavaScript 自身也对数组感到迷惑。如果对数组进行 typeof 操作,返回将是"object",这显然是有问题的。如果要判断一个

值是不是数组,得使用 Array.isArray(value)。

includes 函数也与 indexOf 类似,只不过前者不返回序数,而是返回 true 来代表数组中存在搜索值,返回 false 来代表不存在。

every

some

find

findIndex

filter 方法也与 find 类似,只不过返回的是一个新数组,其中依次包含那些遍历处理时返回幻真值的元素。

forEach 和 find 方法都有提前退出的能力(every 和 some 就是 forEach 的可提前退出形态)。map、reduce 和 filter 则没有这个能力。

reverse 方法会将数组逆序重排。与 sort 方法一样,这个方法也具有破坏性。

“纯净之树”:

“非纯之树”:

“污染之树”(本该纯净):

◆ 第 8 章 对象 ○ ● ○ ○ ○

JavaScript 为“对象”一词赋予了新的含义。在 JavaScript 中,除了两种底型之外(null 和 undefined),万物皆对象。

当对一个对象执行 typeof 操作的时候,返回值是字符串"object"。

Object.assign 函数可以将一个对象中的属性复制到另一个对象中。你可以通过这个函数来将一个对象复制到一个空对象上。

Object.create(prototype)可以将一个已有的对象继承到一个新对象中。该已有对象将作为新对象的原型。

同理,这个新对象还可以继续作为下一个新对象的原型。JavaScript 并没有限制原型链的长度,但是我的建议是不要让整条原型链过长。

我们用原型做得最多的事情就是来存储函数。实际上,JavaScript 这门语言本身的各种对象都是这么做的。当我们用对象字面量创建对象的时候,JavaScript 默认其继承自 Object.prototype。类似地,数组方法继承自 Array.prototype;数值类型方法则继承自 Number.prototype;字符串方法继承自 String.prototype;甚至连函数方法也是继承自 Function.prototype 的。数组方法和字符串方法都比较有用,而 Object.prototype 的方法则比较鸡肋。

Object.keys(object)函数会将传入对象的所有自有属性(不包括继承属性)的键名作为字符串放入一个数组中并返回。这样一来,你就可以用数组的各种方法去处理对象的属性了。 键名在数组中的顺序是按属性的插入时间来排列的。如果你想改变排列顺序,只需调用数组的 sort 方法即可。

需要注意的是,该操作并不是深度冻结,只有对象最顶层的属性会被冻结。

Object.freeze(object)和 const 表达式做的是完全不同的事。 Object.freeze 作用于值,而 const 则作用于变量。

但是,如果原型被冻结,就会出问题。如果对象原型中的一个属性是不可变的,那么该对象就无法拥有同名的自有属性了。

JavaScript 的一个设计错误是对象上的属性名必须为字符串。有时候,我们的确需要用一个对象或者数组作为键名。很可惜的是,JavaScript 中的对象会在这种情况下做一件蠢事——直接把要作为键名的值通过 toString 方法进行转换。我们之前也看到了,对象的 toString 方法返回的完全是糟粕。

对于这种情况,JavaScript 也算有自知之明,为我们准备了备用方案——WeakMap。这类对象的键名是对象,不能是字符串,并且它的接口也与普通的对象完全不同

一种只允许字符串作为键名,而另一种居然只允许对象作为键名。就不能好好地设计出一种既支持字符串又支持对象作为键名的类型吗?

WeakMap 并不允许我们检视对象中的内容。除非拥有对应的键名,否则无法访问其中的内容。WeakMap 与 JavaScript 的垃圾回收机制可以融洽相处。如果 WeakMap 中的一个键名在外没有了任何副本,那么这个键名所对应的属性会被自动删除。这可以防止一些潜在的内存泄漏情况。

WeakMap 这个名字起得就够差劲了,Map 更不知所云。它与数组的 map 方法没有半点关系,更与绘制地图毫不沾边。所以我一直不推崇 Map,但是 WeakMap 和数组的 map 方法则是吾之所爱。

JavaScript 还有一种叫 Symbol 的类型,具有 WeakMap 的一些能力。但我不推荐使用 Symbol,因为它真的很多余。我个人的习惯就是不使用各种多余的功能,以此来简化操作。

◆ 第 9 章 字符串 ○ ● ○ ○ ●

我们可以通过 String.fromCharCode 函数来创建字符串,该函数接收任意多个参数。字符串中的元素可以通过 charCodeAt 访问,但是不可以修改——字符串是不可变的。与数组一样,字符串也拥有 length 属性。

String.prototype 原型包含一些可用于字符串的方法。concat 和 slice 方法与数组的对应方法类似,而 indexOf 方法则大相径庭。字符串 indexOf 方法的参数并不是数,而是字符串。它将会将传入的模式串与该字符串进行匹配,找到模式串在主串中出现的第一个位置并返回。

startsWith、endsWith 和 contains 三个方法是对 indexOf 和 lastIndexOf 这两个方法的包装。

字符串的全等运算非常有用。这也是我认为不需要 Symbol 的原因之一,毕竟内容相同的字符串会被认为是同一个对象。不过在 Java 之类的语言中,字符串是不能全等的。

◆ 第 10 章 底型 ○ ● ○ ● ○

底型是用于指示递归数据结构结尾的特殊值,也可用于表示值不存在。在一般的编程语言中,常以 nil、none、nothing 或者 null 表示。 JavaScript 有两种底型:null 和 undefined。其实 NaN 也可以算作一种底型,主要用于表示不存在的数值。不过我认为过多底型属于语言设计上的失误。

这其实很神奇,你定义了一个未定义(undefined)的变量。

undefined 毕竟不是对象,所以基于它获取属性就会触发异常。这就使得写访问路径的表达式成为了一件麻烦事。

◆ 第 11 章 语句 ○ ● ○ ● ●

JavaScript 有三种语句可以在模块或者函数中声明变量:let、function 和 const。

function 声明会被提升。也就是说,在运行时该语句的声明会被提升到模块或者函数的顶部。所有通过 function 语句创建出来的 let 语句也会被提升。所以 function 声明不应该被放到某一个区块中,而应该置于一个函数体或者模块内。将其置于 if、switch、while、do 或者 for 等语句中是一种非常不好的实践。我们会在第 12 章详细说明。

我会尽可能地让变量声明使用 const,以此来提高代码的纯度 1(详见第 19 章)。 1 纯度(purity)反映不确定性,值越小不确定性越低。——译者注

在 JavaScript 语句的位置上,有意义的表达式其实只有三类:赋值、调用和 delete。

我不建议使用自增运算符++或者--。这两个运算符都是早期设计出来用于操作指针的。

JavaScript 有两种分支语句:if 和 switch。我们其实只需要一个,甚至都不需要。

我不推荐使用 switch,它真的是托尼·霍尔的 case 语句与 FORTRAN 的 goto 语句的邪恶结合体。

我们可以用对象来代替 switch 语句。为不同的 case 在对象中挂载函数,并在函数里实现该 case 需要运行的逻辑即可。

我坚持认为 else if 的写法就应该只是为了替代类 case 的写法。

总之,我不建议使用 for 语句。

我个人最喜欢的中断语句是 return。它会中断函数的运行,并指定返回值。

◆ 第 12 章 函数 ○ ● ● ○ ○

函数对象中包含一个 Function.prototype 的委托连接。函数对象通过该连接集成了两个不是特别重要的方法,分别是 apply 和 call。

◆ 第 13 章 生成器 ○ ● ● ○ ●

ES6 引入了一个新特性,叫生成器(generator)。当时,标准的制定过程受到了 Python 的极大影响,生成器也应运而成。但是出于一些原因,我个人不建议使用生成器。

看看之前用生成器写的复杂代码,还是写一个返回函数的函数吧,这样更简单。

◆ 第 14 章 异常 ○ ● ● ● ○

异常对象在 C++与 Java 中是必要的,而在 JavaScript 中则是多余的。

JavaScript 在这方面就做得很好。如果 try 执行成功了,我们就能得到一个很好的运行结果。JavaScript 灵活的类型设计也足以让我们处理各种预期的情况。 如果真的有一些预期之外的情况发生,那么 catch 子语句就会开始工作。

◆ 第 15 章 程序 ○ ● ● ● ●

现在大家都已经承认,将 JavaScript 嵌入 HTML 页面是一个不良实践。这种做法对设计不利,因为它在表示和行为之间没有分隔;对性能不利,因为页面中的代码无法被 gzip 压缩或被缓存;对安全性也不利,因为它是 XSS 的温床。我们应该始终遵循 W3C 的内容安全策略(Content Security Policy,CSP),禁止所有的内联源码。

◆ 第 16 章 this ● ○ ○ ○ ○

Self 语言是 Smalltalk 语言的一种方言,用原型替代了类。一个对象可以直接继承自另一个对象。原来的模型由于高耦合性而使扩展性变得脆弱和膨胀,而 Self 语言的原型特性是一种出色的简化。原型模型相比之下更轻巧、更具表现力。 JavaScript 实现了原型模型的一个怪异变体。

原型最常被用作方法的容器。相似的对象都有相似的方法,所以将所有方法挂载到同一个共享原型上比将它们在每个对象上挂载一遍更节约内存。 那么,原型上的函数如何知道自己在执行的时候应该作用于哪个对象呢?这就要用到 this 了。

构造函数调用就是在调用该函数之前加一个 new 运算符。new 运算符做了下面这些事:

尽管 class 语法从语义上看是类,但在本质上根本不是类。它只是一种模仿类的写法的语法糖。这种写法保留了传统模型糟糕的一面——扩展(extend),将类高度耦合在了一起。上一章就提到过,这种高耦合的写法会提高程序的脆弱性,是一种不良实践。

因此我建议大家遵循去 this 化的原则。你会发现这是一个明智的决定,生活也会因此变得更加阳光明媚。我并不是要夺走你的 this,只是想让你成为一个无忧无虑的程序员。用“类”写代码的程序员终将走向一片凄迷的“代码坟场”。

◆ 第 17 章 非类实例对象 ● ○ ○ ○ ●

去 this 化的一种方案就是多态。每个可以识别特定消息的对象都可以处理该消息,而究竟如何处理则取决于对象的内在逻辑。

但是当逻辑复杂起来的时候,继承带来的问题就接踵而至了。继承会引起类之间的高耦合。类的更改可能会引起其子类、孙类等的错误。这些类慢慢会变成腐化的“家族”。

所以,我推崇两类对象: ● 只包含方法的“硬对象”,它们通过闭包来捍卫私有数据的尊严,具有多态性与封装性; ● 只包含数据的“软对象”,它们不具有任何行为,只是一类可以被函数处理的数据集合。

后来我学聪明了,只让构造函数有一个参数。这个参数是一个对象,通常是以对象字面量的形式传入的,但有时候也可以来自其他代码源,如 JSON 数据。 这么做有好几个好处:

◆ 第 21 章 日期 ● ○ ● ○ ●

JavaScript 本可以做得比 Java 更好,然而糟心的是,它错把 Java 当了榜样。

getMonth 方法的返回结果是从 0 算起的,毕竟这是程序员的习惯。也就是说,getMonth 的返回值范围是 0 ~ 11。但 getDate 不是从 0 算起的,它的返回值范围是 1 ~ 31。这种不一致性就是错误的根源。

而且 JavaScript 居然也犯了同样的错误。请务必使用 getFullYear 和 setFullYear 来规避这个错误。

ISO 8601 是用于表示日期和时间的国际化标准。JavaScript 被要求能正常解析像 2018-11-06 这种形式的 ISO 日期字符串。将最高有效数据放在字符串首,将最低有效数据置于其尾,这种表示法可比美国标准的 11/06/2018 有意义多了。好处之一是,这样的日期字符串是可以按字典序排序的。

事实上,JavaScript 的大多数设计错误源于“借鉴”Java。

◆ 第 22 章 JSON ● ○ ● ● ○

要是让我再做一次的话,我会选用 decode 和 encode 这两个名字。 JSON.parse(text, reviver)

其实 JSON 最糟糕的地方就是它的名字。它是 JavaScript 对象表示法(JavaScript Object Notation)的缩写。

◆ 第 23 章 测试 ● ○ ● ● ●

如果程序员给计算机提供的程序不完美,就相当于交给了它一张许可证,允许它在最坏的时候做最坏的事。这并不是计算机的错,而是你的错。

当时大多数的软件是被烧录到 ROM(Read-Only Memory,只读存储器)芯片中的,而 ROM 中的错误是无法被修正的。

缓存可以减轻 Web 浏览器中的一些膨胀现象,然而浏览器并不擅长缓存。懒加载器(lazy loader)和 tree-shaking 等工具可以延缓一些非必要代码的加载,甚至可以移除这些代码,但是这相当于为了解决膨胀而引入了新的“膨胀因子”。