简介
为了巩固一下身为前端开发小菜鸟必备的脸面,从而让脸皮更加厚实一些,在这条道路活的更加滋润一些,故此开始整理一些JavaScript(脸面)使用的一些基础细节。
欢迎各位大牛莅临指导,从而让我在加厚脸皮的道路上走的更远,从而可以用脸皮挡子弹,挡导弹,挡核弹,然后无敌。哈哈哈!
今天探究一下深浅拷贝的原理和实现。
浅拷贝的原理和实现
-
浅拷贝的原理
通过创建一个新的对象,来接受需要复制或者引用的对象值。如果对象属性是基本数据类型,则直接复制基本类型的值给新对象;如果对象属性是引用数据类型,则复制的是内存中的地址给新对象,这时复制的地址指向的对象是具有共享性的,当其他地方修改该地址指向的对象后,新创建的对象中复制的地址所指向的对象也是会同步变化的。
-
浅拷贝的实现
-
Object.create()
语法: Object.create(proto) // proto: 新创建对象的原型对象。
Object.create(proto, propertiesObject) // propertiesObject:如果该参数被指定且不为 undefined,则该传入对象的自有可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)将为新创建的对象添加指定的属性值和对应的属性描述符。let a = { name: 'niu', age: 18, sex: 'gril', sym: Symbol(1), list: [ 1, 2, 3, 4 ], obj: { car: 'BMW', hourse: 'large hourse' }, say() { console.log('I am saying') } } a.pointer = a; Object.defineProperty(a, 'test', { value: 'testValue', enumerable: false }) let b = Object.create(a) a.name = 'jiao' a.obj.car = 'bieke' console.log(a) console.log(b)
通过上述代码可以得出,使用Object.create方法可以:- 源对象作为目标对象的原型对象,可以通过原型链访问到源对象的属性和方法;
- 当源对象改变自身的属性或者方法的时候,两者都是同步更改的,从而验证了源对象仅仅只是目标对象的原型对象,它们之间仅仅只是引用的关系。
-
Object.assign()
语法:Object.assign(target, …sources),该方法的第一个参数是拷贝的目标对象,后面的参数是拷贝的来源对象(可以是多个来源)。
let a = { name: 'niu', age: 18, sex: 'gril', sym: Symbol(1), list: [ 1, 2, 3, 4 ], obj: { car: 'BMW', hourse: 'large hourse' }, say() { console.log('I am saying'); } } let b = { eat() { console.log('I am eating') }, age: 20 } a.pointer = a; Object.defineProperty(b, 'test', { value: 'testValue', enumerable: false }) let c = Object.assign({}, a, b); a.name = 'jiao' a.list.push(5) console.log(a) console.log(b) console.log(c)
从上面的代码中可以看到,当修改了对象a的name属性值为’jiao‘时,并不影响对象c的name属性,然而当在对象a的list属性中push了一个新的元素后,对象c的list属性也发生了改变。对象c将对象a的sym属性成功的进行了拷贝,而对于对象b的test属性无法拷贝,从而证实了Object.assign方法实现了我们想要的结果。同时使用该方法的时候需要注意以下几点:
- 它不会拷贝对象的继承属性;
- 它不会拷贝对象的不可枚举属性;
- 他可以拷贝对象的Symbol属性。
-
扩展运算符方式
使用js的扩展运算符,在构造对象的同时完成浅拷贝的功能。
语法:let cloneObj = {…obj1, …, …objX};
let cloneArr = […arr1, …, …arrX]。let a = { name: 'niu', age: 18, sex: 'gril', sym: Symbol(1), list: [ 1, 2, 3, 4 ], obj: { car: 'BMW', hourse: 'large hourse' }, say() { console.log('I am saying'); } } let b = { eat() { console.log('I am eating') }, age: 20 } Object.defineProperty(b, 'test', { value: 'testValue', enumerable: false }) let c = {...a, ...b}; console.log(b) console.log(c) a.name = 'jiao' a.list.push(5) let d = [1, 2, 3, { name: 'niu' }, 4] let e = [4, 5, 6, 7, 8, 9, { sex: 'boy' }] let f = [...d, ...e]; f[0] = 10; d[3].name = 'jiao'; e[6].sex = 'girl' console.log(d) console.log(e) console.log(f)
主要不同的地方在于使用语法的不同,其他地方同Object.assign方法类似,都具有相同的缺陷。
-
slice方法拷贝数组
slice方法具有局限性,它仅仅只针对数组类型,返回一个新的数组对象,不会改变原数组对象。
语法:arr.slice(begin, end)。let a = [1, 2, 3, { name: 'niu' }, 4] let b = a.slice(); b[0] = 10; a[3].name = 'jiao'; console.log(a) console.log(b)
通过对比发现使用slice方法,对数组中的基本数据类型进行了赋值操作,对数组中的引用数据类型进行了赋址操作,符合浅拷贝的要求。同时也暴露出浅拷贝的限制-它只能拷贝一层对象,如果存在对象的多层嵌套,那么浅拷贝就无能无力了。 -
concat方法拷贝数组
concat方法同slice方法一样,具有局限性,它也是仅仅只针对数组类型,返回一个新的数组对象,不会改变原数组对象。
语法: let newArr = arr.concat(arr1, arr2,…,arrX)let a = [1, 2, 3, { name: 'niu' }, 4] let b = [4, 5, 6, 7, 8, 9, { sex: 'boy' }] let c = [].concat(a, b) c[0] = 10; a[3].name = 'jiao'; b[6].sex = 'girl' console.log(a) console.log(b) console.log(c)
-
原生实现一个浅拷贝
- 对基本数据类型做一个赋值操作;
- 对引用数据类型开辟一个新的存储,并且拷贝一层对象属性。
// 浅拷贝 function shallowClone(obj) { if (typeof obj === 'object' && obj !== null) { let properties = Object.getOwnPropertyNames(obj) let target = Array.isArray(obj) ? [] : {} for (let i = 0; i < properties.length; i++) { target[properties[i]] = obj[properties[i]]; } return target } else { return obj } } let a = { name: 'niu', sex: 'boy', age: 18, } Object.defineProperty(a, 'weight', { value: 60, enumerable: false }) let b = [1,2,3,4,5,6] console.log(shallowClone(a)) // {age: 18, name: 'niu', sex: 'boy', weight: 60} console.log(shallowClone(b)) // [1,2,3,4,5,6]
-
深拷贝的原理和实现
-
深拷贝的原理
将一个对象从内存中完整的拷贝出来一份给目标对象,并在堆内存中开辟出一个全新的空间去存储新的对象。新对象的修改不会影响原对象,两者实现真正的分离。
-
深拷贝的实现
-
JSON.stringify方法(简略版)
将一个对象序列化为JSON字符串,并将对象里面的内容转变为字符串;最后再用 JSON.parse() 的方法将JSON 字符串生成一个新的对象。
class Animal { constructor() { this.name = 'name' } listen() { console.log('I am listening') } } let a = { name: 'niu', sex: 'boy', age: 18, say() {}, u: undefined, sym: Symbol(), date: new Date(), reg: new RegExp(/\d/), num: NaN, num1: Infinity, num2: -Infinity } Object.defineProperty(a, 'weight', { value: 60, enumerable: false }) a.__proto__ = new Animal(); function deepClone(target) { return JSON.parse(JSON.stringify(target)) } let b = deepClone(a); a.name = 'jiao'; console.log(a); console.log(b);
-
拷贝的对象的值中如果有函数、undefined、symbol 这几种类型,经过 JSON.stringify 序列化之后的字符串中这个键值对会消失;
-
拷贝 Date 引用类型会变成字符串;
-
无法拷贝不可枚举的属性;
-
无法拷贝对象的原型链;
-
拷贝 RegExp 引用类型会变成空对象;
-
对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;
-
无法拷贝对象的循环应用,即对象成环 (obj[key] = obj)。
-
-
手写递归实现(基础版)
// 递归实现深拷贝 function deepClone(source) { // 基本数据类型 if (typeof source !== 'object' || !source) { return source } // 数组 || 对象 let target = Array.isArray(source) ? [] : {} for (let i in source) { target[i] = deepClone(source[i]); } return target } class Animal { constructor() { this.name = 'name' } listen() { console.log('I am listening') } } let a = { name: 'niu', sex: 'boy', age: 18, say() { console.log('I am saying') }, u: undefined, sym: Symbol(1), arr: [1,2,3,4,5], arr1: new Array(5).fill(1), date: new Date(), err: new Error(), reg: new RegExp(/\d/), num: NaN, num1: Infinity, num2: -Infinity } Object.defineProperty(a, 'weight', { value: 60, enumerable: false }) a.__proto__ = new Animal(); const b = deepClone(a); console.log(a) console.log(b)
通过上图可以得出,基础版并不能兼顾所有的情况,它存在下述的问题:
- 它只是针对普通的引用数据类型做递归复制,而对于Date、RegExp、Error、FormData 这样的引用类型并不能正确地拷贝;
- 无法复制不可枚举的属性;
- 无法拷贝源对象的原型链;
- 无法拷贝对象的循环应用,即对象成环 (obj[key] = obj)。
-
手写递归实现(改进版)
// 递归实现深拷贝 function deepClone(source, hash = new WeakMap()) { let _toString = Object.prototype.toString // 基本数据类型 if (typeof source !== 'object' || !source) { return source } // DOM Node if (source.nodeType && 'cloneNode' in source) { return source.cloneNode(true) } // Date if (_toString.call(source) === '[object Date]') { return new Date(source.getTime()) } // RegExp if (_toString.call(source) === '[object RegExp]') { const flags = []; if (source.global) flags.push('g') if (source.multiline) flags.push('m') if (source.ignoreCase) flags.push('i') return new RegExp(source.source, flags.join('')) } // Error if (_toString.call(source) === '[object Error]') { return new Error(source.message) } // ... Map对象 Set对象等都需要重新生成一个新的实例返回 // 循环引用通过weakMap解决 if (hash.has(source)) { return hash.get(source) } // 获取source对象的所有自身属性的描述符 let own = Object.getOwnPropertyDescriptors(source) // 继承原型链 let target = Object.create(Object.getPrototypeOf(source), own) hash.set(source, target); for (let key of Reflect.ownKeys(source)) { target[key] = deepClone(source[key], hash) } return target } class Animal { constructor() { this.name = 'name' } listen() { console.log('I am listening') } } let a = { name: 'niu', sex: 'boy', age: 18, say() { console.log('I am saying') }, u: undefined, sym: Symbol(1), arr: [1,2,3,4,5], arr1: new Array(5).fill(1), date: new Date(), err: new Error('错误了'), reg: new RegExp(/\d/), num: NaN, num1: Infinity, num2: -Infinity, pointer: { name: 'jiao', sex: 'girl', list: [1,2,3,4,5, {a: 6}], obj: { b: 'b', c: 'c' } } } Object.defineProperty(a, 'weight', { value: 60, enumerable: false }) a.__proto__ = new Animal(); const b = deepClone(a); a.pointer.name = 'jiaojiao' console.log(a) console.log(b)
针对上面的问题,通过下述方法去进行解决:- 针对遍历对象的不可枚举属性,通过使用Reflect.ownKeys() 返回一个由目标对象自身的属性键组成的数组, 结合Object.getOwnPropertyDescriptors()方法返回所有属性的属性值以及属性描述信息进行处理;
- 当参数为RegExp、Error、Date等类型时,直接生成一个新的实例进行返回;
- 拷贝对象的原型链则是通过Object.create创建一个新的对象,并继承传入原对象的原型链处理;
- 使用weakMap类型作为hash表,去检测是否循环引用,如果存在循环,则直接返回weakMap存储的值。
-