关于前端:超全面的自定义深拷贝函数

3次阅读

共计 7058 个字符,预计需要花费 18 分钟才能阅读完成。

对于间接赋值、浅拷贝、深拷贝的区别和图示,之前写 json 的文章里有介绍,能够参考这一篇 json 尽管简略,但这些细节你未必晓得

拷贝第一层

首先,咱们定义一个最简略的浅拷贝,只有可能保留原对象的第一层数据就行~

function deepClone(value) {const newObj = {};
   // 遍历原对象,将其赋值到新对象中
  for (let key in value) {newObj[key] = value[key];
  }
  return newObj;
}

// 定义一个对象
const obj = {
  name: "alice",
  friend: {name: "windy",},
};

// 拷贝进去的新对象
const user = deepClone(obj);
user.name = "kiki";
user.friend.name = "kiki";

console.log("原对象 obj-", obj);
console.log("新对象 user-", user)

批改新对象第一层属性 name 的值时,原对象是不会被批改的,但将新对象中第二层属性 friend.name 的值由 windy 批改为 kiki 时,原对象也被更改了

递归解决

对象的属性中有可能还存在对象的状况,仅一层的判断是不够的,所以须要通过递归来解决,当递归到值不为对象时,间接返回

// 判断传入的数据是否为对象
function isObject(obj) {
  // 函数也是对象 
  return obj !== null && (typeof obj === "function" || typeof obj === "object");
}

function deepClone(value) {
  // 当 value 不为对象时,间接返回 value
  if (!isObject(value)) return value;
  
  const newObj = {};
  for (let key in value) {
    // 递归解决 value 值
    newObj[key] = deepClone(value[key]);
  }
  return newObj;
}

const obj = {
  name: "alice",
  friend: {
    name: "windy",
    address: {city: "Beijing",},
  },
  hobbies: ["swiming", "dancing", "tennis"],
};

const user = deepClone(obj);
user.friend.name = "kiki";
user.friend.address.city = 'Shanghai';

console.log("原对象 obj-", obj);
console.log("新对象 user-", user)

递归调用后,拷贝的对象都是开拓了一块新的内存空间来保留,所以批改新对象的值,原对象不会变动。

但咱们发现一个新的问题,原对象中 hobbies 属性值类型为 “array”,拷贝到新对象中变成了 ”object”。

数组

所以对于数组的解决和对象要辨别开来,对传入的数据进行判断,如果为数组,创立用于复制的数据类型就应该为数组

// 判断传入的数据是否为对象
function isObject(obj) {return obj !== null && (typeof obj === "function" || typeof obj === "object");
}

function deepClone(value) {
  // 当 value 不为对象时,间接返回 value
  if (!isObject(value)) return value;
  
  // 判断 value 值是否为数组,为数组时创立新的数组
  const newObj = Array.isArray(value) ? [] : {};
  
  for (let key in value) {
    // 递归解决 value 值
    newObj[key] = deepClone(value[key]);
  }
  return newObj;
}

const obj = {
  name: "alice",
  friend: {
    name: "windy",
    address: {city: "Beijing",},
  },
  hobbies: ["swiming", "dancing", "tennis"],
  styding() {return "I am reading~";},
};

const user = deepClone(obj);
console.log("原对象 obj-", obj);
console.log("新对象 user-", user)

此时对象和数组都能够正确的拷贝

但咱们在原对象中加了一个 ”studying 办法 ”,拷贝的新对象中 ” 办法 ” 变成了 ” 空对象 ”

办法

函数自身就是为了可能复用,所以对象中的办法在进行深拷贝时,能够复用原来发办法,无需新创建,间接返回原办法即可

// 判断传入的数据是否为对象
function isObject(obj) {return obj !== null && (typeof obj === "function" || typeof obj === "object");
}

function deepClone(value) {
   // 当 value 为 function 时,间接返回原 function
   if (typeof value === "function") return value;
   
  // 当 value 不为对象时,间接返回 value
  if (!isObject(value)) return value;
  
  // 判断 value 值是否为数组,为数组时创立新的数组
  const newObj = Array.isArray(value) ? [] : {};
  
  for (let key in value) {
    // 递归解决 value 值
    newObj[key] = deepClone(value[key]);
  }
  return newObj;
}

const symbolKey = Symbol("symbolKey");
const symbolValue = Symbol("symbolValue");
const obj = {
  name: "alice",
  friend: {
    name: "windy",
    address: {city: "Beijing",},
  },
  hobbies: ["swiming", "dancing", "tennis"],
  styding() {return "I am reading~";},
  [symbolKey]: "key",
  value: symbolValue,
};

const user = deepClone(obj);
console.log("原对象 obj-", obj);
console.log("新对象 user-", user)

原对象和新对象的办法指向同一个内存地址,但咱们个别也不须要对办法进行批改

在原对象中减少了 symbol 别离作为 key 和 value 的场景,symbol 属性作为 key 时,在新对象中间接失落了

Symbol

Symbol 作为 key 也是很常见的,防止 key 值反复,为了获取 symbol 所有的值,须要通过 getOwnPropertySymbols,并增加到新对象中。

// 判断传入的数据是否为对象
function isObject(obj) {return obj !== null && (typeof obj === "function" || typeof obj === "object");
}

function deepClone(value) {
  // 当 value 为 symbol 时,返回一个新 symbol
  if (typeof value === "symbol") return Symbol(value.description);
  
  // 当 value 为 function 时,间接返回原 function
  if (typeof value === "function") return value;
  
  // 当 value 不为对象时,间接返回 value
  if (!isObject(value)) return value;
  
  // 判断 value 值是否为数组,为数组时创立新的数组
  const newObj = Array.isArray(value) ? [] : {};
  
  for (let key in value) {
    // 递归解决 value 值
    newObj[key] = deepClone(value[key]);
  }
  
  // 获取 symbol 为 key 的所有数据
  const symbols = Object.getOwnPropertySymbols(value);
  for (let sym of symbols) {newObj[sym] = deepClone(value[sym]);
  }
  return newObj;
}

const symbolKey = Symbol("symbolKey");
const symbolValue = Symbol("symbolValue");
const set = new Set([1, 2, 3, 4, 5]);
const map = new Map([["name", "alice"],
  ["age", 20],
]);
const obj = {
  name: "alice",
  friend: {
    name: "windy",
    address: {city: "Beijing",},
  },
  hobbies: ["swiming", "dancing", "tennis"],
  styding() {return "I am reading~";},
  [symbolKey]: "key",
  value: symbolValue,
  set,
  map,
};

const user = deepClone(obj);
console.log("原对象 obj-", obj);
console.log("新对象 user-", user);

symbol 属性无论是作为 key 还是 value,都能被胜利拷贝

但新对象拷贝的 map 和 set 的值都变成了 ” 空对象 ”

map 和 set

map 和 set 作为 value 的场景比拟少,所以这里就间接应用的浅拷贝,当值为 map/set 时,创立一个新的 map/set,并返回。


// 判断传入的数据是否为对象
function isObject(obj) {return obj !== null && (typeof obj === "function" || typeof obj === "object");
}

function deepClone(value) {
  // 当 value 为 set 时,返回一个新 set
  if (value instanceof Set) return new Set([...value]);
  
  // 当 value 为 map 时,返回一个新 map
  if (value instanceof Map) return new Map([...value]);
  
  // 当 value 为 symbol 时,返回一个新 symbol
  if (typeof value === "symbol") return Symbol(value.description);
  
  // 当 value 为 function 时,间接返回原 function
  if (typeof value === "function") return value;
  
  // 当 value 不为对象时,间接返回 value
  if (!isObject(value)) return value;
  
  // 判断 value 值是否为数组,为数组时创立新的数组
  const newObj = Array.isArray(value) ? [] : {};
  
  for (let key in value) {
    // 递归解决 value 值
    newObj[key] = deepClone(value[key]);
  }
  
  // 获取 symbol 为 key 的所有数据
  const symbols = Object.getOwnPropertySymbols(value);
  for (let sym of symbols) {newObj[sym] = deepClone(value[sym]);
  }
  return newObj;
}

const symbolKey = Symbol("symbolKey");
const symbolValue = Symbol("symbolValue");
const set = new Set([1, 2, 3, 4, 5]);
const map = new Map([["name", "alice"],
  ["age", 20],
]);

const obj = {
  name: "alice",
  friend: {
    name: "windy",
    address: {city: "Beijing",},
  },
  hobbies: ["swiming", "dancing", "tennis"],
  styding() {return "I am reading~";},
  [symbolKey]: "key",
  value: symbolValue,
  set,
  map,
};

const user = deepClone(obj);
console.log("原对象 obj-", obj);
console.log("新对象 user-", user);

map 和 set 也胜利拷贝到新对象中了

循环援用

对象是有可能存在循环援用,在 window 中就存在 window 属性,而且能够一直的调用。

如果须要拷贝的对象也是有属性指向本身的话,如 obj.info = obj。
咱们下面的深拷贝函数就会呈现死循环,报 RangeError 栈溢出的谬误。

所以还须要对深拷贝的代码进行优化,定义一个 map/weakMap 用于保留传入的 value 值和新创建的对象值,每一次对函数调用时,先判断传入的 value 是否曾经拷贝过,如果拷贝过,就间接返回之前拷贝的值,避免出现死循环。

// 判断传入的数据是否为对象
function isObject(obj) {return obj !== null && (typeof obj === "function" || typeof obj === "object");
}

function deepClone(value, map = new WeakMap()) {
  // 当 value 为 set 时,返回一个新 set
  if (value instanceof Set) return new Set([...value]);
  
  // 当 value 为 map 时,返回一个新 map
  if (value instanceof Map) return new Map([...value]);
  
  // 当 value 为 symbol 时,返回一个新 symbol
  if (typeof value === "symbol") return Symbol(value.description);
  
  // 当 value 为 function 时,间接返回原 function
  if (typeof value === "function") return value;
  
  // 当 value 不为对象时,间接返回 value
  if (!isObject(value)) return value;
  
  // 当 map 中存在 value 时,间接返回 map 中的 value
  if (map.has(value)) {return map.get(value);
  }
  
  // 判断 value 值是否为数组,为数组时创立新的数组
  const newObj = Array.isArray(value) ? [] : {};
  
  // 将函数接管到的 value 和新创建的 obj 保留到 map 中
  map.set(value, newObj);
  
  for (let key in value) {
    // 递归解决 value 值
    newObj[key] = deepClone(value[key], map);
  }
  
  // 获取 symbol 为 key 的所有数据
  const symbols = Object.getOwnPropertySymbols(value);
  for (let sym of symbols) {newObj[sym] = deepClone(value[sym], map);
  }
  return newObj;
}

const symbolKey = Symbol("symbolKey");
const symbolValue = Symbol("symbolValue");
const set = new Set([1, 2, 3, 4, 5]);
const map = new Map([["name", "alice"],
  ["age", 20],
]);

const obj = {
  name: "alice",
  friend: {
    name: "windy",
    address: {city: "Beijing",},
  },
  hobbies: ["swiming", "dancing", "tennis"],
  styding() {return "I am reading~";},
  [symbolKey]: "key",
  value: symbolValue,
  set,
  map,
};

obj.info = obj;
const user = deepClone(obj);
console.log("原对象 obj-", obj);
console.log("新对象 user-", user);

以上就实现了自定深拷贝的所有步骤,应用深拷贝时,不必放心批改一处变量,另一处通过拷贝获取到的变量也被扭转的状况,可能无效升高代码的 bug 率。

对于 js 高级,还有很多须要开发者把握的中央,能够看看我写的其余博文,继续更新中~

正文完
 0