前言 && 心智模型 Mental Model

心智模型

心智模型/心智模式的理论是基于一个试图对某事做出合理解释的个人会发展可行的方法的假设,在有限的领域知识和有限的信息处理能力上,产生合理的解释。心智模型是对思维的高级建构,心智模型表征了主观的知识。通过不同的理解解释了心智模型的概念、特性、功用。心智模型是个体为了要了解和解释他们的经验,所建构的知识结构,该模型受限于个体关于他们经验的内隐理论(Implicit Theories),这可能有很多或很少的正确性。

简单的说就是对于事物基于自己心智的理解。

作者举了一个例子。

如下的一段代码:

1
2
3
let a = 10;
let b = a;
a = 0;

在看到这段代码是,注意自己脑中发生了什么,比如会产生这样的独白:

  • let a = 10
    • 声明一个变量为 a,把它设为 10
  • let b = a
    • 声明一个变量 b,把它设为 a
    • a 是多少来着?是 10,所以 b也就是 10
  • a = 0
    • a 设置 0
  • 所以现在 a0b10。这就是我们的答案

也许我们的独白会不太一样:比如会说 赋予 而不是 设为。或者可能逻辑顺序也不一样,也可能答案都不一样。

对每个基本的编程概念(比如变量)和基于此的操作(比如赋值)都有一套深根于你脑子中的类比。有一些或许来自于现实,有一些或许演化于你之前学过的其他领域,比如数学中的数字。这些类比可能会重叠并相互矛盾,但它们还是可以帮你理解代码中发生了什么。

这些直觉(比如变量的盒子化)影响了我们这一生阅读代码的方式。但有时,我们的心智模型是错误的。

一个好的心智模型会帮你更快地找出并修复 bug,更好地理解他人的代码,并且更自信于你的代码。

快编慢码(Coding, Fast and Slow)

一旦可以,我们就会依赖于「快」系统。我们这套系统和很多动物一样,它给了我们像一直走路而不摔倒这种神奇的能力。这种「快」系统善于模式匹配(对于生存来说很有必要)和「本能反应」。但不善于筹划。

独特的是,由于额叶的发展,人类也拥有「慢」思维系统。 这种「慢」系统负责复杂的逐步推理。它使我们能够规划未来的事件、进行争论、遵循数学证明。

由于使用「慢」系统非常费力,因此即使在处理诸如编程之类的智力任务时,我们也倾向于默认使用「快」系统。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function duplicateSpreadsheet(original) {
  if (original.hasPendingChanges) {
    throw new Error('You need to save the file before you can duplicate it.');
  }
  let copy = {
    created: Date.now(),
    author: original.author,
    cells: original.cells,
    metadata: original.metadata,
  };
  copy.metadata.title = 'Copy of ' + original.metadata.title;
  return copy;
}

比如这么一段代码,我们可能会发现这个函数用于复制电子表格,如果是未保存的原始电子表格,会报错。

我们可能不会注意到这个函数会不慎修改原电子表格的标题。

在「快」模式下,我们基于命名、注释、整体结构来猜想一段代码的用途。在「慢」模式下,我们一步步地追溯代码的运作方式。

JavaScript 宇宙

代码与值(Code and Values)

img

原始值(Primitive Values)

原始值(Primitive Values) 就是数字(number)或者字符串(string)等等。所有原始值都有一些共同点。我的代码无法影响他们(指原始值是 immutable)。在宇宙星球中坐着比喻为星星一样,无情二遥远,但是当我们需要时,它们总是会在那里。

对象和函数(Objects and Functions)

对象(object)和函数(function)也是值,但它们并不原始。当我们在控制台打印如下代码时:

1
2
3
console.log({});
console.log([]);
console.log(x => x * 2);

显示它们的方式与原始值不同。某些浏览器会在他们之前显示一个箭头,或者在单击它们的时候展现地很特别。与原始值不同的是对象和函数就像是漂浮在我们代码附件的石块(原始值像是星星)。它们更加近 ,我们可以操纵它们。

表达式(Expressions)

我们可以使用表达式来提问,JavaScript 将用值来回答,比如 2+2 将被用 4 来回答。(不知道这里提这个是什么含义?

img

值的类型(Types of Values)

可以使用 typeof来检查一个值的类型。

原始值(Primitive Values)

  • 未定义(Undefined) (undefined),用于无意中漏掉的值。
  • 空值(Null) (null),用于有意漏掉的值。
  • 布尔值(Booleans) (truefalse),用于逻辑操作符。
  • 数字(Numbers) (-1003.14 之类的),用于数学计算。
  • 字符串(Strings) ("hello""abracadabra" 之类的),用于文本。
  • 符号(Symbols) (不常用),用于隐藏实现细节。
  • 大型整数(BigInts) (不常用,是新的),用于数学中的大数字。

对象和函数(Objects and Functions)

  • 对象(Objects) ({} 之类的),用于将相关的数据和代码分组。
  • 函数(Functions) (x => x * 2 之类的),用于引用代码。

没有别的类型了。除了列举过的类型外,没有别的基础类型了。所有剩下的都是对象(object)。比如数组、日期、正则表达式,都是 JavaScript 中的对象:

1
2
3
console.log(typeof([])); // "object"
console.log(typeof(new Date())); // "object"
console.log(typeof(/(hello|goodbye)/)); // "object"		

值和变量 Values and Variables

是看一个小例子,看看如下代码输出的结果是什么?

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

这段代码打印 yikes。如果使用 strict mode 的话,会报错。

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

我们无法改变原始值。

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

1
2
3
4
5
6
7
let arr = [212, 8, 506];
let str = 'hello';
console.log(arr[0]); // 212
console.log(str[0]); // "h"
arr[0] = 420;
console.log(arr); // [420, 8, 506]
str[0] = 'j'; // ???

所有的原始值都是不可变的,同样无法在原始值上设置属性。

1
2
let fifty = 50
fifty.shades = 'gray'

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

矛盾之处?(A Contradiction?)

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

一段看似和上文矛盾的代码。

变量是电线(Variables Are Wires)

再回到上面的例子,pet 输出的是新的值 The Kraken。这是咋回事呢?

变量不是值,变量指向值。

在我的宇宙中,一个变量是一根电线。它有两个端点,并且有一条方向:从我代码中的一个命名出发,最终指向我宇宙中的某个值。

比如把变量 pet指向Narwhal

img

给变量赋值(Assigning a Value to a Variable)

img

我在这里所做的只是告诉 JavaScript 把左侧的「电线」(即变量 pet)指向右侧的值(即 "The Kraken")。除非我之后再重新赋值,否则它将一直指向该值。

一个赋值语句的左侧是电线,右侧是表达式。像 2 的数字或者像 "The Kraken" 的字符串也是表达式呢。这种表达式被称作字面量(literals)——因为我们字面地写出了它们的值。

读取变量的值(Reading a Value of a Variable)

1
console.log(pet)

当我们写下 pet 时,我们是向 JavaScript 问了这么个问题:「pet 的当前值是多少?」为了回答这个问题,JavaScript 沿着 pet 的「电线」,找到末端的值后反馈给我们。

名词和动词(Nouns and Verbs)

1
2
3
4
5
6
7
8
function double(x) {
  x = x * 2;
}

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

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

结合起来说(Putting It Together)

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

一个图片的动态示例。

小测试

  1. 仅通过编辑函数 feed是否可以改变输出,为什么?不可以

    1
    2
    3
    
    let pets = 'Tom and Jerry';
    feed(pets);
    console.log(pets[0]);
    
  2. 仅通过编辑函数 feed 来改变输出嘛?为什么?可以

    1
    2
    3
    
    let pets = ['Tom', 'Jerry'];
    feed(pets);
    console.log(pets[0]);
    

Counting the Values

未定义(Undefined)

undefined 就是个普通的原始值,跟 2 或者 "hello" 是一样的。

空值(Null)

可以把 null 想象成 undefined 的姐妹。它们的表现相似。比如,当你打算访问它的属性时,会抛错。与 undefined 相似,null 是其自身类型的唯一值

但是 null 也有特殊的一点。

1
console.log(typeof(null)) // object

是个历史 bug,但是为了不影响先有代码,所以这个一直没有被修复。

为什么需要同时有 nullundefined 呢?因为这可以帮你把「(可能导致 undefined 的)编码错误」和「(可能被你表示为 null)的缺失数据」区分开。然而,这只是一个约定,JavaScript 并不会强制这种用法。

布尔值(Booleans)

布尔值只有两个:truefalse

img

数字(Numbers)

1
2
3
console.log(typeof(28)); *// "number"*
console.log(typeof(3.14)); *// "number"*
console.log(typeof(-140)); *// "number"*

有限精度

1
2
console.log(0.1 + 0.2 === 0.3); // false
console.log(0.1 + 0.2 === 0.30000000000000004); // true

0.10.2 都是被「四舍五入」到最接近的可用数字。而四舍五入的错误会不断累积,因此将它们相加并不能得出 0.3

浮动小数点(Floating Decimal Point)

浮点数学运算的另一个有趣方面是,数字的精度是「浮动的」,它取决于数字的大小。我们离 0 越近,数字的精度就越大,数字之间「挨」得也越近:

img

当我们从 0 开始向任一方向移动时,我们便开始丢失精度。在某个时刻,即便是两个紧挨着的数字也会相差得比 1 还要远:

1
2
3
4
5
6
console.log(Number.MAX_SAFE_INTEGER);     // 9007199254740991
console.log(Number.MAX_SAFE_INTEGER + 1); // 9007199254740992
console.log(Number.MAX_SAFE_INTEGER + 2); // 9007199254740992
console.log(Number.MAX_SAFE_INTEGER + 3); // 9007199254740994
console.log(Number.MAX_SAFE_INTEGER + 4); // 9007199254740996
console.log(Number.MAX_SAFE_INTEGER + 5); // 9007199254740996

特殊数字(Special Numbers)

浮点数学运算包含一些特殊数字。你可能偶尔会遇到 NaNInfinity-Infinity-0。之所以它们会存在,是因为有时你可能会执行诸如 1 / 0 之类的操作,而 JavaScript 需要以某种方式表示其结果。

1
2
3
4
5
let scale = 0;
let a = 1 / scale; // Infinity
let b = 0 / scale; // NaN
let c = -a; // -Infinity
let d = 1 / c; // -0

NaN 尤其有意思。NaN0 / 0 这种不正确的数学计算的结果,代表「非数(Not a Number)」。

总结

  • **JavaScript 实现了一种叫做「浮点数学」的标准。**越靠近 0,数字越精确,反之越不精确。
  • 1 / 0 或者 0 / 0 这类不正确的数学操作的结果是特殊的数字。NaN 是这些特殊数字中的一员。
  • **typeof(NaN)number,因为它这个值本身确实是数字。**只不过因为代表了「不正确的」数字这个含义,而被叫做「非数」。

大数(BigInts)

对于精度要求很高的金融计算,大数会很有用。

1
2
3
4
5
6
let alot = 9007199254740991n; // 注意末尾的 n
console.log(alot + 1n); // 9007199254740992n
console.log(alot + 2n); // 9007199254740993n
console.log(alot + 3n); // 9007199254740994n
console.log(alot + 4n); // 9007199254740995n
console.log(alot + 5n); // 9007199254740996n

字符串(Strings)

字符串代表了 JavaScript 中的文本。有三种写字符串的方式(单引号、双引号、反引号),但是结果都一样。空字符串也是字符串。

字符串不是对象

字符串属性是特殊的,并不和对象属性的表现一致。例如,你不能给 cat[0] 赋值。字符串是原始值,而所有的原始值都是不可变的。

1
2
3
4
let cat = 'Cheshire';
console.log(cat.length); // 8
console.log(cat[0]); // "C"
console.log(cat[1]); // "h"

符号(Symbols)

1
2
let alohomora = Symbol();
console.log(typeof(alohomora)); // "symbol"

对象(Objects)

对象包含数组、日期、正则表达式和其他非原始值的值:

1
2
3
4
5
console.log(typeof({})); // "object"
console.log(typeof([])); // "object"
console.log(typeof(new Date())); // "object"
console.log(typeof(/\d+/)); // "object"
console.log(typeof(Math)); // "object"

Making Our Own Objects

每当我们使用 {} 这种对象字面量时,我们就「创建」了全新的对象值:

1
2
let shrek = {};
let donkey = {};

img

对象会消失吗?(Do Objects Disappear?)

JS 自带垃圾回收机制,当我们的代码中没有值相关的引用时,这些值可能会消失。

函数(Functions)

1
2
3
4
5
6
for (let i = 0; i < 7; i++) {
  let dig = function() {
    // Do nothing
  };
  console.log(dig);
}

这段代码有几个函数。7 个。

每当我们执行一行包含函数声明的代码时,一个全新的函数值在我们的宇宙中出现了。

img

每当我们执行像 let dwarf = {} 的代码时,一个全新的对象是如何出现的。我们创建对象,我们也创建函数。

测试

下面代码运行后的变量和值的示意图。图 B 正确

1
2
3
let spaghetti = function () { return 2 + 2 };
let fettuccine = spaghetti;
let gnocchi = function () { return 2 + 2 };

img

参考资料

https://songkeys.github.io/categories/JavaScript/