JavaScript | 2020-07-23 19:16:10 1298次 2次
一、浅拷贝
浅拷贝,只拷贝一层,如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。提示:以下的拷贝不涉及原型上方法。
第一种:采用 es6 中的扩展运算符:
let source = { d: 4, e: { e1: 'source e1' } } let s = {...source} source.d = 5 source.e.e1 = 'source new value' console.log(s) //打印结果 d: 4 e: {e1: "source new value"} __proto__: Object
第二种:可以通过 Object.assign 实现:
let source = { d: 4, e: { e1: 'source e1' } } let s = Object.assign({}, source) source.d = 5 source.e.e1 = 'source new value' console.log(s) //结果如下 d: 4 e: {e1: "source new value"}
第三种:采取遍历的方式进行拷贝也是一样的效果:
function cloneShallow(source) { let target = {}, hasProperty = Object.prototype.hasOwnProperty; for (let key in source) { if (hasProperty.call(source, key)) { target[key] = source[key]; } } return target; } let s = cloneShallow(source) source.d = 5 source.e.e1 = 'source new value' console.log(s) //打印结果 d: 4 e: {e1: "source new value"} __proto__: Object
实现了将 source 拷贝到 target 上,可以看到对于 source 中的第二层(引用类型数据)修改后,目标对象中的 e1 同样被修改了,但是基本数据类型的值是不受影响的,验证了开头的那句话。
通过第三种这种方式,我们可以模拟实现下 Object.assign 方法:
Object.defineProperty(Object, "assignSelf", { value: function (arg) { //目标参数不能为 undefined或者null if (arg == null) { throw new TypeError('Cannot convert undefined or null to object'); } // Object.assign(1, {a:1}) 将target包装为对象 let target = Object(arg), hasOwnProperty = Object.prototype.hasOwnProperty; for (let i = 1; i < arguments.length; i++) { let nextSource = arguments[i]; // 过滤待拷贝的空项 if (nextSource != null) { for (let nextKey in nextSource) { //只要自身上属性 if (hasOwnProperty.call(nextSource, nextKey)) { target[nextKey] = nextSource[nextKey]; } } } } //返回目标 return target; }, writable: true, configurable: true });
二、深拷贝
深拷贝相当于不用拷贝了,而是我要抄袭你,完完全全的抄袭你,拷贝完成后两个对象相互不影响,因为连内存也拷贝过来了,而不是引用。比如我们最常用的深拷贝方法:
let source = { d: 4, e: { e1: 'source e1' } } let s = JSON.parse(JSON.stringify(source)) source.e.e1 = 'this is new value' console.log(s, source) //打印结果s d: 4 e: e1: "source e1" //打印结果 source d: 4 e: e1: "this is new value"
可见此时的 source 中第二层引用类型的数值修改后,并不会影响已经拷贝出来的 s 数据。
采用浅拷贝遍历的方式再进行递归处理也可以深拷贝:
//是否对象 function isObject(val){ return (typeof val === 'object' && val != null); } function deepClone(source){ let target = {}, hasOwnProperty = Object.prototype.hasOwnProperty; for(let k in source){ if(hasOwnProperty.call(source, k)){ if(isObject(source[k])){ //递归调用 target[k] = deepClone(source[k]) }else{ target[k] = source[k] } } } return target; } // 继续使用source进行拷贝 let b = deepClone(source); sources.e.e1 = 111 console.log(b.e.e1) //source e1 并不会再被修改
看着是可以了,继续修改下待拷贝的数据源:
let source = { d: 4, e: { e1: 'source e1' }, f: [1, 2, 3] }
此时打印 f 会发现很奇怪,因为本身是一个数组,但是我们在定义 target 时是一个对象,所以需要考虑数组的情况:
function deepClone(source){ // 判断一下就好了 或者 source instanceof array let target = Array.isArray(source) ? [] : {} ... }
对象中也可以使用 Symbol 作为 key,所以再考虑一下 symbol 的情况:
let sym = Symbol('1') let source = { d: 4, e: { e1: 'source e1' }, f: [1,2,3], [sym]: 1 } console.log(Object.keys(source)) //["d", "e", "f"] console.log(Object.getOwnPropertySymbols(source)) // [Symbol(1)] console.log(Reflect.ownKeys(source)) // ["d", "e", "f", Symbol(1)]
可以看到通过 keys 这种方式拿不到 Symbol 类型,必须通过 getOwnPropertySymbols 特有的方法才可以取到,其中 Reflect 将来会取代 Object,通过 ownKeys 方式可以全部拿到,那么可以通过两种方式来实现拷贝,第一种就是在原来的基础上再增加一个 symbol key 遍历:
function deepClone(source){ ... let symKeys = Object.getOwnPropertySymbols(source); if (symKeys.length) { // 如果存在 symbol key symKeys.forEach(symKey => { if (isObject(source[symKey])) { target[symKey] = deepClone(source[symKey], hash); } else { target[symKey] = source[symKey]; } }); } ... }
第二种方式直接使用 Reflect 上的方法,全部遍历一次:
function deepClone(source){ let target = Array.isArray(source) ? [] : {}; Reflect.ownKeys(source).forEach(k => { if(isObject(source[k])){ target[k] = deepClone(source[k]) }else{ target[k] = source[k] } }) return target; } let s = deepClone(source) source.e.e1 = 111 source.f.push(111) console.log(s) // 结果 d: 4 e: {e1: "source e1"} f: (3) [1, 2, 3] Symbol(1): 1
在使用 Reflect.ownKeys 时没有进行 hasOwnProperty 的判断,因为这个方法是不会枚举原型上的属性:
let s = Object.create({a: 1}) Reflect.ownKeys(s) // [] //使用 for in 则可以 for( let k in s){ console.log(k) //a }
三、深拷贝循环引用
再继续考虑一种情况,如果数据中出现了循环引用的情况,以上的方式就会无限递归爆栈:
let source = { d: 4, a: {} } source.a.a = source.a source.e = source
可以考虑使用 Map 来存储对象,但是这里更好的方式是使用 WeakMap,区别是后者只能将对象格式作为键名(null 除外):
... function deepClone(source, hash = new WeakMap()){ if(!isObject(source)){return source} if(hash.has(source)){ return hash.get(source) } let target = Array.isArray(source) ? [] : {}; //这里记录的是引用 hash.set(source, target) Reflect.ownKeys(source).forEach(k => { if(isObject(source[k])){ target[k] = deepClone(source[k], hash) }else{ target[k] = source[k] } }) return target; } let s = deepClone(source) source.e.e.d = 10 console.log(s) //结果 a: {a: {…}} d: 4 e: a: {a: {…}} d: 4 e: {d: 4, a: {…}, e: {…}}
四、深拷贝递归转遍历
采用递归的方式数据量大的时候可能会爆栈,深拷贝的方式貌似找不到尾递归优化的方式,所以可以改为队列遍历的方式实现,可以先参考下树和数组转换 分别采用了递归和遍历的方式实现。
function deepClone(x) { const root = {}; // 队列 const loopList = [{ parent: root, key: undefined, data: x, }]; let hash = new WeakMap(); while(loopList.length) { // 广度优先 const { parent, key, data } = loopList.shift(); // res记录层 let res = parent; if (key) { res = parent[key] = Array.isArray(data) ? [] : {}; } hash.set(data, res) Reflect.ownKeys(data).forEach(k => { let item = data[k]; if(isObject(item)){ if(hash.has(item)){ res[k] = hash.get(item) }else{ // 追加队列数据 loopList.push({ parent: res, key: k, data: item, }) } }else{ res[k] = item; } }) } return root; } let s = {a: 1, b: { c: 1 }, arr:[1,2,3]} s.d = s let news = deepClone(s) s.d.a = 2 s.b.c = 10 s.arr.push(4) //打印 news 拷贝后数据,数据正常
这里面采取广度优先遍历的方式,不断的构造追加队列,直到为基本数据类型才赋值,队列中包含 parent, key, data 三个主要标记往哪里挂载和键值信息,同时也需要处理相互引用的数据。
2人赞