js手写实现让面试官惊艳的拷贝函数

386 阅读5分钟

如果按下面这么写的话,面试官看完眉头一皱,"好了下一个~"

function deepClone(obj) {
  if (typeof obj !== "object" || obj === null) {
    // 如果不是复杂数据类型,直接返回
    return obj;
  }
  let cloneObj = Array.isArray(obj) ? [] : {};
  // 遍历对象或数组
  for (let key in obj) {
    // 如果是对象的属性,递归调用deepClone
    if (obj.hasOwnProperty(key)) {
      cloneObj[key] = deepClone(obj[key]);
    }
  }
  return cloneObj;
}

在上述的简单深拷贝实现中,只考虑了数组和普通对象,并没有考虑其他对象,且没有考虑循环引用的问题,可能带来死循环。

目录解读:

重点注意:

  • 考虑null和基本数据类型
  • 考虑循环引用
  • 考虑日期、正则、弱引用对象
  • 考虑Map、Set对象
  • 考虑函数、箭头函数
  • 考虑不可枚举对象
  • 深拷贝应用场景

循环引用

使用弱引用WeakMap,防止内存泄漏

判断是否存在该引用,存在则直接返回。

// visited = new WeakMap (当函数参数传递)
if (visited.has(obj)) {
  return visited.get(obj);
}

日期、正则、弱引用对象

判断并重新new实例一个它们的构造函数

const constructor = obj.constructor;
if(/^(RegExp|Date|WeakMap|WeakSet)/.test(constructor)) {
    const res = new constructor(obj);
    visited.set(obj, res); // 避免循环引用
    return res; // 直接返回
}

Map、Set对象

const constructor = obj.constructor;
// Map
if (constructor === Map) {
  const map = new Map();
  visited.set(obj, map); // 避免循环引用
  obj.forEach((value, key) => {
    map.set(key, _completeDeepClone(value, visited)); // 递归拷贝深层对象
  });
  return map;
}
// Set
if (constructor === Set) {
  const set = new Set();
  visited.set(obj, set); // 避免循环引用
  obj.forEach((value) => {
    set.add(_completeDeepClone(value, visited)); // 递归拷贝深层对象
  });
  return set;
}

函数、箭头函数

注:虽然考虑函数没有任何意义,但是也体现了自身能力的一部分嘛

箭头函数和函数的区别

箭头函数没有自己的 constructor 属性,它继承了外部作用域的 constructor 属性。,不能用new实例化,因为它不存在原型对象,没有prototype属性,且没有自己的this,它的this继承于外层作用域。

我们可以拿prototype属性来判断

function isFunc(fn) {
    return fn instanceof Function && !fn.hasOwnProperty('prototype');
}

那箭头函数如何拷贝?

因为箭头函数没有自己的this,且没有构造函数和原型链,我们可以直接使用toString()方法转为字符串,然后直接使用eval或者new Function()重新创建内存保存。

简单介绍一下 evalnew Function

  • newFunction

new Function 是 JavaScript 中的一个内置函数,它可以动态创建一个函数(运行时生成,不会有函数提升)。

const func = new Function('arg1', 'arg2', 'return arg1 + arg2;'); console.log(func(1, 2)); // 3

需要注意的是,使用 new Function 创建的函数的作用域链是相对较为简单的,它只包含函数本身和全局作用域。

  • eval

eval 它的作用是将传入的字符串参数作为代码进行解析和执行。

const res = eval(`${obj.toString()}`);
// 或者
const res = new Function(`return ${obj.toString}`)();

函数:分为普通函数和构造函数

考虑到原型链继承,和构造函数的自身属性问题,我们无法直接使用上述方法直接执行字符串创建新内存。

但是可以用寄生组合式继承来继承原型链和方法。

const res = function(...args) {
    return obj.apply(res, args);
}
visited.set(obj, res); // 避免循环引用
// 继承自身静态属性(只能通过构造函数自身访问),比如 obj.a = 1
Object.keys(obj).forEach(key => res[key] = obj[key]);

// 寄生组合式继承
res.prototype = Object.create(obj.prototype);
res.prototype.constructor = res; // 构造函数指向自己
return res;

不可枚举对象

包括 Symbol 类型和设置了 enumerable 为 false 的成员

对于 Symbol 类型,我们可以用 Object.getOwnPropertySymbols(obj) 来获取,

而对于除了 Symbol 类型的成员,我们都可以用 Object.getOwnPropertyNames(obj) 来获取属性名。

那怎么获取它们的数据描述符属性呢?

image.png

直接使用 Object.getOwnPropertyDescriptor 可以获取属性描述符。如下:

const { value, writable, enumerable, configurable } = Object.getOwnPropertyDescriptor(obj, key);

并用 Object.defineProperty 定义就行。

实现:

// 处理普通对象和数组(数组也可以存在键值对)
const result = Array.isArray(obj) ? [] : {};
visited.set(obj, result); // 避免循环引用
// 获取对象的所有属性名,包括不可枚举属性
const props = Object.getOwnPropertyNames(obj); // 不可枚举类型
const symbolProps = Object.getOwnPropertySymbols(obj); // Symbol类型
props.concat(symbolProps).forEach(key => {
    const descriptor = Object.getOwnPropertyDescriptor(obj, key);
    if(descriptor) {
        const { value, writable, enumerable, configurable } = descriptor;
        Object.defineProperty(result, key, {
            value: _completeDeepClone(value, visited),
            writable, enumerable, configurable
        })
    }
});
return result;

这样我们就实现一个相对来说完整的深拷贝!!

最终实现

function _completeDeepClone(obj, visited = new WeakMap()) {
    if (typeof obj !== "object" || obj === null) return obj;
    // 防止循环引用
    if (visited.has(obj)) {
      return visited.get(obj);
    }
    // 获取对象的构造函数
    const constructor = obj.constructor;
    // 处理特殊对象类型~正则对象和时间对象
    if(/^(RegExp|Date|WeakMap|WeakSet)/.test(constructor.name)) {
        const res = new constructor(obj);
        visited.set(obj, res);
        return res;
    }
    // Map
    if (constructor === Map) {
      const map = new Map();
      visited.set(obj, map);
      obj.forEach((value, key) => {
        map.set(key, _completeDeepClone(value, visited));
      });
      return map;
    }
    // Set
    if (constructor === Set) {
      const set = new Set();
      visited.set(obj, set);
      obj.forEach((value) => {
        set.add(_completeDeepClone(value, visited));
      });
      return set;
    }
    // 处理函数对象和箭头函数
    if(constructor === Function) {
        let res;
        if(!obj.hasOwnProperty("prototype")) { // 箭头函数
            res = new Function(`return ${obj.toString()}`)()
            visited.set(obj, res); // 避免循环引用
            return res;
        }
        // 考虑到构造函数和普通对象
        res = function(...args) {
            return obj.call(this, ...args);
        }
        visited.set(obj, res); // 避免循环引用
        // 普通对象的自身的属性,比如fn.a = 2这种静态属性
        Object.keys(obj).forEach(key => res[key] = obj[key]);
        // 原型继承,寄生组合继承,复制该函数的原型链
        res.prototype = Object.create(obj.prototype);
        res.prototype.constructor = res;
        return res;
    }
    // 处理普通对象和数组
    const result = Array.isArray(obj) ? [] : {};
    visited.set(obj, result);
    // 获取对象的所有属性名,包括不可枚举属性
    const props = Object.getOwnPropertyNames(obj); // 不可枚举类型
    const symbolProps = Object.getOwnPropertySymbols(obj); // Symbol类型
    props.concat(symbolProps).forEach(key => {
        const descriptor = Object.getOwnPropertyDescriptor(obj, key);
        if(descriptor) {
            const { value, writable, enumerable, configurable } = descriptor;
            Object.defineProperty(result, key, {
                value: _completeDeepClone(value, visited),
                writable, enumerable, configurable
            })
        }
    });
    return result;
}

深拷贝业务场景?

不得不说,难免有些sx面试官还会问这个问题,服了u。

05CC72BC.jpg

  1. 数据缓存:当需要对某个对象进行数据缓存时,为了避免对象引用带来的问题,可以使用深拷贝来创建一个独立的对象存储数据。
  2. 数据传递:在多个模块之间传递数据时,为了避免数据被污染,也可以使用深拷贝将数据拷贝到新的对象中,确保数据的独立性。
  3. 状态保存:在一些需要保存状态的场景中,比如撤销/重做功能、多步操作等,为了保证状态不被覆盖,也可以使用深拷贝来保存状态。
  4. 对象转换:当需要将一个对象转换为另一个对象时,可以使用深拷贝来创建一个与原对象结构相同的新对象,并将原对象的数据复制到新对象中。

但是深拷贝也会带来内存占用过大问题,在实际业务中尽量不用到。

完结撒花

05CCFAD8.gif