exposir / TIL

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

《Javascript 悟道》读书笔记 #83

Closed exposir closed 2 years ago

exposir commented 2 years ago

悟道

基本原则

ppt 中会包含大量的只有少数人知道的奇闻趣事 因为 js 很多地方的确有点奇怪,但是可能自己对 js 不是很精通,可能觉得 js 这么设计是很有道理的,在老道的吐槽下,可以达到对 js 祛魅的效果 本位很多的观点都是十分主观,我也不进行评论,还原作者的观点,以作者为准。 最后本次分享主要是达到一个抛砖引玉的效果,这本书和一般的技术书还是很不一样的,大家对这本书感兴趣可以去阅读一下。

命名

因为 JavaScript 对于变量名的长度没有限制,所以不要吝惜你的起名才华。我希望你在命名的时候尽可能描述清楚被赋名者的含义,而不要使用各种隐晦的缩写。

JavaScript 的命名能以下划线(_)或者美元符号($)开头和结尾,还能以数字结尾,但我认为你不该这么做。JavaScript 允许我们做很多本不该做的事情。这些命名习惯应该留给代码生成器或者宏处理器,而人类应该去做人类该做的事情。

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

大多数语言命名时使用空格

FORTRAN 首先打破了桎梏,允许在命名时使用空格。然而,后来包括 JavaScript 在内的大多数编程语言没有继承这个优良传统,反而学习了它的一些糟粕。例如,使用等号(=)表示赋值,用圆括号(())而不是花括号({})包裹 if 语句的条件表达式。

不过在出现这么一门语言之前,我还是推荐你使用下划线分隔变量名中的多个单词。这是因为,万一哪天真有更好的编程语言出现,这种命名法可以让你最便捷地将代码迁移至下一门语言。

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

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

保留字

存空间有限的另一个遗留产物,因为保留字的设计可以给编译器节约少许字节。 我们现在不必再受这些事情的困扰了。可惜这几十年来,人们的思维已经固化。对于现代程序员来说,保留字的设计真的是糟粕。扪心自问,你能否记住所有的保留字? 还有一种糟心的情况是,你在起变量名的时候,尽管有个单词可以完美地阐释该变量的意义,但很不巧,它是一个你从来不用的保留字,甚至是一个还没有被实现的预保留字。 此外,保留字对于现代编程语言的设计者来说也不是好东西。脆弱的保留字策略会使我们不能干净利落地为一门流行语言添加新特性,给我们添堵。真希望能有一门强硬的新语言出现,让我们不用再为“五斗保留字”折腰。

数值

JavaScript 只有一种数值类型这件事经常被人们诟病,但我反而认为这是 JavaScript 最成功的设计之一:这个设计让程序员不必浪费时间在几种相似的数据类型之间做选择,毕竟有时候花了时间还会选错;能避免那些由于数据类型之间的转换而造成的错误;甚至还可以避免整数类型的溢出错误。

JavaScript 的“整数”可比 Java 的整数可靠多了,因为它们不会溢出。

作为一门编程语言,Java 的数值运算系统在算错的时候甚至连 Warning 都不会报。int 类型总出错,还怎么指望通过它来避免错误呢?

零是独一无二的。理论上来说,在一个数值系统中只应存在一个零。然而事不遂人愿

在 IEEE754 标准中有两个零:0 和-0。你知道 JavaScript 为帮你抹平 0 与-0 的不同做了多大努力吗?它让我们几乎可以忽略-0 的存在。不过仍然需要注意以下几种情况:

NaN

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

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

因此,当我们要判断一个值是不是 NaN 时,应当使用 Number.isNaN(value)。Number.isFinite(value)函数会在值为 NaN、Infinity 或者-Infinity 的时候返回 false。

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

布尔

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

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

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

剩下的值就全都是幻真的了,比如空对象、空数组,甚至"false"和"0"这样看起来像幻假的字符串。

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

C 语言程序员有一个流派,就是利用隐式类型转换“特性”让条件判断尽可能简洁。

理论上,一个条件判断的结果只应为 true 或 false,其余的值都应该在编译时就抛错。然而 JavaScript 并非如此,它的条件表达式可以写得如 C 语言般简洁。当条件判断语句意外地传入了错误类型的值时,JavaScript 不会报错。这就很可能让程序进入另一个本不该进入的条件分支。Java 就不一样,它要求条件判断位中的值必须是布尔类型,这样可以避免很多潜在的错误。唉,真希望 JavaScript 也是这样的。

虽然 JavaScript 并没有学习这些好榜样,但我还是希望你能假装它已经做到了,然后在条件判断位中始终使用布尔类型。如果我们在编码的时候严于律己,就能写出更好的程序。

数组

数组真是最伟大的数据结构。

JavaScript 的第一个版本并没有将数组设计进去,但由于 JavaScript 的对象实在太强大了,以至于几乎没人发现这个纰漏。如果不考虑性能,数组能做的事,对象基本上都能做。

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

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

自古以来,人们都习惯用 1 来代表计数的开始。从 20 世纪 60 年代中期开始,一小股有影响力的程序员认为,计数应当从 0 开始。到了今天,几乎所有程序员都习惯了这种以 0 开始的计数法。不过,其他人(包括大多数数学家)还是习惯以 1 开始。数学家通常将原点标记为 0,但还是将有序集合的第一个元素标为 1。至于他们为什么这么做,至今仍是个谜。

有一个论点是从 0 开始可以提高效率,但并没有什么有利的证据;还有一个主张其正确性的论点是从 0 开始可以减少“差一错误”(off-by-wun)1,但人们对此也表示怀疑。也许有一天我们能找到有力的证据来证明,对于程序员来说从 0 开始更好。

差一错误是在计数时由于边界条件判断失误导致结果多了一或少了一的错误,通常指 计算机编程循环 多了一次或者少了一次的程序错误,属于 [逻辑错误]的一种。

indexOf

如果遍历了整个数组还没有匹配的值,则返回-1。我个人认为这个设计有错误,因为-1 也是一个数,与其他返回的序号都是数值类型。

JavaScript 还有很多类似的设计错误,这些底型在一开始设计的时候就有问题。

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

reduce 方法有逆序版本 reduceRight,而可怜的 forEach、map、filter 和 find 都没有这种令人羡慕的逆序版本。

我甚至怀疑正是这些缺憾才导致 for 语句一直没有被废除。

对象

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

在其他语言中,这类数据结构通常被称为哈希表(hashtable)、映射表(map)、记录(record)、结构体(struct)、关联数组(associative array)或字典(dictionary)

我建议不要在对象中存储 undefined。尽管 JavaScript 允许我们在对象中存储这个值,并且会正确返回 undefined 值,但是当对象中不存在某个属性的时候,JavaScript 返回的也是 undefined。这会产生二义性。我个人认为,为某个属性赋值 undefined 意在删除该属性,然而 JavaScript 并没有这样做。要删除某个属性,正确的做法是使用 delete 运算符。

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

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

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

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

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

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

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

底型

底型是用于指示递归数据结构结尾的特殊值,也可用于表示值不存在。在一般的编程语言中,常以 nil、none、nothing 或者 null 表示。

JavaScript 有两种底型:null 和 undefined。其实 NaN 也可以算作一种底型,主要用于表示不存在的数值。不过我认为过多底型属于语言设计上的失误。

在 JavaScript 中,可以说只有 null 和 undefined 不是对象。如果基于它们去访问一些属性,就会触发异常。

从一方面看,null 和 undefined 是非常类似的;但从另外一些方面来看,它们的行为又不一样——互有交集,却又无法完全相互替代。 有时候,它们的表象一致,但是实际表现不同,这就很容易造成混乱。我们经常不得不花时间决定当下到底该使用哪个底型,这些虚无缥缈的理论又会导致更多混乱,而混乱就是各种 bug 之源。如果只保留两者之一,程序将更美好。我们虽然不可能改变 JavaScript 这门编程语言来只留一种底值,但是可以从自身做起,只用一种 2。我个人建议淘汰 null,只用 undefined。

因为 JavaScript 自身也在用 undefined。如果你用 let 或者 var 声明一个变量却没有初始化它,这个值就是 undefined。这其实很神奇,你定义了一个未定义(undefined)的变量。如果你调用一个函数,却没往其中传入足量的参数,那么那些没有传参数的值就是 undefined;如果你访问一个对象中不存在的属性,得到的也是 undefined;数组也一样,如果你访问其中不存在的元素,得到的还是 undefined。

只有在创建空对象的时候,我才会使用 null——Object.create(null)。不过我也是不得已而为之,因为 Object.create()或者 Object.create(undefined)会触发异常,这是语言规范的设计错误造成的。

type of null === ‘’object 这就有可能导致一些程序逻辑上的错误。这也是我认为应该避免使用 null 的原因之一。

this

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

2007 年,多个研究性项目尝试开发出 JavaScript 的安全子集,而其中最大的问题就是 this 的管理。在方法调用中,this 会被绑定到对应的对象上。这种行为有时候是好的,但在其作为函数被调用时,this 就会被绑定到全局对象上,这就是一件糟糕的事了。我建议的方案是完全取消 this,因为我认为它既没用又会造成问题。如果将 this 从 JavaScript 中移除,JavaScript 仍是一门图灵完备的语言。所以,我自身已经开始了去 this 化的编程方式,这样就可以免受其害了。自从这么做之后,我发现用 JavaScript 编程并没有变难,反而变简单了,写出来的程序也更轻巧、优雅。因此我建议大家遵循去 this 化的原则。你会发现这是一个明智的决定,生活也会因此变得更加阳光明媚。我并不是要夺走你的 this,只是想让你成为一个无忧无虑的程序员。用“类”写代码的程序员终将走向一片凄迷的“代码坟场”。

this 真是个坏家伙。

写在最后

这些改进的最大受益者就是现在可以用更少精力来完成更多、更好工作的程序员。这些改进的最大对手同样是这些程序员,他们通常以怀疑和敌对的态度去迎接这些新范式。他们利用自己的知识和经验来提出令人信服的论点,然而事后再看,这些论点全是错的。他们待在旧范式的舒适区,不愿意接受新范式。大家都很难说出新范式和糟糕思想之间的区别。

以上每个转变都花了 20 多年才完成。函数式编程花的时间甚至翻了一番。所花时间如此之长,仅仅是因为我们的惯性思维作怪。新范式必须在老一代程序员退休或者去世后才有出头之日。

普朗克在物理学中观测到了类似的现象:葬礼越多,科学越进步。