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); });
这个 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 17
和Deno 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);
以前的版本:v8
Node.js 中的模块(自 Node 11 起)直接公开了结构化序列化 API,但此功能仍标记为“实验性”,并可能在未来版本中更改或删除。如果您使用的是兼容版本,克隆对象非常简单:
const v8 = require('v8'); const structuredClone = obj => { return v8.deserialize(v8.serialize(obj)); };
5.2 在浏览器中使用(chrome)
structuredClone()
不仅性能卓越,而且还受到所有主要浏览器的支持。
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);
来自:caniuse
5.3 局限性structuredClone()
虽然structuredClone()
解决了该方法的大部分(不是全部)JSON.stringify()
缺陷。但它目前也有一些值得关注的局限性。
- 不可拷贝函数:如果要复制包含函数的对象,
DataCloneError
将抛出异常。
// Error! structuredClone({ fn: () => { } })
- 无法拷贝 DOM 节点:它还会
DataCloneError
在您尝试克隆 DOM 节点时抛出。
// Error! structuredClone({ element: document.body })
- 无法拷贝属性描述符、
setter
和getter
。 - 无法拷贝原型:结构化克隆不会复制原型链。如果您复制 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);
有时候需要考虑循环引用的场景,比如:
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); // ... };
五、性能测试
社区甚至有人对这些方法做了性能测试,鉴于篇幅长度和文章的重点,我就不在此做过多的讨论了,链接已放出,各位有兴趣的同学可以自行测试。
按性能进行深拷贝: 根据基准从最佳到最差排名