这个题目主要是希望考察面试者的 JavaScript 基本功(ES5 + ES6,甚至是一些新的JS API,比如结构化克隆:structuredClone
),需要对对象和数组的常用方法有一定的认知和熟悉度。如果能有条理地口头说出一些自己在实际项目中常用的深浅拷贝手段,并且能自己手写一个具有特定使用场景的深拷贝方法,当然是更好的了。
一、浅拷贝
按照 MDN 的说法,在JavaScript中,内置对象复制操作(扩展运算符(...)
、Object.assign()
、Object.create()
、Array.prototype.concat()
、Array.prototype.slice()
和 Array.from()
)创建的是浅拷贝。
浅拷贝指的是只复制一层,这对于只包含原始值的数组或对象来说很适用。
1、对象
1.1 Object.assign(obj1, obj2)
在 ES6 之前,Object.assign()
可能是最流行的深拷贝对象的方法了,它跟上一个 JSON.parse(JSON.stringify(object))
相比,能拷贝包含函数元素的对象。
const user = { name: "iankevin", age: 28, job: "Web Developer", incrementAge: function () { this.age++; } }; let clone = Object.assign({}, user);
1、对象
1.1 Object.assign(obj1, obj2)
在 ES6 之前,Object.assign() 可能是最流行的深拷贝对象的方法了,它跟上一个 JSON.parse(JSON.stringify(object)) 相比,能拷贝包含函数元素的对象。
const user = { name: "iankevin", age: 28, job: "Web Developer", incrementAge: function () { this.age++; } }; let clone = Object.assign({}, user);
这个方法也有一个问题的,它在对象存在嵌套的场景不适用,比如:
const user = { name: "iankevin", age: 28, job: "Web Developer", location: { city: "beijing", }, incrementAge: function () { this.age++; }, }; const clone = Object.assign({}, user); clone.incrementAge(); console.log(user.age); // 28 console.log(clone.age); // 29 clone.location.city = "shanghai"; console.log(clone.location.city); // shanghai console.log(user.location.city); // shanghai
1.2 Object.create(object)
其实这个方法的效果跟上一个类似,也是只能深拷一层,深层次的嵌套不适用:
const user = { name: "iankevin", age: 28, job: "Web Developer", location: { city: "beijing", }, incrementAge: function () { this.age++; }, }; const clone = Object.create(user); clone.incrementAge(); console.log(user.age); // 28 console.log(clone.age); // 29 clone.location.city = "shanghai"; console.log(clone.location.city); // shanghai console.log(user.location.city); // shanghai
1.3 ES6 扩展运算符(...)
ES6 语法中的扩展运算符(...)能最简便实现常见场景的深拷贝,但它的缺陷跟 Object.assign()
类似,对嵌套的对象深拷贝无能为力。
const user = { name: "iankevin", age: 28, job: "Web Developer", location: { city: "beijing", }, incrementAge: function () { this.age++; }, }; const clone = { ...user }; clone.incrementAge(); console.log(user.age); // 28 console.log(clone.age); // 29 clone.location.city = "shanghai"; console.log(clone.location.city); // shanghai console.log(user.location.city); // shanghai
2、数组
2.1 Array.prototype.concat()
这个方法是数组的内置方法,对一维数组是”深拷贝“:
let originalData = ["noodles", "eggs", "flour", "water"]; let cloneData = [].concat(originalData); cloneData[0] = "tomato"; console.log(JSON.stringify(originalData)); // ["noodles","eggs","flour","water"] console.log(JSON.stringify(cloneData)); // ["tomato","eggs","flour","water"]
但是对二维及更多维的数组,则不太适用,比如:
let originalData = ["noodles", { list: ["eggs", "flour", "water"] }]; let cloneData = [].concat(originalData); console.log(JSON.stringify(cloneData)); // ["noodles",{"list":["eggs","flour","water"]}] cloneData[1].list = ["rice flour", "water"]; cloneData[0] = "tomato"; console.log(originalData[1].list); // [ "rice flour", "water" ] console.log(JSON.stringify(originalData)); // ["noodles",{"list":["rice flour","water"]}] console.log(JSON.stringify(cloneData)); // ["tomato",{"list":["rice flour","water"]}]
2.2 Array.prototype.slice()
这个方法跟 Array.prototype.concat()
类似,是数组的内置方法,对一维数组是”深拷贝“:
let originalData = ["noodles", "eggs", "flour", "water"]; let cloneData = originalData.slice(0); cloneData[0] = "tomato"; console.log(JSON.stringify(originalData)); // ["noodles","eggs","flour","water"] console.log(JSON.stringify(cloneData)); // ["tomato","eggs","flour","water"]
同样的,对二维及更多维的数组,则不太适用,比如:
let originalData = ["noodles", { list: ["eggs", "flour", "water"] }]; let cloneData = originalData.slice(0); console.log(JSON.stringify(cloneData)); // ["noodles",{"list":["eggs","flour","water"]}] cloneData[1].list = ["rice flour", "water"]; cloneData[0] = "tomato"; console.log(originalData[1].list); // [ "rice flour", "water" ] console.log(JSON.stringify(originalData)); // ["noodles",{"list":["rice flour","water"]}] console.log(JSON.stringify(cloneData)); // ["tomato",{"list":["rice flour","water"]}]
2.3 Array.from()
这个方法跟 Array.prototype.concat()
类似,是数组的内置方法,对一维数组是”深拷贝“:
let originalData = ["noodles", "eggs", "flour", "water"]; let cloneData = originalData.slice(0); cloneData[0] = "tomato"; console.log(JSON.stringify(originalData)); // ["noodles","eggs","flour","water"] console.log(JSON.stringify(cloneData)); // ["tomato","eggs","flour","water"]
同样的,对二维及更多维的数组,则不太适用,比如:
let originalData = ["noodles", { list: ["eggs", "flour", "water"] }]; let cloneData = Array.from(originalData); console.log(JSON.stringify(cloneData)); // ["noodles",{"list":["eggs","flour","water"]}] cloneData[1].list = ["rice flour", "water"]; cloneData[0] = "tomato"; console.log(originalData[1].list); // [ "rice flour", "water" ] console.log(JSON.stringify(originalData)); // ["noodles",{"list":["rice flour","water"]}] console.log(JSON.stringify(cloneData)); // ["tomato",{"list":["rice flour","water"]}]
三、深拷贝
对于包含其他对象或数组的对象和数组,拷贝这些对象就需要深拷贝。否则,对嵌套引用所做的更改将更改嵌套在原始对象或数组中的数据。
1、JSON.parse(JSON.stringify(object))
这个大概是最被大家常用的深拷贝方法了。
const originalData = { name: "iankevin", age: 28, location: { city: "beijing", info: { address: "haidian", }, }, }; const cloneData = JSON.parse(JSON.stringify(originalData)); cloneData.age = 18; cloneData.location.city = "shanghai"; cloneData.location.info.address = "baoshan"; console.log("originalData", originalData); console.log("cloneData", cloneData);
好用是好用,但是需要注意的是,当你的对象中含有如下类型的元素时,这个方法将不再适用:
Date
functions
undefined
Infinity
RegExps
Maps
Sets
Blobs
FileLists
ImageDatas
sparse Arrays(稀疏数组)
Typed Arrays
complex types
来看一个例子:
const originalData = { undefined: undefined, // 会连同 key 一起消失 notANumber: NaN, // 转成null infinity: Infinity, // 转成null regExp: /.*/, // 会被转为空对象 date: new Date("1999-12-31T23:59:59"), // 日期会被字符串化 func: function () {}, // 连同 key 一起消失 num: Number, // 连同 key 一起消失 }; const faultyClonedData = JSON.parse(JSON.stringify(originalData)); console.log(faultyClonedData.undefined); // undefined console.log(faultyClonedData.notANumber); // null console.log(faultyClonedData.infinity); // null console.log(faultyClonedData.regExp); // {} console.log(faultyClonedData.date); // "1999-12-31T15:59:59.000Z" console.log(faultyClonedData.func); // undefined console.log(faultyClonedData.num); // undefined
2、Lodash - cloneDeep
官方文档:lodash.cloneDeep
Lodash 是非常常用的工具函数库了,它的深拷贝不仅可以应对常见的复杂类型的数据,还可以对嵌套对象进行深拷贝。
import { cloneDeep } from "lodash"; const user = { name: "iankevin", age: 28, location: { city: "beijing", }, incrementAge: function () { this.age++; }, birthday: new Date(), regex: /1-9/gi, money: Infinity, girlfriend: null, future: undefined, no: NaN, }; const clone = cloneDeep(user); console.log("clone: ", clone); clone.incrementAge(); console.log(user.age); // 28 console.log(clone.age); // 29 clone.location.city = "shanghai"; console.log(user.location.city); // beijing console.log(clone.location.city); // shanghai
可能唯一不好的地方就是会在项目中引入一个依赖了。
类似的通过库引入的方式,还有 Ramda.clone() 和 underscorejs:_.clone()
3、rfdc.clone
rfdc 什么意思呢?:
Really Fast Deep Clone
,项目地址:rfdc,如果您正在处理一个大的、复杂的对象,例如从 3MB-15MB 大小的 JSON 文件加载的对象,这样的库将很有用。它号称速度提高了大约 400%。
语法:
require('rfdc')(opts = { proto: false, circles: false }) => clone(obj) => obj2
简单用法:
const clone = require('rfdc')() clone({a: 1, b: {c: 2}}) // => {a: 1, b: {c: 2}}
3.1
proto
选项
将原型属性以及自己的属性复制到新对象中。允许将原型上的可枚举属性复制到克隆的对象中(不是复制到它的原型上,而是直接复制到对象上),这样会稍微快一点。
用代码来解释:
require('rfdc')({ proto: false })(Object.create({a: 1})) // => {} require('rfdc')({ proto: true })(Object.create({a: 1})) // => {a: 1}
将proto设置为true将提供额外的2%的性能提升。
3.2
circles
选项
跟踪循环引用将以额外
25%
的开销降低性能。即使一个对象没有任何循环引用,跟踪的开销也是有代价的。默认情况下,如果一个带有循环引用的对象被传递给rfdc
,它将会抛出(类似于JSON.stringify
抛出)。
使用
circles
选项来检测并保留对象中的循环引用。如果性能很重要,可以尝试从对象中移除循环引用(设置为未定义),然后在克隆后手动添加回来,而不是使用这个选项。
3.3 支持的数据类型
所有的
JSON types
:
Object
Array
Number
String
null
还支持:
Date
(copied)undefined
(copied)Buffer
(copied)TypedArray
(copied)Map
(copied)Set
(copied)Function
(referenced)AsyncFunction
(referenced)GeneratorFunction
(referenced)arguments
(copied to a normal object)
所有其他类型的输出值都与
JSON.parse(JSON.stringify(o))
的输出相同。
还有两个比较推荐的:
但是按照作者的测试,
fastest-json-copy
可能更快点,但它有很大的局限性,以至于它很少有用。例如,它对Date
和Map
实例的处理与空{}
相同。它不能处理循环引用。plain-object-clone
的能力也非常有限。