JavaScript中,引用类型数据 存储于堆内存中,因为JavaScript不允许直接访问内存位置,因此操作对象实际上是操作该对象的引用,即其内存位置所处地址。
对于引用类型数据的操作,如果直接修改了对象的值,即修改了该引用内存地址上的数据,将影响其他引用此地址的对象。
拷贝对象就是经典的操作,了解区分浅拷贝/深拷贝操作,可以避免实际开发中的一些问题。
浅拷贝
浅拷贝只会对一层对象开辟新的内存空间进行存放,对于嵌套对象的深层属性仍是原来的引用地址。
Object.assign()
const obj = {name: 'donggua',props: {value: 1}}const newObj = Object.assign({}, obj);newObj.name = '_donggua';newObj.props.value++;obj; // { name: 'donggua', props: { value: 2 } }newObj; // { name: '_donggua', props: { value: 2 } }
Array.prototype.concat()
const arr = [1, 2, 3, [4, 5]];const ary = arr.concat();ary[0] = 4;ary[3][0] = 1;arr; // [1, 2, 3, [1, 5]];ary; // [4, 2, 3, [1, 5]];
Array.prototype.slice()
const arr = [1, 2, 3, [4, 5]];const ary = arr.slice();ary[0] = 4;ary[3][0] = 1;arr; // [1, 2, 3, [1, 5]];ary; // [4, 2, 3, [1, 5]];
ES6 扩展运算符
const obj = {name: 'donggua',props: {value: 1}}const newObj = { ...obj };newObj.name = '_donggua';newObj.props.value++;obj; // { name: 'donggua', props: { value: 2 } }newObj; // { name: '_donggua', props: { value: 2 } }
const arr = [1, 2, 3, [4, 5]];const ary = [...arr];ary[0] = 4;ary[3][0] = 1;arr; // [1, 2, 3, [1, 5]];ary; // [4, 2, 3, [1, 5]];
手写实现
function clone(obj) {if (typeof obj === "object" && obj !== null) {const result = Array.isArray(obj) ? [] : {};for (const key in obj) {if (obj.hasOwnProperty(key)) {result[key] = obj[key];}}return result;}return obj;}
深拷贝
深拷贝将对象深层进行完整的精确拷贝,对新对象的任何修改不会影响原对象数据。
JQuery.extend()
import $ from "jquery";const obj = {name: 'donggua',props: {value: 1}}const newObj = $.extend(true, {}, obj1);newObj.name = '_donggua';newObj.props.value++;obj; // { name: 'donggua', props: { value: 1 } }newObj; // { name: '_donggua', props: { value: 2 } }
JSON.parse(JSON.stringify())
const obj = {name: 'donggua',props: {value: 1}}const newObj = JSON.parse(JSON.stringify(obj));newObj.name = '_donggua';newObj.props.value++;obj; // { name: 'donggua', props: { value: 1 } }newObj; // { name: '_donggua', props: { value: 2 } }
JSON.stringify() 存在一定的弊端:
Date类型,转换后会调用toJSON转为字符串类型const d = new Date();d.toJSON() === JSON.parse(JSON.stringify(d)); // true
undefined、Symbol、任意函数将被忽略 ```javascript const obj = { fn: function() {}, value: undefined, unique: Symbol() }
const o = JSON.parse(JSON.stringify(obj)); o; // {}
- `RegExp`、`Error`、`Set`、`Map` 等特殊对象属性转换后将得到空对象```javascriptconst obj = {reg: new RegExp('\\w+'),e: new Error('err')}const o = JSON.parse(JSON.stringify(obj))o; // { reg: {}, err: {} }
JSON.stringify()只能序列化对象的可枚举的自有属性,对于构造函数生成的对象属性,将丢失
其 constructor 的正确指向
function fn() {}const obj = {value: new fn()}obj.value.constructor; // ƒ fn() {}const o = JSON.parse(JSON.stringify(obj))o.value.constructor; // ƒ Object() { [native code] }
NaN、Infinity、-Infinity将被 当成null处理- 循环引用的对象将报错
messageChannel
vue.nextTick源码曾使用的Web API,在了解这个API时发现可以用于深拷贝,详见 知乎 - 亦河的文章
function cloneUsingChannel(obj) {return new Promise(resolve => {const channel = new MessageChannel();channel.port1.onmessage = e => resolve(e.data)channel.port2.postMessage(obj);})}
但该方法存在一个缺陷,当拷贝对象带有函数属性时,将抛出错误:
手写递归实现
- 深拷贝就是在手写浅拷贝基础上加上递归
- 借用
Map数据映射记录已拷贝对象,解决循环引用问题 - 对特殊对象,通过其原型方法实例化新的对象(Map、Set、RegExp、Error、Date)
- 函数拷贝注意区分箭头函数(借鉴引用了掘金 - ConardLi的文章)
```javascript
function deepClone(obj, map = new Map()) {
if (!isObject) {
return obj;
}
// 对象映射,对重复对象直接获取缓存,解决循环引用栈溢出
if (map.get(obj)) {
return obj;
}
map.set(obj, true);
const _ctor = obj.constructor; // 获取对象原型,部分特殊对象使用其原型方法创建新的实例
let result;
switch (_ctor) {
case Object:
case Array:
case Map: case Set:result = Array.isArray(obj) ? [] : {};for (const key in obj) {if (obj.hasOwnProperty(key)) {result[key] = deepClone(obj[key]);}}// 冻结对象多一步处理if (Object.isFrozen(obj)) {result = Object.freeze(result);}
case RegExp: case Date:result = new _ctor([...obj]);break;
case Error:result = new RegExp(obj);break;
case Function:result = new Error(obj.message);break;
default:result = cloneFunc(obj);break;
} return result; }result = obj;
/**
- 拷贝函数
- 出处: https://juejin.cn/post/6844903929705136141#heading-12
*/
function cloneFunc(fn) {
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=().+(?=)\s+{)/;
const fnString = fn.toString();
// 箭头函数 采用 new Funtion 创建新的匿名函数或直接返回其本身
if (!fn.prorotype) {
return fn; } } // 分别匹配 函数参数 和 函数体 const param = paramReg.exec(fnString); const body = bodyReg.exec(fnString); if (!body) return null; if (param) { const paramArr = param[0].split(‘,’); return new Function(…paramArr, body[0]); } else { return new Function(body[0]); } }try {return Function("return (" + fnString + ")()")} catch {
function isObject(val) { return [‘object’, ‘function’].includes(typeof val) && val !== null; } ```
总结
实际开发中常用的拷贝可能更多的是浅拷贝,对于深拷贝操作更多的是借用第三方库如 lodash.cloneDeep()
对于 JSON.stringify() 更应该了解注意其局限性。
而通过动手实现递归深拷贝,从实践中体会实现一个 API 需要考虑的各类情况和边界条件,代入实际开发中应该更加细心全方位的思考。
