我来给你写一个滴水不漏的深拷贝!面试官:怎么把getter和setter拷贝上?😅😅😅

扯皮

关于深拷贝相关的文章真的是太多了,但是看了很多实现的文章都不完整...可能得集合好几篇文章才能算一个功能全一点的🤨🤨🤨

其实深拷贝本身的细节就很多,因为完整的深拷贝要考虑各种数据类型,因此也是一道高频考题,不管是问实现原理还是手撕,几乎五场面试下来必有一场被问到

因此有了本篇文章,尽量考虑多种数据类型的拷贝以后方便回看复习😉。并且如标题所说,这是前段时间秋招时数码宝贝一个面试官问的逆天问题,咱们放到最后来讨论一下,如果不想看深拷贝实现的话直接划到最后即可

为了防止有人说我标题党,先上图为敬,面经链接就不放了。因为数码宝贝那段时间高强度约面,估计也是同一个面试官问的:

正文

数组和对象

咱们从最简单的开始,首先就是针对于一个普通的对象,其属性可能是数组或对象

只需要考虑引用值的判断并创建新的引用值即可,简单粗暴没有任何难度,需要注意的点就是使用 typeof 判断类型时关于 null 的判断,以及 for...in 遍历原型链的属性,通过 hasOwnProperty 进行限制:

javascript 复制代码
function deepClone(origin) {
  const target = {};
  for (const key in origin) {
    if (origin.hasOwnProperty(key)) {
      if (typeof origin[key] !== "object" || origin[key] === null) target[key] = origin[key];
      else if (Array.isArray(origin[key])) {
        target[key] = Array.from(origin[key]);
      } else {
        target[key] = Object.assign({}, origin[key]);
      }
    }
  }
  return target;
}

我们来测试一下看看,重点看引用值是否相同:

javascript 复制代码
const obj1 = {
  a: 1,
  b: [1, 2, 3],
  c: {
    name: "c",
    info: "d",
  },
  d: null,
};

const obj2 = deepClone(obj1);

function testDeepClone(obj1, obj2) {
  for (const key in obj1) {
    if (obj1[key] === obj2[key] && typeof obj1[key] === "object" && obj1[key] !== null) return false;
  }
  return true;
}

console.log(testDeepClone(obj1, obj2)); // true

递归处理

上面只是一个入门案例,没有递归的深拷贝就没有灵魂,下面考虑这样的情况:属性可能是引用值,而引用值的属性可能又是引用值

既然要改造为递归那就要考虑出口问题,实际上就是针对于引用值我们需要递归拷贝,而遇到基本数据类型直接返回即可,也就是说基本数据类型是递归的出口,最后将内部创建的引用值返回即可:

javascript 复制代码
function deepClone(origin) {
  if (typeof origin !== "object" || origin === null) return origin;
  const target = Array.isArray(origin) ? [] : {};
  for (const key in origin) {
    if (origin.hasOwnProperty(key)) {
      target[key] = deepClone(origin[key]);
    }
  }
  return target;
}

啧啧啧,感觉使用了递归代码更加简洁了🤔。确实是这样,毕竟数组也可以使用 for...in 进行遍历,虽然 MDN 上并不推荐吧...但是能用就行。

我们还是来简单测试一下,着重测试嵌套引用值即可:

javascript 复制代码
const obj1 = {
  a: 1,
  b: [1, 2, 3, { bb: "hello" }],
  c: {
    name: "c",
    info: "d",
    eee: [4, 5, 6],
  },
  d: null,
};

const obj2 = deepClone(obj1);

function testDeepClone(obj1, obj2) {
  if (obj1.c.eee === obj2.c.eee) return false;
  if (obj1.b[3] === obj2.b[3]) return false;
  return true;
}

console.log(testDeepClone(obj1, obj2)); // true

其实到此为止已经可以称得上深拷贝的实现了,个人在面试过程中也是先手撕这一版,之后自己再延申出下面的内容。

循环引用问题

循环引用也是深拷贝常被问到的问题,简单描述就是一个对象的属性值又引用了该对象本身

javascript 复制代码
const obj1 = {
  self: null,
};
obj1.self = obj1;

这样造成的问题就是在递归遍历时会不断进入递归,之后爆掉函数调用栈。

解决方法其实也很简单,就是将引用值先存储起来,下次遇到该引用值时我们返回存储的值而不是再次递归,存储的键是原引用值,存储的值是内创建的新引用值,而这里选择的存储结构就是 WeakMap,其弱引用的特性配合深拷贝的场景再好不过:

javascript 复制代码
function deepClone(origin, map = new WeakMap()) {
  if (typeof origin !== "object" || origin === null) return origin;
  if (map.has(origin)) return map.get(origin);
  const target = Array.isArray(origin) ? [] : {};
  map.set(origin, target);
  for (const key in origin) {
    if (origin.hasOwnProperty(key)) {
      target[key] = deepClone(origin[key], map);
    }
  }
  return target;
}

现在就不再出现爆栈错误了,而是形成嵌套引用:

特殊引用类型

这里的特殊引用类型指:Date、RegExp、Map、Set、Number、String ...

以深拷贝的角度来讲,我们只会根据它们是否能够遍历将其分为两类,能够遍历就走后面的递归遍历逻辑,不能遍历处理方式就和基础数据类型类似只不过需要创建新的引用值再返回

那么问题来了,这么多特殊引用类型怎么区分创建呢?难道枚举出来它们所有的构造函数再一个一个判断创建吗?🤔

实际上根本不需要区分,因为对象上的原型会保存 constructor,不管什么类型的对象我们通过原型链拿到其构造器直接创建即可:

javascript 复制代码
const r1 = /test/g;
const r2 = new r1.constructor(r1);
const r3 = new r1.constructor();
console.log(r2, r3, r1 === r2); // /test/g /(?:)/ false

const n1 = Number(100);
const n2 = new n1.constructor(n1);
const n3 = new n1.constructor();
console.log(n2, n3, n1 === n2); // Number {100} Number{0} false

const d1 = new Date();
const d2 = new d1.constructor(d1);
const d3 = new d1.constructor();
console.log(d2, d3, d1 === d2); // ...(中国标准时间)   ...(中国标准时间) false

我们在使用构造函数时选择传入原来的对象,以此获取原对象的内容的同时又创建了新的对象

OK,下面继续完善我们的深拷贝吧,之前创建引用类型只考虑了数组和对象所以可以使用字面量创建,现在我们统一使用构造函数创建

还记得上面我们提到分类的问题吗?针对于遍历的特殊类型其实就这几个:Map、Set、Object、Array、Arguments

这里的 Arguments 可能会有些疑惑,指的就是函数里的 arguments,我们都知道它是类数组对象,或者称之为伪数组,而它的内部是有迭代器的:

之所以把它单独列出来是因为它通过 Object.prototype.toString 获取的类型是:[object Arguments] ,并不是简简单单的对象类型,但是可以跟对象数组类型一样无脑 for...in 遍历即可

当然针对于 Map 和 Set 添加元素的方法都不相同,因此需要单独判断进行遍历处理,在遍历时针对于 key 和 value 考虑引用值也要进行递归

javascript 复制代码
// 分辨特殊类型
const getType = (obj) => Object.prototype.toString.call(obj);

// 筛选出能够被遍历的类型
const canTranverse = {
  "[object Map]": true,
  "[object Set]": true,
  "[object Object]": true,
  "[object Array]": true,
  "[object Arguments]": true
};

function deepClone(origin, map = new WeakMap()) {
  if (typeof origin !== "object" || origin === null) return origin;
  const constructor = origin.constructor;
  if (!canTranverse[getType(origin)]) return new constructor(origin);

  if (map.has(origin)) return map.get(origin);
  const target = new constructor();
  map.set(origin, target);

  if (origin instanceof Map) {
    origin.forEach((value, key) => {
      target.set(deepClone(key, map), deepClone(value, map));
    });
    return target;
  }

  if (origin instanceof Set) {
    origin.forEach((value) => {
      target.add(deepClone(value, map));
    });
    return target;
  }

  for (const key in origin) {
    if (origin.hasOwnProperty(key)) {
      target[key] = deepClone(origin[key], map);
    }
  }
  return target;
}

来测试一下看看,没有多大问题:

javascript 复制代码
const obj1 = {
  a: new Date(),
  b: /test/g,
  c: new Map([
    ["key1", 123],
    ["key2", "hello"],
    ["key3", { name: "ss", age: 22 }],
  ]),
  d: new Set([1, 2, 3]),
  e: new Number(10),
  f: new String("world"),
};
const obj2 = deepClone(obj1);
function testDeepClone(obj1, obj2) {
  for (const key in obj1) if (obj1[key] === obj2[key]) return false;
  if (obj1["c"].get("key3") === obj2["c"].get("key3")) return false;
  return true;
}

console.log(testDeepClone(obj1, obj2)); // true

你以为到这里就结束了?笑了🤣,当我拿出 Object.create(null) 时请问阁下如何应对?

应对不了一点,修改原型链的操作确实属于 hack 操作,那只能特殊判断处理了,针对于这些特殊对象都进行一个个判断区分处理吧。

Symbol 的处理

关于 Symbol 的基本概念不再介绍,我们知道 Symbol 被分为三类:

  1. 普通 Symbol
  2. 带有描述的 Symbol
  3. 全局共享的 Symbol

普通 Symbol 和 带有描述的 Symbol 都是唯一的,而全局共享的 Symbol 如果 key 相同则创建出来的 Symbol 也是相同的

javascript 复制代码
const s1 = Symbol();
const s2 = Symbol();

const s3 = Symbol("hello");
const s4 = Symbol("hello");

const s5 = Symbol.for("key");
const s6 = Symbol.for("key");

console.log(s1 === s2, s3 === s4, s5 === s6); // false false true

那么问题来了,如果给你任意一个类型的 Symbol,你能判断出它属于上述哪种类型并且创建对应该类型的 Symbol 吗?🤔

其实这三种类别越特殊就越有其独特的判断方法,比如全局共享的 Symbol 可以通过 keyFor 方法来获取创建全局 Symbol 的 key,而描述 Symbol 又可以通过 Symbol.prototype.description 来获取其描述内容,最后剩下的就是普通 Symbol 了。当然这里的全局共享 Symbol 我们就无法完全 clone 一份了,毕竟用 key 做标识,因此直接使用相同 key 创建即可:

javascript 复制代码
function cloneSymbol(s) {
  // 判断全局共享 symbol
  const key = Symbol.keyFor(s);
  if (key) return Symbol.for(key);
  // 判断带有描述的 symbol
  const desc = s.description;
  if (desc) return Symbol(desc);
  // 普通 symbol
  return Symbol();
}

现在克隆一个 Symbol 变量已经没有问题了,下一步就是把这部分内容添加到我们的深拷贝中

我们知道 Symbol 既可以是对象的属性又可以是属性值,所以我们就需要考虑这两种情况:

javascript 复制代码
const s1 = Symbol();
const s2 = Symbol();

const obj = {
  [s1]: s2,
};

console.log(obj); // {Symbol(): Symbol()}

value 可以通过 typeof 来判断,但它的 key 就不是那么简单了,因为 for...in 本身无法遍历 Symbol 类型的 key,所以针对于 Symbol 的获取可以使用额外的 API:Object.getOwnPropertySymbols,它返回一个包含给定对象自有 Symbol 属性的数组。

当然针对于属性的遍历有很多方案,比如万能的 Reflect.ownKeys,这里不考虑兼容性的话看个人的习惯使用

现在拿到了 Symbol 属性的数组,我们直接遍历进行处理即可:

javascript 复制代码
const getType = (obj) => Object.prototype.toString.call(obj);

const canTranverse = {
  "[object Map]": true,
  "[object Set]": true,
  "[object Object]": true,
  "[object Array]": true,
  "[object Arguments]": true,
};

// 增加对 symbol clone 处理
function cloneSymbol(s) {
  const key = Symbol.keyFor(s);
  if (key) return Symbol.for(key);
  const desc = s.description;
  if (desc) return Symbol(desc);
  return Symbol();
}

function deepClone(origin, map = new WeakMap()) {
  if (typeof origin !== "object" || origin === null) {
    // 额外判断 value 为 symbol 类型
    return typeof origin === "symbol" ? cloneSymbol(origin) : origin;
  }

  const constructor = origin.constructor;
  if (!canTranverse[getType(origin)]) return new constructor(origin);

  if (map.has(origin)) return map.get(origin);
  const target = new constructor();
  map.set(origin, target);

  if (origin instanceof Map) {
    origin.forEach((value, key) => {
      target.set(deepClone(key, map), deepClone(value, map));
    });
    return target;
  }

  if (origin instanceof Set) {
    origin.forEach((value) => {
      target.add(deepClone(value, map));
    });
    return target;
  }

  for (const key in origin) {
    if (origin.hasOwnProperty(key)) {
      target[key] = deepClone(origin[key], map);
    }
  }

   // 增加对 symbol key 的处理
  const symbols = Object.getOwnPropertySymbols(origin);
  symbols.forEach((s) => {
    target[cloneSymbol(s)] = deepClone(origin[s], map);
  });

  return target;
}

来测试一下没什么大的毛病:

javascript 复制代码
const s1 = Symbol();
const s2 = Symbol();

const s3 = Symbol("hello");
const s4 = Symbol("hello");

const s5 = Symbol.for("key");
const s6 = Symbol.for("key");

const obj1 = {
  [s1]: s2,
  [s3]: s4,
  [s5]: s6,
};

const obj2 = deepClone(obj1);

function testDeepClone(obj1, obj2) {
  const s1 = Object.getOwnPropertySymbols(obj1);
  const s2 = Object.getOwnPropertySymbols(obj2);
  s1.forEach((s, index) => {
    if (s === s2[index] && !Symbol.keyFor(s)) return false;
    if (obj1[s] === obj2[s2[index]] && !Symbol.keyFor(s)) return false;
    if (Symbol.keyFor(s) && s !== s2[index]) return false;
    if (Symbol.keyFor[obj1[s]] && obj1[s] !== obj2[s2[index]]) return false;
  });
  return true;
}

console.log(testDeepClone(obj1, obj2)); // true

到此深拷贝如果已经写到这种程度相当不错了,证明面试者对整个深拷贝的细节把控还是比较优秀的。

函数的处理

其实针对于函数的拷贝个人觉得是比较有争论的,因为函数的作用我们可以理解为一个功能的实现,它不像对象、数组这些存储结构是存储信息的容器

而拷贝的目的我们更多是为了拷贝信息,而不是拷贝功能,如果功能还需要单独创建新的一份的话那个人觉得完全就是无意义的耗费内存空间,直接把这个功能拿过来用不好么,也就是浅拷贝即可,深拷贝完全没有必要

当然如果说你想要这样操作的话...那我无言以对:

javascript 复制代码
function test() {
  console.log("hello");
}

const obj1 = {
  fn: test,
};
const obj2 = {
  fn: test,
};
// 给函数对象上挂载信息,不想共享到 obj2.fn 上...
test.aaa = 100;

console.log(test);
console.log(obj1.fn.aaa, obj2.fn.aaa); // 100 100

所以废话不多说,直接开始吧

"函数在 JS 中是一等公民" 这句话可不是随便说说的,它的灵活度太高了,所以这里我们实现一个简单的函数拷贝方案。

首先我们将函数先进行分类:

  1. 普通函数
  2. 箭头函数
  3. 异步函数(async)
  4. 生成器函数

姑且先分为这四类,在实现拷贝之前我们肯定要对它们进行一一判断,实际上 Object.prototype.toString 就能够区分出这几个特殊的函数:

javascript 复制代码
function test1() {}

const test2 = () => {};

async function test3() {}

function* test4() {}

console.log(Object.prototype.toString.call(test1)); // [object Function]
console.log(Object.prototype.toString.call(test2)); // [object Function]
console.log(Object.prototype.toString.call(test3)); // [object AsyncFunction]
console.log(Object.prototype.toString.call(test4)); // [object GeneratorFunction]

当然可以看到普通函数和箭头函数是相同的,但是箭头函数有太多特点了,比如它没有原型,因此直接这样判断即可:

javascript 复制代码
function IsArrowFunction(fn) {
  return !fn.prototype;
}

OK 现在所有函数类型都能够进行区分了,那最大的问题来了,怎么拷贝一个函数呢?

我们要实现深拷贝,那也就是说要重新创建一个函数,重新创建一个函数有什么手段? Function 构造函数可以,具体使用方式见 MDN 文档

Function() 构造函数 - JavaScript | MDN (mozilla.org)

也就是说我们需要提取函数的参数列表以及函数体 ,这...也还行吧,把原来的函数字符串化之后通过正则匹配,也能实现。但这种方式基本上就和箭头函数说拜拜了,new Function 肯定创建的是函数对象,那它一定属于普通函数具有原型的,而且最大的问题是后面那俩哥们是一点办法都没啊,咋创建异步函数和生成器函数?

思来想去就想到了一种万能的方案,那就是 eval,我们尝试一下看看:

javascript 复制代码
function createFunction(fn) {
  let cloneFn = null;
  eval(`cloneFn=${fn.toString()}`);
  console.log(cloneFn === fn, cloneFn);
  return cloneFn;
}

绝了家人们,好像都不用再区分类型了, eval 直接通杀了😆😆😆:

javascript 复制代码
function test1() {
  console.log("test1");
}

const test2 = () => {
  console.log("test2");
};

async function test3() {
  console.log("test3");
}

function* test4() {
  console.log("test4");
}

function createFunction(fn) {
  let cloneFn = null;
  eval(`cloneFn=${fn.toString()}`);
  console.log(cloneFn === fn, cloneFn);
  return cloneFn;
}

createFunction(test1);
createFunction(test2);
createFunction(test3);
createFunction(test4);

结束!我们直接把这部分放到之前实现的深拷贝当中,我们在最开始判断基础数据类型的地方再加一层对函数判断:

javascript 复制代码
const getType = (obj) => Object.prototype.toString.call(obj);

const canTranverse = {
  "[object Map]": true,
  "[object Set]": true,
  "[object Object]": true,
  "[object Array]": true,
  "[object Arguments]": true,
};

// 判断函数类型
const functionMap = {
  "[object Function]": true,
  "[object AsyncFunction]": true,
  "[object GeneratorFunction]": true,
};
   
// clone 函数
function createFunction(fn) {
  let cloneFn = null;
  eval(`cloneFn=${fn.toString()}`);
  return cloneFn;
}

function cloneSymbol(s) {
  const key = Symbol.keyFor(s);
  if (key) return Symbol.for(key);
  const desc = s.description;
  if (desc) return Symbol(desc);
  return Symbol();
}

function deepClone(origin, map = new WeakMap()) {
  if (typeof origin !== "object" || origin === null) {
    return typeof origin === "symbol"
      ? cloneSymbol(origin)
      // 增加对函数类型的判断
      : functionMap[getType(origin)]
      ? createFunction(origin)
      : origin;
  }

  const constructor = origin.constructor;
  if (!canTranverse[getType(origin)]) return new constructor(origin);
  if (map.has(origin)) return map.get(origin);
  const target = new constructor();
  map.set(origin, target);

  if (origin instanceof Map) {
    origin.forEach((value, key) => {
      target.set(deepClone(key, map), deepClone(value, map));
    });
    return target;
  }

  if (origin instanceof Set) {
    origin.forEach((value) => {
      target.add(deepClone(value, map));
    });
    return target;
  }

  for (const key in origin) {
    if (origin.hasOwnProperty(key)) {
      target[key] = deepClone(origin[key], map);
    }
  }

  const symbols = Object.getOwnPropertySymbols(origin);
  symbols.forEach((s) => {
    target[cloneSymbol(s)] = deepClone(origin[s], map);
  });

  return target;
}

还是再来测试一下吧:

javascript 复制代码
function test1(a, b) {
  console.log("test1");
}

const test2 = () => {
  console.log("test2");
};

async function test3() {
  console.log("test3");
}

function* test4() {
  console.log("test4");
}

const obj1 = {
  fn1: test1,
  fn2: test2,
  fn3: test3,
  fn4: test4,
};

const obj2 = deepClone(obj1);

function testDeepClone(obj1, obj2) {
  for (const key in obj1) {
    console.log(obj1[key]);
    if (obj1[key] === obj2[key]) return false;
  }
  return true;
}

console.log(testDeepClone(obj1, obj2)); // true

到此函数的拷贝也算是勉强实现了,但是使用 eval 确实算是比较取巧,如果把 eval 禁用掉的话那方法就剩一开始提到的 Function 构造函数了,而且无法再进行类型区分,以及需要自己手写正则匹配参数和函数体...

函数本身是比较灵活的,所以这种实现方案可能也不是万能的,但是 eval 这个思路是我们需要学习的,以后被问到了确实可以跟面试官扯一下,至少在谈到函数拷贝时要有自己的思路😜

getter setter

终于来到这个逆天的问题了,很明显这道题并没有要求手写只是谈谈思路,但是如果以前从没想到这块的话临场发挥真的很难答出来

我们先分析一下,我第一时间听到 getter 和 setter 时想到了这两种:

  1. 对象访问属性和设置属性
  2. 针对于某个属性的访问器描述符 get 和 set

不管是哪种我们都需要获取到 getter 和 setter,才能实现拷贝的功能

get 和 set 说白了也就是函数,但是直接访问肯定是拿不到的,遍历也跟普通属性没什么区别:

javascipt 复制代码
const obj = {
  arr: [],
  get value() {
    return "hello";
  },
  set arrVal(val) {
    this.arr.push(val);
  },
};
console.log(obj.value); // hello
console.log(obj.arrVal); // undefined

for (const key in obj) {
  console.log(key, obj[key]);
}

我们来看它的具体结构:

真没办法获取到吗🤔?并不是,Object.getOwnPropertyDescriptors 满足你,该 API 能够返回给定对象的所有自有属性描述符,还不够? Object.getOwnPropertyDescriptor 还能够指定属性获取其描述符

看来要解决这个问题就得靠它了,我们先来看 Object.getOwnPropertyDescriptors 获取内容:

狠狠的拿捏😎😎😎,这不就都拿到了:

javascript 复制代码
console.log(Object.getOwnPropertyDescriptor(obj, "value").get);
console.log(Object.getOwnPropertyDescriptor(obj, "arrVal").set);

我们把之前上面拷贝函数时用到的 createFunction 来这里试试:

javascript 复制代码
function createFunction(fn) {
  let cloneFn = null;
  eval(`cloneFn=${fn.toString()}`);
  return cloneFn;
}
const get = Object.getOwnPropertyDescriptor(obj, "value").get
const fn = createFunction(get);
console.log(fn === get);

坏了,好像不行唉😔,在 createFunction 内部报错了:

我们打印一下这里的 fn.toString(), 发现它和普通函数还是有区别的:

我们尝试改造一下这个字符串,把前面的 "get" 给换成 "function" 试试🧐:

javascript 复制代码
function createFunction(fn) {
  let cloneFn = null;
  eval(`cloneFn=${fn.toString().replace(/get/, () => "function")}`);
  return cloneFn;
}

const get = Object.getOwnPropertyDescriptor(obj, "value").get;
const fn = createFunction(get);
console.log("check:", fn === get); // false

好起来了家人们!成了!现在我们成功复制了 getter,而 setter 也同理

就差最后一步了,修改!😎

javascript 复制代码
const get = Object.getOwnPropertyDescriptor(obj, "value").get;

const fn = createFunction(get);
Object.getOwnPropertyDescriptor(obj, "value").get = fn;

console.log(Object.getOwnPropertyDescriptor(obj, "value").get === get); // true

不对劲,哥们你这 getter 不会是只读不可写的吧 😮,套娃看一手:

javascript 复制代码
console.log(Object.getOwnPropertyDescriptor(Object.getOwnPropertyDescriptor(obj, "value"), "get"));

好像不是这个问题🤔应该是我们这种方法不对,我们换另一种改法,用 defineProperty 试试:

javascript 复制代码
const get = Object.getOwnPropertyDescriptor(obj, "value").get;

const fn = createFunction(get);
console.log(fn === get); // false

Object.defineProperty(obj, "value", {
  get: fn,
});

console.log(fn === Object.getOwnPropertyDescriptor(obj, "value").get); // true
console.log(get === Object.getOwnPropertyDescriptor(obj, "value").get); // false

console.log(obj.value); // hello

拿下!也没有影响到 value 属性的访问,偷天换日!😎😎😎 类比一下 setter 应该也大差不差的思路

不过要想把它集成到深拷贝当中还是有些困难的,因为这样的话你就不能简简单单去遍历属性了,还需要针对该属性去看看属性描述符有没有设置 getter 和 setter ,然后拷贝对应的函数并利用 defineProperty 赋值

思路确实就是这样,我就不再添加到深拷贝上了,毕竟这个需求有些逆天...如果面试当中真被问到这个问题,相信答到现在这种程度已经足够让面试官满意了

End

实际面试当中手撕深拷贝写到循环引用就差不多了,一些特殊类型也能考虑到相信就能够过关,symbol 都有些"超纲"的感觉...

当然卷无止境,面试当中哪怕不写但讲一些 symbol 和 函数的处理也是比较加分的,当然也可以加上这个逆天的问题🤣

最后附上完整深拷贝代码:

javascript 复制代码
const getType = (obj) => Object.prototype.toString.call(obj);

const canTranverse = {
  "[object Map]": true,
  "[object Set]": true,
  "[object Object]": true,
  "[object Array]": true,
  "[object Arguments]": true,
};

const functionMap = {
  "[object Function]": true,
  "[object AsyncFunction]": true,
  "[object GeneratorFunction]": true,
};
   
function createFunction(fn) {
  let cloneFn = null;
  eval(`cloneFn=${fn.toString()}`);
  return cloneFn;
}

function cloneSymbol(s) {
  const key = Symbol.keyFor(s);
  if (key) return Symbol.for(key);
  const desc = s.description;
  if (desc) return Symbol(desc);
  return Symbol();
}

function deepClone(origin, map = new WeakMap()) {
  if (typeof origin !== "object" || origin === null) {
    return typeof origin === "symbol"
      ? cloneSymbol(origin)
      : functionMap[getType(origin)]
      ? createFunction(origin)
      : origin;
  }

  const constructor = origin.constructor;
  if (!canTranverse[getType(origin)]) return new constructor(origin);
  if (map.has(origin)) return map.get(origin);
  const target = new constructor();
  map.set(origin, target);

  if (origin instanceof Map) {
    origin.forEach((value, key) => {
      target.set(deepClone(key, map), deepClone(value, map));
    });
    return target;
  }

  if (origin instanceof Set) {
    origin.forEach((value) => {
      target.add(deepClone(value, map));
    });
    return target;
  }

  for (const key in origin) {
    if (origin.hasOwnProperty(key)) {
      target[key] = deepClone(origin[key], map);
    }
  }

  const symbols = Object.getOwnPropertySymbols(origin);
  symbols.forEach((s) => {
    target[cloneSymbol(s)] = deepClone(origin[s], map);
  });

  return target;
}
相关推荐
崔庆才丨静觅11 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606112 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了12 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅12 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅12 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅13 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment13 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅13 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊13 小时前
jwt介绍
前端
爱敲代码的小鱼13 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax