【面试官系列】请讲讲你知道的关于对象和数组深、浅拷贝的一些技巧 ~(二)

简介: 【面试官系列】请讲讲你知道的关于对象和数组深、浅拷贝的一些技巧 ~(二)

4、jQuery

虽然jQuery的时代已逐渐离我们远去,但是它留下的一些工具方法仍然是值得我们学习的瑰宝,毕竟是经历过那么多年、那么多实际项目检验过的!

基本用法:

$(function () {
  const cloneData = {};
  const originalData = {
    name: "iankevin",
    age: 28,
    location: {
      city: "beijing",
      info: {
        address: "haidian",
      },
    },
    incrementAge: function () {
      this.age++;
    },
  };
  /* originalData 合并到 cloneData 中 */
  $.extend(cloneData, originalData);
  cloneData.incrementAge();
  console.log("cloneData", cloneData);
  console.log("originalData", originalData);
});

image.png这个 API 的用法:

  • 非深度克隆:extend(object_dest, object_source);
  • 深度克隆:extend(true, object_dest, object_source);

可以来看看它的实现:

/**
 * This is a quasi clone of jQuery's extend() function.
 * by Romain WEEGER for wJs library - www.wexample.com
 * @returns {*|{}}
 */
function extend() {
    // Make a copy of arguments to avoid JavaScript inspector hints.
    var to_add, name, copy_is_array, clone,
    // The target object who receive parameters
    // form other objects.
    target = arguments[0] || {},
    // Index of first argument to mix to target.
    i = 1,
    // Mix target with all function arguments.
    length = arguments.length,
    // Define if we merge object recursively.
    deep = false;
    // Handle a deep copy situation.
    if (typeof target === 'boolean') {
        deep = target;
        // Skip the boolean and the target.
        target = arguments[ i ] || {};
        // Use next object as first added.
        i++;
    }
    // Handle case when target is a string or something (possible in deep copy)
    if (typeof target !== 'object' && typeof target !== 'function') {
        target = {};
    }
    // Loop trough arguments.
    for (false; i < length; i += 1) {
        // Only deal with non-null/undefined values
        if ((to_add = arguments[ i ]) !== null) {
            // Extend the base object.
            for (name in to_add) {
                // We do not wrap for loop into hasOwnProperty,
                // to access to all values of object.
                // Prevent never-ending loop.
                if (target === to_add[name]) {
                    continue;
                }
                // Recurse if we're merging plain objects or arrays.
                if (deep && to_add[name] && (is_plain_object(to_add[name]) || (copy_is_array = Array.isArray(to_add[name])))) {
                    if (copy_is_array) {
                        copy_is_array = false;
                        clone = target[name] && Array.isArray(target[name]) ? target[name] : [];
                    }
                    else {
                        clone = target[name] && is_plain_object(target[name]) ? target[name] : {};
                    }
                    // Never move original objects, clone them.
                    target[name] = extend(deep, clone, to_add[name]);
                }
                // Don't bring in undefined values.
                else if (to_add[name] !== undefined) {
                    target[name] = to_add[name];
                }
            }
        }
    }
    return target;
}
/**
 * Check to see if an object is a plain object
 * (created using "{}" or "new Object").
 * Forked from jQuery.
 * @param obj
 * @returns {boolean}
 */
function is_plain_object(obj) {
    // Not plain objects:
    // - Any object or value whose internal [[Class]] property is not "[object Object]"
    // - DOM nodes
    // - window
    if (obj === null || typeof obj !== "object" || obj.nodeType || (obj !== null && obj === obj.window)) {
        return false;
    }
    // Support: Firefox <20
    // The try/catch suppresses exceptions thrown when attempting to access
    // the "constructor" property of certain host objects, i.e. |window.location|
    // https://bugzilla.mozilla.org/show_bug.cgi?id=814622
    try {
        if (obj.constructor && !this.hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf")) {
            return false;
        }
    }
    catch (e) {
        return false;
    }
    // If the function hasn't returned already, we're confident that
    // |obj| is a plain object, created by {} or constructed with new Object
    return true;
}

5、结构化克隆

注意: 全局 structuredClone 已经适用于 FF 94+Chrome 98+Safari 15.4+Edge 98+Node 17Deno 1.14,因此适用于所有主流浏览器的当前版本。

HTML 标准包括一个内部结构化克隆/序列化算法,可以创建对象的深度克隆。它仍然限于某些内置类型,但除了 JSON 支持的少数类型外,它还支持 Dates、RegExps、Maps、Sets、Blobs、FileLists、ImageDatas、sparse Arrays、Typed Arrays,将来可能还会支持更多类型。它还保留了克隆数据中的引用,允许它支持会导致 JSON 错误的循环和递归结构。

5.1 Node.js 中使用

全局structuredClone 函数Node 17.0 + 可以使用:

const clone = structuredClone(original);

以前的版本:v8Node.js 中的模块(自 Node 11 起)直接公开了结构化序列化 API,但此功能仍标记为“实验性”,并可能在未来版本中更改或删除。如果您使用的是兼容版本,克隆对象非常简单:

const v8 = require('v8');
const structuredClone = obj => {
  return v8.deserialize(v8.serialize(obj));
};

5.2 在浏览器中使用(chrome)

structuredClone()不仅性能卓越,而且还受到所有主要浏览器的支持。

image.png

const originalData = {
  name: "iankevin",
  birthday: new Date(),
  socials: [
    {
      name: "weixin",
      url: "kevinliao222xxx",
    },
    {
      name: "weibo",
      url: "weibo-212112",
    },
  ],
};
const cloneData = structuredClone(originalData);
cloneData.name = "tom";
console.log("originalData", originalData);

image.png来自:caniuse

5.3 局限性structuredClone()

虽然structuredClone()解决了该方法的大部分(不是全部)JSON.stringify()缺陷。但它目前也有一些值得关注的局限性。

  • 不可拷贝函数:如果要复制包含函数的对象,DataCloneError将抛出异常。
//  Error!
structuredClone({ fn: () => { } })
  • 无法拷贝 DOM 节点:它还会DataCloneError在您尝试克隆 DOM 节点时抛出。
//  Error!
structuredClone({ element: document.body })
  • 无法拷贝属性描述符、settergetter
  • 无法拷贝原型:结构化克隆不会复制原型链。如果您复制 a 的实例Class,则复制的对象将不再是 this 的实例Class。返回一个普通对象代替原来的Class.
class mainClass { 
  greet = 'hello' 
  Method() { /* ... */ }
}
const instanceClass = new mainClass()
const copied = structuredClone(instanceClass)
//  { greet: 'hello' }
copied instanceof instanceClass // false

查看可在MDN上复制的受支持类型的完整列表;不在此列表中的任何内容都无法复制。

6、手写深拷贝

如果不想引入一个依赖包来增加产物体积,可以自己手写一个符合自己业务需求的深拷贝方法。

先来个乞丐版:

function deepClone(from, to) {
    if (from == null || typeof from != "object") return from;
    if (from.constructor != Object && from.constructor != Array) return from;
    if (from.constructor == Date || from.constructor == RegExp || from.constructor == Function ||
        from.constructor == String || from.constructor == Number || from.constructor == Boolean)
        return new from.constructor(from);
    to = to || new from.constructor();
    for (var name in from) {
        to[name] = typeof to[name] == "undefined" ? deepClone(from[name], null) : to[name];
    }
    return to;
}

测试:

var obj = {
    date: new Date(),
    func: function(q) { return 1 + q; },
    num: 123,
    text: "asdasd",
    array: [1, "asd"],
    regex: new RegExp(/aaa/i),
    subobj: {
        num: 234,
        text: "asdsaD"
    }
}
var clone = deepClone(obj);

image.png有时候需要考虑循环引用的场景,比如:

const a = {}; 
a['selfref'] = a; 
a['text'] = 'something'; 
alert(a.selfref.text); 

更健壮的版本(处理了循环引用):

// 定义一个深拷贝函数,参数为源对象,已访问过的对象数组 _visited 和 已拷贝过的对象数组 _copiesVisited
function deepCopy(src, _visited, _copiesVisited) {
    // 如果源对象是 null 或者不是 object 类型,则直接返回源对象
    if (src === null || typeof(src) !== 'object'){
        return src;
    }
    // 如果源对象有 clone 方法,调用该方法进行深拷贝并返回结果
    if (typeof src.clone == 'function'){
        return src.clone(true);
    }
    // 如果源对象是 Date 类型,返回该对象的副本
    if (src instanceof Date){
        return new Date(src.getTime());
    }
    // 如果源对象是 RegExp 类型,返回该对象的副本
    if (src instanceof RegExp){
        return new RegExp(src);
    }
    // 如果源对象是 DOM Element 类型,返回该对象的副本
    if (src.nodeType && typeof src.cloneNode == 'function'){
        return src.cloneNode(true);
    }
    // 初始化被访问对象的数组。这里用来检测循环引用
    if (_visited === undefined){
        _visited = [];
        _copiesVisited = [];
    }
    // 检测这个对象是否已经被访问过
    var i, len = _visited.length;
    for (i = 0; i < len; i++) {
        if (src === _visited[i]) {
            return _copiesVisited[i];
        }
    }
    // 如果源对象是数组类型
    if (Object.prototype.toString.call(src) == '[object Array]') {
        // 切片拷贝数组
        var ret = src.slice();
        // 将该源对象推入已访问过的对象数组
        _visited.push(src);
        // 将该源对象的副本推入已拷贝过的对象数组
        _copiesVisited.push(ret);
        // 对数组中每个元素进行深拷贝,并将拷贝结果赋值给对应位置的元素
        var i = ret.length;
        while (i--) {
            ret[i] = deepCopy(ret[i], _visited, _copiesVisited);
        }
        return ret; // 返回拷贝的结果
    }
    // 获取源对象的原型对象
    var proto = (Object.getPrototypeOf ? Object.getPrototypeOf(src) : src.__proto__);
    if (!proto) {
        proto = src.constructor.prototype; 
    }
    // 创建一个继承该原型对象的新对象
    var dest = object_create(proto);
    // 将该源对象推入已访问过的对象数组
    _visited.push(src);
    // 将该新对象推入已拷贝过的对象数组
    _copiesVisited.push(dest);
    // 遍历源对象上的所有属性,对每个属性进行深拷贝,并把拷贝结果赋值给目标对象的相应属性
    for (var key in src) {
        dest[key] = deepCopy(src[key], _visited, _copiesVisited);
    }
    return dest; // 返回拷贝的结果
}
// 如果当前环境没有 Object.create 方法,则定义一个兼容的版本
var object_create = Object.create;
if (typeof object_create !== 'function') {
    object_create = function(o) {
        function F() {}
        F.prototype = o;
        return new F();
    };
}

四、开发中可能会遇到过的一个关于拷贝的bug

在一个非常常见的电商订单场景,当时封装了一个自定义 hooks;

const initialOrderData = {
  totalPayment: 0, 
  orderList: [], // 这里是数组,如果不注意可能会造成bug,要特別注意
};
const useOrderDataHandler = () => {
  const [orderData, setOrderData] = useState(initialOrderData);
  const addOrder = (newOrder) => {
    setOrderData((prev) => {
      const newTotalPayment = prev.totalPayment + newOrder.DiscountedTotalPrice;
      // 这里的操作会造成下面 resetOrderData 的 bug
      const newOrderList = prev.orderList;
      newOrderList.unshift(newOrder);
      return {
        totalPayment: newTotalPayment,
        orderList: newOrderList,
      };
    });
  };
  const resetOrderData = () => {
    setOrderData(initialOrderData);
    // 这里出现 Bug!!!
    // 因为 initialOrderData 中的 orderList
    // 已经被 addOrder 的 newOrderList.unshift(newOrder) 操作改变了
    // 导致 orderList 无法被 reset 回 []
  };
  // ......
  return {
    addOrder,
    resetOrderData,
    // ...
  };
};

怎么改变呢?

const initialOrderData = {
  totalPayment: 0,
  orderList: [],
};
const useOrderDataHandler = () => {
  // 深拷贝,确保 initialOrderData 中的 orderList 被完全复制,不会与原始值互相影响
  const clonedInitialOrderData = JSON.parse(JSON.stringify(initialOrderData));
  const [orderData, setOrderData] = useState(clonedInitialOrderData);
  // ...
};

五、性能测试

社区甚至有人对这些方法做了性能测试,鉴于篇幅长度和文章的重点,我就不在此做过多的讨论了,链接已放出,各位有兴趣的同学可以自行测试。

按性能进行深拷贝: 根据基准从最佳到最差排名image.png

相关文章
|
7月前
|
编译器 C++ Python
【C/C++ 泡沫精选面试题02】深拷贝和浅拷贝之间的区别?
【C/C++ 泡沫精选面试题02】深拷贝和浅拷贝之间的区别?
125 1
|
5月前
|
存储 缓存 监控
Java面试题:在Java中,对象何时可以被垃圾回收?编程中,如何更好地做好垃圾回收处理?
Java面试题:在Java中,对象何时可以被垃圾回收?编程中,如何更好地做好垃圾回收处理?
76 0
|
4月前
|
JavaScript
【Vue面试题九】、Vue中给对象添加新属性界面不刷新?
这篇文章讨论了Vue中给对象动态添加新属性时界面不刷新的问题,并提供了三种解决方案:使用`Vue.set()`方法来确保新属性是响应式的并触发视图更新,使用`Object.assign()`创建新对象以合并新属性,以及作为最后手段的`$forceUpdate()`进行强制刷新。文章还简要分析了Vue 2和Vue 3在数据响应式实现上的差异。
|
4月前
|
JavaScript
【Vue面试题八】、为什么data属性是一个函数而不是一个对象?
这篇文章解释了为什么在Vue中组件的`data`属性必须是一个函数而不是一个对象。原因在于组件可能会有多个实例,如果`data`是一个对象,那么这些实例将会共享同一个`data`对象,导致数据污染。而当`data`是一个函数时,每次创建组件实例都会返回一个新的`data`对象,从而确保了数据的隔离。文章通过示例和源码分析,展示了Vue初始化`data`的过程和组件选项合并的原理,最终得出结论:根实例的`data`可以是对象或函数,而组件实例的`data`必须为函数。
【Vue面试题八】、为什么data属性是一个函数而不是一个对象?
|
4月前
|
存储 JavaScript 前端开发
JS浅拷贝及面试时手写源码
JS浅拷贝及面试时手写源码
|
5月前
|
存储 缓存 算法
Java面试题:给出代码优化的常见策略,如减少对象创建、使用缓存等。
Java面试题:给出代码优化的常见策略,如减少对象创建、使用缓存等。
68 0
|
5月前
|
设计模式 存储 缓存
Java面试题:结合建造者模式与内存优化,设计一个可扩展的高性能对象创建框架?利用多线程工具类与并发框架,实现一个高并发的分布式任务调度系统?设计一个高性能的实时事件通知系统
Java面试题:结合建造者模式与内存优化,设计一个可扩展的高性能对象创建框架?利用多线程工具类与并发框架,实现一个高并发的分布式任务调度系统?设计一个高性能的实时事件通知系统
65 0
|
7月前
|
Java 程序员 C语言
2024年Python最新【Python学习教程】Python类和对象_python中类和对象的讲解,Python最新面试题
2024年Python最新【Python学习教程】Python类和对象_python中类和对象的讲解,Python最新面试题
2024年Python最新【Python学习教程】Python类和对象_python中类和对象的讲解,Python最新面试题
|
7月前
|
搜索推荐 开发工具 Python
2024年最新【Python 基础教程】对时间日期对象的侃侃而谈,面试必考题
2024年最新【Python 基础教程】对时间日期对象的侃侃而谈,面试必考题
2024年最新【Python 基础教程】对时间日期对象的侃侃而谈,面试必考题
|
7月前
|
安全 Java
【JAVA面试题】什么是对象锁?什么是类锁?
【JAVA面试题】什么是对象锁?什么是类锁?