十六种前端常见手写题,你都掌握了吗?

302 阅读9分钟

大家好,我是忆白,本文整理了十六种前端面试常考的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) );