> 文章列表 > 拷贝、原型原型链

拷贝、原型原型链

拷贝、原型原型链

浅拷贝

将原对象或原数组的引用直接赋给新对象,新数组

新对象只是对原对象的一个引用,而不复制对象本身。新旧对象还是共享同一块内存

如果属性是一个基本数据类型,拷贝的就是基本数据类型的值

如果属性是引用类型,拷贝的是内存地址。存在数据“共享”,一个对象改变了这个地址指向的属性值会影响到另一个对象的属性值

浅拷贝的实现方式

Object.assign()

ES6 中 Object 内置对象的方法,该方法可以用于 js 对象的合并等用途,其中一个用途就是可以进行浅拷贝

语法:Object.assign(target, ...sources)
target:拷贝的目标对象
sources:拷贝的来源对象,可以是多个来源

使用 Object.assign() 的注意点:

  • 不会拷贝对象的继承属性
  • 不会拷贝对象的不可枚举属性
  • 可以拷贝 Symbol 类型的属性 

扩展运算符

利用扩展运算符,可以在构造对象时完成浅拷贝的功能

语法:let cloneObj = { ...obj }

扩展运算符和 Object.assign() 有同样的缺陷,也就是实现的功能相差不大

但如果属性都是基本数据类型,使用扩展运算符进行浅拷贝会更方便 

数组 concat()

使用 concat 方法连接一个含有引用值的数组时,需要注意修改原数组中的元素属性,因为它会影响拷贝后连接的数组

concat 方法只适用于数组的浅拷贝,使用场景比较局限

数组 slice()

slice 方法也只适用于数组的浅拷贝,使用场景比较局限

语法:arr.slice(begin, end)
begin 开始截取的元素下标
end 结束截取的元素下标(不包括)

slice 方法会返回一个新的数组对象,不会改变原数组

总结

1. Object.assign()
2. ...  扩展运算符
3. Array.prototype.concat()
4. Array.prototype.slice()

浅拷贝的限制在于只能拷贝一层属性,如果存在嵌套属性,浅拷贝就差点意思

此时就要用到深拷贝,解决多层对象嵌套问题,彻底实现拷贝

手写浅拷贝

根据浅拷贝的定义,如果要手动封装一个浅拷贝方法,大致思路如下:

判断源数据类型,如果是基本类型,直接返回源数据;如果是引用类型,for...in 遍历源数据内部属性/元素(浅拷不会拷贝不可枚举属性),判断是否是源数据自由属性/元素(浅拷贝不会拷贝继承属性),内部属性/元素为基本类型,直接赋值;内部属性/元素为引用类型,复制地址,数据共享

代码如下:

const shallowClone = (target) => {if (typeof target === 'object' && target !== null) {const cloneTarget = target instanceof Array ? [] : {}for (let item in object) {if (target.hasOwnProperty(item)) {cloneTarget[item] = target[item]}}return cloneTarget} else {return target}
}let obj = { a: 1, b: 2, c: { cc: 3 } }
let cloneObj = shallowClone(obj)
cloneObj.b = 3
// 此时 obj 中的 b 值仍为 2
cloneObj.c.cc = 4
// 此时 obj 中的 cc 值也变为 4,证明共享了一片内存,浅拷贝
console.log(obj, cloneObj)

可实现普通对象、数组对象、函数对象和基本数据类型的浅拷贝 

深拷贝

浅拷贝只是创建了一个新的对象,复制了源对象的基本类型的值;对于引用数据,只复制了地址,拷贝出来的数据和源数据共用同一个内存空间,只是多了个指向该空间的引用

深拷贝和浅拷贝的最大不同之处在于:深拷贝对于引用数据类型,会在堆内存中完全开辟一个新的内存空间,并将源对象完全复制过来存放

这两个对象是相互独立,互不影响的,彻底实现了内存上的分离

总的来说,深拷贝的原理可以总结为:

将源对象从内存中完整地拷贝出来一份给目标对象,并在堆内存中开辟一个全新的空间存放新对象,且新对象的修改不会改变源对象,二者实现真正的分离

深拷贝的实现方式

JSON.stringify

是目前开发过程中最简单的实现深拷贝的方法,其实就是将一个对象序列化为 JSON 字符串,并将对象里面的内容转换成字符串,最后再用 JSON.parse 方法将 JSON 字符串生成一个新的对象

let arr = [1, 2, { a: 3 }]
let cloneArr = JSON.parse(JSON.stringify(arr))
cloneArr[2].a = 4
// 此时 arr 中第三个元素对象的属性 a 的值仍为 3
console.log(arr, cloneArr)let obj = { a: 1, b: 2, c: { cc: 3 } }
let cloneObj = JSON.parse(JSON.stringify(obj))
cloneObj.b = 22
cloneObj.c.cc = 33
// 修改 cloneObj 的属性值,不会影响 obj 的属性值
console.log(obj, cloneObj)

使用 JSON.stringify 实现深拷贝需要注意:

  • 拷贝对象的属性值中如果有函数、undefined、Symbol 这几种类型,经过 JSON.stringify 序列化之后的字符串中这个键值对会消失
  • 拷贝的 Date 类型数据会变成字符串
  • 无法拷贝不可枚举的属性
  • 无法拷贝对象的原型
  • 拷贝 RegExp 引用类型会变成空对象
  • 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null
  • 无法拷贝对象的循环应用,即对象成环 obj[key] = obj
function Obj() {this.funFun = function () {}this.obj = { a: 1 }this.arr = [1, 2, 3]this.und = undefinedthis.reg = /123/this.date = new Date()this.NaN = NaNthis.infinity = Infinitythis.sym = Symbol(1)
}let caseObj = new Obj()
Object.defineProperty(caseObj, 'innumerable', {enumerable: false,value: 'innumerable',
})
console.log('实例对象:', caseObj)let cloneObj = JSON.parse(JSON.stringify(caseObj))
console.log('克隆对象:', cloneObj)

 

使用 JSON.stringify 方法实现深拷贝对象,虽然还有很多无法实现的功能,但这种方法足以满足日常的开发需求,并且是最简单和快捷的

如果需求比较严格,就需要对复杂一些的属性值进行单独处理

基础版(可适用于大部分情况)

思路:封装 deepClone 函数。判断参数是否是引用类型,不是直接返回;是则通过 for...in 遍历传入参数的属性。如果属性值是引用类型则再次递归调用该函数;如果属性值是基本数据类型则直接复制

const deepClone = (obj) => {if (typeof obj === 'object' && obj !== null) {let cloneObj = Array.isArray(obj) ? [] : {}for (let item in obj) {if (typeof obj[item] === 'object' && obj[item] !== null) {cloneObj[item] = deepClone(obj[item])} else {cloneObj[item] = obj[item]}}return cloneObj} else {return obj}
}let caseObj = new Obj()
Object.defineProperty(caseObj, 'innumerable', {enumerable: false,value: 'innumerable',
})
console.log('实例对象:', caseObj)let cloneObj = deepClone(caseObj)
console.log('克隆对象:', cloneObj)

虽然利用递归可以实现深拷贝,但和 JSON.stringify 一样, 仍然有一些问题存在:

  • 不能复制不可枚举属性和 Symbol 类型属性
  • 无法拷贝属性描述符和原型链
  • 只针对普通对象和数组对象做递归复制,而对于 Date、RegExp、Error、Function 这样的引用类型并不能正确拷贝
  • 对象属性里面成环,循环引用的情况无法解决

基础版写法简单,可以应对大部分情况,但仍有缺陷

改进版

针对上面所说的问题,先讲思路:

  • 当参数为 Date、RegExp 类型,则直接生成一个新的实例返回
  • 利用 Object.getOwnPropertyDescriptors 方法可以获得对象的所有属性,以及对应的特性,顺便结合 Object.create 方法创建一个新对象,并继承传入原对象的原型链
  • 遍历对象的不可枚举属性和 Symbol 类型属性,可以用 Reflect.ownKeys 方法
  • 利用 WeakMap 类型作为 Hash 表,因为 WeakMap 是弱引用类型,可以有效防止内存泄漏,作为检测循环引用很有帮助。如果存在循环,则引用直接返回 WeakMap 存储的值

代码如下:

// 判断是否是复杂引用类型数据
const isComplexDataType = (obj) =>(typeof obj === 'object' || typeof obj === 'function') && obj !== null
const deepClone = function (obj, hash = new WeakMap()) {// 日期对象和正则对象直接通过构造函数返回if (obj.constructor === Date) return new Date(obj)if (obj.constructor === RegExp) return new RegExp(obj)// 循环引用使用 weakMap 解决if (hash.has(obj)) return hash.get(obj)// 获取所有属性的描述符let allDesc = Object.getOwnPropertyDescriptors(obj)// 继承原型链,遍历传入参数所有属性的特性let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)hash.set(obj, cloneObj)for (const key of Reflect.ownKeys(obj)) {cloneObj[key] = isComplexDataType(obj[key])? deepClone(obj[key], hash): obj[key]}return cloneObj
}
let obj = {num: 0,str: '',boolean: true,unf: undefined,nul: null,obj: { name: '我是一个对象', id: 1 },arr: [0, 1, 2],func: function () {console.log('我是一个函数')},date: new Date(0),reg: new RegExp('/我是一个正则/ig'),[Symbol('1')]: 1,
}
Object.defineProperty(obj, 'innumerable', {enumerable: false,value: '不可枚举属性',
})
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
// 设置 loop 成循环引用属性
obj.loop = obj
let cloneObj = deepClone(obj)
obj.arr.push(4)
console.log('实例对象:', obj)
console.log('克隆对象:', cloneObj)

总结

1. JSON.parse(JSON.stringify())
2. 递归操作
3. cloneDeep
4. Jquery.extend()

原型原型链

原型

每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。这个对象就是通过调用构造函数创建的对象的原型

使用原型对象的好处:在原型上定义的属性和方法可以被对象实例共享

无论如何,只要创建一个函数,就会按照特定的规则为这个函数创建一个 prototype 属性(指向原型对象)

默认情况下,所有原型对象自动获得一个名为 constructor 的属性,指回与之关联的构造函数

在自定义构造函数时,原型对象默认只会获得 constructor 属性,其他的所有方法都继承自 Object。每次调用构造函数创建一个新实例,这个实例的内部 [[Prototype]] 指针就会被赋值为构造函数的原型

脚本中没有访问这个 [[Prototype]] 特性的标准方式,但 Firefox、Safari 和 Chrome 会在每个对象上暴露 _proto_ 属性,通过这个属性可以访问对象的原型

在其他实现中,这个特性被完全隐藏

关键在于理解这一点:实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有

闭包