拷贝的问题主要是针对引用类型javascript
对于这个问题,首先让咱们先简单回顾一下 JavaScript 的基本知识vue
一、JavaScript 包含两种不一样数据类型的值:基本类型(原始值)
和 引用类型
java
基本类型有如下几种,具体以下:
string、number、boolean、null、undefined、symbol、bigIntgit引用类型具体有:
Object(Object、Array、Function...)github
在将一个值赋给变量时,解析器必须肯定这个值是基本类型仍是引用类型算法
二、JavaScript 的变量存储方式 -- 栈(stack)
和 堆(heap)
segmentfault
栈
:自动分配内存空间,系统自动释放,里面存放的是基本类型的值和引用类型的地址指针堆
:动态分配内存,大小不定,也不会自动释放,里面存放引用类型的值三、JavaScript 值传递与址传递
基本类型与引用类型最大的区别实际就是传值与传址的区别数组
let a = 1;
let b = a;
b++;
console.log(a, b) // 1, 2
复制代码
let a = ['a', 'b', 'c'];
let b = a;
b.push('d');
console.log(a) // ['a', 'b', 'c', 'd']
console.log(b) // ['a', 'b', 'c', 'd']
复制代码
分析:markdown
那么如何解决上面出现的问题,这里就引出了浅拷贝或者深拷贝了。JS 的基本类型不存在浅拷贝仍是深拷贝的问题,主要是针对引用类型数据结构
浅拷贝
:拷贝的级别浅。浅拷贝是指复制对象时只对第一层键值对进行复制,若对象内还有对象则只能复制嵌套对象的地址指针
深拷贝
:拷贝级别更深。深拷贝是指复制对象时是彻底拷贝,即便嵌套了对象,拷贝后二者也相互不影响,修改一个对象的属性不会影响另外一个。原理实际上是递归把那些值是对象的属性再次进入对象内部进行复制
slice
、concat
如果数组,数组元素均为基本数据类型,可利用数组的一些方法如 slice
、concat
返回一个新数组的特性来实现拷贝(此时至关于深拷贝)
若数组的元素是引用类型(Object,Array),slice
和 concat
对对象数组的拷贝仍是浅拷贝,拷贝以后数组各个元素的指针仍是指向相同的存储地址
let arr = ['one', 'two', 'three'];
let newArr = arr.concat();
newArr.push('four')
console.log(arr) // ["one", "two", "three"]
console.log(newArr) // ["one", "two", "three", "four"]
let arr = ['one', 'two', 'three'];
let newArr = arr.slice();
newArr.push('four')
console.log(arr) // ["one", "two", "three"]
console.log(newArr) // ["one", "two", "three", "four"]
let arr = [{a:1}, 'two', 'three'];
let newArr = arr.concat();
newArr[0].a = 2;
console.log(arr) // [{a: 2},"two","three"]
console.log(newArr) // [{a: 2},"two","three"]
复制代码
Object assign()
该方法能够把任意多个的源对象自身的可枚举属性拷贝给目标对象,而后返回目标对象。Object assign()
对对象的拷贝仍是浅拷贝
let arr = {
a: 'one',
b: 'two',
c: 'three'
};
let newArr = Object.assign({}, arr)
newArr.d = 'four'
console.log(arr); // {a: "one", b: "two", c: "three"}
console.log(newArr); // {a: "one", b: "two", c: "three", d: "four"}
let arr = {
a: 'one',
b: 'two',
c: {a: 1}
};
let newArr = Object.assign({}, arr)
newArr.c.a = 3;
console.log(arr); // {a: "one", b: "two", c: {a: 3}}
console.log(newArr); // {a: "one", b: "two", c: {a: 3}}
复制代码
原理:遍历对象,而后把属性和属性值放在一个新对象并返回
function clone(obj) {
// 只拷贝对象
if (typeof src !== 'object') return;
// 根据 obj 的类型判断是新建一个数组仍是对象
let newObj = Obejct.prototype.toString.call(obj) == '[object Array]' ? [] : {};
for(let prop in newObj) {
if(newObj.hasOwnProperty(prop)) {
newObj[prop] = obj[src];
}
}
return newObj;
}
复制代码
JSON.parse(JSON.stringify(arr))
:不只适用于数组还适用于对象
let a = {
name: "tn",
book: {
title: "JS",
price: "45"
}
}
let b = JSON.parse(JSON.stringify(a));
console.log(b);
// {
// name: "tn",
// book: {title: "JS", price: "45"}
// }
a.name = "change";
a.book.price = "55";
console.log(a);
// {
// name: "change",
// book: {title: "JS", price: "55"}
// }
console.log(b);
// {
// name: "tn",
// book: {title: "JS", price: "45"}
// }
复制代码
改变变量 a 中的引用属性后对 b 没有任何影响,这就是深拷贝的魔力
对数组深拷贝以后,改变原数组页不会影响到拷贝以后的数组
// 木易杨
let a = [0, "1", [2, 3]];
let b = JSON.parse(JSON.stringify( a.slice(1) ));
console.log(b); // ["1", [2, 3]]
a[1] = "99";
a[2][0] = 4;
console.log(a); // [0, "99", [4, 3]]
console.log(b); // ["1", [2, 3]]
复制代码
但该方法有局限性
undefined
symbol
new Date()
undefined
、symbol
和 函数
会被直接忽略
// 木易杨
let obj = {
name: "tn",
a: undefined,
b: Symbol("tn"),
c: function() {}
}
let b = JSON.parse(JSON.stringify(obj));
console.log(b); // {name: "tn"}
复制代码
循环引用状况下会报错
let obj = {
a: 1,
b: {
c: 2,
d: 3
}
}
obj.a = obj.b;
obj.b.c = obj.a;
let b = JSON.parse(JSON.stringify(obj)); // Uncaught TypeError: Converting circular structure to JSON
复制代码
new Date
状况下转换结果不正确
new Date(); // Mon Dec 24 2018 10:59:14 GMT+0800 (China Standard Time)
JSON.stringify(new Date()); // ""2018-12-24T02:59:25.776Z""
JSON.parse(JSON.stringify(new Date())); // "2018-12-24T02:59:41.523Z"
复制代码
解决方法转成字符串或者时间戳
let date = (new Date()).valueOf();
JSON.stringify(date); // "1625905818735"
JSON.parse(JSON.stringify(date)); // 1625905818735
复制代码
正则状况下
let obj = {
name: "tn",
a: /'123'/
}
console.log(obj); // {name: "tn", a: /'123'/}
let b = JSON.parse(JSON.stringify(obj));
console.log(b); // {name: "tn", a: {}}
复制代码
ES6 扩展运算符[...]
:不只适用于数组还适用于对象,只有原始值能够深拷贝,当含有引用值时进行浅拷贝
原理:在拷贝时判断一下属性值的类型,如果对象则递归调用深拷贝函数,深拷贝是彻底拷贝了原对象的内容并寄存在新的内存空间,指向新的内存地址
function deepClone1(src, target) {
var target = target || {};
for (let prop in src) {
if (src.hasOwnProperty(prop)) {
if(src[prop] !== null && typeof(src[prop]) === 'object') {
target[prop] = Object.prototype.toString.call(src[prop]) == '[object Array]' ? [] : {};
deepClone(src[prop], target[prop]);
} else {
target[prop] = src[prop];
}
}
}
return target;
}
// test
var a = {
name: "tn",
book: {
title: "JS",
price: "45"
},
a1: undefined,
a2: null,
a3: 123
}
var b = deepClone(a);
console.log(b);
// {
// a1: undefined,
// a2: null,
// a3: 123,
// book: {
// title: "JS",
// price: "45"
// },
// name: "tn"
// }
复制代码
咱们知道 JSON
没法深拷贝循环引用,遇到这种状况会抛出异常
其实就是循环检测,设置一个数组或哈希表存储已拷贝过的对象,当检测到当前对象已存在于哈希表中时,取出该值并返回
function deepClone2(src, hash = new WeakMap()) {
var target = Object.prototype.toString.call(src) == '[object Array]' ? [] : {};
if (hash.has(src)) return hash.get(src); // 新增代码,查哈希表
hash.set(src, target); // 新增代码,哈希表设值
for (let prop in src) {
if (src.hasOwnProperty(prop)) {
if(src[prop] !== null && typeof(src[prop]) === 'object') {
target[prop] = deepClone2(src[prop], hash);
} else {
target[prop] = src[prop];
}
}
}
return target;
}
var a = {
name: "tn",
book: {
title: "JS",
price: "45"
},
a1: undefined,
a2: null,
a3: 123
};
a.circleRef = a;
var b = deepClone2(a);
console.log(b);
// {
// a1: undefined,
// a2: null,
// a3: 123,
// book: {title: "JS", price: "45"},
// circleRef: {name: "tn", book: {…}, a1: undefined, a2: null, a3: 123, …},
// name: "tn"
// }
复制代码
上面使用了 ES6
中的 WeakMap
来处理,在 ES5
下可使用数组来处理
function deepClone2(src, uniqueList) {
var target = Object.prototype.toString.call(src) == '[object Array]' ? [] : {};
if (!uniqueList) uniqueList = []; // 新增代码,初始化数组
// 数据已经存在,返回保存的数据
var uniqueData = find(uniqueList, src);
if (uniqueData) {
return uniqueData.target;
};
// 数据不存在,保存源数据,以及对应的引用
uniqueList.push({
source: src,
target: target
});
for (let prop in src) {
if (src.hasOwnProperty(prop)) {
if(src[prop] !== null && typeof(src[prop]) === 'object') {
target[prop] = deepClone2(src[prop], uniqueList);
} else {
target[prop] = src[prop];
}
}
}
return target;
}
// 用上面用例测试 OK
复制代码
如今已经很完美的解决了循环引用的状况,但其实还有一种状况是引用丢失,咱们看下面的例子
var obj1 = {};
var obj2 = {a: obj1, b: obj1};
obj2.a === obj2.b; // true
var obj3 = deepClone1(obj2);
obj3.a === obj3.b; // false
复制代码
引用丢失在某些状况下是有问题的,如上面的对象 obj2,obj2 的键值 a 和 b 同时引用了同一个对象 obj1,使用 deepClone1 进行深拷贝后就丢失了引用关系变成了两个不一样的对象
其实上面的 deepClone2 已经解决了这个问题,由于存储了已拷贝过的对象
var obj3 = deepClone2(obj2);
obj3.a === obj3.b; // true
复制代码
Symbol
在 ES6
下才有,须要一些方法来检测出 Symble
类型
方法一:Object.getOwnPropertySymbols(...)
该方法能够查找一个给定对象的符号属性时返回一个 ?symbol 类型的数组。注意,每一个初始化的对象都是没有本身的 symbol 属性的,所以这个数组可能为空,除非你已经在对象上设置了 symbol 属性(来自MDN)
var obj = {};
var a = Symbol("a"); // 建立新的 symbol 类型
var b = Symbol.for("b"); // 从全局的 symbol 注册?表设置和取得symbol
obj[a] = "localSymbol";
obj[b] = "globalSymbol";
var objectSymbols = Object.getOwnPropertySymbols(obj);
console.log(objectSymbols.length); // 2
console.log(objectSymbols) // [Symbol(a), Symbol(b)]
console.log(objectSymbols[0]) // Symbol(a)
复制代码
思路就是先查找有没有 Symbol
属性,若是查找到则先遍历处理 Symbol
状况,而后再处理正常状况
function deepClone3(src, hash = new WeakMap()) {
var target = Object.prototype.toString.call(src) == '[object Array]' ? [] : {};
if (hash.has(src)) return hash.get(src); // 新增代码,查哈希表
hash.set(src, target); // 新增代码,哈希表设值
let symKeys = Object.getOwnPropertySymbols(src); // 查找
if (symKeys.length) { // 查找成功
symKeys.forEach(symKey => {
if (src[prop] !== null && typeof(src[prop]) === 'object') {
target[symKey] = deepClone3(src[symKey], hash);
} else {
target[symKey] = src[symKey];
}
});
}
for (let prop in src) {
if (src.hasOwnProperty(prop)) {
if(src[prop] !== null && typeof(src[prop]) === 'object') {
target[prop] = deepClone3(src[prop], hash);
} else {
target[prop] = src[prop];
}
}
}
return target;
}
var a = {
name: "tn",
book: {
title: "JS",
price: "45"
},
a1: undefined,
a2: null,
a3: 123
};
var sym1 = Symbol("a"); // 建立新的symbol类型
var sym2 = Symbol.for("b"); // 从全局的symbol注册?表设置和取得symbol
a[sym1] = "localSymbol";
a[sym2] = "globalSymbol";
var b = deepClone3(a);
console.log(b);
// {
// a1: undefined
// a2: null
// a3: 123,
// book: {title: "JS", price: "45"},
// circleRef: {name: "tn", book: {…}, a1: undefined, a2: null, a3: 123, …},
// name: "tn",
// [Symbol(a)]: "localSymbol",
// [Symbol(b)]: "globalSymbol"
// }
复制代码
方法二:Reflect.ownKeys(...)
返回一个由目标对象自身的属性键组成的数组。它的返回值等同于
Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
(来自MDN)
Reflect.ownKeys({z: 3, y: 2, x: 1}); // [ "z", "y", "x" ]
Reflect.ownKeys([]); // ["length"]
var sym = Symbol.for("comet");
var sym2 = Symbol.for("meteor");
var obj = {[sym]: 0, "str": 0, "773": 0, "0": 0,
[sym2]: 0, "-1": 0, "8": 0, "second str": 0};
Reflect.ownKeys(obj); // [ "0", "8", "773", "str", "-1", "second str", Symbol(comet), Symbol(meteor) ]
// 注意顺序
// Indexes in numeric order,
// strings in insertion order,
// symbols in insertion order
复制代码
function deepClone3(src, hash = new WeakMap()) {
var target = Object.prototype.toString.call(src) == '[object Array]' ? [] : {};
if (hash.has(src)) return hash.get(src);
hash.set(src, target);
Reflect.ownKeys(src).forEach(key => { // 改动
if (src[key] !== null && typeof(src[key]) === 'object') {
target[key] = deepClone3(src[key], hash);
} else {
target[key] = src[key];
}
});
return target;
}
// 测试 ok
复制代码
这里使用了 Reflect.ownKeys()
获取全部的键值,同时包括 Symbol
,对 src
遍历赋值便可
上面使用的都是递归方法,可是有个问题是可能会爆栈,错误提示以下
// RangeError: Maximum call stack size exceeded
复制代码
详情请参考这篇文章:深拷贝的终极探索(99%的人都不知道)
上面的方式能够知足基本的场景的需求,如有更复杂的需求可本身实现。一些框架和库的也有对应的解决方案,如:jQuery.extend()
、lodash
浅拷贝
对于一层结构的 Array
和 Object
想要拷贝一个副本时使用 vue
的 mixin
是浅拷贝的一种复杂型式
深拷贝
复制深层次的 object
数据结构,如想对某个数组或对象的值进行修改,但又要保留原数组或对象的值不被修改,此时就能够用深拷贝来建立一个新的数组或对象
javascript中的深拷贝和浅拷贝?
JavaScript 如何完整实现深度Clone对象?
ithub lodash源码
MDN 结构化克隆算法 jQuery v3.2.1 源码