神族九帝's blog 神族九帝's blog
首页
网盘 (opens new window)
线报 (opens new window)
商城 (opens new window)
  • 复习指导
  • HTML
  • CSS
  • JavaScript
  • 设计模式
  • 浏览器
  • 手写系列
  • Vue
  • Webpack
  • Http
  • 前端优化
  • 项目
  • 面试真题
  • 算法
  • 精选文章
  • 八股文
  • 前端工程化
  • 基础篇
  • 进阶篇
  • 高级篇
  • 计算机基础
  • 高频考点
  • 精简题
  • 综合问题
  • 复习题
  • vue
  • vue2源码学习
  • 剖析vuejs内部运行机制
  • TypeScript 入门实战笔记
  • vue3源码学习
  • 2周刷完100道前端优质面试真题
  • npm发包
  • 重学node
  • 前端性能优化方法与实战
  • webpack原理与实战
  • webGl
  • 前端优化
  • Web3
  • 更多
  • 网站
  • 资源
  • Vue资源
  • 收藏的一些API
  • 未来要做的事
  • 宝塔面板+青龙面板
  • 安卓手机当服务器使用
  • 京东自动评价代码
  • 搭建x-ui免流服务器(已失效)
  • 海外联盟
  • 好玩的docker
  • 导航
GitHub (opens new window)

神族九帝,永不言弃

首页
网盘 (opens new window)
线报 (opens new window)
商城 (opens new window)
  • 复习指导
  • HTML
  • CSS
  • JavaScript
  • 设计模式
  • 浏览器
  • 手写系列
  • Vue
  • Webpack
  • Http
  • 前端优化
  • 项目
  • 面试真题
  • 算法
  • 精选文章
  • 八股文
  • 前端工程化
  • 基础篇
  • 进阶篇
  • 高级篇
  • 计算机基础
  • 高频考点
  • 精简题
  • 综合问题
  • 复习题
  • vue
  • vue2源码学习
  • 剖析vuejs内部运行机制
  • TypeScript 入门实战笔记
  • vue3源码学习
  • 2周刷完100道前端优质面试真题
  • npm发包
  • 重学node
  • 前端性能优化方法与实战
  • webpack原理与实战
  • webGl
  • 前端优化
  • Web3
  • 更多
  • 网站
  • 资源
  • Vue资源
  • 收藏的一些API
  • 未来要做的事
  • 宝塔面板+青龙面板
  • 安卓手机当服务器使用
  • 京东自动评价代码
  • 搭建x-ui免流服务器(已失效)
  • 海外联盟
  • 好玩的docker
  • 导航
GitHub (opens new window)
  • 复习指导

  • HTML

  • CSS

  • JavaScript

    • 思维导图
    • 数据类型
    • 隐式类型转换
    • 闭包
    • 原型和原型链
    • 继承
    • 数组扁平化flat
    • new操作符做了什么
    • 数组去重性能
    • 防抖与节流函数
    • 深浅拷贝
      • 浅拷贝方法
        • 手动实现
        • Object.assign
        • concat
        • slice
        • 展开运算符
      • 深拷贝
        • JSON.parse(JSON.stringify())
        • 简易版深拷贝
        • 解决循环引用
        • 拷贝特殊对象
        • 拷贝函数
        • bug
        • 完整的深拷贝
      • 参考链接
    • Event Loop
    • 数组方法和类数组
    • 数组排序
    • 异步编程
    • 获取URL参数
    • 模块规范
    • tree-shaking的原理
    • 隔离沙箱
  • 设计模式

  • 浏览器

  • 手写系列

  • Vue

  • Webpack

  • Http

  • 前端优化

  • 项目

  • 面试真题

  • 算法

  • 精选文章

  • 八股文

  • 前端工程化

  • 面试题
  • JavaScript
wu529778790
2019-06-16

深浅拷贝

浅拷贝只拷贝一层对象,如果有对象的嵌套就要用深拷贝,也就是彻底的拷贝

# 浅拷贝方法

# 手动实现

const shallowClone = (target) => {
  if (typeof target === "object" && target !== null) {
    const cloneTarget = Array.isArray(target) ? [] : {};
    for (let prop in target) {
      if (target.hasOwnProperty(prop)) {
        cloneTarget[prop] = target[prop];
      }
    }
    return cloneTarget;
  } else {
    return target;
  }
};

精简一下,去掉判断,改为 Object.keys

const shallowClone = (target) => {
  if (typeof target === "object" && target !== null) {
    const cloneTarget = Array.isArray(target) ? [] : {};
    for (let prop in Object.keys(target)) {
      cloneTarget[prop] = target[prop];
    }
    return cloneTarget;
  } else {
    return target;
  }
};

# Object.assign

Object.assgin() 拷贝的是对象的属性的引用,而不是对象本身。

let obj = { name: "shenzjd.com", age: 18 };
const obj2 = Object.assign({}, obj, { name: "blog.shenzjd.com" });
console.log(obj2); // {name: "blog.shenzjd.com", age: 18}

# concat

let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr); // [1,2,3]

# slice

let arr = [1, 2, 3];
let newArr = arr.slice();
newArr[1] = 100;
console.log(arr); // [1,2,3]

# 展开运算符

let arr = [1, 2, 3];
let newArr = [...arr];
newArr[1] = 100;
console.log(arr); // [1,2,3]

# 深拷贝

# JSON.parse(JSON.stringify())

能覆盖大部分场景,但是严格意义上来说,有如下问题

  • 无法解决循环引用
const a = { val: 2 };
a.target = a;

JSON.parse(JSON.stringify(a));

拷贝 a 会出现系统栈溢出,因为出现了无限递归的情况

20210616143305

  • 无法拷贝特殊的对象: RegExp,Date,Set,Map 等
  • 无法拷贝函数

# 简易版深拷贝

const deepClone = (target) => {
  if (typeof target === "object" && target !== null) {
    const cloneTarget = Array.isArray(target) ? [] : {};
    for (let prop in target) {
      if (target.hasOwnProperty(prop)) {
        cloneTarget[prop] = deepClone(target[prop]); // 浅拷贝没有递归,只有一层
      }
    }
    return cloneTarget;
  } else {
    return target;
  }
};

# 解决循环引用

let a = { val: 100 };
a.target = a;
deepClone(a);

20210616143850

创建一个 Map。记录下已经拷贝过的对象,如果说已经拷贝过,那直接返回它

const isObject = (target) => {
  (typeof target === "object" || typeof target === "function") &&
    target !== null;
};
const deepClone = (target, map = new Map()) => {
  if (map.get(target)) return target;
  if (isObject(target)) {
    map.set(target, true);
    const cloneTarget = Array.isArray(target) ? [] : {};
    for (let prop in target) {
      if (target.hasOwnProperty(prop)) {
        cloneTarget[prop] = deepClone(target[prop]); // 浅拷贝没有递归,只有一层
      }
    }
    return cloneTarget;
  } else {
    return target;
  }
};

测试

let a = { val: 100 };
a.target = a;
deepClone(a);

20210616144258

好像是没有问题了, 拷贝也完成了。但还是有一个潜在的坑, 就是 map 上的 key 和 map 构成了强引用关系,这是相当危险的。我给你解释一下与之相对的弱引用的概念你就明白了:

在计算机程序设计中,弱引用与强引用相对, 是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。 --百度百科

被弱引用的对象可以在任何时候被回收,而对于强引用来说,只要这个强引用还在,那么对象无法被回收

拿上面的例子说,map 和 a 一直是强引用的关系, 在程序结束之前,a 所占的内存空间一直不会被释放。

怎么解决这个问题?

很简单,让 map 的 key 和 map 构成弱引用即可。ES6 给我们提供了这样的数据结构,它的名字叫 WeakMap,它是一种特殊的 Map, 其中的键是弱引用的。其键必须是对象,而值可以是任意的。

稍微改造一下即可:

const deepClone = (target, map = new WeakMap()) => {
  //...
};

# 拷贝特殊对象

用 Object.prototype.toString.call()判断

可遍历对象

["object Map"];
["object Set"];
["object Array"];
["object Object"];
["object Arguments"];
const getType = Object.prototype.toString.call(obj);
const canTraverse = {
  "[object Map]": true,
  "[object Set]": true,
  "[object Array]": true,
  "[object Object]": true,
  "[object Arguments]": true,
};
const isObject = (target) => {
  (typeof target === "object" || typeof target === "function") &&
    target !== null;
};
const deepClone = (target, map = new WeakMap()) => {
  if (!isObject(target)) return target;
  let type = getType(target);
  let cloneTarget;
  if (!canTraverse[type]) {
    // 这里处理不能遍历的对象
    return;
  } else {
    // 这波操作相当关键,可以保证对象的原型不丢失!
    let ctor = target.prototype;
    cloneTarget = new ctor();
  }

  if (map.get(target)) return target;
  map.put(target, true);

  // 处理Map
  if (type === mapTag) {
    target.forEach(item, (key) => {
      cloneTarget.set(deepClone(key), deepClone(item));
    });
  }

  // 处理 Set
  if (type === setTag) {
    target.forEach((item) => {
      target.add(deepClone(item));
    });
  }

  // 处理数组和对象
  for (let prop in target) {
    if (target.hasOwnProperty(prop)) {
      cloneTarget[prop] = deepClone(target[prop]);
    }
  }
  return cloneTarget;
};

不可遍历的对象

const boolTag = "[object Boolean]";
const numberTag = "[object Number]";
const stringTag = "[object String]";
const dateTag = "[object Date]";
const errorTag = "[object Error]";
const regexpTag = "[object RegExp]";
const funcTag = "[object Function]";

对于不可遍历的对象,不同的对象有不同的处理

const handleRegExp = (target) => {
  const { source, flags } = target;
  return new target.constructor(source, flags);
};

const handleFunc = (target) => {
  // 待会的重点部分
};

const handleNotTraverse = (target, tag) => {
  const Ctor = targe.constructor;
  switch (tag) {
    case boolTag:
    case numberTag:
    case stringTag:
    case errorTag:
    case dateTag:
      return new Ctor(target);
    case regexpTag:
      return handleRegExp(target);
    case funcTag:
      return handleFunc(target);
    default:
      return new Ctor(target);
  }
};

# 拷贝函数

函数分为两种,每个普通函数都是 Function 的实例,而箭头函数不是任何类的实例,每次调用都是不一样的引用

  • 普通函数
  • 箭头函数

这里只需要处理普通函数,箭头函数直接返回他本身就可以

怎么区分

利用原型,箭头函数没有原型

const handleFunc = (func) => {
  // 箭头函数直接返回自身
  if (!func.prototype) return func;
  const bodyReg = /(?<={)(.|\n)+(?=})/m;
  const paramReg = /(?<=\().+(?=\)\s+{)/;
  const funcString = func.toString();
  // 分别匹配 函数参数 和 函数体
  const param = paramReg.exec(funcString);
  const body = bodyReg.exec(funcString);
  if (!body) return null;
  if (param) {
    const paramArr = param[0].split(",");
    return new Function(...paramArr, body[0]);
  } else {
    return new Function(body[0]);
  }
};

# bug

const target = new Boolean(false);
const Ctor = target.constructor;
new Ctor(target); // 结果为 Boolean {true} 而不是 false

对于这样一个 bug,我们可以对 Boolean 拷贝做最简单的修改, 调用 valueOf: new target.constructor(target.valueOf())

但实际上,这种写法是不推荐的。因为在 ES6 后不推荐使用【new 基本类型()】这 样的语法,所以 es6 中的新类型 Symbol 是不能直接 new 的,只能通过 new Object(SymbelType)

const handleNotTraverse = (target, tag) => {
  const Ctor = targe.constructor;
  switch (tag) {
    case boolTag:
      return new Object(Boolean.prototype.valueOf.call(target));
    case numberTag:
      return new Object(Number.prototype.valueOf.call(target));
    case stringTag:
      return new Object(String.prototype.valueOf.call(target));
    case errorTag:
    case dateTag:
      return new Ctor(target);
    case regexpTag:
      return handleRegExp(target);
    case funcTag:
      return handleFunc(target);
    default:
      return new Ctor(target);
  }
};

# 完整的深拷贝

const getType = (obj) => Object.prototype.toString.call(obj);

const isObject = (target) =>
  (typeof target === "object" || typeof target === "function") &&
  target !== null;

const canTraverse = {
  "[object Map]": true,
  "[object Set]": true,
  "[object Array]": true,
  "[object Object]": true,
  "[object Arguments]": true,
};
const mapTag = "[object Map]";
const setTag = "[object Set]";
const boolTag = "[object Boolean]";
const numberTag = "[object Number]";
const stringTag = "[object String]";
const symbolTag = "[object Symbol]";
const dateTag = "[object Date]";
const errorTag = "[object Error]";
const regexpTag = "[object RegExp]";
const funcTag = "[object Function]";

const handleRegExp = (target) => {
  const { source, flags } = target;
  return new target.constructor(source, flags);
};

const handleFunc = (func) => {
  // 箭头函数直接返回自身
  if (!func.prototype) return func;
  const bodyReg = /(?<={)(.|\n)+(?=})/m;
  const paramReg = /(?<=\().+(?=\)\s+{)/;
  const funcString = func.toString();
  // 分别匹配 函数参数 和 函数体
  const param = paramReg.exec(funcString);
  const body = bodyReg.exec(funcString);
  if (!body) return null;
  if (param) {
    const paramArr = param[0].split(",");
    return new Function(...paramArr, body[0]);
  } else {
    return new Function(body[0]);
  }
};

const handleNotTraverse = (target, tag) => {
  const Ctor = target.constructor;
  switch (tag) {
    case boolTag:
      return new Object(Boolean.prototype.valueOf.call(target));
    case numberTag:
      return new Object(Number.prototype.valueOf.call(target));
    case stringTag:
      return new Object(String.prototype.valueOf.call(target));
    case symbolTag:
      return new Object(Symbol.prototype.valueOf.call(target));
    case errorTag:
    case dateTag:
      return new Ctor(target);
    case regexpTag:
      return handleRegExp(target);
    case funcTag:
      return handleFunc(target);
    default:
      return new Ctor(target);
  }
};

const deepClone = (target, map = new WeakMap()) => {
  if (!isObject(target)) return target;
  let type = getType(target);
  let cloneTarget;
  if (!canTraverse[type]) {
    // 处理不能遍历的对象
    return handleNotTraverse(target, type);
  } else {
    // 这波操作相当关键,可以保证对象的原型不丢失!
    let ctor = target.constructor;
    cloneTarget = new ctor();
  }

  if (map.get(target)) return target;
  map.set(target, true);

  if (type === mapTag) {
    //处理Map
    target.forEach((item, key) => {
      cloneTarget.set(deepClone(key, map), deepClone(item, map));
    });
  }

  if (type === setTag) {
    //处理Set
    target.forEach((item) => {
      cloneTarget.add(deepClone(item, map));
    });
  }

  // 处理数组和对象
  for (let prop in target) {
    if (target.hasOwnProperty(prop)) {
      cloneTarget[prop] = deepClone(target[prop], map);
    }
  }
  return cloneTarget;
};

# 参考链接

  • https://sanyuan0704.top/my_blog/blogs/javascript/js-api/005.html (opens new window)
编辑 (opens new window)
上次更新: 2022/05/24, 14:27:28
防抖与节流函数
Event Loop

← 防抖与节流函数 Event Loop→

最近更新
01
好玩的docker
07-04
02
我的第一个NFT
07-03
03
海外迅雷
06-01
更多文章>
Power by vuepress | Copyright © 2015-2023 神族九帝
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×