大家好,我是忆白,本文整理了十六种前端面试常考的JavaScript手写题。如果你觉得还不错,可以给我点个赞~
1. 使用Promise封装一个函数,发送AJAX请求
- 封装一个函数 sendAJAX 发送 GET AJAX 请求
- 参数 URL
- 返回结果 Promise 对象
function sendAJAX(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.responseType = 'json'
xhr.open('GET', url);
xhr.send();
// 处理事件
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
// 判断成功
if (xhr.status >= 200 && xhr.status < 300) {
// 成功的结果
resolve(xhr.response);
} else {
reject(xhr.status);
}
}
}
})
}
// 功能测试
sendAJAX('https://api.apiopen.top/getJoke')
.then(value=>{
console.log(value);
}, reason=>{
console.warn(reason);
})
2. 函数节流throttle
function throttle(fn, delay = 100) {
let timer = null
return function () {
if (timer) {
return
}
timer = setTimeout(() => {
fn.apply(this, arguments)
timer = null
}, delay)
}
}
3. 函数防抖
function debounce(fn, delay = 500) {
// timer 是闭包中的
let timer = null
return function () {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(this, arguments)
timer = null
}, delay)
}
}
4. 手写一个JS函数,实现数组扁平化
4.1 Array Flatten拍平一层
- 写一个 JS 函数,实现数组扁平化,只减少一级嵌套
- 如输入[1, [2, [3]], 4],输出 [1, 2, [3], 4]
思路:
- 定义空数组 arr = []。遍历当前数组
- 如果 item 非数组,则累加到 arr
- 如果 item 是数组,则遍历之后累加到 arr
// 一、使用push 实现
function flatten1(arr) {
const res = [];
arr.forEach(item => {
// 如果是数组,就遍历并累加到 res 中
if (Array.isArray(item)) {
item.forEach(n => res.push(n));
} else { // 不是数组,直接push到res中
res.push(item);
}
})
return res;
}
// 二、使用concat实现,concat是一级扁平化,不改变原数组
function flatten2(arr) {
let res = [];
// 不管是不是数组,都直接concat连接。
arr.forEach(item => {
res = res.concat(item);
})
return res;
}
// 功能测试
const arr = [1, [2, [3], 4], 5];
console.log( flatten2(arr) );
4.2 Array Flatten 彻底拍平
- 写一个 JS 函数,实现数组扁平化,减少所有嵌套的层级
- 如输入[1,[2,[3]],4],输出[1,2,3,4]
- 思路:先实现一级扁平化,然后递归调用,直到全部扁平
// 2.1 使用push
function flattenDeep1(arr) {
const res = [];
arr.forEach(item => {
if (Array.isArray(item)) {
const flatItem = flattenDeep1(item); // 递归
flatItem.forEach(n => res.push(n));
} else {
res.push(item)
}
})
return res;
}
// 2.2 使用concat
function flattenDeep2(arr) {
let res = [];
arr.forEach(item => {
if (Array.isArray(item)) {
const flatItem = flattenDeep2(item);
res = res.concat(flatItem);
} else {
res = res.concat(item);
}
})
return res;
}
// 功能测试2
const arr = [1, [2, [3,['a', 'b'], 4], 5], 6];
console.log( flattenDeep1(arr) );
5. 手写一个getType函数,获取详细的数据类型
5.1 常见的类型判断
- typeof —— 只能判断值类型,其他就是 function 和 object
- instanceof —— 需要两个参数来判断,而不是获取类型
5.2 枚举各种类型
- 通过typeof 判断值类型和 function
- 其他引用类型通过instatnceof 来逐个识别
- 但是,这样可能会忽略某些类型,或者增加了新类型时,需要修改代码
5.3 实现
- 使用 Object.prototype.toString.call(x)
- 注意,不能直接调用 x.toString()
// Object.prototype.toString.call(x)返回 [object String]、[object Array]、[object Map]等
function getType(x) {
const originType = Object.prototype.toString.call(x);
// 取到空格的索引
const spaceIndex = originType.indexOf(' ');
// 取空格之后,直到倒数第二个,因为倒数第一个字符是 ']'
const type = originType.slice(spaceIndex + 1, -1);
return type.toLowerCase(); // 'string'
}
// 功能测试
console.log( getType(null) ); // null
console.log( getType(undefined) ); // undefined
console.log( getType(100) ); // number
console.log( getType('abc') ); // string
console.log( getType(true) ); // boolean
console.log( getType(Symbol()) ); // symbol
console.log( getType({}) ); // object
console.log( getType([]) ); // array
console.log( getType(() => {}) ); // function
console.log( getType(Promise.resolve()) ); // promise
console.log( getType(new Error()) ); // error
6. new 一个对象发生了什么?请手写代码表示
6.1 new 一个对象的过程
- class 是 function 的语法糖
- 创建一个空对象 obj,继承构造函数的原型(原型上定义了方法)
- 执行构造函数(把 obj 作为 this)
- 返回 obj
function myNew(constructor, ...args) {
// 1. 创建一个空对象,继承 constructor 的原型
const obj = Object.create(constructor.prototype)
// 2. 将 obj 作为 this,执行 constructor,传入参数
constructor.apply(obj, args)
// 3. 返回 obj
return obj
}
// 功能测试
function Foo(name, n) {
this.name = name
this.city = '北京'
this.n = n
}
Foo.prototype.getName = function() {
return this.name;
}
// const f = new Foo('小白', 100)
const f = myNew(Foo, '小白', 100)
console.log(f);
说明:
- {} 创建的空对象,原型指向 Object.prototype
Object.create(p)可以创建空对象,原型指向传入的参数p
7. 深度/广度优先遍历一个DOM树
7.1 公共访问节点的函数
function visitNode(n) {
if (n instanceof Comment) {
// 注释
console.log('Common node --- ', n.textContent);
}
if (n instanceof Text) {
// 文本
const t = n.textContent.trim()
// 去掉换行符
if (t) {
console.log('Text node --- ', t);
}
}
if (n instanceof HTMLElement) {
// element,打印标签名
console.log('Element node --- ', `<${n.tagName.toLowerCase()}>`);
}
}
7.2 深度优先遍历
// 深度优先遍历
function dfs(root) {
visitNode(root);
// root.childNodes(获取所有节点,包括元素、文本和注释) 和 root.children(只获取元素,不获取文本和注释) 不一样
const childNodes = root.childNodes
if (childNodes.length) {
childNodes.forEach(child => {
dfs(child) // 递归
})
}
}
// 功能测试
const box = document.getElementById('box')
if (box !== null) {
dfs(box);
}
7.3 广度优先遍历
// 广度优先遍历
function bfs(root) {
const queue = [];
// 根节点入队列
queue.push(root)
while(queue.length) {
const curNode = queue.shift(); // 出队
if (curNode == null) break;
visitNode(curNode);
// 访问一个节点时,把当前节点子节点都入队
const childNodes = curNode.childNodes;
if (childNodes.length) {
childNodes.forEach(child => queue.push(child))
}
}
}
// 功能测试
const box = document.getElementById('box')
if (box !== null) {
bfs(box)
}
注意: root.children和root.childNodes不同,前者只获取元素,后者除了获取元素还获取文本和注释。
7.4 深度优先遍历可以不用递归吗?
- 可以不用递归,用栈
- 因为递归本身就是栈
// 深度优先遍历2,栈实现
function dfs2(root) {
const stack = [];
// 根节点压栈
stack.push(root);
while(stack.length > 0) {
const curNode = stack.pop();
if (curNode == null) break;
visitNode(curNode);
// 当前出栈节点的子节点压栈
const childNodes = curNode.childNodes
if (childNodes.length > 0) {
// 深度优先遍历的子节点需要反顺序压栈,因为要先遍历第一个子节点,就必须最后一个入栈
Array.from(childNodes).reverse().forEach(child => stack.push(child))
}
}
}
8. 手写LazyMan,实现sleep机制
- 支持 sleep 和 eat 两个方法
- 支持链式调用
思路:
- 由于有 sleep 功能,函数不能直接在调用时触发
- 初始化一个列表(数组),把函数注册进去
- 由每个 item 触发 next 执行(遇到 sleep 则异步触发)
- 当所有函数注册好之后,在构造函数中异步开启next()调用第一个任务
class LazyMan {
constructor(name) {
this.name = name;
this.tasks = [];
// 等下面所有任务task都注册完之后,再触发next执行
setTimeout(() => {
this.next();
});
}
// next
next() {
const task = this.tasks.shift() // 取出任务列表的第一个任务
if (task) task();
}
eat(food) {
const task = () => {
console.log(`${this.name} eat ${food}`);
// 立刻执行下一个任务
this.next();
}
this.tasks.push(task);
return this // 链式调用
}
sleep(seconds) {
const task = () => {
console.log(`${this.name} 开始睡觉`);
setTimeout(() => {
console.log(`${this.name} 已经睡了 ${seconds}s,开始执行下一个任务`);
this.next(); // xx秒后执行下一个任务
}, seconds * 1000);
}
this.tasks.push(task);
return this; // 支持链式调用
}
}
// 功能测试
const me = new LazyMan('小白');
me.eat('苹果').eat('香蕉').sleep(2).eat('葡萄').eat('西瓜').sleep(2).eat('橘子');
9. 手写一个 curry 函数,把其他函数柯里化
比如add(a,b,c),柯里化之后可以 add(a)(b)(c)调用
9.1 分析
- curry 返回的是一个函数 fn
- 执行 fn,中间状态返回函数,如add(1)(2)(3) 中的 add(1) 或者 add(1)(2)
- 最后返回执行结果,如 add(1)(2)(3)
function curry(fn) {
const fnArgsLength = fn.length // 传入函数可以接收的参数长度
let args = []; // 存放接收的参数
function calc(...newArgs) {
// 积累参数
args = [...args, ...newArgs];
if (args.length < fnArgsLength) {
// 参数未全部接收,就继续返回calc函数
return calc
} else {
// 参数已经全部接收,就调用函数返回执行结果
return fn.apply(this, args.slice(0, fnArgsLength))
}
}
return calc();
}
// 功能测试
function add(a, b, c) {
console.log(this);
return a + b + c;
}
const curryAdd = curry(add);
const res = curryAdd(10)(20)(30); // 60
console.log(res);
10. instanceof 原理是什么,请用代码表示
10.1 原理
- 例如
f instanceof Foo - 顺着
f.__proto__向上查找(原型链) - 看能否找到 Foo.prototype
// instance 实例,origin 构造函数
function myInstanceof(instance, origin) {
if (instance == null) return false;
const type = typeof instance;
// 排除值类型,值类型 instanceof 都返回false
if (type !== 'object' && type !== 'function') {
return false;
}
let tempInstance = instance; // 防止修改 instance
while(tempInstance) {
if (tempInstance.__proto__ === origin.prototype) {
return true;
}
// 未匹配到,就顺着原型链继续往上找
tempInstance = tempInstance.__proto__;
}
return false;
}
// 功能测试
console.log(myInstanceof({}, Object)); // true
console.log(myInstanceof([], Object)); // true
console.log(myInstanceof([], Array)); // true
console.log(myInstanceof({}, Array)); // false
console.log(myInstanceof('ab', String)); // flase
11. 手写bind函数
11.1 bind的应用
- 返回一个新函数,但不执行
- 绑定 this 和部分参数
- 如果是箭头函数,无法改变this,只能改变参数
11.2 分析
- 返回新函数
- 绑定 this
- 同时绑定执行的参数(apply 或者 call)
// context保存传入需要指向的this,bindArgs保存剩余参数
Function.prototype.myBind = function(context, ...bindArgs) {
const self = this; // 因为调用时,fn.myBind,所以this指向当前函数本身
return function(...args) {
// 拼接参数,因为调用myBind时可能没有传全部参数,后续再调用可能还会传入args中
const newArgs = bindArgs.concat(args);
// 在返回的新函数中,执行原来的函数,并改变this为context
return self.apply(context, newArgs);
}
}
// 功能测试
function fn(a, b, c) {
console.log(this, a, b, c);
}
const fn1 = fn.myBind({x: 100}, 10);
fn1(20, 30)
12. 手写 call 和 apply
12.1 call 和 apply 应用
- bind 返回一个新函数(不执行),call 和 apply 会立即执行函数
- 绑定 this
- 传入执行参数
12.2 分析:如何在函数执行时绑定 this
- 如 const obj = {x: 100, fn() { this.x }}
- 执行 obj.fn(),此时 fn 内部的 this 就指向 obj
- 可借此来实现函数绑定 this
12.3 手写 call 代码
Function.prototype.myCall = function(context, ...args) {
// 经测试,真实call如果传入null或者undefind最终调用会指向全局对象,所以这里赋值globalThis
if (context == null) context = globalThis;
// 如果是值类型,会返回值类型的包装类型,如new String,new Number;
// 而new Object()会根据我们传入的值类型,自动判断
if (typeof context !== 'object') context = new Object(context)
// 为避免覆盖context的键,使用Symbol
const fnKey = Symbol();
context[fnKey] = this; // this是当前执行的函数,把该函数放入context中
// 此时fn是context的方法,自然调用时this就指向context
const res = context[fnKey](...args);
delete context[fnKey]; // 调用完删除context上的函数,防止污染
return res;
}
// 功能测试
function fn(a, b, c) {
console.log(this, a, b ,c);
}
fn.myCall({x: 100}, 10, 20, 30);
12.4 手写 apply 代码
apply和call只有传参不一样
// apply和call只有传参不一样
Function.prototype.myApply = function(context, args=[]) {
if (context == null) context = globalThis;
if (typeof context !== 'object') context = new Object(context)
const fnKey = Symbol();
context[fnKey] = this; // this是当前执行的函数,把该函数放入context中
const res = context[fnKey](...args);
delete context[fnKey]; // 调用完删除context上的函数,防止污染
return res;
}
// 功能测试
function fn(a, b, c) {
console.log(this, a, b ,c);
}
fn.myApply({x: 200}, [100, 200, 300]);
13. 手写 EventBus 自定义事件
- on 和 once 注册函数,存储起来
- emit 时找到对应的函数,执行
- off 找到对应的函数,从对象中删除
13.1 注意区分 on 和 once
- on 绑定的事件可以多次执行,除非被 off 卸载
- once 绑定的函数 emit 一次即删除,也可以未执行而被 off 卸载
- 数据结构上需要标识出 on 和 once
13.2 实现方式一,on和once统一存储
-
on和once绑定的事件,存储在同一个数组中,以对象形式{fn: fn,isOnce: true}
-
isOnce用于区分是不是once绑定的事件,如下
// 数组保存对象,对象fn保存对应事件监听的函数,isOnce标识是否是once绑定的 { 'key1': [ { fn: fn1, isOnce: false}, { fn: fn2, isOnce: false}, { fn: fn3, isOnce: true} ] 'key2': [] // 有序 'key3': [] }实现代码:
class EventBus {
constructor() {
this.events = {};
}
on(type, fn, isOnce = false) {
const events = this.events;
if (events[type] == null) {
events[type] = []; // 初始化 key 的 fn 数组
}
events[type].push({fn, isOnce});
}
once(type, fn) {
this.on(type, fn, true);
}
off(type, fn) {
// 如果没有传fn,解绑这个type事件所有函数
if (!fn) {
this.events[type] = [];
} else {
// 否则,解绑单个 fn
const fnList = this.events[type];
if (fnList) {
this.events[type] = fnList.filter(item => item.fn !== fn);
}
}
}
emit(type, ...args) {
const fnList = this.events[type];
if (fnList === null) return;
// 注意,使用filter遍历,因为可能要过滤once绑定的
this.events[type] = fnList.filter(item => {
const { fn, isOnce } = item;
fn(...args);
// 如果是once绑定的,只执行一次,就过滤掉
if (!isOnce) {
return true;
} else {
return false;
}
})
}
}
// 功能测试
const e = new EventBus()
function fn1(a, b) { console.log('fn1', a, b) }
function fn2(a, b) { console.log('fn2', a, b) }
function fn3(a, b) { console.log('fn3', a, b) }
e.on('key1', fn1)
e.on('key1', fn2)
e.once('key1', fn3)
e.on('xxxxxx', fn3)
e.emit('key1', 10, 20) // 触发 fn1 fn2 fn3
e.off('key1', fn1)
e.emit('key1', 100, 200) // 触发 fn2(因为fn1解绑了,fn3是once,只触发一次)
13.3 实现方式二,on和once分开存储
- on 和 once 绑定的事件分开存储
events -> { key1: [fn1, fn2], key2: [fn1, fn2] }onceEvents -> { key1: [fn1, fn2], key2: [fn1, fn2] }
class EventBus2 {
constructor() {
this.events = {};
this.onceEvents = {};
}
on(type, fn) {
const events = this.events
if (events[type] == null) events[type] = [];
events[type].push(fn);
}
once(type, fn) {
const onceEvents = this.onceEvents;
if (onceEvents[type] == null) onceEvents[type] = [];
onceEvents[type].push(fn);
}
off(type, fn) {
if (!fn) {
// 解绑所有事件
this.events[type] = [];
this.onceEvents[type] = [];
} else {
// 解绑单个事件
const fnList = this.events[type];
const onceFnList = this.onceEvents[type];
if (fnList) {
this.events[type] = fnList.filter(curFn => curFn !== fn)
}
if (onceFnList) {
this.onceEvents[type] = onceFnList.filter(curFn => curFn !== fn)
}
}
}
emit(type, ...args) {
const fnList = this.events[type];
const onceFnList = this.onceEvents[type];
if (fnList) {
fnList.forEach(f => f(...args));
}
if (onceFnList) {
onceFnList.forEach(f => f(...args));
// once 只执行一次就删除
this.onceEvents[type] = [];
}
}
}
// 功能测试
const e = new EventBus2()
function fn1(a, b) { console.log('fn1', a, b) }
function fn2(a, b) { console.log('fn2', a, b) }
function fn3(a, b) { console.log('fn3', a, b) }
e.on('key1', fn1)
e.on('key1', fn2)
e.once('key1', fn3)
e.on('xxxxxx', fn3)
e.emit('key1', 10, 20) // 触发 fn1 fn2 fn3
e.off('key1', fn1)
e.emit('key1', 100, 200) // 触发 fn2(因为fn1解绑了,fn3是once,只触发一次)
14. 用 JS 实现一个 LRU 缓存,分析数据结构特点,使用Map
14.1 什么是 LRU 缓存
- LRU - Least Recently Used 最近使用
- 如果内存优先,只缓存最近使用的,删除“沉水”数据
- 核心 API 两个:get set
- 和力扣 146题-LRU 缓存,基本相同
14.2 分析
- 用哈希表存储数据,这样 get set 才够快 O(1)时间复杂度
- 必须是有序的,常用数据放前面,“沉水”数据放在后面
- 哈希表 + 有序,就是 Map —— 其他都不行
class LRUCache {
constructor(length) {
if (length < 1) throw new Error('invalid length')
this.length = length;
this.data = new Map();
}
set(key, value) {
const data = this.data;
// 如果已经存在,就删除重新添加,就会到最前面
if (data.has(key)) {
data.delete(key);
}
data.set(key, value);
if (data.size > this.length) {
// 如果超出了容量,则删除 Map 最老的元素
const delKey = data.keys().next().value;
data.delete(delKey);
}
}
get(key) {
const data = this.data;
if (!this.data.has(key)) return null;
const value = data.get(key);
// 删掉重新添加,保证最近使用的是最新
data.delete(key);
data.set(key, value);
return value;
}
}
// 功能测试
// 假设越往右是最近使用过的,长度为2
const lruCache = new LRUCache(2)
lruCache.set(1, 1) // {1=1}
lruCache.set(2, 2) // {1=1, 2=2}
console.log(lruCache.get(1)) // 输出1 相当于复习了,所以1=1提前 {2=2, 1=1}
lruCache.set(3, 3) // {1=1, 3=3}
console.log(lruCache.get(2)) // 输出null
lruCache.set(4, 4) // {3=3, 4=4}
console.info(lruCache.get(1)) // 输出null
console.info(lruCache.get(3)) // 输出3 {4=4, 3=3}
console.info(lruCache.get(4)) // 输出4 {3=3, 4=4}
15. 不用Map如何实现 LRU 缓存?
15.1 LRU 使用 Map 是基于两个特点
-
哈希表(get set 速度快)
-
有序
-
可结合 Object + Array
// 执行 LRU.set('a', 1) LRU.set('b', 2) LRU.set('c', 3) 后的数据 const obj1 = { value: 1, key: 'a' } const obj2 = { value: 2, key: 'b' } const obj3 = { vaule: 3, key: 'c' } const data = [obj1, obj2, obj3] const map = { 'a': obj1, 'b': obj2, 'c': obj3 }
15.2 但是依然存在性能问题,Array 操作慢
- 移除“沉水”数据,用数组 shift 效率太低
- get set 时移动数据,用数组 splice 效率太低
15.3 把数组改为双向链表
- 可快速增加元素
- 可快速删除元素
- 可快速移动元素位置
15.4 代码(TypeScript)
interface IListNode {
value: any;
key: string; // 存储 key ,方便删除(否则删除时就需要遍历 this.data )
prev?: IListNode;
next?: IListNode;
}
export default class LRUCache {
private length: number;
private data: { [key: string]: IListNode } = {};
private dataLength: number = 0; // 保存对象中的数据长度
private listHead: IListNode | null = null; // 双向链表头部
private listTail: IListNode | null = null; // 双向链表尾部
constructor(length: number) {
if (length < 1) throw new Error("invalid length");
this.length = length;
}
// 移动到末尾(最新鲜的位置)
private moveToTail(curNode: IListNode) {
const tail = this.listTail;
// 如果当前要移动的节点已经在末尾,直接return就行
if (tail === curNode) return;
// -------------- 1. 让 prevNode nextNode 断绝与 curNode 的关系 --------------
const prevNode = curNode.prev;
const nextNode = curNode.next;
if (prevNode) {
if (nextNode) {
prevNode.next = nextNode;
} else {
delete prevNode.next;
}
}
if (nextNode) {
if (prevNode) {
nextNode.prev = prevNode;
} else {
delete nextNode.prev;
}
if (this.listHead === curNode) this.listHead = nextNode;
}
// -------------- 2. 让 curNode 断绝与 prevNode nextNode 的关系 --------------
delete curNode.prev;
delete curNode.next;
// -------------- 3. 在 list 末尾重新建立 curNode 的新关系 --------------
if (tail) {
tail.next = curNode;
curNode.prev = tail;
}
this.listTail = curNode;
}
// 尝试清理长度
private tryClean() {
// 积累的数据比限制数量大
while (this.dataLength > this.length) {
const head = this.listHead;
if (head == null) throw new Error("head is null");
const headNext = head.next;
if (headNext == null) throw new Error("headNext is null");
// 1. 断绝 head 和 next 的关系
delete headNext.prev;
delete head.next;
// 2. 重新赋值 listHead
this.listHead = headNext;
// 3. 清理 data ,重新计数
delete this.data[head.key];
this.dataLength = this.dataLength - 1;
}
}
get(key: string): any {
const data = this.data;
const curNode = data[key];
if (curNode == null) return null;
if (this.listTail === curNode) {
// 本身在末尾(最新鲜的位置),直接返回 value
return curNode.value;
}
// 否则,curNode 移动到末尾
this.moveToTail(curNode);
return curNode.value;
}
set(key: string, value: any) {
const data = this.data;
const curNode = data[key];
// 没有就新增数据
if (curNode == null) {
// 新增数据
const newNode: IListNode = { key, value };
// 新节点直接移动到末尾
this.moveToTail(newNode);
data[key] = newNode;
this.dataLength++;
// 如果添加的是第一个元素,就把链表的listHead初始化
if (this.dataLength === 1) this.listHead = newNode;
} else {
// 修改现有数据
curNode.value = value;
// 移动到末尾
this.moveToTail(curNode);
}
// 尝试清理长度
this.tryClean();
}
}
// 功能测试
const lruCache = new LRUCache(2);
lruCache.set("1", 1); // {1=1}
lruCache.set("2", 2); // {1=1, 2=2}
console.info(lruCache.get("1")); // 1 {2=2, 1=1}
lruCache.set("3", 3); // {1=1, 3=3}
console.info(lruCache.get("2")); // null
lruCache.set("4", 4); // {3=3, 4=4}
console.info(lruCache.get("1")); // null
console.info(lruCache.get("3")); // 3 {4=4, 3=3}
console.info(lruCache.get("4")); // 4 {3=3, 4=4}
16. 手写一个深拷贝,考虑Map、Set、循环引用
16.1 使用 JSON.stringify 和 parse
- 无法转换函数
- 无法转换 Map Set
- 无法转换循环引用
16.2 普通深拷贝
- 只考虑 Object Array
- 无法转换 Map Set 和循环引用
function deepClone(obj) {
if (typeof obj !== 'object' || obj == null) return obj
let result;
if (obj instanceof Array) {
result = [];
} else {
result = {};
}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = deepClone(obj[key]);
}
}
return result;
}
16.3 进阶深拷贝
- 考虑 Object、Array、Map、Set
- 考虑循环引用
- 使用 map 避免循环引用,保存原始属性到已经克隆的属性的映射,每次克隆前取一下这个属性的映射,如果能取到,说明已经克隆过,直接return
// 2. 进阶深拷贝 —— 考虑 Object、Array、Map、Set;考虑循环引用
// 这里map用于存储原始对象中的属性引用,和克隆对象已经克隆属性的映射
function deepClone(obj, map = new WeakMap()) {
if (typeof obj !== 'object' || obj == null) return obj
// 避免循环引用,如果map中有,obj的映射,说明这个属性已经克隆过,直接return
const objFromMap = map.get(obj)
if (objFromMap) return objFromMap
// 存储最终结果
let target = {}
map.set(obj, target)
// Map
if (obj instanceof Map) {
target = new Map();
obj.forEach((v, k) => {
// map的key和value都有可能是引用类型,继续递归深拷贝
const v1 = deepClone(v, map);
const k1 = deepClone(k, map);
target.set(k1, v1);
})
}
// Set
if (obj instanceof Set) {
target = new Set();
obj.forEach(v => {
const v1 = deepClone(v, map);
target.add(v1);
})
}
// Array
if (obj instanceof Array) {
target = obj.map(item => deepClone(item, map));
}
// Object
for (const key in obj) {
const val = obj[key];
const val1 = deepClone(val, map);
target[key] = val1;
}
return target;
}
// 功能测试
const a = {
set: new Set([10, 20, 30]),
map: new Map([['x', 10], ['y', 20]]),
info: {
city: '北京',
},
fn: () => { console.info(100) }
}
a.self = a;
console.info( deepClone(a) );