在 JavaScript 中,对象可谓是一个很是重要的知识点。什么原型链啊,拷贝啊,继承啊,建立啊等等等等。在我以前的文章中已经对对象的建立和继承作了一个简单的介绍,【JavaScript】ES5/ES6 建立对象与继承,那么这篇文章主要是针对对象的拷贝。数组
2018-07-31更新: 循环引用以及包装对象拷贝bash
咱们先定义一个构造函数,建立好一个等待拷贝的对象。如下操做不考虑循环引用、Date 对象以及 RegExp 对象的拷贝等问题。函数
function Person(name, age, job, ) {
this.name = name
this.age = age
this.job = job
this.height = function () { }
this.weight = Symbol.for('weight')
this.friend = {
name: 'kangkan',
age: 15
}
}
Person.prototype.hobby = function () {
return ['football', 'basketball']
}
const person = new Person('mike', null, undefined)
复制代码
对象不一样于 Number、String 等基础类型,它是一个引用类型,也就说它的值是保存在堆上,经过内存地址来访问的。简单来看post
const a = {one: 1}
const b = {one: 1}
a === b // false
复制代码
若是 obejct1 的引用地址和 object2 一致,那么这就是浅拷贝,实现方式有三种。ui
const a = {one: 1}
const b = a
b === a // true
a.two = 2
console.log(b.two) // 2
复制代码
const simpleClone = function (target) {
if (typeof target !== 'object') {
throw new TypeError('arguments must be a Object!')
}
let obj = {}
// 设置原型
const prototype = Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(obj, prototype)
// 设置属性
Reflect.ownKeys(target).forEach((key) => {
obj[key] = target[key]
})
return obj
}
const clonePerson = simpleClone(person)
复制代码
能够看出拷贝的结果仍是使人满意的。this
下图 Object.assign(person) 应为 Object.assign({}, person)spa
经过这个方法也能达到相同的效果prototype
const simpleClonePerson = Object.assign({}, person)
复制代码
const simpleClonePerson = {...person}
复制代码
可是这里有个问题,原型对象丢失了。没法判断 simpleClonePerson 的实例。3d
可是操做一下 clonePerson.friend 对象,给它添加一个属性就会发现,person 对应的也增长了一个新属性。这不是咱们的预期。code
也就说经过 simpleClone 和 Object.assign 拷贝的对象只有第一层是深拷贝,第二层就是浅拷贝了。是对引用地址的拷贝。
简单来讲,以上的浅拷贝方法,在对象深度只有一层的时候其实就是深拷贝。可是当对象的深度大于1,那么对象里面的对象就没法完成深拷贝了。
深拷贝的方法也有两种。
const clonePerson = JSON.parse(JSON.stringify(person))
复制代码
从图中也能看出来,利用 JSON 的方法也是会有不少缺点的。
缺点1:会忽略 undefined
缺点2:不能序列化函数
缺点3:没法拷贝 Symbol
递归拷贝其实也就是在浅拷贝的遍历拷贝上新增了一些东西
const deepClone = function (target) {
if (typeof target !== 'object') {
throw new TypeError('arguments must be a Object!')
}
let obj = {}
// 设置原型
const prototype = Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(obj, prototype)
// 设置属性
Reflect.ownKeys(target).forEach((key) => {
const value = target[key]
if (value !== null && typeof value === 'object') {
obj[key] = deepClone(value)
} else {
obj[key] = value
}
})
return obj
}
复制代码
达到了想要的效果。
咱们扩展一下 Person 构造函数
function Person(name, age, job, ) {
this.name = name
this.age = age
this.job = job
this.height = function () { }
this.weight = Symbol.for('weight')
this.friend = {
name: 'kangkan',
age: 15
}
this.family = new Person2()
this.date = new Date('2018-06-06')
this.regExp = /test/ig
}
function Person2() { }
复制代码
能够看到这里就多了一个 date 属性和 regExp 属性,若是经过以前普通的 deepClone 的话,会出现以下结果。
因此咱们须要对 deepClone 方法进行必定的改造
const deepClone = function (target) {
if (typeof target !== 'object') {
throw new TypeError('arguments must be a Object!')
}
let obj = {}
// 设置原型
const prototype = Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(obj, prototype)
// 设置属性
Reflect.ownKeys(target).forEach((key) => {
const value = target[key]
// 在此处进行改造
try {
const Constructor = Reflect.getPrototypeOf(value).constructor
// 这里只针对 Date 对象和 RegExp 对象进行简单的说明
if (Constructor === Date || Constructor === RegExp) {
obj[key] = new Constructor(value.valueOf())
} else {
obj[key] = deepClone(value)
}
} catch (e) {
obj[key] = value
}
})
return obj
}
复制代码
咱们再来看看打印结果
person.family = person // 此处出现循环引用
const deepClone = function (target) {
if (typeof target !== 'object') {
throw new TypeError('arguments must be a Object!')
}
let obj = {}
// 设置原型
const prototype = Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(obj, prototype)
// 设置属性
Reflect.ownKeys(target).forEach((key) => {
const value = target[key]
try {
const Constructor = Reflect.getPrototypeOf(value).constructor
if (Constructor === Date || Constructor === RegExp) {
obj[key] = new Constructor(value.valueOf())
} else {
obj[key] = deepClone(value)
}
} catch (e) {
obj[key] = value
}
})
return obj
}
复制代码
由上图能够看到,经过 deepClone 方法进行深拷贝,一旦出现循环引用会致使栈溢出。
咱们须要对 deepClone 方法再次进行改造
const deepClone = function (target) {
if (typeof target !== 'object') {
throw new TypeError('arguments must be a Object!')
}
// 已经访问过的对象集合
const visitedObjs = []
// 克隆的对象集合
const clonedObjs = []
const clone = function (source) {
if (visitedObjs.indexOf(source) === -1) { // 这里是判断该原对象是否被访问过
visitedObjs.push(source) // 放入数组中
const obj = {} // 建立一个待克隆的新对象
// 设置原型
const prototype = Reflect.getPrototypeOf(source)
Reflect.setPrototypeOf(obj, prototype)
clonedObjs.push(obj); // 将其置入克隆对象集合中
// 设置属性
Reflect.ownKeys(source).forEach((key) => {
const value = source[key]
try {
const Constructor = Reflect.getPrototypeOf(value).constructor
if (Constructor === Date || Constructor === RegExp) {
obj[key] = new Constructor(value.valueOf())
} else {
obj[key] = clone(value) // 此处不能再递归调用 deepClone,而是要改成 clone 方法
}
} catch (e) {
obj[key] = value
}
})
return obj
} else {
// 若是该对象已经被访问过了,则直接从克隆对象中返回。返回的对象的索引是 source 在 visitedObjs 中的索引位置。
return clonedObjs[visitedObjs.indexOf(source)]
}
}
return clone(target)
}
复制代码
再来看看效果
写了这么多主要仍是了解一些对象的拷贝问题,从上面的一步步改造也能够看出来要真想写完美这个功能也是得一番功夫的。因此最后你们仍是去用 lodash 吧,哈哈哈哈哈。