前言
一个变量的值为基本数据类型时,其值是独立存储的,将该变量复制给另一个变量时,会新生成一个值,赋予另一个变量,因此没有深浅拷贝,或者说都是深拷贝。
对象的存储是由变量存储一个内存地址,该地址存储指向存储着的值。将对象赋予另一变量时,只会把内存地址赋予另一变量,也就是浅拷贝,内存的值是共享的
拷贝:是指通过一定程序将某个变量的值复制至另一个变量的过程。
浅拷贝:只是复制对象最外层的属性也就是赋值了引用地址,导致两个变量仍指向同一内存,一旦值被修改,两边都会产生变化
深拷贝:复制整个对象最外层和深层的所有属性,值被修改互不影响
勉强会工作的前端(简单使用JSON方法)
工作中,我们需要处理的大多为对象中包裹的数据类型都为基本数据类型,也就是数字-布尔值-字符串
const obj1 = {
a: {
b: 1, //最终为数字
c: true, //最终为数字
d: "1", //最终为数字
},
};
const obj2 = JSON.parse(JSON.stringify(obj1));
了解JSON方法
JSON方法含有序列化JSON.stringify(将对象序列化为JSON字符串)与反序列化JSON.parse(将JSON字符串转换为新的对象),使用的前提是对象的全部属性都是可序列化的。
JSON.stringify(value[, replacer [, space]])
value:序列化的值
replacer:
如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;
replacer(key,val)//第一次的值是整个value,第二次开始才是
如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;
如果该参数为 null 或者未提供,则对象所有的属性都会被序列化。
space:缩进空白字符串,用于美化输出
- 序列化注意点
undefined:对象中直接忽视,数组中转换为null输出- 不可枚举对象
Object.create(null,{x:{value:'x',enumerable:false}}) Symbol和在转换过程中会被直接忽视函数在转换过程中会被直接忽视。不过单独转换时JSON.stringify(function(){})orJSON.stringify(undefined)会被转换为undefined- 包含循环引用的对象会直接报出错误
NaN和infinity格式的数字和null会被转换为nullDate对象会被转换为字符串(同使用了Date.toString()的结果)- 其他对象,包括
Map、Set、WeakMap、WeakSet只会序列化自身可枚举对象,默认没有转换为{},同时会失去构造函数
由于拥有以上缺点,最好JSON方法仅适用于已知的只包含基本数据类型**字符串,数组,布尔值**
自实现深拷贝(公共函数中的deepClone)
第一步:实现数据类型判断函数
根据上个的方法,得出,拷贝必须根据不同类型进行不同的处理才能得到正确的值
/**
* @retrun {isString:fundtion(),...} 返回一个对象,里存放着判断函数
*/
function getIsTpyeofFn() {
'use strict';
// 非严格模式下--undefined会执行全window
const types =
"Array Object String Date RegExp Function Boolean Number Null Undefined".split(
" "
);
function type() {
// toString函数会返回数据类型,从索引为8开始截取字符串,能得到数据类型的值
return Object.prototype.toString.call(this).slice(8, -1);
}
const _ = {};
for (let index = 0; index < types.length; index++) {
const cur = types[index];
_["is" + cur] = (function (self) {
return function (elem) {
return type.call(elem) === self;
};
})(cur);
}
return _;
}
第二步:实现深拷贝函数
核心思想:根据不同类型不同处理,当值为Object或者Array时深入递归处理
/**
* @return 返回一个新的正则对象
*/
function cloneRegExp(reg) {
let flag = ''
if (reg.ignoreCase) {
flag +='i'
}
if (reg.global) {
flag +='g'
}
if (reg.multiline) {
flag +='im'
}
let cloneReg = new RegExp(reg,flag)
if (reg.lastIndex) {
cloneReg.lastIndex = reg.lastIndex
}
return cloneReg
}
function deepClone(source) {
const _ = getIsTpyeofFn();
// 维护两个存储循环引用的数组
let parents = [];// 将拆分并存储所有对象地址--第一个对象地址存储存放 source本身
let children = [];// 将拆分并存储所有对象地址--第一个对象地址存储存放 目标本身
// 用于递归的_clone函数
function _clone(parent) {
// 先处理基本数据类型--直接返回
if (parent === null) {
return null;
}
if (typeof parent !== "object") {
return parent;
}
let child,proto;
// 下面的都为对象
if (_.isArray(parent)) {
// 处理数组对象
child = []
} else if (_.isRegExp(parent)) {
// 处理正则对象
child = cloneRegExp(parent)
} else if (_.isDate(parent)) {
// 处理Date对象
child = new Date(parent.getTime())
} else {
// 处理对象原型
// 创建一个原型指向parent的新对象
child = Object.create(Object.getPrototypeOf(parent))
}
// 处理循环引用
if (parents.indexOf(parent) !== -1) {
// 如果父数组存在本对象,说明之前已经被引用过,直接返回次对象
return children[index]
}
// 没有引用过,则添加至parents和children数组中
parents.push(parent)
children.push(child)
// 遍历对象属性
for (const prop in parent) {
if (Object.hasOwnProperty.call(parent, prop)) {
child[prop] = _clone(parent[prop])
}
}
return child
}
return _clone(source)
}
jQurey中的深拷贝解析
jQuery中有两个复制方法,分别是
$.clone()和$.extends()其中
clone是用于拷贝Dom对象而
extends是用于拷贝js对象
extends函数
核心思想,递归变量 对象和数组,遇到基本数据类型直接返回
jQuery.extend = jQuery.fn.extend = function () {
// options是一个缓存遍历,存储arguments[i]
// name是用来接收要被扩展对象的key,src改变之前target对象上每个key对应的value
// copy传入对象上每个key对应的value,copyIsArray判断是否为一个数组
// clone深克隆中用来临时存储对象或者数组的src
var src, copyIsArray, copy, name, options, clone;
(target = arguments[0] || {}), (i = 1), (length = arguments.length);
deep = false;
// 如果传递的第一个参数为boolean类型,为true代表深拷贝,为false代表浅拷贝
if (typeof target === "boolean") {
deep = target;
// 如果传递了第一个参数为boolean值,则待克隆的对象为第二个参数
target = arguments[i] || {};
i++;
}
// 如果是基本数据类型
if (typeof target !== "object" && !jQuery.isFunction(target)) {
target = {};
}
// 如果只传递一个参数,那么克隆的是jquery自身
if (i === length) {
target = this;
i--;
}
for (; i < length; i++) {
// 仅需要处理不是null 与undefined类型的数据
if(options = arguments[i] !=null){
//遍历对象的所有属性
for (const name in options) {
src = target[name]
copy = options[name]
// 阻止循环引用
if(target ===copy){
continue
}
// 递归处理对象和数值
if (deep&©&&(jQuery.isPlainObject(copy))||(copyIsArray = jQuery.isArray(copy))) {
if (copyIsArray) {
copyIsArray = false;
clone = src && jQuery.isArray(src)?src :[]
}else{
clone = src && jQuery.isPlainObject(src)?src:{}
}
// 将原始值的name属性赋值给target目标对象
target[name] = jQuery.extend(deep,clone,copy)
}else{
//简单值,直接赋值
target[name] = copy
}
}
}
}
return target
};
extends函数有一个缺陷就是不能拷贝重复引用
lodash工具之深拷贝函数解析
_.cloneDeep(value)这个方法类似_.clone,除了它会递归拷贝value。(注:也叫深拷贝)。
二话不多说贴上源码
cloneDeep方法
/**
* This method is like `_.clone` except that it recursively clones `value`.
*
* @static
* @memberOf _
* @since 1.0.0
* @category Lang
* @param {*} value The value to recursively clone.
* @returns {*} Returns the deep cloned value.
* @see _.clone
* @example
*
* var objects = [{ 'a': 1 }, { 'b': 2 }];
*
* var deep = _.cloneDeep(objects);
* console.log(deep[0] === objects[0]);
* // => false
*/
function cloneDeep(value) {
return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG);
}
这个方法类似_.clone,除此之外还递归的拷贝了value。
一个参数:value(需要深拷贝的值);会直接返回一个深拷贝的对象
baseClone方法
cloneDeep里边会调用baseClone方法同时把value传入
/** Used to compose bitmasks for cloning. */
/** 组成位掩码用于拷贝 */
/** cloneDeep中CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG=> 1 | 4 = 5 */
var CLONE_DEEP_FLAG = 1,
CLONE_SYMBOLS_FLAG = 4;
/**
* The base implementation of `_.clone` and `_.cloneDeep` which tracks
* traversed objects.
*
* @private
* @param {*} value The value to clone.
* @param {boolean} bitmask The bitmask flags.
* 1 - Deep clone 为1时深拷贝
* 2 - Flatten inherited properties 为2时打平集成属性
* 4 - Clone symbols 为4时复制symbols类型数据
* @param {Function} [customizer] The function to customize cloning.
* @param {Function} [customizer] 自定义clone函数.
* @param {string} [key] The key of `value`.
* @param {Object} [object] The parent object of `value`.
* @param {Object} [stack] 跟踪遍历对象,存储他们的副本.
* @returns {*} Returns the cloned value.
*/
function baseClone(value, bitmask, customizer, key, object, stack) {
var result,
isDeep = bitmask & CLONE_DEEP_FLAG, // 5 & 1 = 1 (位与运算)深拷贝
isFlat = bitmask & CLONE_FLAT_FLAG, // 5 & 2 = 0 (位与运算)不进行原型复制
isFull = bitmask & CLONE_SYMBOLS_FLAG; // 5 & 4 = 4 (位与运算)对symbol进行拷贝
// customizer 自定义克隆,并返回函数的返回值
if (customizer) {
result = object ? customizer(value, key, object, stack) : customizer(value);
}
if (result !== undefined) {
return result;
}
//基础数据类型直接返回
if (!isObject(value)) {
return value;
}
// 由于基本数据类型都被上面原地返回,下面处理都是对象类型的值
// 数组处理
var isArr = isArray(value);
if (isArr) {
// 初始化数组,使其结构上类似原数组
result = initCloneArray(value);
if (!isDeep) {
// 不进行深拷贝的直接返回浅拷贝值
return copyArray(value, result);
}
}
// 对非数组处理
else {
var tag = getTag(value),
isFunc = tag == funcTag || tag == genTag;
// tag 返回类型文本 '[object Symbol]'
// isFunc 是否为函数
if (isBuffer(value)) {
// 对于二进制流对象克隆后直接返回不进行递归
return cloneBuffer(value, isDeep);
}
// 处理普通对象类型跟类似数组类型还有 单纯的函数对象
if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
// 对于函数,没有父对象时不进行拷贝直接返回空对象
// 原型链复制
result = (isFlat || isFunc) ? {} : initCloneObject(value);
if (!isDeep) {
return isFlat
? copySymbolsIn(value, baseAssignIn(result, value))
: copySymbols(value, baseAssign(result, value));
}
} else {
if (!cloneableTags[tag]) {
// 是否为可拷贝对象--错误对象,函数对象,weakMap对象-没有父对象时,返回空对象,有父对象则返回原值
return object ? value : {};
}
// 对于非常规类型对象,通过各自类型分别进行处理。
result = initCloneByTag(value, tag, isDeep);
}
}
// 用栈将引用存储起来
stack || (stack = new Stack);
var stacked = stack.get(value);
if (stacked) {
// 发现循环引用,直接返回循环引用
return stacked;
}
stack.set(value, result);
// set数据处理--遍历递归
if (isSet(value)) {
value.forEach(function(subValue) {
result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack));
});
} else if (isMap(value)) {
// map数据处理--遍历递归
value.forEach(function(subValue, key) {
result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack));
});
}
// 此处得到getAllKeys
var keysFunc = isFull
? (isFlat ? getAllKeysIn : getAllKeys)
: (isFlat ? keysIn : keys);
// 获取拷贝对象的所有属性键-遍历并递归--然后对result进行赋值
var props = isArr ? undefined : keysFunc(value);
arrayEach(props || value, function(subValue, key) {
if (props) {
key = subValue;
subValue = value[key];
}
// Recursively populate clone (susceptible to call stack limits).
assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack));
});
// 返回深拷贝的result
return result;
}
核心原理
深度拷贝:对于普通对象跟数组,使用baseclone函数进行遍历递归,递归的结束条件是得到一个拷贝后的对象或者基本数据类型
// 获取拷贝对象的所有属性键-遍历并递归--然后对result进行赋值
var props = isArr ? undefined : keysFunc(value);
arrayEach(props || value, function(subValue, key) {
if (props) {
key = subValue;
subValue = value[key];
}
// Recursively populate clone (susceptible to call stack limits).
assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack));
});
循环引用:创建一个栈。将每个引用类型都存入栈中,返现被拷贝对象有循环引用时,返回拷贝后的对象,组程新的循环引用
// 用栈将引用存储起来
stack || (stack = new Stack);
var stacked = stack.get(value);
if (stacked) {
// 发现循环引用,直接返回循环引用
return stacked;
}
基本数据类型处理:统一判断为非对象,直接返回原值。基本数据类型的值每一个都是互不影响的
//基础数据类型直接返回
if (!isObject(value)) {
return value;
}
数组跟RegExp.exce()返回的数组:使用initCloneArray生成与拷贝数组一致的数组结构result,用于保存遍历的结果
// 数组处理
var isArr = isArray(value);
if (isArr) {
// 初始化数组,使其结构上类似原数组
result = initCloneArray(value);
...
}
function initCloneArray(array) {
var length = array.length,
result = new array.constructor(length);
// Add properties assigned by `RegExp#exec`.
// `RegExp#exec`的判断逻辑,长度大于0,索引为1的值是字符串且拥有index属性
if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) {
result.index = array.index;
result.input = array.input;
}
return result;
}
对象和函数:二进制对象,返回拷贝对象,不进行递归,错误对象,weakmap弱引用对象,函数存在父对象时,返回原函数,否则返回空对象。 其他对象类型,根据匹配出的类型,初始化为对应的对象类型
var tag = getTag(value),
isFunc = tag == funcTag || tag == genTag;
// tag 返回类型文本 '[object Symbol]'
// isFunc 是否为函数
if (isBuffer(value)) {
// 对于二进制流对象克隆后直接返回不进行递归
return cloneBuffer(value, isDeep);
}
// 处理普通对象类型跟类似数组类型还有 单纯的函数对象
if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
// 对于函数,没有父对象时不进行拷贝直接返回空对象
// 原型链复制
result = (isFlat || isFunc) ? {} : initCloneObject(value);
if (!isDeep) {
return isFlat
? copySymbolsIn(value, baseAssignIn(result, value))
: copySymbols(value, baseAssign(result, value));
}
} else {
if (!cloneableTags[tag]) {
// 是否为可拷贝对象--错误对象,函数对象,weakMap对象-没有父对象时,返回空对象,有父对象则返回原值
return object ? value : {};
}
// 对于非常规类型对象,通过各自类型分别进处理。
result = initCloneByTag(value, tag, isDeep);
}
function initCloneByTag(object, tag, isDeep) {
// 保存原型对象,使得拷贝后仍保持原型链(通过new 构造函数生成对象)
var Ctor = object.constructor;
switch (tag) {
case arrayBufferTag:
return cloneArrayBuffer(object);
case boolTag:
case dateTag:
return new Ctor(+object);
case dataViewTag:
return cloneDataView(object, isDeep);
case float32Tag: case float64Tag:
case int8Tag: case int16Tag: case int32Tag:
case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag:
return cloneTypedArray(object, isDeep);
case mapTag:
return new Ctor;
case numberTag:
case stringTag:
return new Ctor(object);
case regexpTag:
return cloneRegExp(object);
case setTag:
return new Ctor;
case symbolTag:
return cloneSymbol(object);
}
}
正则类型:巧用正则的转换为字符后,固定类型,获取正则对象的标志参数
// 适用于获取以字母结尾的字符串,这些字符串也就是正则的标志参数
const reFlags = /\w*$/
function cloneRegExp(regexp) {
// 返回当前匹配的文本
const result = new regexp.constructor(regexp.source, reFlags.exec(regexp))
// 下一次匹配的起始索引
result.lastIndex = regexp.lastIndex
return result
}
symbol类型:
const symbolValueOf = Symbol.prototype.valueOf
function cloneSymbol(symbol) {
return symbolValueOf ? Object(symbolValueOf.call(symbol)) : {};
}
对于map和set数据类型:使用forEach函数遍历,再使用baseClone进行递归
if (tag == mapTag) {
value.forEach((subValue, key) => {
result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack))
})
return result
}
if (tag == setTag) {
value.forEach((subValue) => {
result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack))
})
return result
}
参考:木易杨大佬的文章Lodash是如何实现深拷贝的
性能对比
测试代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script
type="text/javascript"
src="https://cdn.jsdelivr.net/g/lodash@4(lodash.min.js+lodash.fp.min.js)"
></script>
<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
<script>
const lodashClone = _.cloneDeep;
const jqueryClone = jQuery.extend;
/**
* @return 返回一个新的正则对象
*/
function cloneRegExp(reg) {
let flag = "";
if (reg.ignoreCase) {
flag += "i";
}
if (reg.global) {
flag += "g";
}
if (reg.multiline) {
flag += "im";
}
let cloneReg = new RegExp(reg, flag);
if (reg.lastIndex) {
cloneReg.lastIndex = reg.lastIndex;
}
return cloneReg;
}
function deepClone(source) {
const _ = getIsTpyeofFn();
// 维护两个存储循环引用的数组
let parents = []; // 将拆分并存储所有对象地址--第一个对象地址存储存放 source本身
let children = []; // 将拆分并存储所有对象地址--第一个对象地址存储存放 目标本身
// 用于递归的_clone函数
function _clone(parent) {
// 先处理基本数据类型--直接返回
if (parent === null) {
return null;
}
if (typeof parent !== "object") {
return parent;
}
let child, proto;
// 下面的都为对象
if (_.isArray(parent)) {
// 处理数组对象
child = [];
} else if (_.isRegExp(parent)) {
// 处理正则对象
child = cloneRegExp(parent);
} else if (_.isDate(parent)) {
// 处理Date对象
child = new Date(parent.getTime());
} else {
// 处理对象原型
// 创建一个原型指向parent的新对象
child = Object.create(Object.getPrototypeOf(parent));
}
// 处理循环引用
if (parents.indexOf(parent) !== -1) {
// 如果父数组存在本对象,说明之前已经被引用过,直接返回次对象
return children[index];
}
// 没有引用过,则添加至parents和children数组中
parents.push(parent);
children.push(child);
// 遍历对象属性
for (const prop in parent) {
if (Object.hasOwnProperty.call(parent, prop)) {
child[prop] = _clone(parent[prop]);
}
}
return child;
}
return _clone(source);
}
/**
* @retrun {isString:fundtion(),...} 返回一个对象,里存放着判断函数
*/
function getIsTpyeofFn() {
"use strict";
// 非严格模式下--undefined会执行全window
const types =
"Array Object String Date RegExp Function Boolean Number Null Undefined".split(
" "
);
function type() {
// toString函数会返回数据类型,从索引为8开始截取字符串,能得到数据类型的值
return Object.prototype.toString.call(this).slice(8, -1);
}
const _ = {};
for (let index = 0; index < types.length; index++) {
const cur = types[index];
_["is" + cur] = (function (self) {
return function (elem) {
return type.call(elem) === self;
};
})(cur);
}
return _;
}
var objects = [{ a: 1 }, { b: 2 }];
function test(n) {
console.time(`deepClone拷贝${n}次,使用时间为`);
for (let index = 0; index < n; index++) {
deepClone(objects);
}
console.timeEnd(`deepClone拷贝${n}次,使用时间为`);
console.time(`lodashClone拷贝${n}次,使用时间为`);
for (let index = 0; index < n; index++) {
lodashClone(objects);
}
console.timeEnd(`lodashClone拷贝${n}次,使用时间为`);
console.time(`jqueryClone拷贝${n}次,使用时间为`);
for (let index = 0; index < n; index++) {
jqueryClone(objects);
}
console.timeEnd(`jqueryClone拷贝${n}次,使用时间为`);
}
test(100);
test(1000);
test(10000);
test(100000);
test(1000000);
</script>
</body>
</html>
测试结果
| 测试次数/时间(ms) | lodashClone | jqueryClone | deepClone |
|---|---|---|---|
| 百次 | 1.15 | 0.36 | 0.14 |
| 千次 | 6.78 | 3.68 | 5.80 |
| 万次 | 19.19 | 43.38 | 27.43 |
| 十万次 | 136.45 | 344.67 | 265.28 |
| 百万次 | 1352.85 | 3507.43 | 2592.95 |
结果分析
百次以内,自实现用时最少,因为针对业务,逻辑也比较少。
千次以内,Jq实现用时最少,lodash用时最长。
万次以上,Jq实现用时最长,lodash用时最短,如果自实现的话,还可以针对业务对自实现深拷贝根据lodash源码进行优化。
业务使用
平时我们用的深拷贝多数用于,普通对象以及数组,跟正则,可以使用lodash的clonedeep方法进行项目开发。
最优解是,针对业务简写clonedeep方法,生成统一的工具函数deepclone