本期模块,我们以下面的代码片段作为开头:
1 | let reaction = 'yikes'; |
你觉得结果会是什么?因为我们还没开始学,所以不确定的话也没关系。但可以试用你现有的 JavaScript 知识答一答。
现在我希望你花点时间,一步步地写下你对每一行代码的思考过程。同时注意你心智模型中的任何缺陷或者不确定的地方,并把它们写下来。如果你有任何疑惑的话,也试着尽可能清楚地表达出来。
剧透预警!
没写完的话,不要继续滚动哦。
...
...
...
...
...
...
...
...
...
答案来啦。这段代码会打印 "yikes"
,或者如果你正处于 strict mode (严格模式)的话,会报错。总之不会打印 "likes"
的。
Yikes.(哎呀。)
原始值是不可变的(Primitive Values Are Immutable)
你答对了嘛?这看起来像是那种在 JavaScript 面试中会被问到的琐碎问题,在现实工作中反而不会接触太多。即使如此,它也说明了有关原始值的重要一点。
我们无法改变原始值。
我会用一个小例子解释这句话。字符串(是原始值)和数组(不是原始值,是对象)在表面上有一些相似之处。一个数组是一串项目(item),一个字符串是一串字符(character)。
1 | let arr = [212, 8, 506]; |
你可以像访问字符串的首个字符那样访问首个数组项。感觉上字符串几乎就是数组(实际不是):
1 | console.log(arr[0]); // 212 |
你可以改变首个数组项:
1 | arr[0] = 420; |
所以,直觉上说,你也可以很容易地对字符串做同样的事情:
1 | str[0] = 'j'; // ??? |
但是你并不能。
这就是我们要添加到心智模型上的重要一点:字符串是原始值。这将很有意义。
所有原始值都是不可变的。「不可变(Immutable)」是拉丁语中「无法改变(unchangeable)」的一种说法,表示「只读」。你不能随便搅和原始值。完全不能。
如果你打算在原始值上设置一个属性,无论是数字、字符串还是其他内容,JavaScript 都不允许你这么做。它要么沉默回绝,要么高声报错,具体如何取决于你的代码身处哪个模式。
总之,这样做永远行不通:
1 | let fifty = 50; |
50
是作为数字的原始值,你不能它在上面设置属性。
请您欣赏 MC Hammer - U Can't Touch This(无法触摸)
在我的 JavaScript 宇宙中,所有的原始值都位于离我的代码更远的外圈中,就像远处的星星一样。这提醒着我,即使我可以从代码中引用它们,也无法改变它们。它们纹丝不动。
我有一种奇异的安逸感。
矛盾之处?(A Contradiction?)
我刚刚证明了原始值是只读的,或者,按这个时代的说法,是不可变的。下面是一个检测你的心智模型的代码片段:
1 | let pet = 'Narwhal'; |
跟之前一样,用几句话写下你的思考过程。别急。注意你对每一行的每一步思考。字符串的不可变性有没有在这里体现呢?
剧透预警。
...
...
...
...
...
...
...
...
...
如果你觉得我是想把你头弄晕,那你完全正确!答案是 "The Kraken"
——字符串的不可变性在这里没有体现。
如果你答错了也不要气馁。因为刚刚的例子确实很自相矛盾。
这是一个重要的认识。
当你刚接触一门语言的时候,可能会很想忽略一些矛盾之处。毕竟,如果总纠结这些矛盾的话,就陷入一个学无所成的黑洞了。但是,既然你现在正致力于构建一种心智模型,那么就需要质疑矛盾。正是矛盾揭示了心智模型的缺陷。
变量是电线(Variables Are Wires)
让我们再看看这个例子:
1 | let pet = 'Narwhal'; |
我们知道字符串是原始值,所以不能改变。但是变量 pet
确实变成了 "The Kraken"
。咋回事呢?
这看上去是个矛盾,但其实不是。我们只说过不能改变的是原始值,但还没有说过变量。
为了完善心智模型,我们需要解开一个相关概念。
变量不是值。
变量指向值。
在我的宇宙中,一个变量是一根电线。它有两个端点,并且有一条方向:从我代码中的一个命名出发,最终指向我宇宙中的某个值。
比如说,我可以把变量 pet
指向 "Narwhal"
这个值。
1 | let pet = 'Narwhal'; |
在这之后,有两件事情我可以对变量做。
给变量赋值(Assigning a Value to a Variable)
我可以做的一件事情是赋予变量某个其他的值。
1 | pet = 'The Kraken'; |
我在这里所做的只是告诉 JavaScript 把左侧的「电线」(即变量 pet
)指向右侧的值(即 "The Kraken"
)。除非我之后再重新赋值,否则它将一直指向该值。
注意,我不能在左侧放任何东西:
1 | 'war' = 'peace'; // 不可以。(在控制台试一下吧。) |
一条赋值语句的左侧必须是根「电线」。目前,我们只知道变量是「电线」。但是,我们将在之后的模块中说一说另一种「电线」。或许,你可以猜到它是什么?(提示:它涉及到了 []
或者 .
,并且我们已经见过它好几次了。)
还有另一条规则。
一条赋值语句的右侧必须是个表达式。它可以简单,比如 2
或者 'hello'
,也可以复杂,比如:
1 | pet = count + ' Dalmatians'; |
此处,count + ' Dalmatians'
是一个表达式,即向 JavaScript 提的问题。JavaScript 会用一个值来回答(比如 "101 Dalmatians"
)。然后,名为 pet
的电线就会指向这个值。
如果说右侧必须是表达式,是否意味着在代码中,像 2
的数字或者像 "The Kraken"
的字符串也是表达式呢?对的!这种表达式被称作字面量(literals)——因为我们字面地写出了它们的值。
读取变量的值(Reading a Value of a Variable)
我也可以读取一个变量的值,比如,打印一下:
1 | console.log(pet); |
不足为奇。
但是注意,我们传递给 console.log
的,并不是变量 pet
。虽然通俗地我们可以这么说,但实际上我们并不能向函数传递变量。我们传递的是变量 pet
的当前值。怎么回事呢?
事实上,像 pet
这样的变量名也可以是表达式!当我们写下 pet
时,我们是向 JavaScript 问了这么个问题:「pet
的当前值是多少?」为了回答这个问题,JavaScript 沿着 pet
的「电线」,找到末端的值后反馈给我们。
所以,同样的表达式,在不同的时间,会给我们不同的值!
名词和动词(Nouns and Verbs)
谁会在乎你所说的「传递变量」和「传递值」这俩概念呢?纠结这种差异的话,难道不是无药可救的书呆子嘛?我当然不鼓励你去打扰同事并纠正他们,甚至不鼓励你对你自己这么做。这么做会浪费每个人的时间。
但是在你的脑海中,你需要弄清楚,针对每个概念,你可以做什么。你不能溜自行车。你不能飞鳄梨。你不能唱蚊子。你也不能传递变量——至少,在 JavaScript 中不能。
下面是个说明这些细节为什么重要的小例子:
1 | function double(x) { |
如果我们认为 double(money)
传递的是一个变量,我们可以料想到的是 x = x * 2
会使这个变量翻倍。但事实并非如此。我们知道 double(money)
的意思是「算出 money
的值,然后向 double
传递这个值」。所以答案是 10
。真是个陷阱呀!
你脑中有没有什么不同的 JavaScript 名词和动词呢?它们之间又是如何联系的呢?简要列出你常用的一些吧。
结合起来说(Putting It Together)
现在我们回顾心智模型的第一个例子:
1 | let x = 10; |
我建议你拿一张纸,或者用一个绘画软件来画一个图像,表现出变量 x
和 y
的「电线」每一步发生了什么。
第一行基本没做什么:
- 声明一个叫
x
的变量。- 为变量
x
搞一根电线。
- 为变量
- 给
x
赋值10
。- 把
x
的电线指向10
这个值。
- 把
第二行很短,却做了很多事情:
- 声明一个叫
y
的变量。- 为变量
y
搞一根电线。
- 为变量
- 给
y
赋予x
的值10
。- 计算表达式:
x
。- 我们想问的「问题」是
x
。 - 沿着
x
的电线,找到答案是10
这个值。
- 我们想问的「问题」是
- 表达式
x
的结果是10
这个值。 - 因此,给
y
赋予x
的值10
。 - 把
y
的电线指向10
这个值。
- 计算表达式:
最后,我们来到第三行:
- 给
x
赋值0
。- 把
x
的电线指向0
这个值。
- 把
最后,变量 x
指向 0
这个值,变量 y
指向 10
这个值。注意, y = x
并不表示把 y
指向 x
。我们不能把变量相互指!变量总是指向*值*的。当我们看到一条赋值语句,我们先「问问」右侧的值是多少,再把左侧的「电线」指向结果。
上期,我提过把变量当成盒子是很常见的思路。我们正在构建的宇宙却没有任何盒子,只有电线!这看上去似乎有点恼人。我们为啥不能把 0
和 10
这俩值放入变量,而不是指向变量呢?
为了解释诸如「严格相等」、「对象标识」、「突变」等众多其他概念,使用电线将尤为重要。我们将坚持使用电线,因此你不妨现在就开始习惯它们!
我的宇宙充满了电线。
回顾(Recap)
- 原始值是不可变的。我们无法在代码中影响或改变它们。它们原封不动。比如,我们无法在字符串值上设置属性,因为它是原始值。数组不是原始的,因此我们可以设置它们的属性。
- 变量不是值。每个变量都指向一个特定的值。我们可以使用赋值运算符
=
来更改其指向的值。 - 变量就像电线。「电线」虽然不是 JavaScript 的概念,但是可以帮助我们想象变量如何指向值。还有另一种不是变量的「电线」,但我们尚未讨论。
- 注意矛盾之处。如果你学到的两件事似乎相互矛盾,不要灰心。通常,这表明其后藏着一个更深层的真理。
- 名词和动词很重要。我们正在建立一个心智模型,以便对宇宙中会发生或不会发生的事情都一样充满信心。口头表述草率一点也可以,但是我们的思路必须准确。
练习(Exercises)
本期模块同样提供有练习给你!
点击这里用几个小测验来巩固心智模型吧。
小测验见附。
不要跳过!
即使你可能熟悉变量的概念,这些练习也可以帮助你巩固我们正在构建的心智模型。在进入更复杂的主题之前,我们需要这个基础。
下期,我们将进一步学习值的不同类型,并且挨个看看各有什么特别之处。
小测验
下面的代码运行后会发生什么?
这段代码本身是否正确呢?为什么?
1
2
3let numberOfTentacles = 10;
numberOfTentables = 'eight';
console.log(typeof(numberOfTentables));下面这个例子有些许不同,运行后会发生什么?
不同之处何在?尝试用心智模型进行解释。
1
2
3let numberOfTentacles = 10;
console.log(typeof(numberOfTentables));
numberOfTentables = 'eight';下一段代码的运行结果是?
试着用心智模型解释。
1
2
3let answer = true;
answer.opposite = false;
console.log(answer.opposite);下一段代码的运行结果是?
试着用心智模型解释。
1
2null = 10;
console.log(null)画出下面代码运行后的变量和值的示意图。
如果没有笔和纸,可以用 https://www.excalidraw.com 这类在线绘图软件绘画。
1
2
3let it = 'be';
let them = 'eat cake';
it = them;以下哪一个更符合你的图像呢?
这段代码会打印
"T"
。我们的同事在另一个文件里写了函数feed
。我们并不知道它是用来干嘛的。我们的同事可以仅通过编辑函数
feed
来改变输出嘛?为什么?1
2
3let pets = 'Tom and Jerry';
feed(pets);
console.log(pets[0]);这段代码会打印
"Tom"
。我们的同事在另一个文件里写了函数feed
。我们并不知道它是用来干嘛的。我们的同事可以仅通过编辑函数
feed
来改变输出嘛?为什么?1
2
3let pets = ['Tom', 'Jerry'];
feed(pets);
console.log(pets[0]);告诉我到目前为止你对本期模块和 Just JavaScript 的看法。
觉得有什么地方讲得很有见地吗?还是令人困惑?我很想知道!
答案
答案:代码正确。会打印
"string"
。在 JavaScript 中,变量并没有类型,只有值有类型。
typeof(numberOfTentacles)
的答案取决于当时该变量指向了哪个值。当我们询问变量
numberOfTentacles
的类型时,它正指向字符串值"eight"
。所以我们拿到的结果是"string"
。答案:会打印
"number"
。在 JavaScript 中,变量并没有类型,只有值有类型。
typeof(numberOfTentacles)
的答案取决于当时该变量指向了哪个值。当我们询问变量
numberOfTentacles
的类型时,它正指向数字值10
。所以我们拿到的结果是"number"
。答案:这段代码无法设置属性。
布尔值是原始的。原始值又是不可变的。我们无法改变它们,而在值上设置属性正是所谓的「改变」。
如果我们的代码运行在严格模式(strict mode),在原始值傻姑娘设置属性将导致一个错误。如果没有在严格模式下运行的话,代码会什么都不会做。不管是什么模式下,总之我们不能在布尔值上设置属性。
答案:这段代码会报错。
报错的原因是赋值语句的左侧必须得是「电线」。变量是「电线」,所以可以出现在左侧。但是像
null
这样的字面量并不是「电线」,所以想为它赋值的话,将毫无意义。答案:图像 B 是正确的。它表示两个变量都指向同个字符串值
"eat cake"
。图像 A 不符合我们正在构建的心智模型。它表示值被放入了变量,但是我们的心智模型中,值是指向变量的。
图像 C 错误点事它表示一个变量指向另一个变量。变量只能指向值,不能指向其他变量。
图像 D 错误点与上面二者类似。
答案:不能。我们的同事无法改变这段代码打印
T
这个事实。原因是,当我们调用一个函数时,我们总是传递值,而不是变量。例子中,我们传递的是一个字符串值——并且和所有原始量一样,字符串是不可变的。我们的同事无法搅和字符串
"Tom and Jerry"
,所以它的首字符将永远是'T'
。答案:能。我们的同事可以让这段代码打印其他东西。
注意我们的同事无法更改变量所指向的是哪个值。我们总是传递值,而不是变量。然而,我们的同事可以影响数组本身,并更改其中的元素。
原因是,不同于字符串,数组是可变的。数组并不是原始值,而是对象!以防你忘记了,下面这份列表将再次提醒你哪些值是原始的。任何不在这份列表中的都是对象。而我们的同事是可以更改对象的。
- 原始值(Primitive Values)
- 未定义(Undefined) (
undefined
),用于无意中漏掉的值。 - 空值(Null) (
null
),用于有意漏掉的值。 - 布尔值(Booleans) (
true
和false
),用于逻辑操作符。 - 数字(Numbers) (
-100
、3.14
之类的),用于数学计算。 - 字符串(Strings) (
"hello"
、"abracadabra"
之类的),用于文本。 - 符号(Symbols) (不常用),用于隐藏实现细节。
- 大型整数(BigInts) (不常用,是新的),用于数学中的大数字。
- 未定义(Undefined) (
- 对象和函数(Objects and Functions)
- 对象(Objects) (
{}
之类的),用于将相关的数据和代码分组。 - 函数(Functions) (
x => x * 2
之类的),用于引用代码。
- 对象(Objects) (
- 原始值(Primitive Values)
最后两题如果答错了也不要担心。
细心的读者可能发现,我们还没有构建出能足够自信回答最后两题的心智模型。这俩问题暴露出了我们还需要查漏补缺的一些地方!比如我们还没有提到过「属性」,以及「传递」一个值的含义。
注意你回答问题时用到的心智模型。如果你的答案中出现了我们还没讨论过的字眼,那么可能表明你用混了不同的心智模型。这并不是坏事——毕竟,我们正在构建的心智模型还不够完善。但是,还请注意一下差别。
我们的目标,就是一步步地,把这些差距填补起来。