如何深拷贝一个对象
代码语言:javascript复制function copyFn(obj) {
if (obj == null) {
return null
}
var result = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === 'object') {// 如果是对象,再次调用该方法自身result[key] = copyFn(obj[key]); } else {
result[key] = obj[key];
}
}
}
return result;
}
代码语言:javascript复制Object.prototype.clone = function() {
var copy = (this instanceof Array) ? [] : {};
for (var attr in this) {
if (this.hasOwnProperty(attr)){
copy[attr] = typeof(this[attr])==='object' ? clone(this[attr]) : this[attr];}
}
return copy;
};
对象是 JS 中基本类型之一,而且和原型链、数组等知识息息相关。不管是面试中,还是实际开发中我们都会碰见深拷贝对象的问题。
顾名思义,深拷贝就是完完整整的将一个对象从内存中拷贝一份出来。所以无论用什么办法,必然绕不开开辟一块新的内存空间。
通常有下面两种方法实现深拷贝:
- 迭代递归法
- 序列化反序列化法
我们会基于一个测试用例对常用的实现方法进行测试并对比优劣:
代码语言:javascript复制let test = {
num: 0,
str: '',
boolean: true,
unf: undefined,
nul: null,
obj: {
name: '我是一个对象',
id: 1
},
arr: [0, 1, 2],
func: function() {
console.log('我是一个函数')
},
date: new Date(0),
reg: new RegExp('/我是一个正则/ig'),
err: new Error('我是一个错误')
}
let result = deepClone(test)
console.log(result)
for (let key in result) {
if (isObject(result[key]))
console.log(`${key}相同吗? `, result[key] === test[key])
}
// 判断是否为对象
function isObject(o) {
return (typeof o === 'object' || typeof o === 'function') && o !== null
}
迭代递归法
这是最常规的方法,思想很简单:就是对对象进行迭代操作,对它的每个值进行递归深拷贝。
for...in 法
代码语言:javascript复制// 迭代递归法:深拷贝对象与数组
function deepClone(obj) {
if (!isObject(obj)) {
throw new Error('obj 不是一个对象!')
}
let isArray = Array.isArray(obj)
let cloneObj = isArray ? [] : {}
for (let key in obj) {
cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
}
return cloneObj
}
结果:
迭代递归法结果.png
我们发现,arr 和 obj 都深拷贝成功了,它们的内存引用已经不同了,但 func、date、reg 和 err 并没有复制成功,因为它们有特殊的构造函数。
Reflect 法
代码语言:javascript复制// 代理法
function deepClone(obj) {
if (!isObject(obj)) {
throw new Error('obj 不是一个对象!')
}
let isArray = Array.isArray(obj)
let cloneObj = isArray ? [...obj] : { ...obj }
Reflect.ownKeys(cloneObj).forEach(key => {
cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
})
return cloneObj
}
结果:
代理法结果
我们发现,结果和使用 for...in 一样。那么它有什么优点呢?读者可以先猜一猜,答案我们会在下文揭晓。
lodash中的深拷贝实现
著名的 lodash 中的 cloneDeep 方法同样是使用这种方法实现的,只不过它支持的对象种类更多,具体的实现过程读者可以参考 lodash 的 baseClone 方法。
我们把测试用例用到的深拷贝函数换成lodash的:
代码语言:javascript复制let result = _.cloneDeep(test)
结果:
lodash深拷贝结果.png
我们发现,arr、obj、date、reg深拷贝成功了,但 func 和 err 内存引用仍然不变。
为什么不变呢?这个问题留给读者自己去探寻,嘿嘿~不过可以提示下,这跟 lodash 中的 cloneableTags 有关。
由于前端中的对象种类太多了,所以 lodash 也给用户准备了自定义深拷贝的方法 cloneDeepWith,比如自定义深拷贝 DOM 对象:
代码语言:javascript复制function customizer(value) {
if (_.isElement(value)) {
return value.cloneNode(true);
}
}
var el = _.cloneDeepWith(document.body, customizer);
console.log(el === document.body);
// => false
console.log(el.nodeName);
// => 'BODY'
console.log(el.childNodes.length);
// => 20
序列化反序列化法
这个方法非常有趣,它先把代码序列化成数据,再反序列化回对象:
代码语言:javascript复制// 序列化反序列化法
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj))
}
结果:
序列化反序列化法结果.png
我们发现,它也只能深拷贝对象和数组,对于其他种类的对象,会失真。这种方法比较适合平常开发中使用,因为通常不需要考虑对象和数组之外的类型。
进阶
- 对象成环怎么办? 我们给 test 加一个 loopObj 键,值指向自身:
test.loopObj = test
这时我们使用第一种方法中的 for..in 实现和 Reflect 实现都会栈溢出:
环对象深拷贝报错
而使用第二种方法也会报错:
但 lodash 却可以得到正确结果:
lodash 深拷贝环对象.png
为什么呢?我们去 lodash 源码看看:
lodash 应对环对象办法.png
因为 lodash 使用的是栈把对象存储起来了,如果有环对象,就会从栈里检测到,从而直接返回结果,悬崖勒马。这种算法思想来源于 HTML5 规范定义的结构化克隆算法,它同时也解释了为什么 lodash 不对 Error 和 Function 类型进行拷贝。
当然,设置一个哈希表存储已拷贝过的对象同样可以达到同样的目的:
代码语言:javascript复制function deepClone(obj, hash = new WeakMap()) {
if (!isObject(obj)) {
return obj
}
// 查表
if (hash.has(obj)) return hash.get(obj)
let isArray = Array.isArray(obj)
let cloneObj = isArray ? [] : {}
// 哈希表设值
hash.set(obj, cloneObj)
let result = Object.keys(obj).map(key => {
return {
[key]: deepClone(obj[key], hash)
}
})
return Object.assign(cloneObj, ...result)
}
这里我们使用 WeakMap 作为哈希表,因为它的键是弱引用的,而我们这个场景里键恰好是对象,需要弱引用。
- 键值不是字符串而是 Symbol
我们修改一下测试用例:
代码语言:javascript复制var test = {}
let sym = Symbol('我是一个Symbol')
test[sym] = 'symbol'
let result = deepClone(test)
console.log(result)
console.log(result[sym] === test[sym])
运行 for...in 实现的深拷贝我们会发现:
拷贝失败了,为什么?
因为 Symbol 是一种特殊的数据类型,它最大的特点便是独一无二,所以它的深拷贝就是浅拷贝。
但如果这时我们使用 Reflect 实现的版本:
成功了,因为 for...in 无法获得 Symbol 类型的键,而 Reflect 是可以获取的。
当然,我们改造一下 for...in 实现也可以:
代码语言:javascript复制function deepClone(obj) {
if (!isObject(obj)) {
throw new Error('obj 不是一个对象!')
}
let isArray = Array.isArray(obj)
let cloneObj = isArray ? [] : {}
let symKeys = Object.getOwnPropertySymbols(obj)
// console.log(symKey)
if (symKeys.length > 0) {
symKeys.forEach(symKey => {
cloneObj[symKey] = isObject(obj[symKey]) ? deepClone(obj[symKey]) : obj[symKey]
})
}
for (let key in obj) {
cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
}
return cloneObj
}
- 拷贝原型上的属性
众所周知,JS 对象是基于原型链设计的,所以当一个对象的属性查找不到时会沿着它的原型链向上查找,也就是一个非构造函数对象的 __proto__ 属性。
我们创建一个 childTest 变量,让 result 为它的深拷贝结果,其他不变:
代码语言:javascript复制let childTest = Object.create(test)
let result = deepClone(childTest)
这时,我们最初提供的四种实现只有 for...in 的实现能正确拷贝,为什么呢?原因还是在结构化克隆算法里:原形链上的属性也不会被追踪以及复制。
落在具体实现上就是:for...in 会追踪原型链上的属性,而其它三种方法(Object.keys、Reflect.ownKeys 和 JSON 方法)都不会追踪原型链上的属性:
- 需要拷贝不可枚举的属性 第四种情况,就是我们需要拷贝类似属性描述符,setters 以及 getters 这样不可枚举的属性,一般来说,这就需要一个额外的不可枚举的属性集合来存储它们。类似在第二种情况使用 for...in 拷贝 Symbol 类型键时: 我们给 test 变量里的 obj 和 arr 属性定义一下属性描述符:
Object.defineProperties(test, {
'obj': {
writable: false,
enumerable: false,
configurable: false
},
'arr': {
get() {
console.log('调用了get')
return [1,2,3]
},
set(val) {
console.log('调用了set')
}
}
})
然后实现我们的拷贝不可枚举属性的版本:
代码语言:javascript复制function deepClone(obj, hash = new WeakMap()) {
if (!isObject(obj)) {
return obj
}
// 查表,防止循环拷贝
if (hash.has(obj)) return hash.get(obj)
let isArray = Array.isArray(obj)
// 初始化拷贝对象
let cloneObj = isArray ? [] : {}
// 哈希表设值
hash.set(obj, cloneObj)
// 获取源对象所有属性描述符
let allDesc = Object.getOwnPropertyDescriptors(obj)
// 获取源对象所有的 Symbol 类型键
let symKeys = Object.getOwnPropertySymbols(obj)
// 拷贝 Symbol 类型键对应的属性
if (symKeys.length > 0) {
symKeys.forEach(symKey => {
cloneObj[symKey] = isObject(obj[symKey]) ? deepClone(obj[symKey], hash) : obj[symKey]
})
}
// 拷贝不可枚举属性,因为 allDesc 的 value 是浅拷贝,所以要放在前面
cloneObj = Object.create(
Object.getPrototypeOf(cloneObj),
allDesc
)
// 拷贝可枚举属性(包括原型链上的)
for (let key in obj) {
cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key], hash) : obj[key];
}
return cloneObj
}
结果:
结语
- 日常深拷贝,建议序列化反序列化方法。
- 面试时遇见面试官搞事情,写一个能拷贝自身可枚举、自身不可枚举、自身 Symbol 类型键、原型上可枚举、原型上不可枚举、原型上的 Symol 类型键,循环引用也可以拷的深拷贝函数:
// 将之前写的 deepClone 函数封装一下
function cloneDeep(obj) {
let family = {}
let parent = Object.getPrototypeOf(obj)
while (parent != null) {
family = completeAssign(deepClone(family), parent)
parent = Object.getPrototypeOf(parent)
}
// 下面这个函数会拷贝所有自有属性的属性描述符,来自于 MDN
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
function completeAssign(target, ...sources) {
sources.forEach(source => {
let descriptors = Object.keys(source).reduce((descriptors, key) => {
descriptors[key] = Object.getOwnPropertyDescriptor(source, key)
return descriptors
}, {})
// Object.assign 默认也会拷贝可枚举的Symbols
Object.getOwnPropertySymbols(source).forEach(sym => {
let descriptor = Object.getOwnPropertyDescriptor(source, sym)
if (descriptor.enumerable) {
descriptors[sym] = descriptor
}
})
Object.defineProperties(target, descriptors)
})
return target
}
return completeAssign(deepClone(obj), family)
}
- 有特殊需求的深拷贝,建议使用 lodash 的 copyDeep 或 copyDeepWith 方法。
更多资料
https://www.hangge.com/blog/cache/detail_1802.html
(1)我们定义一个 clone 方法实现深度复制功能(Deep Copy),其内部实现原理就是将对象、数组的属性遍历一遍,赋给一个新的对象。
代码语言:javascript复制//自定义的复制方法
function clone(obj) {
var copy = {};
for (var attr in obj) {
copy[attr] = typeof(obj[attr])==='object' ? clone(obj[attr]) : obj[attr];
}
return copy;
}
//测试样例
var a = {v1:1, v2:2};
var b = clone(a);
b.v1 = 3;
console.log("对象a:",a);
console.log("对象b:",b);
(2)也可以直接给 Object 增加个 clone 方法,其内部实现原来同上面是一样的。
代码语言:javascript复制//自定义的复制方法
Object.prototype.clone = function() {
var copy = (this instanceof Array) ? [] : {};
for (var attr in this) {
if (this.hasOwnProperty(attr)){
copy[attr] = typeof(this[attr])==='object' ? clone(this[attr]) : this[attr];
}
}
return copy;
};
//测试样例
var a = {v1:1, v2:2};
var b = a.clone();
b.v1 = 3;
console.log("对象a:",a);
console.log("对象b:",b);
代码语言:javascript复制简单阐述一下es6中新增的原始数据类型和其作用
symbol
symbol是基本数据类型,每一个symbol值都是一个全局唯一的字符串,你永远不会知道它里面存的什么,symbol值可以作为对象的属性的键。
Symbol的特点
- 独一无二
- 不能隐式转换
- 不能与其它数据类型做运算
- 不能使用点运算符进行操作
总结:
- window.Symbol()
- Symbol()前面不能加new
- Symbol() !== Symbol()
- object[Symbol()]='xxx' 可以作为对象的键,创造私有属性
symbol作用: 可以造一个隐藏属性 使用方法:在一个块级作用域里使用symbol
代码语言:javascript复制{
let a = Symbol()
let object = {
name: 'ooo',
age: 10,
[a]: '隐藏属性'
}
window.object = object
}
上面的代码我们可以访问到它里面的name和age但是我们不能访问到a,包括我们直接通过object[Symbol]也访问不到它
Set
set类型是Object里面的一种 Set对象里面只要有重复的值他都会只保留一个,无论是原始值还是对象引用。
作用:实现数组去重
- 不使用Set实现数组去重
var a = [1,2,3,1,5,6,2]
function uniq(array){
var result = []
var hash = {}
for(var i =0;i<array.length;i ){
hash[array[i]]=true
}
for(key in hash){
result.push(key)
}
return result
}
uniq(a)
//["1", "2", "3", "5", "6"]
问题: 1.他无法区分数字和字符串 2.如果数组里面有对象我们的去重方法就会失败,因为对象里面的下标(键)只能是字符串
- 使用Set实现数组去重
其中Array.from是将任意类型转化成一个数组
Map
可以允许任何类型作为对象的键,弥补了object只能使用字符串作为键的问题
- 通过for...of进行遍历 .keys()拿到所有的key
for(let key of map.keys()){
console.log(key)
}
.values()拿到所有的value
代码语言:javascript复制for(let key of map.values()){
console.log(key)
}
.entries()拿到所有的key和value
代码语言:javascript复制for(let key of map.entries()){
console.log(key)
}
weakSet和WeakMap是弱引用,放在它们的key里面的东西会自然消失,它无法知道自己有哪些值
更多资料
https://blog.csdn.net/m0_38134431/article/details/83897315
在任何时代,
教育说起来都是一件高大上的事,
但却没有什么真正有价值的东西是教得会的,
没有任何一种文化模因
可以说清楚一个个体的全部问题。
在任何时代,
想要抓住人性的弱点来赚钱都非常容易,
没有一点高级。
相反,想要建设一种文化,
耐心地拆除信息壁垒,
并且能够坚持下来,
那真不是一般的不易。
在任何时代,
在一秒钟内看到本质的人,
和花半辈子看不清的人,
自然是不一样的命运。
每一天,你将受到才哥的理论,结合历史、政治、文化、艺术、商业故事,令人防不胜防的高纯度无死角知识轰炸。以及,不间断的私藏书籍、电影推荐。
有时候,某件事虽记不清楚,但总感觉这样的事在很久很久以前发生过...
有时候,某个人虽从未见过,但总感觉面前的人在另一个时空里曾遇见......
那时候相忘于江湖的事,或许穿越了时光,有了新的世界;
那时候不远万里追寻的梦,也许穿行过人海,也有了新的意义;
而对于我们来说,那个惦念的江湖,那个执着的梦,其核心都是在偌大的世界里,寻找到更真实更好的自己!
一个人在比较了自己与别人的力量和弱点之后,如果仍然看不出差别的话, 那么他将很容易被敌人打败。人们常常抱怨,人生中有太多的敌人,包括在学习 中遇到的沟壑,事业上难爬的高山,生活中隐藏的陷阱,稍一不慎,就将前功尽 弃或跌入沟壑。可是,人们哪里知道,真正的敌人不在眼前,也不在背后,就在 自己面前。只有战胜各种艰难困苦,才能赢得成功。当然,要战胜艰难险阻,首 先要战胜自己,如果连自己都不能战胜,何谈战胜别人和各种险阻。
世界著名游泳健将佛洛伦丝·查德威克,在一次从卡得林那岛游向加利福尼 亚海湾时,已经在海水中浸泡了16小时的她,看见眼前大雾茫茫、一望无际的 海面时,顿时精疲力竭、浑身困乏,没有继续坚持下去,失去了一次创造世界纪 录的机会。后来,她才得知自己已经快要到达成功的彼岸了,阻碍她成功的不是 茫茫无际的大海,而是自己的疑惑、自我放弃,最终被自己的潜意识所打倒。之后过了两个多月,佛洛伦丝·查德威克又一次重游了加利福尼亚海湾,每一刻她 都不停的对自己说:“离彼岸越来越近了”,潜意识激发了她“我一定能到达彼 岸”的信号,经过不懈的努力和坚持,她终于实现目标,成功到达了彼岸。
我们无法避免在追求成功的道路中所遇到的荆棘与挫折,但是,当你将挫折当成痛苦去对待的时候,疲惫和痛苦将纠缠着你,失望和放弃将一直笼罩着你。如果我们把每天经历过的种种痛苦、烦恼仔细分析一下,会发现,这痛苦、这困 扰的来源很大一部分是战胜不了自己。很多时候,我们在困难和挫折面前,否定 了自己,以至于让自己打败了你。所以,很多人失败,通常是输给了自己。