在本期模块中,我们将仔细研究 JavaScript 世界以及其中的值。但是,在此之前,我们需要指出一头房间里的大象——
JavaScript 世界究竟是不是真实的?
JavaScript 模拟器
当我向 JavaScript 世界提问时,它会用一个值来回答我。我当然不是自己弄出这些值的。所有的变量、电线、值,它们一一构成了我的世界。我身处的 JavaScript 世界对我来说绝对是真实的——正如你所生活的世界对你而言也是真实的。
但有时,在执行下一条指令之前,会有片刻的寂静——那是下一个函数调用之前的滴答空当。就像是《黑客帝国》里的故障,我的整个世界暂停了片刻,在那一刻,我看到了比我的世界更大的景观。
在出现于我眼前的世界里,没有变量,没有值,没有表达式,也没有字面量。取而代之的,是夸克、原子、电子、水、生命。
在那里,被称为「人类」的芸芸众生用着被叫做「计算机」的特殊机器来模拟我的 JavaScript 宇宙。他们这么做,有些人是为了娱乐消遣,有些人是为了盈私牟利,也有些人根本毫无理由,只是心血来潮。每日每夜,我的整个世界在他们的操作中消失殆尽又涅槃重生成亿上万次。
或许我的 JavaScript 世界根本不是真实的。
这意味着我们有两条学习途径。
从外界学习(Studying From the Outside)
一种学习途径是从外界学习。
或许,你会专注研究起我的世界的模拟器(即 JavaScript 引擎)真正是如何工作的。比如说,你可能会了解到,这串文本(即我的世界中的值)是存储在硅芯片中的一串字节序列。
这种方法使我们将精力集中在人和计算机的物理世界上。有一些课程使用的就是这种方法。但是我的学习途径截然不同的。
从内部学习(Studying From the Inside)
我们将从内部学习 JavaScript 世界。请精神上将自己带入到 JavaScript 世界,并与我肩并肩。我们将观察这个宇宙的定律并进行实验,就像物理学家在物理宇宙中所做的一样。
我们将了解 JavaScript 世界的本质,而无需考虑其实现方式。这类似于物理学家在不回答物理世界是否真实的问题的情况下去谈论恒星的性质。没关系!我们仍然可以用它自己的术语来描述它。
我的心智模型不会试着回答诸如「计算机内存中的值如何表示?」之类的问题,因为答案一直在变啊!事实上,哪怕是你的程序正在运行,该问题的答案也会变。关于 JavaScript「真正」是如何表示内存中的数字、字符串、对象这个问题,如果你只听到了一个简而言之的概论,那么它很可能是错的。
对我来说,每个字符串都是一个值。不是「指针」或「内存地址」,而是值。在我的宇宙中,值就够了。请别被「内存单元」或者其他低级的比喻分散了你构建 JavaScript 的精准高级心智模型的注意力。这世界不过就是一只驮着一只一直驮下去的乌龟群啊!
译者按:
「这世界不过就是一只驮着一只一直驮下去的乌龟群啊!(It's turtles all the way down anyway!)」是个关于无限回归(infinite regress)认知论。来源于巨型乌龟支撑着地球的神话传说。而这只乌龟又在另一只更大的乌龟的背上,以此一直驮下去,形成无限长的乌龟。这个说法有很多不同的变体,流传较广的是霍金在《时间简史》中的一个段子:
一位著名的科学家(据说是贝特郎·罗素)曾经作过一次关于天文学方面的讲演。他描述了地球如何绕着太阳运动,以及太阳又是如何绕着我们称之为星系的巨大的恒星群的中心转动。演讲结束之时,一位坐在房间后排的矮个老妇人站起来说道:「你说的这些都是废话。这个世界实际上是驮在一只大乌龟的背上的一块平板。」这位科学家很有教养地微笑着答道:「那么这只乌龟是站在什么上面的呢?」「你很聪明,年轻人,的确很聪明,」老妇人说,「不过,这世界就是一只驮着一只一直驮下去的乌龟群啊!」
此处,作者引用了《编码:隐匿在计算机软硬件背后的语言》(Code: The Hidden Language of Computer Hardware and Software)这本介绍计算机工作原理的书籍。书中一个很重要的概念就是一层又一层架构上的抽象(abstraction)。当我们搭建某一层架构时,只需要了解它的下一层,甚至只需要了解接口即可,无需掌握深层次的知识原理。
如果你之前学过一门更低级的语言,也请试着摒弃像「引用类型」、「堆栈分配」、「写入时复制」等等这些直觉。这些模型反而让我们难以清楚地了解 JavaScript 程序中什么事情可以做,什么事情又不能做。我们将仅在真正重要的地方研究一些较低级别的细节。它们可以作为我们心智模型的补充,但不会作为其基础。
译者按:此处的「低级」并不是好坏强弱上的,而是实现层面上较原始的。低级语言是个计算机术语,一般指机器代码或汇编语言。可以说,越「接近硬件」,越低级。
作为替代,我们心智模型的基础是「我们的世界充满值」。每个值都属于几种内置类型之一。其中一些是原始的,这使得这些类型的值不可变。而变量是从代码中的命名指向值的「电线」。我们将基于此继续构建模型。
至于这些奇怪的梦,我也不再三顾虑了。还有电线等着我去连,还有问题等着我去问,还有函数等着我去调用呢。我可得抓紧了!
当我看着星星时,它们如此明亮。
当我眨眼时,它们还会如此吗?
我耸耸肩。
「实现细节。」
屈「值」可数(Counting the Values)
数数伯爵(Count von Count)是我童年的榜样。 如果你不知道《芝麻街》(美国一档著名的儿童教育电视节目),那告诉你好了,他最喜欢的消遣就是数数。今天,数数伯爵将和大家一起数一数 JavaScript 中的每一个值。
你可能会想:数数有什么意义?我们难道是在上算数课吗?数数的本质是把事物彼此区分。只有当你能清晰看到两个分开的苹果时,你才会说有「两个苹果」。把值彼此区分是理解 JavaScript 相等性的关键——这也将是我们的下一个主题。
就像维吉尔指引着但丁穿越九层地狱一样,数数伯爵将陪着我们穿越 JavaScript 的「天体」来邂逅不同的值:布尔值、数字、字符串等。就把它当成是一次观光旅行吧!
未定义(Undefined)
我们的旅途将从「未定义」类型出发。数数伯爵会很高兴地看到这种类型只有唯一一个值——undefined
。
1 | console.log(typeof(undefined)); // "undefined" |
它叫未定义,所以你可能会以为它不存在。但它是一个值,一个非常真实的值!就像黑洞一样,undefined
是很暴躁的,而且经常带来麻烦。比如,妄图从它身上读取属性的话,你的程序就会崩溃:
1 | let person = undefined; |
噢,好吧。不过幸运的是,整个 JavaScript 宇宙只有一个 undefined
。你可能会问:它究竟为什么会存在呢?在 JavaScript 中,它代表「无意漏掉的值」这个概念。
想使用它的话,你可以在代码中写下 undefined
,就像写 2
或者 "hello"
一样。但是,undefined
也会经常「自然生长」。在某些情况下,当 JavaScript 不知道你想要什么值时,它就会蹦出来。比如,如果你忘了给变量赋值,变量就会指向 undefined
:
1 | let bandersnatch; |
这之后,你可以把这个变量指向另一个值,或者你愿意的话,也可以再次指向 undefined
。
不要太纠结「未定义」这个名字。你会容易认为 undefined
是某种可变的状态,比如说,认为「该变量还没有定义」。但这是一种完全误导的思考方式!实际上,如果你读取一个真正意义上未定义的变量(或者说在 let
语句之前读取),那么会收到报错:
1 | console.log(jabberwocky); // ReferenceError! |
这跟 undefined
一点关系都没有。
真正来说,undefined
就是个普通的原始值,跟 2
或者 "hello"
是一样的。
使用时要谨慎。
空值(Null)
你可以把 null
想象成 undefined
的姐妹。它们的表现相似。比如,当你打算访问它的属性时,会抛错:
1 | let mimsy = null; |
与 undefined
相似,null
是其自身类型的唯一值。然而,null
也是个骗子。由于 JavaScript 中的一个 bug,它会被装扮成一个对象:
1 | console.log(typeof(null)); // "object" (骗人啊!) |
你或许认为这意味着 null
就是一个对象了。不要掉入陷阱啊!null
是一个原始值,它和对象的表现并不相同。不幸的是,typeof(null)
是个历史性的事故,我们将不得不永远忍受下去。
在实践中,null
被用作「有意漏掉的值」。为什么需要同时有 null
和 undefined
呢?因为这可以帮你把「(可能导致 undefined
的)编码错误」和「(可能被你表示为 null
)的缺失数据」区分开。然而,这只是一个约定,JavaScript 并不会强制这种用法。这二者,有的人会尽可能地都不去用!
我也不会责备他们。
布尔值(Booleans)
如同昼夜一样,布尔值也只有两个:true
和 false
。
1 | console.log(typeof(true)); // "boolean" |
我们可以用它们进行一些逻辑操作:
1 | let isSad = true; |
现在,数数伯爵想来测测你的心智模型了。打开画图软件或者拿出纸笔,画一下上面这段代码中的变量、值,以及它们之间的电线吧。
剧透预警!
没完成不要滚动。
...
...
...
...
...
...
...
...
...
看看你的答案与下面的图像是否相符。
首先,确认 isHappy
指向 false
,isFeeling
指向 true
,isConfusing
指向 false
。(如果你答案不同,那么是在过程中出错了,再一步步地每行过一遍看看。)
接着,确认你的图像中只有一个 true
和一个 false
。数数伯爵坚持认为这很重要。无论内存中存了多少布尔值,在我们的心智模型中都只有两个。
数字(Numbers)
目前,我们已经数了有四个值了:null
、undefined
、true
、false
。
先缓一缓,因为我们马上给心智模型再加上七百八十四载、六千九百五十六正、八万九千四百六十五涧、五十二亿六千九百六十六沟、七十二万亿九千九百六十二万穣、三千六百四十三万秭、二十一万兆三千八百二十二亿垓、一千一百三十四亿九千六百四十五万京、六万九千四百二十一万兆、三千九百亿、六百八十万、两千、五百、二十、八个值!
我说的,当然就是数字:
1 | console.log(typeof(28)); // "number" |
数字乍一看不是那么重要。但让我们再仔细看看!
有限精度(Limited Precision)
有一个流传甚广的代码片段告诉你 JavaScript 中的数字是有问题的:
1 | console.log(0.1 + 0.2 === 0.3); // false |
事实上,这个表现并不只体现在 JavaScript 中。同时,如果我们还记得 JavaScript 宇宙也只是个模拟,那么也就说得通了。
浮点数学是个聪明的发明,可以用特定长度(例如 64)的数位来表示一系列数字(包括分数)。
这种数学方法支撑着 JavaScript 中的数字。
它或许可以让你想到黑胶唱片的数字化:输入是模拟信号,但是作为数字信号的输出结果一定会是它可以存储的最接近的值:
0.1
和 0.2
都是被「四舍五入」到最接近的可用数字。而四舍五入的错误会不断累积,因此将它们相加并不能得出 0.3
。
浮动小数点(Floating Decimal Point)
浮点数学运算的另一个有趣方面是,数字的精度是「浮动的」,它取决于数字的大小。我们离 0
越近,数字的精度就越大,数字之间「挨」得也越近:
当我们从 0
开始向任一方向移动时,我们便开始丢失精度。在某个时刻,即便是两个紧挨着的数字也会相差得比 1
还要远:
1 | console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991 |
这似乎令人困惑。但是,对于大多数实际计算而言,浮点数的效果很好。它平衡了「表示范围广泛的数字」、「合理的高精度」、「可预测的内存使用」这三者之间的权衡。
在我们的 JavaScript 宇宙中,每个可以用 64 位浮点数所表示的数字都只有唯一一个数字值。
特殊数字(Special Numbers)
值得注意的是,浮点数学运算包含一些特殊数字。你可能偶尔会遇到 NaN
、Infinity
、-Infinity
、-0
。之所以它们会存在,是因为有时你可能会执行诸如 1 / 0
之类的操作,而 JavaScript 需要以某种方式表示其结果。浮点数学标准详细说明了它们如何运作的,以及使用它们时会发生什么。
下面这个例子说明了特殊数字会如何出现在你的代码中:
1 | let scale = 0; |
在所有特殊数字中,NaN
尤其有意思。NaN
是 0 / 0
这种不正确的数学计算的结果,代表「非数(Not a Number)」。
你可能会疑惑为什么「非数」是数:
1 | console.log(typeof(NaN)); // "number" |
然而,这也不是什么 bug。从 JavaScript 的角度看,NaN
确实是个数字的值,而不是空值、未定义、字符串值,或者别的什么类型。但是在浮点数学中,这个术语的名字叫做「非数」。因此它在 JavaScript 中确实是数字的值,只是因为「代表不正确的结果」这个概念在浮点数学中恰好被叫做「非数」。
让我们复习一下 JavaScript 数字:
- JavaScript 实现了一种叫做「浮点数学」的标准。越靠近
0
,数字越精确,反之越不精确。 - 像
1 / 0
或者0 / 0
这类不正确的数学操作的结果是特殊的数字。NaN
是这些特殊数字中的一员。 typeof(NaN)
是number
,因为它这个值本身确实是数字。只不过因为代表了「不正确的」数字这个含义,而被叫做「非数」。
大数(BigInts)
大数才刚被添加到 JavaScript 中,所以还并不常见。如果你用的是老旧浏览器,它们也运行不起来。普通的数字并不能很精确地表示很大的数字,所以「大数」被添加进来填补这一空白:
1 | let alot = 9007199254740991n; // 注意末尾的 n |
没啥有意思的!对于精度要求很高的金融计算,大数会很有用。但记住,天下没有免费的午餐。真正的大数字操作可能会耗费时间和资源。
我们的宇宙中有多少大数呢?技术规范说它们可以有任意精度,也就是说在我们的宇宙中,有无穷多的大数,它们一一对应着数学中的每个数。
是啊……
如果听起来很奇怪,想想你是不是已经对「数学中存在无穷个数字」这个概念毋庸置疑了呢?(如果没有,再花点时间好好想想!)从「数学宇宙」跳到「JavaScript 宇宙」也不是个大飞跃吧。
(然后,我们可以再跳到「百事宇宙」。)
当然了,实践中,我们没法在计算机内塞满所有可能的大数。如果我们这么做,某个时刻计算机会崩溃或者卡住。但是从概念上讲,数数伯爵可以永不停歇地数着大数。
字符串(Strings)
字符串代表了 JavaScript 中的文本。有三种写字符串的方式(单引号、双引号、反引号),但是结果都一样:
1 | console.log(typeof("こんにちは")); // "string" |
空字符串也是字符串:
1 | console.log(typeof('')); // "string" |
字符串不是对象(Strings Aren’t Objects)
所有字符串都有内置属性:
1 | let cat = 'Cheshire'; |
这并不意味着字符串是对象!字符串属性是特殊的,并不和对象属性的表现一致。例如,你不能给 cat[0]
赋值。字符串是原始值,而所有的原始值都是不可变的。
每个能想到的字符串都各有一个值(A Value for Every Conceivable String)
在我们的宇宙中,每个能想到的字符串都有一个唯一的值。是的,包括你奶奶的娘家姓,你十年前用别名发布的同人小说,以及还没写出来的《黑客帝国 5》的剧本。
当然,自然不能把所有可能的字符串都塞进计算机的内存芯片里。但是所有可能的字符串的想法却可以塞进你的脑袋。我们的 JavaScript 宇宙模型是给人类的,不是给计算机的!
这或许又提出了一个问题。是代码创建了字符串吗?
1 | // 在你的控制台试试 |
还是说,代码仅仅召唤出了我们宇宙中已经存在的字符串?
这个问题的答案取决于我们是在「从外部」还是「从内部」学习 JavaScript。
在我们的心智模型之外,这个答案取决于特定的实现方式。字符串的表示形式到底是单个内存块,还是多个内存块,还是一条绳子,还是其他东西,又是何时被回收的,都取决于 JavaScript 引擎。
但在我们的心智模型之内,这个问题没有任何意义。我们无法用实验来说明在 JavaScript 宇宙中字符串是「被创建」还是「被召唤」的。
为了简化我们的心智模型,我们将说所有可能的字符串值从一开始就已经存在,并且每个不同的字符串都各有一个值。
符号(Symbols)
符号是个相对来说比较新的 JavaScript 特性:
1 | let alohomora = Symbol(); |
在不深入了解对象和属性的情况下,很难解释符号的目的和行为,因此现在我们将跳过它们。对不住啦,符号君!
对象(Objects)
最后,我们来到了对象!
对象包含数组、日期、正则表达式和其他非原始值的值:
1 | console.log(typeof({})); // "object" |
不同于之前的一切,对象并不是原始值。这意味着,它们在默认情况下是可变的。我们可以用 .
或者 []
来访问它们的属性:
1 | let rapper = { name: 'Malicious' }; |
创建我们自己的对象(Making Our Own Objects)
有一件事很让数数伯爵对对象感兴趣——我们可以创建更多的对象!我们可以创建我们自己的对象!
在我们的心智模型中,所有我们讨论过的,像 null
、undefined
、布尔值、数字、字符串这种原始值,都「一直存在」。我们不能「创建」一个新的字符串或者新的数字,我们只能「召唤」这些值:
1 | let sisters = 3; |
对象的特殊之处就是,我们可以创建更多的对象。每当我们使用 {}
这种对象字面量时,我们就「创建」了全新的对象值:
1 | let shrek = {}; |
数组、日期和其他对象也都一样。比如,[]
这个数组字面量会创建一个新的数组的值,一个之前从未存在过的值。
对象会消失吗?(Do Objects Disappear?)
你可能好奇:那么对象会消失吗?还是说它们会一直在那呢?JavaScript 被设计成在代码中无法直接观测到这个现象(虽然这个事实可能会发生改变)。
总之,你无法摧毁你创建出来的对象:
1 | let junk = {}; |
甚至是你可能在其他语言中看到过的 delete
语句对于 JavaScript 中的变量也没有用:
1 | delete junk; // 没有任何用(或者在严格模式下报错) |
(delete
只对属性有用。)
JavaScript 是个垃圾回收语言。实践中,这意味着如果我没能在代码中沿着任何电线触碰到某些值,那么这些值可能最终会从我的宇宙中消失,
但是,JavaScript 并不能保证何时会回收。
大多数时候,我都无需去思考这件事情。但是如果我需要修复内存泄漏,「电线」会是个用来思考的方便比喻。
在我的宇宙中,对象和函数在我的代码附近漂浮。这提醒着我可以操作它们,也可以创建更多的它们。
函数(Functions)
把函数当作我代码之外的值来思考会特别奇怪。毕竟,函数就是我的代码啊。又或许,它们其实不是?
请看这个例子:
1 | for (let i = 0; i < 7; i++) { |
我们创建出了多少个对象?我们还没谈到「范围」,所以如果你无法想出这段代码的图像,也不要紧。答案是我们创建出了七个对象——循环的每次迭代各一个。
再来看看这个例子:
1 | for (let i = 0; i < 7; i++) { |
你看到了几个函数呢?一个?七个?
剧透预警!
...
...
...
...
...
...
...
...
...
上面的代码包含了一个函数定义,但是它创建了七个函数值!这也是为什么把函数从代码的概念中剥离很重要。
每当我们执行一行包含函数声明的代码时,一个全新的函数值在我们的宇宙中出现了。
这或许也能提醒你,每当我们执行像 let dwarf = {}
的代码时,一个全新的对象是如何出现的。我们创建对象,我们也创建函数。在未来的模块中,我们将继续深入对象和函数的细节。
复习(Recap)
好长的旅途啊!我们看尽了 JavaScript 中的每个类型。让我们再和数数伯爵一起回顾一下每种类型都有哪些值吧:
- 未定义(Undefined):只有一个值,
undefined
. - 空值(Null):只有一个值,
null
. - 布尔值(Booleans):有两个值,
true
和false
. - 数字(Numbers):浮点数学中的每个数都各有一个值.
- 大数(BigInts):所有能想到的数字都各有一个值。
- 字符串(Strings):所有能想到的字符串都各有一个值。
下面这些类型比较特殊,因为我们可以创建我们自己的值:
- 对象(Objects):我们所创建的所有对象都各有一个值。
- 函数(Function):我们所遍历的所有函数定义都各有一个值。
造访 JavaScript 的不同「天体」真是有趣啊。既然我们已经数完了所有值,我们也就知道它们彼此不同的原因了。比如,写下 2
或者 "hello"
总是会「召唤」出相同的数字或者字符串的值,而写下 {}
或者声明函数总是会「创建」一个全新而不同的值。这个想法对于理解 JavaScript 中的「相等性」至关重要,这将是下一个模块的主题。
练习(Exercises)
本期模块同样提供有练习给你!
点击这里用几个小测验来巩固心智模型吧。
小测验见附。
不要跳过!
即使你可能熟悉值的不同类型,这些练习也可以帮助你巩固我们正在构建的心智模型。在进入更复杂的主题之前,我们需要这个基础。
下期,我们将学习 JavaScript 中的「相等性」。
小测验
画出下面代码运行后的变量和值的示意图。
如果没有笔和纸,可以用 https://www.excalidraw.com 这类在线绘图软件绘画。
1
2
3let dwarves = 7;
let continents = '7';
let worldWonders = 3 + 4;以下哪一个更符合你的图像呢?
画出下面代码运行后的变量和值的示意图。
如果没有笔和纸,可以用 https://www.excalidraw.com 这类在线绘图软件绘画。
1
2
3let shampoo;
let soap = null;
soap = shampoo;以下哪一个更符合你的图像呢?
画出下面代码运行后的变量和值的示意图。
如果没有笔和纸,可以用 https://www.excalidraw.com 这类在线绘图软件绘画。
1
2
3
4
5let isSad = false;
let isHappy = !isSad;
let isFeeling = isSad || isHappy;
let isConfusing = isSad && isHappy;
isSad = true;以下哪一个更符合你的图像呢?
画出下面代码运行后的变量和值的示意图。
如果没有笔和纸,可以用 https://www.excalidraw.com 这类在线绘图软件绘画。
1
2
3let spaghetti = function () { return 2 + 2 };
let fettuccine = spaghetti;
let gnocchi = function () { return 2 + 2 };以下哪一个更符合你的图像呢?
告诉我到目前为止你对本期模块和 Just JavaScript 的看法。
觉得有什么地方讲得很有见地吗?还是令人困惑?我很想知道!
答案
答案:图 A 正确。它表示
dwarves
和worldWonders
指向数字值7
,continents
指向字符串值"7"
。图 B 错误点是
7
和"7"
应该是两个不同的值——一个是数字,另一个是字符串。并不应该是同个值。图 C 不符合我们的心智模型。因为它表示出了多个
7
。在我们的心智模型中,每个不同的数字只会各自有一个值。图D 错误点是变量之间不能相互指。变量必须指向值。
答案:图 A 正确。它表示变量
soap
和shampoo
都指向undefined
这个值。图 B 错在它表示出了两个
undefined
。我们的宇宙中只有一个undefined
值。图 C 和图 D 都错在
shampoo
没有指向任何值。在一个变量声明之后,它总是会指向一个值的。答案:图 C 正确。它表示除了
isConfusing
外的所有变量都指向true
值,isConfusing
指向false
值。图 A 错在变量应该指向值,而不是表达式。
图 B 错在出现多个
true
值。我们的心智模型中,只有一个true
和一个false
。图 D 错在计算错误。
答案:图 B 正确。它表示
spaghetti
和fettucine
指向同个函数值,而gnocchi
指向另一个函数值。图 A 和 C 错在它们表示变量指向了值
4
。但是fettucine
应该仅指向一个函数值。我们没有调用那个函数,因此它的返回值是无关的。图 D 错在它表示所有变量都指向同个函数值。但是,每个不同的函数定义应该创建一个新函数值。所以
spaghetti
和gnocchi
应该指向不同的函数值。