本文要解决的问题:javascript
部分代码可在这里找到:Github。若是发现错误,欢迎指出。java
JavaScript 中的数据类型能够分为两种:基本类型值(Number, Boolean, String, NULL, Undefined)和引用类型值(Array, Object, Date, RegExp, Function)。 基本类型值指的是简单的数据段,而引用类型值指那些可能由多个值构成的对象。git
基本数据类型是按值访问的,由于能够直接操做保存在变量中的实际的值。引用类型的值是保存在内存中的对象,与其余语言不一样,JavaScript 不容许直接访问内存中的位置,也就是说不能直接操做对象的内存空间。在操做对象时,其实是在操做对象的引用而不是实际的对象。 为此,引用类型的值是按引用访问的。github
除了保存的方式不一样以外,在从一个变量向另外一个变量复制基本类型值和引用类型值时,也存在不一样:segmentfault
看下面的代码:数组
// 基本类型值复制
var string1 = 'base type';
var string2 = string1;
// 引用类型值复制
var object1 = {a: 1};
var object2 = object1;
复制代码
下图能够表示两种类型的变量的复制结果:bash
至此,咱们应该理解:在 JavaScript 中直接复制对象其实是对引用的复制,会致使两个变量引用同一个对象,对任一变量的修改都会反映到另外一个变量上,这是一切问题的缘由所在。闭包
理解了 JavaScript 中拷贝对象的问题后,咱们就能够讲讲深拷贝和浅拷贝的区别了。考虑这种状况,你须要复制一个对象,这个对象的某个属性仍是一个对象,好比这样:函数
var object1 = {
a: 1,
obj: {
b: 'string'
}
}
复制代码
浅拷贝
存在两种状况:性能
object1
和 object2
之间仍是会相互影响。// 最简单的浅拷贝
var object2 = object1;
// 拷贝第一层属性
function shallowClone(source) {
if (!source || typeof source !== 'object') {
return;
}
var targetObj = source.constructor === Array ? [] : {};
for (var keys in source) {
if (source.hasOwnProperty(keys)) {
// 简单的拷贝属性
targetObj[keys] = source[keys];
}
}
return targetObj;
}
var object3 = shallowClone(object1);
// 改变原对象的属性
object1.a = 2;
object1.obj.b = 'newString';
// 比较
console.log(object2.a); // 2
console.log(object2.obj.b); // 'newString'
console.log(object3.a); // 1
console.log(object3.obj.b); // 'newString'
复制代码
浅拷贝存在许多问题,须要咱们注意:
拷贝对象
的原型与原对象
的原型不一样,拷贝对象只是 Object 的一个实例。不能理解这些概念?能够看看下面的代码:
function Parent() {
this.name = 'parent';
this.a = 1;
}
function Child() {
this.name = 'child';
this.b = 2;
}
Child.prototype = new Parent();
var child1 = new Child();
// 更改 child1 的 name 属性的描述符
Object.defineProperty(child1, 'name', {
writable: false,
value: 'Mike'
});
// 拷贝对象
var child2 = shallowClone(child1);
// Object {value: "Nicholas", writable: false, enumerable: true, configurable: true}
console.log(Object.getOwnPropertyDescriptor(child1, 'name'));
// 这里新对象的 name 属性的描述符已经发生了变化
// Object {value: "Nicholas", writable: true, enumerable: true, configurable: true}
console.log(Object.getOwnPropertyDescriptor(child2, 'name'));
child1.name = 'newName'; // 严格模式下报错
child2.name = 'newName'; // 能够赋值
console.log(child1.name); // Mike
console.log(child2.name); // newName
复制代码
上面的代码经过构造函数 Child
构造一个对象 child1
,这个对象的原型是 Parent
。而且修改了 child1
的 name
属性的描述符,设置 writable
为 false
,也就是这个属性不能再被修改。若是要直接给 child1.name
赋值,在严格模式下会报错,在非严格模式则会赋值失败(但不会报错)。
咱们调用前面提到的浅拷贝函数 shallowClone
来拷贝 child1
对象,生成了新的对象 child2
,输出 child2
的 name
属性的描述符,咱们能够发现 child2
的 name
属性的描述符与 child1
已经不同了(变成了可写的)。在 VSCode 中开启调试模式,查看 child1
和 child2
的原型,咱们也会发现它们的原型也是不一样的:
child1
的原型是 Parent
,而 child2
的原型则是 Object
。
经过上面的例子和简短的说明,咱们能够大体理解浅拷贝存在的一些问题,在实际使用过程当中也能有本身的判断。
深拷贝
就是将对象的属性递归的拷贝到一个新的对象上,两个对象有不一样的地址,不一样的引用,也包括对象里的对象属性(如 object1 中的 obj 属性),两个变量之间彻底独立。
既然浅拷贝有那么多问题,咱们为何还要说浅拷贝?一来是深拷贝的完美实现不那么容易(甚至不存在),并且可能存在性能问题,二来是有些时候的确不须要深拷贝,那么咱们也就不必纠结于与深拷贝和浅拷贝了,没有必要跟本身过不去不是?
一句话:根据本身的实际需选择不一样的方法。
前面已经介绍了对象的两种浅拷贝方式,这里就不作说明了。下面介绍其余的几种方式
Object.assign()
用于将一个或多个源对象中的全部可枚举的属性
值复制到目标对象。Object.assign()
只是浅拷贝,相似上文提到的 shallowClone
方法。
var object1 = {
a: 1,
obj: {
b: 'string'
}
};
// 浅拷贝
var copy = Object.assign({}, object1);
// 改变原对象属性
object1.a = 2;
object1.obj.b = 'newString';
console.log(copy.a); // 1
console.log(copy.obj.b); // `newString`
复制代码
Object.getOwnPropertyNames()
返回由对象属性组成的一个数组,包括不可枚举的属性(除了使用 Symbol 的属性)。
function shallowCopyOwnProperties( source ) {
var target = {} ;
var keys = Object.getOwnPropertyNames( original ) ;
for ( var i = 0 ; i < keys.length ; i ++ ) {
target[ keys[ i ] ] = source[ keys[ i ] ] ;
}
return target ;
}
复制代码
若是咱们须要拷贝原对象的原型和描述符,咱们可使用 Object.getPrototypeOf
和 Object.getOwnPropertyDescriptor
方法分别获取原对象的原型和描述符,而后使用 Object.create
和 Object.defineProperty
方法,根据原型和属性的描述符建立新的对象和对象的属性。
function shallowCopy( source ) {
// 用 source 的原型建立一个对象
var target = Object.create( Object.getPrototypeOf( source )) ;
// 获取对象的全部属性
var keys = Object.getOwnPropertyNames( source ) ;
// 循环拷贝对象的全部属性
for ( var i = 0 ; i < keys.length ; i ++ ) {
// 用原属性的描述符建立新的属性
Object.defineProperty( target , keys[ i ] , Object.getOwnPropertyDescriptor( source , keys[ i ])) ;
}
return target ;
}
复制代码
同上,数组也能够直接复制或者遍历数组的元素直接复制达到浅拷贝的目的:
var array = [1, 'string', {a: 1,b: 2, obj: {c: 3}}];
// 直接复制
var array1 = array;
// 遍历直接复制
var array2 = [];
for(var key in array) {
array2[key] = array[key];
}
// 改变原数组元素
array[1] = 'newString';
array[2].c = 4;
console.log(array1[1]); // newString
console.log(array1[2].c); // 4
console.log(array2[1]); // string
console.log(array2[2].c); // 4
复制代码
这没有什么须要特别说明的,咱们说些其余方法
slice()
方法将一个数组被选择的部分(默认状况下是所有元素)浅拷贝到一个新数组对象,并返回这个数组对象,原始数组不会被修改。 concat()
方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
这两个方法均可以达到拷贝数组的目的,而且是浅拷贝,数组中的对象只是复制了引用:
var array = [1, 'string', {a: 1,b: 2, obj: {c: 3}}];
// slice()
var array1 = array.slice();
// concat()
var array2 = array.concat();
// 改变原数组元素
array[1] = 'newString';
array[2].c = 4;
console.log(array1[1]); // string
console.log(array1[2].c); // 4
console.log(array2[1]); // string
console.log(array2[2].c); // 4
复制代码
实现深拷贝的方法大体有两种:
JSON.stringify
和 JSON.parse
方法下面就两种方法详细说说
JSON.stringify
和JSON.parse
是 JavaScript 内置对象 JSON 的两个方法,主要是用来将 JavaScript 对象序列化为 JSON 字符串和把 JSON 字符串解析为原生 JavaScript 值。这里被用来实现对象的拷贝也算是一种黑魔法吧:
var obj = { a: 1, b: { c: 2 }};
// 深拷贝
var newObj = JSON.parse(JSON.stringify(obj));
// 改变原对象的属性
obj.b.c = 20;
console.log(obj); // { a: 1, b: { c: 20 } }
console.log(newObj); // { a: 1, b: { c: 2 } }
复制代码
可是这种方式有必定的局限性,就是对象必须听从JSON的格式,当遇到层级较深,且序列化对象不彻底符合JSON格式时,使用JSON的方式进行深拷贝就会出现问题。
在序列化 JavaScript 对象时,全部函数及原型成员
都会被有意忽略,不体如今结果中,也就是说这种方法不能拷贝对象中的函数。此外,值为 undefined 的任何属性也都会被跳过。结果中最终都是值为有效 JSON 数据类型的实例属性。
递归是一种常见的解决这种问题的方法:咱们能够定义一个函数,遍历对象的属性,当对象的属性是基本类型值得时候,直接拷贝;当属性是引用类型值的时候,再次调用这个函数进行递归拷贝。这是基本的思想,下面看具体的实现(不考虑原型,描述符,不可枚举属性等,便于理解):
function deepClone(source) {
// 递归终止条件
if (!source || typeof source !== 'object') {
return source;
}
var targetObj = source.constructor === Array ? [] : {};
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key) {
if (source[key] && typeof source[key] === 'object') {
targetObj[key] = deepClone(source[key]);
} else {
targetObj[key] = source[key];
}
}
}
return targetObj;
}
var object1 = {arr: [1, 2, 3], obj: {key: 'value' }, func: function(){return 1;}};
// 深拷贝
var newObj= deepClone(object1);
// 改变原对象属性
object1.arr.push(4);
console.log(object1.arr); // [1, 2, 3, 4]
console.log(newObj.arr); // [1, 2, 3]
复制代码
对于 Function 类型,这里是直接复制的,任然是共享一个内存地址。由于函数更多的是完成某些功能,对函数的更改可能就是直接从新赋值,通常状况下不考虑深拷贝。
上面的深拷贝只是比较简单的实现,没有考虑很复杂的状况,好比:
什么是闭包做用域
function myConstructor() {
var myPrivateVar = 'secret' ;
return {
myPublicVar: 'public!' ,
getMyPrivateVar: function() {
return myPrivateVar ;
} ,
setMyPrivateVar( value ) {
myPrivateVar = value.toString() ;
}
};
}
var o = myContructor() ;
复制代码
上面的代码中,对象 o 有三个属性,一个是字符串,另外两个是方法。方法中用到一个变量 myPrivateVar
,存在于 myConstructor()
的函数做用域中,当 myConstructor
构造函数调用时,就建立了这个变量 myPrivateVar
,然而这个变量并非经过构造函数建立的对象 o
的属性,可是它任然能够被这两个方法使用。
所以,若是尝试深拷贝对象 o
,那么拷贝对象 clone
和被拷贝对象 original
中的方法都是引用相同的 myPrivateVar
变量。
可是,因为并无方式改变闭包的做用域,因此这种模式建立的对象不能正常深拷贝是能够接受的。
递归的作法虽然简单,容易理解,可是存在必定的性能问题,对拷贝比较大的对象来讲不是很好的选择。
理论上来讲,递归是能够转化成循环的,咱们能够尝试着将深拷贝中的递归转化成循环。咱们须要遍历对象的属性,若是属性是基本类型,直接复制,若是属性是引用类型(对象或数组),须要再遍历这个对象,对他的属性进行相同的操做。那么咱们须要一个容器来存放须要进行遍历的对象,每次从容器中拿出一个对象进行拷贝处理,若是处理过程当中遇到新的对象,那么再把它放到这个容器中准备进行下一轮的处理,当把容器中全部的对象都处理完成后,也就完成了对象的拷贝。
思想大体是这样的,下面看具体的实现:
// 利用队列的思想优化递归
function deepClone(source) {
if (!source || typeof source !== 'object') {
return source;
}
var current;
var target = source.constructor === Array ? [] : {};
// 用数组做为容器
// 记录被拷贝的原对象和目标
var cloneQueue = [{
source,
target
}];
// 先进先出,更接近于递归
while (current = cloneQueue.shift()) {
for (var key in current.source) {
if (Object.prototype.hasOwnProperty.call(current.source, key)) {
if (current.source[key] && typeof current.source[key] === 'object') {
current.target[key] = current.source[key].constructor === Array ? [] : {};
cloneQueue.push({
source: current.source[key],
target: current.target[key]
});
} else {
current.target[key] = current.source[key];
}
}
}
}
return target;
}
var object1 = {a: 1, b: {c: 2, d: 3}};
var object2 = deepClone(object1);
console.log(object2); // {a: 1, b: {c: 2, d: 3}}
复制代码
(完)