阅读 1688

jsliang 求职系列 - 09 - 手写浅拷贝和深拷贝

一 目录

不折腾的前端,和咸鱼有什么区别

目录
一 目录
二 前言
三 浅拷贝
3.1 手写浅拷贝
3.2 Object.assign
3.3 数组 API
四 深拷贝
4.1 手写深拷贝
4.2 JSON.parse(JSON.stringify())
4.3 函数库 Lodash
4.4 五 框架 jQuery
五 参考文献

二 前言

返回目录

面试官:如何浅拷贝和深拷贝一个数组?

要搞懂为啥问这个问题,还得从引址和引值说起:

  • 我们复制引用基础数据类型,都是拷贝对应的值
let number1 = 1;
let number2 = 2;

console.log(number1, number2); // 1 2

number2 = 3;

console.log(number1, number2); // 1 3

// 复制基本数据类型,并进行修改,是不会改变原本的值(number1)
复制代码
  • 我们复制引用复杂数据类型,都是拷贝对应的址
let obj1 = {
  name: 'jsliang',
};

let obj2 = obj1;

console.log(obj1); // { name: 'jsliang' }
console.log(obj2); // { name: 'jsliang' }

obj2.name = 'zhazhaliang';

console.log(obj1); // { name: 'zhazhaliang' }
console.log(obj2); // { name: 'zhazhaliang' }

// 复制引用数据类型,并进行修改,会改变原本的值(obj1)
复制代码

简单来说,ArrayObject 这些复杂数据类型,它们开辟了一块自己的空间来存放数据,它们引用的是对应的地址。

你可以将 Array 里面的数据看做一家人,那么要查找这家人,是不是就得有个户口,如果你修改某个家庭成员,是不是就修改了这家人户口的信息?

因此,你日常的拷贝数组或者对象,是复制了它们的地址。

如果你想修改的时候不污染原内容,就需要进行深拷贝。

三 浅拷贝

返回目录

如何实现浅拷贝,一般会答 3 种情况:

  1. 手写浅拷贝
  2. 使用 Object.assign
  3. 使用数组 API,如 concat 或者 slice 以及拓展运算符

3.1 手写浅拷贝

返回目录

const shallowClone = (target) => {
  // 设置拷贝结果
  const result = [];

  // 通过 let ... in ... 遍历
  for (let prop in target) {

    // 如果这个属性是 target 自身的
    // 而不是通过原型链给的,那就赋值过来
    if (target.hasOwnProperty(prop)) {
      result[prop] = target[prop];
    }
  }

  // 返回拷贝后的结果
  return result;
}
复制代码
  • for...in:遍历 Object 对象 arr1,将可枚举值列举出来。
  • hasOwnProperty():检查该枚举值是否属于该对象 arr1,如果是继承过来的就去掉,如果是自身的则进行拷贝。

当然,这个只能拷贝数组,那么拷贝对象呢?

我们 “稍微” 完善一下:

// 通过 Object.prototype.toString.call(xxx) 可以判断一个目标的属性
// Object 会返回 "[object Object]"
// Array 会返回 "[object Array]"
const checkedType = (target) => {
  return Object.prototype.toString.call(target).slice(8, -1);
}

// 浅拷贝
const shallowClone = (target) => {
  // 设置结果
  let result;

  // 如果目标是对象,那么设置成对象
  if (checkedType(target) === 'Object') {
    result = {};
  } else if (checkedType(target) === 'Array') { // 如果目标是数组,那么设置成数组
    result = [];
  }

  // 通过 let...in... 遍历数组或者对象
  // 并通过 hasOwnProperty 判断是否为自身的属性
  for (let i in target) {
    if (target.hasOwnProperty(i)) {
      result[i] = target[i];
    }
  }

  // 返回拷贝结果
  return result;
};
复制代码

实例测一测:

/* ———————————————— 测试数组 ———————————————— */
const arr1 = [1, 2, ['jsliang', 'JavaScriptLiang'], 4];
Array.prototype.sayHello = () => console.log('你好');
const arr2 = shallowClone(arr1);
arr2[2].push('LiangJunrong');
arr2[3] = 5;

console.log(arr1); // [ 1, 2, [ 'jsliang', 'JavaScriptLiang', 'LiangJunrong' ], 4 ]
console.log(arr2); // [ 1, 2, [ 'jsliang', 'JavaScriptLiang', 'LiangJunrong' ], 5 ]

/* ———————————————— 测试对象 ———————————————— */
const obj1 = {
  name: 'jsliang',
  like: ['eat', 'play'],
}
Object.prototype.sayHello = () => console.log('你好');
const obj2 = shallowClone(obj1);
console.log(obj2);
obj2.name = 'zhazhaliang';
obj2.like.push('sleep');

console.log(obj1); // { name: 'jsliang', like: [ 'eat', 'play', 'sleep' ] }
console.log(obj2); // { name: 'zhazhaliang', like: [ 'eat', 'play', 'sleep' ] }
复制代码

3.2 Object.assign

返回目录

const obj1 = {
  username: 'LiangJunrong',
  skill: {
    play: ['basketball', 'computer game'],
    read: 'book',
  },
  girlfriend: ['1 号备胎', '2 号备胎', '3 号备胎'],
};

const obj2 = Object.assign({}, obj1);
obj2.username = 'jsliang'; // 修改基本类型
obj2.skill.read = 'computer book'; // 修改二层基本类型
obj2.skill.play = ['footboll']; // 修改二层引用类型
obj2.girlfriend = ['之前的都是瞎吹的!']; // 修改一层引用类型

console.log(obj1);
// { username: 'LiangJunrong',
//   skill: { play: [ 'footboll' ], read: 'computer book' },
//   girlfriend: [ '1 号备胎', '2 号备胎', '3 号备胎' ] }
console.log(obj2);
// { username: 'jsliang',
//   skill: { play: [ 'footboll' ], read: 'computer book' },
//   girlfriend: [ '之前的都是瞎吹的!' ] }
复制代码

Object.assign 用于拷贝对象。

它对于第一层来说,是完全拷贝;对于第二层及以上来说,是简单复制。

3.3 数组 API

返回目录

  • Array.prototype.concat(target)concat() 是数组的一个内置方法,用户合并两个或者多个数组。这个方法不会改变现有数组,而是返回一个新数组。const b = [].concat(a)
  • Array.prototype.slice(start, end)slice() 也是数组的一个内置方法,该方法会返回一个新的对象。slice() 不会改变原数组。const b = a.slice()
  • 展开运算符[...arr] 可以得到一个浅拷贝新数组。

四 深拷贝

返回目录

如果面试官问你深拷贝的实现,jsliang 认为的答题思路应该是这样的:

  1. 手写深拷贝
  2. 借助 JSON.parse(JSON.stringify())
  3. 借助第三方库 Lodash、jQuery 等

然后如果面试官问起如何自己手写实现,那就用下一小节的代码回答吧!

4.1 手写深拷贝

返回目录

// 定义检测数据类型的功能函数
const checkedType = (target) => {
  return Object.prototype.toString.call(target).slice(8, -1);
}

// 实现深度克隆对象或者数组
const deepClone = (target) => {
  // 判断拷贝的数据类型
  // 初始化变量 result 成为最终数据
  let result, targetType = checkedType(target);
  if (targetType === 'Object') {
    result = {};
  } else if (targetType === 'Array') {
    result = [];
  } else {
    return target;
  }

  // 遍历目标数据
  for (let i in target) {
    // 获取遍历数据结构的每一项值
    let value = target[i];
    // 判断目标结构里的每一项值是否存在对象或者数组
    if (checkedType(value) === 'Object' || checkedType(value) === 'Array') {
      // 如果对象或者数组中还嵌套了对象或者数组,那么继续遍历
      result[i] = deepClone(value);
    } else {
      // 否则直接赋值
      result[i] = value;
    }
  }

  // 返回最终值
  return result;
}
复制代码

注意这个深拷贝还是有问题的,例如:

  • 循环引用(通过 Map 或者 Set 记录存储空间)(WeakMap 在这里有更好的作用)
  • 同级引用
  • ……等

这里就不一一去讲具体解决方案了。

简单测一测:

/* ———————————————— 测试数组 ———————————————— */
const arr1 = [1, 2, ['jsliang', 'JavaScriptLiang'], 4];
Array.prototype.sayHello = () => console.log('你好');
const arr2 = deepClone(arr1);
arr2[2].push('LiangJunrong');
arr2[3] = 5;

console.log(arr1); // [ 1, 2, [ 'jsliang', 'JavaScriptLiang' ], 4 ]
console.log(arr2); // [ 1, 2, [ 'jsliang', 'JavaScriptLiang', 'LiangJunrong' ], 5 ]

/* ———————————————— 测试对象 ———————————————— */
const obj1 = {
  name: 'jsliang',
  like: ['eat', 'play'],
}
Object.prototype.sayHello = () => console.log('你好');
const obj2 = deepClone(obj1);
obj2.name = 'zhazhaliang';
obj2.like.push('sleep');

console.log(obj1); // { name: 'jsliang', like: [ 'eat', 'play' ] }
console.log(obj2); // { name: 'zhazhaliang', like: [ 'eat', 'play', 'sleep' ] } 
复制代码

4.2 JSON.parse(JSON.stringify())

返回目录

  • JSON.stringify():将对象转成 JSON 字符串。
  • JSON.parse():将字符串解析成对象。

通过 JSON.parse(JSON.stringify()) 将 JavaScript 对象转序列化(转换成 JSON 字符串),再将其还原成 JavaScript 对象,一去一来我们就产生了一个新的对象,而且对象会开辟新的栈,从而实现深拷贝。

注意,该方法的局限性:
1、不能存放函数或者 Undefined,否则会丢失函数或者 Undefined;
2、不要存放时间对象,否则会变成字符串形式;
3、不能存放 RegExp、Error 对象,否则会变成空对象;
4、不能存放 NaN、Infinity、-Infinity,否则会变成 null;
5、……更多请自行填坑,具体来说就是 JavaScript 和 JSON 存在差异,两者不兼容的就会出问题。

const arr1 = [
  1,
  {
    username: 'jsliang',
  },
];

let arr2 = JSON.parse(JSON.stringify(arr1));
arr2[0] = 2;
arr2[1].username = 'LiangJunrong';
console.log(arr1);
// [ 1, { username: 'jsliang' } ]
console.log(arr2);
// [ 2, { username: 'LiangJunrong' } ]
复制代码

4.3 函数库 Lodash

返回目录

Lodash 作为一个深受大家喜爱的、优秀的 JavaScript 函数库/工具库,它里面有非常好用的封装好的功能,大家可以去试试:

这里我们查看下它的 cloneDeep() 方法:

可以看到,该方法会递归拷贝 value

在这里,我们体验下它的 cloneDeep()

//  npm i -S lodash
var _ = require('lodash');

const obj1 = [
  1,
  'Hello!',
  { name: 'jsliang1' },
  [
    {
      name: 'LiangJunrong1',
    }
  ],
]
const obj2 = _.cloneDeep(obj1);
obj2[0] = 2;
obj2[1] = 'Hi!';
obj2[2].name = 'jsliang2';
obj2[3][0].name = 'LiangJunrong2';

console.log(obj1);
// [
//   1,
//   'Hello!',
//   { name: 'jsliang1' },
//   [
//     { name: 'LiangJunrong1' },
//   ],
// ]

console.log(obj2);
// [
//   2,
//   'Hi!',
//   { name: 'jsliang2' }, 
//   [
//     { name: 'LiangJunrong2' },
//   ],
// ]
复制代码

4.4 五 框架 jQuery

返回目录

当然,不可厚非你的公司还在用着 jQuery,可能还需要兼容 IE6/7/8,或者你使用 React,但是有些场景还使用了 jQuery,毕竟 jQuery 是个强大的框架。

可以尝试下使用 jQuery 的 extend() 进行深拷贝,这里就不贴方法了。

五 参考文献

返回目录


jsliang 的文档库由 梁峻荣 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议 进行许可。
基于 github.com/LiangJunron… 上的作品创作。
本许可协议授权之外的使用权限可以从 creativecommons.org/licenses/by… 处获得。

文章分类
前端
文章标签