【面试官系列】请讲讲你知道的关于对象和数组深、浅拷贝的一些技巧 ~(一)

简介: 【面试官系列】请讲讲你知道的关于对象和数组深、浅拷贝的一些技巧 ~(一)

这个题目主要是希望考察面试者的 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);

image.png好用是好用,但是需要注意的是,当你的对象中含有如下类型的元素时,这个方法将不再适用:

  • 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 是非常常用的工具函数库了,它的深拷贝不仅可以应对常见的复杂类型的数据,还可以对嵌套对象进行深拷贝。image.png

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

image.png可能唯一不好的地方就是会在项目中引入一个依赖了。

类似的通过库引入的方式,还有 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 可能更快点,但它有很大的局限性,以至于它很少有用。例如,它对 DateMap 实例的处理与空 {} 相同。它不能处理循环引用。 plain-object-clone 的能力也非常有限。

相关文章
|
7月前
|
编译器 C++ Python
【C/C++ 泡沫精选面试题02】深拷贝和浅拷贝之间的区别?
【C/C++ 泡沫精选面试题02】深拷贝和浅拷贝之间的区别?
121 1
|
5月前
|
存储 缓存 监控
Java面试题:在Java中,对象何时可以被垃圾回收?编程中,如何更好地做好垃圾回收处理?
Java面试题:在Java中,对象何时可以被垃圾回收?编程中,如何更好地做好垃圾回收处理?
75 0
|
4月前
|
JavaScript
【Vue面试题九】、Vue中给对象添加新属性界面不刷新?
这篇文章讨论了Vue中给对象动态添加新属性时界面不刷新的问题,并提供了三种解决方案:使用`Vue.set()`方法来确保新属性是响应式的并触发视图更新,使用`Object.assign()`创建新对象以合并新属性,以及作为最后手段的`$forceUpdate()`进行强制刷新。文章还简要分析了Vue 2和Vue 3在数据响应式实现上的差异。
|
4月前
|
JavaScript
【Vue面试题八】、为什么data属性是一个函数而不是一个对象?
这篇文章解释了为什么在Vue中组件的`data`属性必须是一个函数而不是一个对象。原因在于组件可能会有多个实例,如果`data`是一个对象,那么这些实例将会共享同一个`data`对象,导致数据污染。而当`data`是一个函数时,每次创建组件实例都会返回一个新的`data`对象,从而确保了数据的隔离。文章通过示例和源码分析,展示了Vue初始化`data`的过程和组件选项合并的原理,最终得出结论:根实例的`data`可以是对象或函数,而组件实例的`data`必须为函数。
【Vue面试题八】、为什么data属性是一个函数而不是一个对象?
|
4月前
|
存储 JavaScript 前端开发
JS浅拷贝及面试时手写源码
JS浅拷贝及面试时手写源码
|
5月前
|
存储 缓存 算法
Java面试题:给出代码优化的常见策略,如减少对象创建、使用缓存等。
Java面试题:给出代码优化的常见策略,如减少对象创建、使用缓存等。
68 0
|
5月前
|
设计模式 存储 缓存
Java面试题:结合建造者模式与内存优化,设计一个可扩展的高性能对象创建框架?利用多线程工具类与并发框架,实现一个高并发的分布式任务调度系统?设计一个高性能的实时事件通知系统
Java面试题:结合建造者模式与内存优化,设计一个可扩展的高性能对象创建框架?利用多线程工具类与并发框架,实现一个高并发的分布式任务调度系统?设计一个高性能的实时事件通知系统
64 0
|
7月前
|
Java 程序员 C语言
2024年Python最新【Python学习教程】Python类和对象_python中类和对象的讲解,Python最新面试题
2024年Python最新【Python学习教程】Python类和对象_python中类和对象的讲解,Python最新面试题
2024年Python最新【Python学习教程】Python类和对象_python中类和对象的讲解,Python最新面试题
|
7月前
|
搜索推荐 开发工具 Python
2024年最新【Python 基础教程】对时间日期对象的侃侃而谈,面试必考题
2024年最新【Python 基础教程】对时间日期对象的侃侃而谈,面试必考题
2024年最新【Python 基础教程】对时间日期对象的侃侃而谈,面试必考题
|
7月前
|
安全 Java
【JAVA面试题】什么是对象锁?什么是类锁?
【JAVA面试题】什么是对象锁?什么是类锁?
下一篇
DataWorks