42. 能不能写一个完整的深拷贝?
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr);//[ 1, 2, 3 ]
slice浅拷贝
开头的例子不就说的这个嘛!
...展开运算符
let arr = [1, 2, 3];
let newArr = [...arr];//跟arr.slice()是一样的效果
- 能不能写一个完整的深拷贝?
简易版及问题
JSON.parse(JSON.stringify());
估计这个api能覆盖大多数的应用场景,没错,谈到深拷贝,我第一个想到的也是它。但是实际上,对于某些严格的场景来说,这个方法是有巨大的坑的。问题如下:
无法解决循环引用的问题。举个例子:
const a = {val:2};
a.target = a;
拷贝a会出现系统栈溢出,因为出现了无限递归的情况。
无法拷贝一写特殊的对象,诸如 RegExp, Date, Set, Map等。
无法拷贝函数(划重点)。
因此这个api先pass掉,我们重新写一个深拷贝,简易版如下:
for (let prop in target) { if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = deepClone(target[prop]);
} }
return cloneTarget;
} else { return target;
} }
现在,我们以刚刚发现的三个问题为导向,一步步来完善、优化我们的深拷贝代码。
解决循环引用
现在问题如下:
let obj = {val : 100};
obj.target = obj;
deepClone(obj);//报错: RangeError: Maximum call stack size exceeded
这就是循环引用。我们怎么来解决这个问题呢?
创建一个Map。记录下已经拷贝过的对象,如果说已经拷贝过,那直接返回它行了。
const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;
const deepClone = (target, map = new Map()) => { if(map.get(target))
return target;
if (isObject(target)) { map.set(target, true);
const cloneTarget = Array.isArray(target) ? []: {};
for (let prop in target) { if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = deepClone(target[prop],map);
} }
return cloneTarget;
} else { return target;
} }
现在来试一试:
console.log(newA)//{ val: 2, target: { val: 2, target: [Circular] } }
好像是没有问题了, 拷贝也完成了。但还是有一个潜在的坑, 就是map 上的 key 和 map 构成了强引用关系,这是相当危险的。我给你解释一下与之相对的弱引用的概念你就明白了:
在计算机程序设计中,弱引用与强引用相对,
是指不能确保其引用的对象不会被垃圾回收器回收的引用。一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。 --百度百科
大白话解释一下,被弱引用的对象可以在任何时候被回收,而对于强引用来说,只要这个强引用还在,那么对象无法被回收。拿上面的例子说,map 和 a一直是强引用的关系,在程序结束之前,a 所占的内
存空间一直不会被释放。
怎么解决这个问题?
很简单,让 map 的 key 和 map 构成弱引用即可。ES6给我们提供了这样的数据结构,它的名字叫 WeakMap,它是一种特殊的Map, 其中的键是弱引用的。其键必须是对象,而值可以是任意的。
稍微改造一下即可:
const deepClone = (target, map = new WeakMap()) => { //...
}
拷贝特殊对象
(1)可继续遍历
对于特殊的对象,我们使用以下方式来鉴别:
Object.prototype.toString.call(obj);
梳理一下对于可遍历对象会有什么结果:
["object Map"] ["object Set"]
["object Array"] ["object Object"]
["object Arguments"]
好,以这些不同的字符串为依据,我们就可以成功地鉴别这些对象。
const getType = Object.prototype.toString.call(obj);
const canTraverse = { '[object Map]': true,
'[object Set]': true, '[object Array]': true,
return target;
let type = getType(target);
let cloneTarget;
if(!canTraverse[type]) { // 处理不能遍历的对象
return;
}else { // 这波操作相当关键,可以保证对象的原型不丢失!
let ctor = target.prototype;
cloneTarget = new ctor();
}
if(map.get(target))
return target;
map.put(target, true);
if(type === mapTag) { //处理Map
target.forEach((item, key) => { cloneTarget.set(deepClone(key), deepClone(item));
})
}
if(type === setTag) { //处理Set
target.forEach(item => { target.add(deepClone(item));
})
}
// 处理数组和对象 for (let prop in target) {
if (target.hasOwnProperty(prop)) { cloneTarget[prop] = deepClone(target[prop]);
} }
return cloneTarget;
}
(2)不可遍历的对象
const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';
对于不可遍历的对象,不同的对象有不同的处理。
const handleFunc = (target) => { // 待会的重点部分
}
const handleNotTraverse = (target, tag) => { const Ctor = targe.constructor;
switch(tag) { case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(target);
case regexpTag:
return handleRegExp(target);
case funcTag:
return handleFunc(target);
default:
return new Ctor(target);
} }
拷贝函数
虽然函数也是对象,但是它过于特殊,我们单独把它拿出来拆解。
提到函数,在JS种有两种函数,一种是普通函数,另一种是箭头函数。每个普通函数都是 Function的实例,而箭头函数不是任何类的实例,每次调用都是不一样的引用。那我们只需要处理普通函数的情况,
箭头函数直接返回它本身就好了。
那么如何来区分两者呢?
答案是: 利用原型。箭头函数是不存在原型的。
代码如下:
const handleFunc = (func) => { // 箭头函数直接返回自身
if(!func.prototype) return func;
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
// 分别匹配函数参数和函数体 const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if(!body) return null;
if (param) { const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
小小的bug
如下所示:
const target = new Boolean(false);
const Ctor = target.constructor;
new Ctor(target); // 结果为 Boolean {true} 而不是 false。
对于这样一个bug,我们可以对 Boolean 拷贝做最简单的修改,调用valueOf: new target.constructor(target.valueOf())。
但实际上,这种写法是不推荐的。因为在ES6后不推荐使用【new 基本类型()】这样的语法,所以es6中的新类型 Symbol 是不能直接 new 的,只能通过 new Object(SymbelType)。
因此我们接下来统一一下:
const handleNotTraverse = (target, tag) => { const Ctor = targe.constructor;
switch(tag) { case boolTag:
return new Object(Boolean.prototype.valueOf.call(target));
case numberTag:
return new Object(Number.prototype.valueOf.call(target));
case stringTag:
return new Object(String.prototype.valueOf.call(target));
case errorTag:
case dateTag:
return new Ctor(target);
case regexpTag:
return handleRegExp(target);
case funcTag:
return handleFunc(target);
default:
return new Ctor(target);
} }
完整代码展示
完整版的深拷贝:
const getType = obj => Object.prototype.toString.call(obj);
const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;
const canTraverse = { '[object Map]': true,
const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';
const handleRegExp = (target) => { const { source, flags } = target;
return new target.constructor(source, flags);
}
const handleFunc = (func) => { // 箭头函数直接返回自身
if(!func.prototype) return func;
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
// 分别匹配函数参数和函数体 const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if(!body) return null;
if (param) { const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else { return new Function(body[0]);
} }
const handleNotTraverse = (target, tag) => { const Ctor = target.constructor;
switch(tag) { case boolTag:
return new Object(Boolean.prototype.valueOf.call(target));
case numberTag:
return new Object(Number.prototype.valueOf.call(target));
case stringTag:
return new Object(String.prototype.valueOf.call(target));
case symbolTag:
return new Object(Symbol.prototype.valueOf.call(target));
case errorTag:
case dateTag:
return new Ctor(target);
case regexpTag:
return handleRegExp(target);
case funcTag:
return handleFunc(target);
default:
全站分卷索引 · 147 卷
- 1HTML
- 22. canvas 相关
- 36. 说说 title 和 alt 属性
- 42. 浮动布局的优点?有什么缺点?清除浮动有哪些方式?
- 53. 使用display:inline-block会产生什么问题?解决方法?
- 64. 布局题:div垂直居中,左右10px,高度始终为宽度一半
- 75. 盒模型
- 87. CSS如何进行圣杯布局
- 98. CSS如何实现双飞翼布局?
- 109. 什么是BFC?
- 1112. 应用场景
- 1213. 画一个对话框
- 1314. 画一个平行四边形
- 1415. 用一个div画五角星
- 15JavaScript
- 163. null是对象吗?为什么?
- 178. 如何创建并使用BigInt?
- 189. typeof 是否能正确判断类型?
- 1910. instanceof能否判断基本数据类型?
- 2012. Object.is和===的区别?
- 2115. == 和 ===有什么区别?
- 2217. 如何让if(a == 1 && a == 2)条件成立?
- 2320. 闭包有哪些表现形式?
- 2421. 如何解决下面的循环输出问题?
- 2522. 原型对象和构造函数有何关系?
- 2623. 能不能描述一下原型链?
- 2725. 函数的arguments为什么不是数组?如何转化成数组?
- 2826. forEach中return有效果吗?如何中断forEach循环?
- 2928. JS中flat---数组扁平化
- 3029. 什么是高阶函数
- 3131. 能不能实现数组map方法 ?
- 3232. 能不能实现数组reduce方法 ?
- 3333. 能不能实现数组 push、pop 方法 ?
- 3434. 能不能实现数组filter方法 ?
- 3536. 能不能实现数组sort方法?
- 3637. 能不能模拟实现一个new的效果?
- 3739. 能不能实现一个 call/apply 函数?
- 3840. 谈谈你对JS中this的理解
- 3941. JS中浅拷贝的手段有哪些?
- 4042. 能不能写一个完整的深拷贝?
- 4143. 数据是如何存储的?
- 4244. V8 引擎如何进行垃圾内存的回收?
- 4345. 描述一下 V8 执行一段JS代码的过程?
- 4446. 宏任务(MacroTask)引入
- 4547. 微任务(MicroTask)引入
- 4649. 理解EventLoop:nodejs
- 4750. nodejs 和 浏览器关于eventLoop的主要区别
- 4853. JS异步编程有哪些方案?为什么会出现这些方案?
- 4954. 能不能简单实现一下 node 中回调函数的机制?
- 5055. Promise 凭借什么消灭了回调地狱?
- 5156. 为什么Promise要引入微任务?
- 5258. 实现Promise的 resolve、reject 和 finally
- 5359. 现Promise的 all 和 race
- 5460. 谈谈你对生成器以及协程的理解
- 5561. 如何让 Generator 的异步代码按顺序执行完毕?
- 5662. 解释一下async/await的运行机制。
- 5763. forEach 中用 await 会产生什么问题?怎么解决这个问题?
- 5864. 关于JS中一些重要的api实现
- 5965. 事件流向
- 602. HTTP有哪些请求方法?
- 615. 如何理解 HTTP 状态码?
- 626. 简要概括一下 HTTP 的特点?HTTP 有哪些缺点?
- 637. 对 Accept 系列字段了解多少?
- 648. 对于定长和不定长的数据,HTTP 是怎么传输的?
- 659. HTTP 如何处理大文件的传输?
- 6610. HTTP 中如何处理表单数据的提交?
- 6711. HTTP1.1 如何解决 HTTP 的队头阻塞问题?
- 6812. 对 Cookie 了解多少?
- 6913. 如何理解 HTTP 代理?
- 7014. 如何理解 HTTP 缓存及缓存代理?
- 7117. 客户端的缓存控制
- 7219. 传统 RSA 握手
- 7320. RSA 和 ECDHE 握手过程的区别
- 7422. HTTP/2 有哪些改进?
- 7523. HTTP/2 中的二进制帧是如何设计的?
- 76TCP协议
- 771. 能不能说一说 TCP 和 UDP 的区别?
- 783. 为什么是三次而不是两次、四次?
- 794. 三次握手过程中可以携带数据么?
- 806. 说说 TCP 四次挥手的过程
- 817. 为什么是四次挥手而不是三次?
- 829. 说说半连接队列和 SYN Flood 攻击的关系
- 8310. 如何应对 SYN Flood 攻击?
- 8412. 说说 TCP 快速打开的原理(TFO)
- 8513. 能不能说说TCP报文中时间戳的作用?
- 8614. TCP 的超时重传时间是如何计算的?
- 8715. 能不能说一说 TCP 的流量控制?
- 8816. 能不能说说 TCP 的拥塞控制?
- 8917. 能不能说说 Nagle 算法和延迟确认?
- 9018. 如何理解 TCP 的 keep-alive?
- 911. 能不能说一说浏览器缓存?
- 922. 能不能说一说浏览器的本地存储?各自优劣如何?
- 933. 说一说从输入URL到页面呈现发生了什么?(网络)
- 944. 说一说从输入URL到页面呈现发生了什么?(解析算法)
- 955. 说一说从输入URL到页面呈现发生了什么?(渲染过程)
- 966. 谈谈你对重绘和回流的理解
- 977. 能不能说一说XSS攻击?
- 988. 能不能说一说CSRF攻击?
- 999. HTTPS为什么让数据传输更安全?
- 10010. 能不能实现事件的防抖和节流?
- 10111. 能不能实现图片懒加载?
- 102Vue
- 1032. mvvm和mvc区别?它和其它框架(jquery)的区别是什么?哪些场景适合?
- 1045. 描述下 vue 从初始化页面--修改数据--刷新页面 UI 的过程?
- 1057. 虚拟 DOM 实现原理
- 10610. Vue 组件间通信有哪些方式?
- 10714. route 和 router 的区别是什么?
- 10816. Vue 的 nextTick 的原理是什么?
- 10919. vuex
- 1101. 简单的反转链表
- 1112. 区间反转
- 1123. 两个一组翻转链表
- 1134. K个一组翻转链表
- 1145. 如何检测链表形成环?
- 1156. 如何找到环的起点
- 1167. 合并两个有序链表
- 1178. 合并 K 个有序链表
- 1189. 判断回文链表
- 119栈和队列
- 1203. 普通的层次遍历
- 1214. 二叉树的锯齿形层次遍历
- 1225. 二叉树的右视图
- 1236. 完全平方数
- 1247. 单词接龙
- 1258. 优先队列
- 1269. 关于堆的说明
- 12711. 实现优先队列
- 12812. 前 K 个高频元素
- 12913. 合并 K 个排序链表
- 13014. 什么是双端队列?
- 13116. 栈实现队列
- 13217. 队列实现栈
- 133二叉树
- 1342. 中序遍历
- 1353. 后序遍历
- 1364. 最大深度
- 1375. 最小深度
- 1386. 对称二叉树
- 1397. 二叉树的最近公共祖先
- 1408. 二叉搜索树的最近公共祖先
- 1419. 二叉树的直径
- 14210. 二叉树的所有路径
- 14311. 二叉树的最大路径和
- 14412. 验证二叉搜索树
- 14513. 将有序数组转换为二叉搜索树
- 14614. 二叉树展开为链表
- 14715. 不同的二叉搜索树II