本文旨在了解如何复制对象、数组和函数以及如何将它们传递到函数中。知道引用类型复制的是什么。了解原始值是通过复制值来复制及传递的。
数值类型 & 引用类型
JavaScript有5种通过复制数值传值的数据类型:Boolean
, null
, undefined
, String
, and Number
。我们称之为原始/基本数据类型
JavaScript还有三种通过引用传值的数据类型:Array
, Function
, and Object
。从专业角度讲,它们都是Objects
, 故而统称为对象。
原始/基本数据类型
若为一个基本数据类型的变量赋值,我们可以认为变量包含了这个原始值。
var x = 10;
var y = 'abc';
var z = null;
这张图形象的展示了变量在内存中的存储情况:
Variables | Values |
---|---|
x | 10 |
y | 'abc' |
z | null |
当我们用 =
将这些变量赋值给其他变量时,我们把这些值拷贝给了这些新变量。他们通过值复制的。
var x = 10;
var y = 'abc';
var a = x;
var b = y;
console.log(x, y, a, b);
// -> 10, 'abc', 10, 'abc'
a
和 x
现在的值都是10
. b
和 y
都拥有值 'abc'
。他们各自独立,拥有相同的值,互不影响:
Variables | Values |
---|---|
x | 10 |
y | 'abc' |
a | 10 |
b | 'abc' |
改变其中一个值并不会影响另一个的值,彼此井水不犯河水,尽管后者曾经复制与它:
var x = 10;
var y = 'abc';
var a = x;
var b = y;
x = 5;
y= 'def';
console.log(x, y, a, b); // -> 5 'def' 10 'abc'
对象
非基本数据类型的变量会保存对值的引用(地址)。该引用指向内存中对象的地址,变量实际不包含该实际值。
对象创建于计算机内存中。当我们写代码 arr = []
, 我们在内存中创建了一个新数组, arr
中现在包含了新数组在内存中的地址。
假设address(地址)是一种新的传递数据的数据类型,就像数字和字符串。address
指向通过引用传递的值的内存地址,就像字符串由''
或 ""
表示, address
由 <>
表示。
当我们赋值引用一个引用型变量时,我们通常这样书写代码:
var arr = [];
arr.push(1);
两步的操作分别是:
1.
Variables | Values | Address | Objects | |
---|---|---|---|---|
arr | <#001> | #001 | [] |
2.
Variables | Values | Address | Objects | |
---|---|---|---|---|
arr | <#001> | #001 | [1] |
值,地址以及 变量 arr
的包含的值 是静态不变的,仅仅是内存中的数组改变了。当我们对arr
进行操作时,例如添加新元素, JavaScript引擎会获取 arr
在内存中的地址 并操作该地址存储的数据。
引用赋值
当一个引用型值即对象被用 =
赋值给另一个变量, 实际上复制过去的是那个引用型值的地址。对象通过引用赋值而不是直接传值。对象本身是静态不变的,唯一改变的 是对象的 引用 、地址。
var reference = [1];
var refCopy = reference;
内存变化:
Variables | Values | Address | Objects | |
---|---|---|---|---|
reference | <#001> | #001 | [1] |
|
refCopy | <#001> |
现在每个变量都包含了同一个数组的引用,它们地址相同,这意味着如果我们改变了这个引用即改变reference
, refCopy
也会随之改变,这一点与基本数据类型的值不一样。
reference.push(2);
console.log(reference, refCopy);
// -> [1, 2], [1, 2]
Variables | Values | Address | Objects | |
---|---|---|---|---|
reference | <#001> | #001 | [1,2] |
|
refCopy | <#001> | [1,2] |
重新赋值 :
重新复制会覆盖旧值:
var obj = { first: 'reference' };
内存变化:
Variables | Values | Address | Objects | |
---|---|---|---|---|
obj |
<#234> | #234 | { first: 'reference' } |
重新赋值:
var obj = { first: 'reference' };
obj = { second: 'ref2' }
Address存储了 obj
的变化 ,第一个对象仍在内存,第二个对象也在:
Variables | Values | Address | Objects | |
---|---|---|---|---|
obj |
<#678> | #234 | { first: 'reference' } |
|
#678 | { second: 'ref2' } |
当已经存在的对象没有被引用时,如上边的 #234 ,JavaScript会启动垃圾回收机制。这就意味着程序员失去了对该对象的所有引用,不能再使用这个对象,所以JavaScript可以安全地删除它。 这时,对象 { first: 'reference' }
不能再被任何变量获取,内存会被回收。
== and ===
当 等式运算符 ==
和 ===
用于引用型变量时, 他们会检查引用。 如果多个变量包含同一项目的引用时, 结果会返回 true
var arrRef = ['Hi!'];
var arrRef2 = arrRef;
console.log(arrRef === arrRef2); // -> true
如果他们是不同的对象,即使它们包含相同的内容, 比较结果也会返回 false
。
var arr1 = ['Hi!'];
var arr2 = ['Hi!'];
console.log(arr1 === arr2); // -> false
对象的比较
如果想比较两个对象的属性是否一样,比较运算符会失去作用。我们必须编一个函数来检查对象的每一条属性和值是否相同。对于两个数组,我们需要一个函数遍历数组每项检查是否相同。
函数传参
当我们传递基本数据类型的值给一个函数时,函数拷贝这个值作为自己的参数。效果和 =
相同:
var hundred = 100;
var two = 2;
function multiply(x, y) {
// PAUSE
return x * y;
}
var twoHundred = multiply(hundred, two);
上例中,我们将 hundred
赋值 100
。当我们把他传递给 multiply
, 变量x
获得值 100
。如果用 =
赋值,值会被复制。 而且,hundred
的值不会被影响。 这是multiply
中 //
的地方在内存中的映射:
Variables | Values | Address | Objects | |
---|---|---|---|---|
hundred |
100 | #333 | function(x, y) {… } |
|
two |
2 | |||
multiply |
<#333> | |||
x |
100 |
|||
y |
2 |
|||
twoHundred |
undefined |
multiply
包含了函数的引用,其他变量则包含基本数据类型的数据。twoHundred
是 undefined
因为我们还没有函数返回结果,在函数返回结果前,它等于undefined
。
纯函数
纯函数是指不影响外部作用域的函数。只要一个函数只接受基本数据类型的值作为参数并且不适用任何外部范围的变量,他就是纯函数不会污染外部作用域。所有纯函数的变量在函数返回结果后会进入JavaScript的垃圾回收机制。
然而,接受一个对象(作为参数)的函数会改变他周围作用域的状态。如果函数接受一个数组的引用并改变了它指向的数组,可能是添加元素,引用这个数组的外部变量会见证这些变化。当函数返回结果后,产生的改变会影响外部作用域。这会导致很难追踪到的负面影响。
许多本地数组函数包含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 }
非纯函数接受了对象,改变了object
的 age
属性 为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
时,原对象并没有受到影响。这个函数就是纯洁纯净的。它没有影响任何外部作用域的对象,甚至传入函数的对象。新的对象需要被返回 并将其存储在一个新变量中否则一旦函数执行完毕就会被回收,该对象在作用于内就再也找不到了。
以下几个例子看看你是否理解了上述内容:
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); // -> { name: 'Alex', age: 25 }
console.log(personObj2); // -> { name: 'John', age: 50 }
解析:
上述函数 等于:
var personObj1 = {
name: 'Alex',
age: 30
};
var person = personObj1;
person.age = 25;
person = {
name: 'John',
age: 50
};
var personObj2 = person;
console.log(personObj1); // -> { name: 'Alex', age: 25 }
console.log(personObj2); // -> { name: 'John', age: 50 }
唯一一点不同是前者
person
在函数结束后就被回收了。
再来几道题:
// 1
var obj = {
innerObj: {
x: 9
}
};
var z = obj.innerObj;
z.x = 25;
console.log(obj.innerObj.x);
// 2
var obj = {
arr: [{ x: 17 }]
};
var z = obj.arr;
z = [{ x: 25 }];
console.log(obj.arr[0].x);
// 3
var obj = {};
var arr = [];
obj.arr = arr;
arr.push(9);
obj.arr[0] = 17;
console.log(obj.arr === [17]);
// 4
function fn(item1, item2) {
if (item2 === undefined) {
item2 = [];
}
item2[0] = item1;
return item2;
}
var w = {};
var x = [w];
var y = fn(w);
var z = fn(w, x);
console.log(x === y);
结果:
25
17
false
false
3和4 需要注意一点:
[5] === [5]
====>false
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。