如果将原始类型的值赋给变量,我们可以将该变量视为包含原始值。例如
const x = 10;
const y = 'abc';
const z = null;
x
包含10
. y
包含'abc'
。为了巩固这个想法,我们设想保留这些变量及其各自值在内存中的样子,如下:
当我们使用=
将这些变量分配给其他变量时,我们将值复制到新变量。它们按值复制。
const x = 10;
const y = 'abc';
const a = x;
const b = y;
console.log(x, y, a, b); // -> 10, 'abc', 10, 'abc'
a
和x
现在都包含10
. b
和y
现在都包含'abc'
。它们是分开的,因为原始值本身被复制了。
改变一个不会改变另一个,彼此是无关联的。
var x = 10;
var y = 'abc';
var a = x;
var b = y;
a = 5;
b = 'def';
console.log(x, y, a, b); // -> 10, 'abc', 5, 'def'
对象
分配了非原始值的变量将被赋予对该值的引用。该引用指向对象在内存中的位置。变量实际上不包含该值。
在计算机内存中的某个位置创建对象。当我们写arr = []
时,我们在内存中创建了一个数组。变量arr接收的是该数组的地址和位置。
让我们假装地址是一个按值传递的新数据类型,就像数字或字符串一样。地址指向内存中通过引用传递的值的位置。就像字符串用引号(’'或“”)表示一样,地址将用箭头括号<>
表示。 当我们分配和使用引用类型变量时,我们编写和看到如下图所示:
var arr = [];
arr.push(1);
请注意,变量arr
包含的值(地址)是静态的。内存中的数组是变化的。
当我们使用arr
做某事时,例如push
一个值,Javascript引擎会转到内存中的arr
位置并使用存储在那里的信息。
按引用分配
当使用=
将引用类型值(一个对象)复制到另一个变量时,复制的其实是该对象在内存中的地址。
var reference = [1];
var refCopy = reference;
内存中的分配,如下图所示
现在,每个变量都包含对同一数组的引用(<#001>
)。这意味着如果我们改变reference
,refCopy
也将看到这些更改:
reference.push(2);
console.log(reference, refCopy); // -> [1, 2], [1, 2]
上述代码, 如下图所示
我们已经将2
推入内存中的数组。当我们使用reference
和refCopy
时,我们指向同一个数组。
重新分配引用
重新分配引用变量将替换旧引用。
var obj = { first: 'reference' };
假设内存图如下
现在我们进行重新分配引用
var obj = { first: 'reference' };
obj = { second: 'ref2' }
存储在obj
中的地址发生了变化。第一个对象仍然存在于内存中,下一个对象也是如此:
当没有对剩余对象的引用时,正如我们在上面的地址#234
中看到的那样,Javascript引擎可以执行垃圾收集。这只是意味着程序员已经丢失了对该对象的所有引用,并且不能再使用该对象,因此引擎可以继续并安全地从内存中删除它。在这种情况下,对象{first:'reference'}
不再可访问,并且可供引擎用于垃圾收集。
扩充
当在引用类型变量上使用相等运算符==
和===
时,如果变量包含对同一对象的引用(内存地址Addresses相同),则比较结果为true。
var arrRef = [’Hi!’];
var arrRef2 = arrRef;
console.log(arrRef === arrRef2); // -> true
var arr1 = ['Hi!'];
var arr2 = ['Hi!'];
console.log(arr1 === arr2); // -> false
如果我们有两个不同的对象并且想要查看它们的属性是否相同,那么最简单的方法是将它们都转换为字符串然后比较字符串。当相等运算符比较基元时,它们只是检查值是否相同。
var arr1str = JSON.stringify(arr1);
var arr2str = JSON.stringify(arr2);
console.log(arr1str === arr2str); // true
另一种选择是递归循环遍历对象并确保每个属性都相同。
通过函数传递参数
当我们将原始值传递给函数时,该函数将值复制到其参数中。它实际上与使用=
相同。
var hundred = 100;
var two = 2;
function multiply(x, y) {
// PAUSE
return x * y;
}
var twoHundred = multiply(hundred, two);
在上面的例子中,我们给出hundred
的值100
.当我们将它传递给multiply
时,变量x
得到该值。该值被复制,好像我们使用了=
赋原始值一样。下面是PAUSE注释行中内存的快照。
纯函数
我们将不影响外部范围内任何内容的函数称为纯函数。(简单点说就是给函数传递的参数不是Object
对象)
只要函数只将原始值作为参数并且不在其周围范围内使用任何变量,它就会自动变纯,因为它不会影响外部范围内的任何内容。
函数返回后,内部创建的所有变量都会被垃圾收集。
但是,接受Object
的函数可以改变其周围范围的状态。
如果函数接受数组引用并改变它指向的数组,引用该数组的周围范围中的变量也会看到该更改。
函数返回后,它所做的更改将在外部作用域中保留。
这可能导致难以追踪的不期望的副作用。
因此,许多数组函数(包括Array.map
和Array.filter
)都被编写为纯函数。
它们接受数组引用并在内部,它们复制数组并使用副本而不是原始副本。
这使得原始版本不受影响,外部范围不受影响,我们返回对全新数组的引用。 让我们来看一个纯粹与不纯的函数的例子。
function changeAgeImpure(person) {
person.age = 25;
return person;
}
var alex = {
name: 'Alex',
age: 30
};
var changedAlex = changeAgeImpure(alex);
console.log(alex); // -> { name: 'Alex', age: 25 }
console.log(changedAlex); // -> { name: 'Alex', age: 25 }
这个不纯的函数接受一个对象,并将该对象的属性年龄更改为25
.因为它作用于给定的引用,它直接更改对象alex
。
请注意,当它返回person
对象时,它将返回传入的完全相同的对象。alex
和alexChanged
包含相同的引用。返回person
变量并将引用存储在新变量中是多余的。
让我们看一下纯函数。
function changeAgePure(person) {
var newPersonObj = JSON.parse(JSON.stringify(person));
newPersonObj.age = 25;
return newPersonObj;
}
var alex = {
name: 'Alex',
age: 30
};
var alexChanged = changeAgePure(alex);
console.log(alex); // -> { name: 'Alex', age: 30 }
console.log(alexChanged); // -> { name: 'Alex', age: 25 }
在这个函数中,我们使用JSON.stringify
将我们传递给的对象转换为字符串,然后使用JSON.parse
将其解析回一个对象。通过执行此转换并将结果存储在新变量中,我们创建了一个新对象。还有其他方法可以做同样的事情,例如循环遍历原始对象并将其每个属性分配给新对象,但这种方式最简单。
新对象具有与原始对象相同的属性,但它是内存中明显独立的对象。 当我们更改此新对象的age
属性时,原始对象不受影响。这个功能现在很纯粹。它不会影响其自身范围之外的任何对象,甚至不会影响传入的对象。
新对象需要返回并存储在新变量中,否则在函数完成后会被JS引擎被垃圾收集。
自测
function changeAgeAndReference(person) {
person.age = 25;
person = {
name: 'John',
age: 50
};
return person;
}
var personObj1 = {
name: 'Alex',
age: 30
};
var personObj2 = changeAgeAndReference(personObj1);
console.log(personObj1); // -> ?
console.log(personObj2); // -> ?
领红包,小赞赏一下吧
本文翻译自 Arnav Aggarwal 的 Explaining Value vs. Reference in Javascript