好的,把你的变量想象成一张纸——一张便签。
注 1:变量是便签。
现在,便签非常小。你只能在上面写一点点信息。如果你想写更多的信息,你需要更多的便签,但这不是问题。想象一下,您有无穷无尽的便签。
注意 2:您有一个无尽的便签供应,其中存储少量信息。
太好了,你可以在便签上写什么?我会写:
- 是或否(布尔值)。
- 我的年龄(一个数字)。
- 我的名字(字符串)。
- 什么都没有(undefined)。
- 涂鸦或其他对我来说毫无意义的东西(null)。
所以我们可以在便签上写一些简单的东西(让我们居高临下,称它们为原始东西)。
注意事项 3:您可以在便签上写原始的东西。
假设我在便利贴上写了30,提醒自己为今晚在我家举办的小聚会买 30 片奶酪(我的朋友很少)。
当我去冰箱贴便利贴时,我看到我妻子在冰箱上贴了另一张便利贴,上面也写着30(提醒我她的生日是本月 30 日)。
问:两个便签是否传达相同的信息?
答:是的,他们都说30。我们不知道是 30 片奶酪还是每月的第 30 天,坦率地说,我们不在乎。对于一个不知道更好的人来说,一切都一样。
var slicesOfCheese = 30;
var wifesBirthdate = 30;
alert(slicesOfCheese === wifesBirthdate); // true
注 4: 两个写有相同内容的便签传达相同的信息,即使它们是两个不同的便签。
今晚我真的很兴奋 - 和老朋友一起出去玩,玩得很开心。然后我的一些朋友打电话给我说他们不能参加聚会了。
所以我走到冰箱前,将便签上的 30 擦掉(不是我妻子的便签 - 这会让她很生气),然后把它改成 20。
注意事项 5:您可以擦除便签上的内容并写下其他内容。
问:这一切都很好,但是如果我的妻子想在我出去买奶酪的时候写一份杂货清单让我去拿怎么办。她需要为每件物品写一个便签吗?
答:不,她会拿一长串纸,然后在纸上写下杂货清单。然后她会写一个便条告诉我在哪里可以找到杂货清单。
那么这里发生了什么?
- 杂货清单显然不是简单(erm...原始)数据。
- 我妻子把它写在一张更长的纸上。
- 她在便签中写下了在哪里可以找到它。
亲爱的,杂货清单在你的键盘下。
回顾一下:
- 实际对象(杂货清单)在我的键盘下。
- 便签告诉我在哪里可以找到它(对象的地址)。
注意 6: 引用值是对对象的引用(可以找到它们的地址)。
问:我们如何知道两张便利贴说的是同一件事?假设我的妻子制作了另一个购物清单,以防我放错了第一个,并为它写了另一个便利贴。两个列表都说同样的事情,但便利贴说同样的事情吗?
答:不。第一个便笺告诉我们在哪里可以找到第一个列表。第二个告诉我们在哪里可以找到第二个列表。两个列表是否说同样的话并不重要。它们是两个不同的列表。
var groceryList1 = ["1 dozen apples", "2 loaves of bread", "3 bottles of milk"];
var groceryList2 = ["1 dozen apples", "2 loaves of bread", "3 bottles of milk"];
alert(groceryList1 === groceryList2); // false
注意事项 7:只有当两个便笺引用同一个对象时,它们才能传达相同的信息。
这意味着如果我的妻子做了两张便签提醒我购物清单在哪里,那么这两张便签包含相同的信息。所以这个:
亲爱的,杂货清单在你的键盘下。
包含与以下相同的信息:
不要忘记杂货清单在您的键盘下方。
在编程方面:
var groceryList1 = ["1 dozen apples", "2 loaves of bread", "3 bottles of milk"];
var groceryList2 = groceryList1;
alert(groceryList1 === groceryList2); // true
这就是您需要了解的关于 JavaScript 中的 primitives 和 references 的全部内容。无需进入 heap 和动态内存分配之类的东西。如果您使用 C/C++ 编程,这一点很重要。
编辑 1: 哦,重要的是,当您传递变量时,您实际上是在传递 原始 值 按值 和参考值参考。
这只是一种复杂的说法,即您正在将写在一张便签上的所有内容复制到另一个上(无论您是复制 primitive 值还是 参考)。
复制引用时,被引用的对象不会移动(例如,我妻子的购物清单将始终留在我的键盘下,但我可以将复制的便签带到任何我想要的地方 - 原始便签仍然会在冰箱)。
编辑 2:回应@LacViet 发表的评论:
首先,我们谈论的是 JavaScript,而 JavaScript 没有 stack 或 heap。它是一种动态语言,JavaScript 中的所有变量都是动态的。为了解释差异,我将其与 C 进行比较。
考虑以下 C 程序:
#include <stdio.h>
int main() {
int a = 10;
int b = 20;
int c = a + b;
printf("%d", c);
return 0;
}
当我们编译这个程序时,我们会得到一个可执行文件。可执行文件分为多个段(或节)。这些段包括堆栈段、代码段、数据段、额外段等。
- 堆栈段用于在调用函数或中断处理程序时存储程序的状态。例如,当函数
f 调用函数g 时,函数f 的状态(当时寄存器中的所有值)将保存在堆栈中。当g 将控制权返回给f 时,这些值就会恢复。
- 代码段包含要由处理器执行的实际代码。它包含一系列处理器必须执行的指令,例如
add eax, ebx(其中add 是操作码,eax 和ebx 是参数)。该指令将寄存器eax和ebx的内容相加,并将结果存储在寄存器eax中。
- 数据段用于为变量保留空间。例如,在上面的程序中,我们需要为整数
a、b和c预留空间。另外,我们还需要为字符串常量"%d"预留空间。因此,保留的变量在内存中具有固定地址(在链接和加载之后)。
- 除了所有这些之外,操作系统还为您提供了一点额外空间。这称为堆。您需要的任何额外内存都是从该空间分配的。以这种方式分配的内存称为动态内存。
我们来看一个动态内存的程序:
#include <stdio.h>
#include <malloc.h>
int main() {
int * a = malloc(3 * sizeof(int));
a[0] = 3;
a[1] = 5;
a[2] = 7;
printf("a: %d\nb: %d\nc: %d\n", a[0], a[1], a[2]);
return 0;
}
因为我们想要动态分配内存,我们需要使用指针。这是因为我们要使用同一个变量来指向任意内存位置(不一定每次都同一个内存位置)。
因此我们创建了一个名为 a 的 int 指针 (int *)。 a 的空间是从数据段分配的(即它不是动态的)。然后我们调用malloc 为堆中的 3 个整数分配连续空间。返回第一个int的内存地址,存放在指针a中。
问:我们学到了什么?
答:为所有变量分配固定数量的空间。每个变量都有一个固定地址。我们还可以从堆中分配额外的内存,并将这些额外内存的地址存储在一个指针中。这称为动态内存方案。
从概念上讲,这类似于我解释的关于变量是便签的内容。所有变量(包括指针都是便签)。然而,指针是特殊的,因为它们引用一个内存位置(这就像在 JavaScript 中引用一个对象)。
但是,相似之处就到此为止了。以下是区别:
- 在 C 中,所有内容都按值传递(包括指针中的地址)。要传递引用,您需要通过指针使用间接。 JavaScript 仅按值传递原语。传递引用由引擎透明地处理,就像传递任何其他变量一样。
- 在 C 语言中,您可以创建指向原始数据类型的指针,例如
int。在 JavaScript 中,您不能创建对像 number 这样的原始值的引用。所有基元始终按值存储。
- 在 C 语言中,您可以对指针执行各种操作。这称为指针算术。 JavaScript 没有指针。它只有参考。因此,您无法执行任何指针运算。
除了这三个之外,C 和 JavaScript 最大的区别在于 JavaScript 中的所有变量实际上都是指针。由于 JavaScript 是一种动态语言,因此可以使用相同的变量在不同的时间点存储 number 和 string。
JavaScript 是一种解释型语言,解释器通常是用 C++ 编写的。因此,JavaScript 中的所有变量都映射到宿主语言中的对象(甚至是原语)。
当我们在 JavaScript 中声明一个变量时,解释器会为它创建一个新的通用变量。然后,当我们为它分配一个值(无论是原始值还是引用)时,解释器只需为其分配一个新对象。在内部,它知道哪些对象是原始对象,哪些是实际对象。
从概念上讲,这就像做这样的事情:
JSGenericObject ten = new JSNumber(10); // var ten = 10;
问:这是什么意思?
答:这意味着JavaScript中的所有值(基元和对象)都是从堆中分配的。甚至变量本身也是从堆中分配的。说原语是从堆栈中分配的,而只有对象是从堆中分配的,这是错误的。这是 C 和 JavaScript 最大的区别。