【Just JavaScript #03】值和变量 Values and Variables

本期模块,我们以下面的代码片段作为开头:

1
2
3
let reaction = 'yikes';
reaction[0] = 'l';
console.log(reaction);

你觉得结果会是什么?因为我们还没开始学,所以不确定的话也没关系。但可以试用你现有的 JavaScript 知识答一答。

现在我希望你花点时间,一步步地写下你对每一行代码的思考过程。同时注意你心智模型中的任何缺陷或者不确定的地方,并把它们写下来。如果你有任何疑惑的话,也试着尽可能清楚地表达出来。

剧透预警!

没写完的话,不要继续滚动哦。

...

...

...

...

...

...

...

...

...

答案来啦。这段代码会打印 "yikes",或者如果你正处于 strict mode (严格模式)的话,会报错。总之不会打印 "likes" 的。

Yikes.(哎呀。)

原始值是不可变的(Primitive Values Are Immutable)

你答对了嘛?这看起来像是那种在 JavaScript 面试中会被问到的琐碎问题,在现实工作中反而不会接触太多。即使如此,它也说明了有关原始值的重要一点。

我们无法改变原始值。

我会用一个小例子解释这句话。字符串(是原始值)和数组(不是原始值,是对象)在表面上有一些相似之处。一个数组是一串项目(item),一个字符串是一串字符(character)。

1
2
let arr = [212, 8, 506];
let str = 'hello';

你可以像访问字符串的首个字符那样访问首个数组项。感觉上字符串几乎就是数组(实际不是):

1
2
console.log(arr[0]); // 212
console.log(str[0]); // "h"

你可以改变首个数组项:

1
2
arr[0] = 420;
console.log(arr); // [420, 8, 506]

所以,直觉上说,你也可以很容易地对字符串做同样的事情:

1
str[0] = 'j'; // ???

但是你并不能。

这就是我们要添加到心智模型上的重要一点:字符串是原始值。这将很有意义。

所有原始值都是不可变的。「不可变(Immutable)」是拉丁语中「无法改变(unchangeable)」的一种说法,表示「只读」。你不能随便搅和原始值。完全不能。

如果你打算在原始值上设置一个属性,无论是数字、字符串还是其他内容,JavaScript 都不允许你这么做。它要么沉默回绝,要么高声报错,具体如何取决于你的代码身处哪个模式

总之,这样做永远行不通:

1
2
let fifty = 50;
fifty.shades = 'gray'; // No!

50 是作为数字的原始值,你不能它在上面设置属性。

请您欣赏 MC Hammer - U Can't Touch This(无法触摸)

在我的 JavaScript 宇宙中,所有的原始值都位于离我的代码更远的外圈中,就像远处的星星一样。这提醒着我,即使我可以从代码中引用它们,也无法改变它们。它们纹丝不动。

我有一种奇异的安逸感。

矛盾之处?(A Contradiction?)

我刚刚证明了原始值是只读的,或者,按这个时代的说法,是不可变的。下面是一个检测你的心智模型的代码片段:

1
2
3
let pet = 'Narwhal';
pet = 'The Kraken';
console.log(pet); // ?

跟之前一样,用几句话写下你的思考过程。别急。注意你对每一行的每一步思考。字符串的不可变性有没有在这里体现呢?

剧透预警。

...

...

...

...

...

...

...

...

...

如果你觉得我是想把你头弄晕,那你完全正确!答案是 "The Kraken"——字符串的不可变性在这里没有体现。

如果你答错了也不要气馁。因为刚刚的例子确实很自相矛盾。

这是一个重要的认识。

当你刚接触一门语言的时候,可能会很想忽略一些矛盾之处。毕竟,如果总纠结这些矛盾的话,就陷入一个学无所成的黑洞了。但是,既然你现在正致力于构建一种心智模型,那么就需要质疑矛盾。正是矛盾揭示了心智模型的缺陷。

变量是电线(Variables Are Wires)

让我们再看看这个例子:

1
2
3
let pet = 'Narwhal';
pet = 'The Kraken';
console.log(pet); // "The Kraken"

我们知道字符串是原始值,所以不能改变。但是变量 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
2
3
4
5
6
7
function double(x) {
x = x * 2;
}

let money = 10;
double(money);
console.log(money); // ?

如果我们认为 double(money) 传递的是一个变量,我们可以料想到的是 x = x * 2 会使这个变量翻倍。但事实并非如此。我们知道 double(money) 的意思是「算出 money,然后向 double 传递这个值」。所以答案是 10。真是个陷阱呀!

你脑中有没有什么不同的 JavaScript 名词和动词呢?它们之间又是如何联系的呢?简要列出你常用的一些吧。

结合起来说(Putting It Together)

现在我们回顾心智模型的第一个例子:

1
2
3
let x = 10;
let y = x;
x = 0;

我建议你拿一张纸,或者用一个绘画软件来画一个图像,表现出变量 xy 的「电线」每一步发生了什么。

第一行基本没做什么:

  • 声明一个叫 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。我们不能把变量相互指!变量总是指向*值*的。当我们看到一条赋值语句,我们先「问问」右侧的值是多少,再把左侧的「电线」指向结果。

上期,我提过把变量当成盒子是很常见的思路。我们正在构建的宇宙却没有任何盒子,只有电线!这看上去似乎有点恼人。我们为啥不能把 010 这俩值放入变量,而不是指向变量呢?

为了解释诸如「严格相等」、「对象标识」、「突变」等众多其他概念,使用电线将尤为重要。我们将坚持使用电线,因此你不妨现在就开始习惯它们!

我的宇宙充满了电线。

回顾(Recap)

  • 原始值是不可变的。我们无法在代码中影响或改变它们。它们原封不动。比如,我们无法在字符串值上设置属性,因为它是原始值。数组是原始的,因此我们可以设置它们的属性。
  • 变量不是值。每个变量都指向一个特定的值。我们可以使用赋值运算符 = 来更改其指向的值。
  • 变量就像电线。「电线」虽然不是 JavaScript 的概念,但是可以帮助我们想象变量如何指向值。还有另一种不是变量的「电线」,但我们尚未讨论。
  • 注意矛盾之处。如果你学到的两件事似乎相互矛盾,不要灰心。通常,这表明其后藏着一个更深层的真理。
  • 名词和动词很重要。我们正在建立一个心智模型,以便对宇宙中会发生不会发生的事情都一样充满信心。口头表述草率一点也可以,但是我们的思路必须准确。

练习(Exercises)

本期模块同样提供有练习给你!

点击这里用几个小测验来巩固心智模型吧。

小测验见附。

不要跳过!

即使你可能熟悉变量的概念,这些练习也可以帮助你巩固我们正在构建的心智模型。在进入更复杂的主题之前,我们需要这个基础。

下期,我们将进一步学习值的不同类型,并且挨个看看各有什么特别之处。

小测验

  1. 下面的代码运行后会发生什么?

    这段代码本身是否正确呢?为什么?

    1
    2
    3
    let numberOfTentacles = 10;
    numberOfTentables = 'eight';
    console.log(typeof(numberOfTentables));
  2. 下面这个例子有些许不同,运行后会发生什么?

    不同之处何在?尝试用心智模型进行解释。

    1
    2
    3
    let numberOfTentacles = 10;
    console.log(typeof(numberOfTentables));
    numberOfTentables = 'eight';
  3. 下一段代码的运行结果是?

    试着用心智模型解释。

    1
    2
    3
    let answer = true;
    answer.opposite = false;
    console.log(answer.opposite);
  4. 下一段代码的运行结果是?

    试着用心智模型解释。

    1
    2
    null = 10;
    console.log(null)
  5. 画出下面代码运行后的变量和值的示意图。

    如果没有笔和纸,可以用 https://www.excalidraw.com 这类在线绘图软件绘画。

    1
    2
    3
    let it = 'be';
    let them = 'eat cake';
    it = them;

    以下哪一个更符合你的图像呢?

  6. 这段代码会打印 "T"。我们的同事在另一个文件里写了函数 feed。我们并不知道它是用来干嘛的。

    我们的同事可以仅通过编辑函数 feed 来改变输出嘛?为什么?

    1
    2
    3
    let pets = 'Tom and Jerry';
    feed(pets);
    console.log(pets[0]);
  7. 这段代码会打印 "Tom"。我们的同事在另一个文件里写了函数 feed。我们并不知道它是用来干嘛的。

    我们的同事可以仅通过编辑函数 feed 来改变输出嘛?为什么?

    1
    2
    3
    let pets = ['Tom', 'Jerry'];
    feed(pets);
    console.log(pets[0]);
  8. 告诉我到目前为止你对本期模块和 Just JavaScript 的看法。

    觉得有什么地方讲得很有见地吗?还是令人困惑?我很想知道!

答案

  1. 答案:代码正确。会打印 "string"

    在 JavaScript 中,变量并没有类型,只有值有类型。typeof(numberOfTentacles) 的答案取决于当时该变量指向了哪个值

    当我们询问变量 numberOfTentacles 的类型时,它正指向字符串值 "eight"。所以我们拿到的结果是 "string"

  2. 答案:会打印 "number"

    在 JavaScript 中,变量并没有类型,只有值有类型。typeof(numberOfTentacles) 的答案取决于当时该变量指向了哪个值

    当我们询问变量 numberOfTentacles 的类型时,它正指向数字值 10。所以我们拿到的结果是 "number"

  3. 答案:这段代码无法设置属性。

    布尔值是原始的。原始值又是不可变的。我们无法改变它们,而在值上设置属性正是所谓的「改变」。

    如果我们的代码运行在严格模式(strict mode),在原始值傻姑娘设置属性将导致一个错误。如果没有在严格模式下运行的话,代码会什么都不会做。不管是什么模式下,总之我们不能在布尔值上设置属性。

  4. 答案:这段代码会报错。

    报错的原因是赋值语句的左侧必须得是「电线」。变量是「电线」,所以可以出现在左侧。但是像 null 这样的字面量并不是「电线」,所以想为它赋值的话,将毫无意义。

  5. 答案:图像 B 是正确的。它表示两个变量都指向同个字符串值 "eat cake"

    图像 A 不符合我们正在构建的心智模型。它表示值被放入了变量,但是我们的心智模型中,值是指向变量的。

    图像 C 错误点事它表示一个变量指向另一个变量。变量只能指向,不能指向其他变量。

    图像 D 错误点与上面二者类似。

  6. 答案:不能。我们的同事无法改变这段代码打印 T 这个事实。

    原因是,当我们调用一个函数时,我们总是传递,而不是变量。例子中,我们传递的是一个字符串值——并且和所有原始量一样,字符串是不可变的。我们的同事无法搅和字符串 "Tom and Jerry",所以它的首字符将永远是 'T'

  7. 答案:能。我们的同事可以让这段代码打印其他东西。

    注意我们的同事无法更改变量所指向的是哪个值。我们总是传递值,而不是变量。然而,我们的同事可以影响数组本身,并更改其中的元素。

    原因是,不同于字符串,数组是可变的。数组并不是原始值,而是对象!以防你忘记了,下面这份列表将再次提醒你哪些值是原始的。任何不在这份列表中的都是对象。而我们的同事是可以更改对象的。

    • 原始值(Primitive Values)
      • 未定义(Undefined) (undefined),用于无意中漏掉的值。
      • 空值(Null) (null),用于有意漏掉的值。
      • 布尔值(Booleans) (truefalse),用于逻辑操作符。
      • 数字(Numbers) (-1003.14 之类的),用于数学计算。
      • 字符串(Strings) ("hello""abracadabra" 之类的),用于文本。
      • 符号(Symbols) (不常用),用于隐藏实现细节。
      • 大型整数(BigInts) (不常用,是新的),用于数学中的大数字。
    • 对象和函数(Objects and Functions)
      • 对象(Objects) ({} 之类的),用于将相关的数据和代码分组。
      • 函数(Functions) (x => x * 2 之类的),用于引用代码。

最后两题如果答错了也不要担心。

细心的读者可能发现,我们还没有构建出能足够自信回答最后两题的心智模型。这俩问题暴露出了我们还需要查漏补缺的一些地方!比如我们还没有提到过「属性」,以及「传递」一个值的含义。

注意你回答问题时用到的心智模型。如果你的答案中出现了我们还没讨论过的字眼,那么可能表明你用混了不同的心智模型。这并不是坏事——毕竟,我们正在构建的心智模型还不够完善。但是,还请注意一下差别。

我们的目标,就是一步步地,把这些差距填补起来。