前端实习八股整理-手写场景代码(JavaScript)

一、手写计时器

1.用setTimeout实现倒计时

function countDown(seconds, callback) {
  let remaining = seconds;

  const timer = setTimeout(function tick() {
    if (remaining <= 0) {
      callback && callback();
      clearTimeout(timer);
    } else {
      console.log(`剩余时间: ${remaining}`);
      remaining--;
      setTimeout(tick, 1000); // 1秒后再次执行
    }
  }, 1000);
}

// 使用示例
countDown(5, () => console.log("倒计时结束!"));

2.用setInterval实现正计时

function startTimer(duration, callback) {
  let elapsed = 0;
  const timer = setInterval(() => {
    elapsed++;
    console.log(`已过去: ${elapsed}`);
    if (elapsed >= duration) {
      clearInterval(timer);
      callback && callback();
    }
  }, 1000);
}

// 使用示例
startTimer(5, () => console.log("计时结束!"));

3.基于performance.now实现更精确的计时器
setTimeout 和 setInterval 受浏览器事件循环影响,可能有延迟。使用 performance.now() 可以获取更高精度的计时:

function preciseTimer(duration, callback) {
  const startTime = performance.now();

  function checkTime() {
    const elapsed = (performance.now() - startTime) / 1000; // 转为秒
    if (elapsed >= duration) {
      callback && callback();
    } else {
      console.log(`精确计时: ${elapsed.toFixed(2)}`);
      requestAnimationFrame(checkTime); // 使用 requestAnimationFrame 优化
    }
  }

  checkTime();
}

// 使用示例
preciseTimer(5, () => console.log("精确计时结束!"));

requestAnimationFrame() 是浏览器提供的一个专为动画优化设计的 API,它会在浏览器下一次重绘(repaint)之前执行指定的回调函数,通常以 60 FPS(帧/秒) 的速率运行(即每帧约 16.7ms)。它的核心目标是实现更流畅、更高效的动画渲染,同时减少不必要的性能开销。

二、手撕解析URL为JS对象

function parseURLParams(url) {
  // 创建一个正则表达式来匹配查询字符串
  const regex = /[?&]([^=#]+)=([^&#]*)/g;
  const params = {};
  let match;

  // 使用正则表达式循环匹配查询字符串
  while ((match = regex.exec(url))) {
    const key = decodeURIComponent(match[1]);
    const value = decodeURIComponent(match[2]);

    // 如果参数名已经存在,将值存储为数组
    if (params[key]) {
      if (!Array.isArray(params[key])) {
        params[key] = [params[key]];
      }
      params[key].push(value);
    } else {
      params[key] = value;
    }
  }

  return params;
}

const url = "https://example.com?name=John&name=Jane&age=30";
const params = parseURLParams(url);
console.log(params); // { name: ["John", "Jane"], age: "30" }

三、手撕getType函数(获取详细的变量类型)

function getType(data) {
  // 获取到 "[object Type]",其中 Type 是 Null、Undefined、Array、Function、Error、Boolean、Number、String、Date、RegExp 等。
  const originType = Object.prototype.toString.call(data)
  // 可以直接截取第8位和倒数第一位,这样就获得了 Null、Undefined、Array、Function、Error、Boolean、Number、String、Date、RegExp 等
  const type = originType.slice(8, -1)
  // 再转小写,得到 null、undefined、array、function 等
  return type.toLowerCase()
}

四、手撕call和apply

Function.prototype.call2 = function (context) {
  var context = context || window
  context.fn = this

  var args = []
  for (var i = 1, len = arguments.length; i < len; i++) {
    args.push('arguments[' + i + ']')
  }

  var result = eval('context.fn(' + args + ')')

  delete context.fn
  return result
}

Function.prototype.apply = function (context, arr) {
  var context = Object(context) || window
  context.fn = this

  var result
  if (!arr) {
    result = context.fn()
  } else {
    var args = []
    for (var i = 0, len = arr.length; i < len; i++) {
      args.push('arr[' + i + ']')
    }
    result = eval('context.fn(' + args + ')')
  }

  delete context.fn
  return result
}

五、手写红绿灯

模拟一个红绿灯变化,红灯 1 秒,绿灯 1 秒,黄灯 1 秒,然后循环

function red() {
  console.log('red')
}

function green() {
  console.log('green')
}

function yellow() {
  console.log('yellow')
}

function light(cb, wait) {
  return new Promise((resolve) => {
    setTimeout(() => {
      cb()
      resolve()
    }, wait)
  })
}

function start() {
  return Promise.resolve()
    .then(() => {
      return light(red, 1000)
    })
    .then(() => {
      return light(green, 1000)
    })
    .then(() => {
      return light(yellow, 1000)
    })
    .finally(() => {
      return start()
    })
}

start()

六、手撕LRU缓存

LRU(Least Recently Used)是一种缓存淘汰策略,它会优先删除最近最少使用的数据。下面提供两种实现方式:使用 Map 的简单实现和不使用 Map 的基础实现。

1.使用 Map 的实现

class LRUCache {
  constructor(capacity) {
    this.cache = new Map()
    this.capacity = capacity
  }

  get(key) {
    if (!this.cache.has(key)) return -1

    // 将访问的元素移到最新使用的位置
    const value = this.cache.get(key)
    this.cache.delete(key)
    this.cache.set(key, value)
    return value
  }

  put(key, value) {
    // 如果 key 已存在,先删除
    if (this.cache.has(key)) {
      this.cache.delete(key)
    }
    // 如果达到容量限制,删除最久未使用的元素
    else if (this.cache.size >= this.capacity) {
      // Map 的 keys() 会按插入顺序返回键
      const firstKey = this.cache.keys().next().value
      this.cache.delete(firstKey)
    }

    this.cache.set(key, value)
  }
}

// 使用示例
const cache = new LRUCache(2)
cache.put(1, 1) // 缓存是 {1=1}
cache.put(2, 2) // 缓存是 {1=1, 2=2}
console.log(cache.get(1)) // 返回 1
cache.put(3, 3) // 删除 key 2,缓存是 {1=1, 3=3}
console.log(cache.get(2)) // 返回 -1 (未找到)

2.使用双向链表的实现(不依赖 Map)

// 双向链表节点
class Node {
  constructor(key, value) {
    this.key = key
    this.value = value
    this.prev = null
    this.next = null
  }
}

class LRUCache {
  constructor(capacity) {
    this.capacity = capacity
    this.cache = {} // 哈希表用于O(1)查找
    this.count = 0
    // 创建头尾哨兵节点
    this.head = new Node(0, 0)
    this.tail = new Node(0, 0)
    this.head.next = this.tail
    this.tail.prev = this.head
  }

  // 将节点移到双向链表头部
  moveToHead(node) {
    this.removeNode(node)
    this.addToHead(node)
  }

  // 从链表中删除节点
  removeNode(node) {
    node.prev.next = node.next
    node.next.prev = node.prev
  }

  // 在链表头部添加节点
  addToHead(node) {
    node.prev = this.head
    node.next = this.head.next
    this.head.next.prev = node
    this.head.next = node
  }

  // 删除链表尾部节点
  removeTail() {
    const node = this.tail.prev
    this.removeNode(node)
    return node
  }

  get(key) {
    if (key in this.cache) {
      const node = this.cache[key]
      this.moveToHead(node)
      return node.value
    }
    return -1
  }

  put(key, value) {
    if (key in this.cache) {
      // 如果 key 存在,更新值并移到头部
      const node = this.cache[key]
      node.value = value
      this.moveToHead(node)
    } else {
      // 创建新节点
      const newNode = new Node(key, value)
      this.cache[key] = newNode
      this.addToHead(newNode)
      this.count++

      // 如果超过容量,删除最久未使用的
      if (this.count > this.capacity) {
        const tail = this.removeTail()
        delete this.cache[tail.key]
        this.count--
      }
    }
  }
}

// 使用示例
const cache = new LRUCache(2)
cache.put(1, 1)
cache.put(2, 2)
console.log(cache.get(1)) // 返回 1
cache.put(3, 3) // 删除 key 2
console.log(cache.get(2)) // 返回 -1 (未找到)
cache.put(4, 4) // 删除 key 1
console.log(cache.get(1)) // 返回 -1 (未找到)
console.log(cache.get(3)) // 返回 3
console.log(cache.get(4)) // 返回 4

实现原理说明:
1.Map 实现版本:
·利用 Map 的特性,它能够记住键的原始插入顺序
·get 操作时将访问的元素移到最后(最新使用)
·put 操作时如果超出容量,删除第一个元素(最久未使用)
2.双向链表实现版本:
·使用哈希表实现 O(1) 的查找
·使用双向链表维护数据的使用顺序
·最近使用的数据放在链表头部
·最久未使用的数据在链表尾部
性能分析:
1.时间复杂度:
·get 操作:O(1)
·put 操作:O(1)
2.空间复杂度:
·O(capacity),其中 capacity 是缓存的容量

七、手写 VNode 对象,表示如下 DOM 节点

<div class="container">
  <img src="x1.png" />
  <p>hello</p>
</div>
const vnode = {
  tag: 'div',
  props: {
    class: 'container',
  },
  children: [
    {
      tag: 'img',
      props: {
        src: 'x1.png',
      },
    },
    {
      tag: 'p',
      props: {},
      children: ['hello'],
    },
  ],
}

八、手写 compose 函数

compose 函数是函数式编程中的一个重要概念,它将多个函数组合成一个函数,从右到左执行。
1.基础实现(使用 reduce)

function compose(...fns) {
  if (fns.length === 0) return (arg) => arg
  if (fns.length === 1) return fns[0]

  return fns.reduce(
    (a, b) =>
      (...args) =>
        a(b(...args))
  )
}

// 使用示例
const add1 = (x) => x + 1
const multiply2 = (x) => x * 2
const addThenMultiply = compose(multiply2, add1)
console.log(addThenMultiply(5)) // (5 + 1) * 2 = 12

九、手写curry函数,实现函数柯里化


function sum(...initialArgs) {
  let total = initialArgs.reduce((acc, val) => acc + val, 0);
  
  const curried = function(...args) {
    if (args.length === 0) {
      return curried;
    }
    total += args.reduce((acc, val) => acc + val, 0);
    return curried;
  };
  
  curried.value = function() {
    return total;
  };
  
  return curried;
}

// 测试用例
console.log(sum(1, 2)(3, 4).value()); // 输出: 10
console.log(sum(1)(2)(3)(4).value());  // 输出: 10
console.log(sum(1, 2, 3)(4).value());  // 输出: 10
console.log(sum(1)(2, 3, 4).value());  // 输出: 10

十、手写Promise

class MyPromise {
  // 构造方法
  constructor(executor) {
    // 初始化值
    this.initValue()
    // 初始化this指向
    this.initBind()
    // 执行传进来的函数
    executor(this.resolve, this.reject)
  }

  initBind() {
    // 初始化this
    this.resolve = this.resolve.bind(this)
    this.reject = this.reject.bind(this)
  }

  initValue() {
    // 初始化值
    this.PromiseResult = null // 终值
    this.PromiseState = 'pending' // 状态
  }

  resolve(value) {
    // 如果执行resolve,状态变为fulfilled
    this.PromiseState = 'fulfilled'
    // 终值为传进来的值
    this.PromiseResult = value
  }

  reject(reason) {
    // 如果执行reject,状态变为rejected
    this.PromiseState = 'rejected'
    // 终值为传进来的reason
    this.PromiseResult = reason
  }
}

十一、手写Promise.all

static all(promises) {
  const result = []
  let count = 0
  return new MyPromise((resolve, reject) => {
    const addData = (index, value) => {
        result[index] = value
        count++
        if (count === promises.length) resolve(result)
    }
    promises.forEach((promise, index) => {
        if (promise instanceof MyPromise) {
            promise.then(res => {
                addData(index, res)
            }, err => reject(err))
        } else {
            addData(index, promise)
        }
    })
  })
}

十二、手写Promise.race

static race(promises) {
  return new MyPromise((resolve, reject) => {
    promises.forEach(promise => {
      if (promise instanceof MyPromise) {
          promise.then(res => {
              resolve(res)
          }, err => {
              reject(err)
          })
      } else {
          resolve(promise)
      }
    })
  })
}

十三、手写Promise.allSettled

static allSettled(promises) {
  return new Promise((resolve, reject) => {
    const res = []
    let count = 0
    const addData = (status, value, i) => {
      res[i] = {
          status,
          value
      }
      count++
      if (count === promises.length) {
          resolve(res)
      }
    }
    promises.forEach((promise, i) => {
      if (promise instanceof MyPromise) {
        promise.then(res => {
          addData('fulfilled', res, i)
        }, err => {
          addData('rejected', err, i)
        })
      } else {
        addData('fulfilled', promise, i)
      }
    })
  })
}

十四、手写防抖Debounce

function debounce(func,delay){
    let debounceTimer;
    return function(){
        const context=this;
        const args=arguments;
        clearTimeout(debounceTimer);
        debounceTimer = setTimeout(()=>{
            func.apply(context,args);
        },delay)
    }
}
windows.addEventListener('resize',debounce(()=>{
    console.log('resizing...')
},500))

十五、手写节流Throttle

function throttle(func,limit){
    let inThrottle;
    return function(){
        const context = this;
        const args = arguments;
        if(!inThrottle){
            func.apply(context,args);
            inThrottle=True;
            setTimeout(()=>{
                inThrottle=false;
            },limit)
        }
    }
}
window.addEventListener('scroll',throttle(()=>{
    console.log('Scrolling')
},1000))

十六、输入一个字符串,将字符串中的-_字符后面的字母变成大写字母

function capitalizeAfterHyphenOrUnderscore(str) {
    return str.replace(/[-_](\w)/g, (match, char) => char.toUpperCase());
}

// 测试用例
console.log(capitalizeAfterHyphenOrUnderscore("hello-world")); // 输出: "helloWorld"
console.log(capitalizeAfterHyphenOrUnderscore("foo_bar_baz")); // 输出: "fooBarBaz"
console.log(capitalizeAfterHyphenOrUnderscore("a-b-c-d"));     // 输出: "aBCD"

十七、手写new操作符

在 JavaScript 中,new 操作符用于创建一个用户定义的对象类型的实例或具有构造函数的内置对象类型的实例。要实现一个类似 new 操作符的功能,可以通过以下步骤来模拟:
1.创建一个空对象:这个空对象将作为新对象的实例。
2.设置原型:将新对象的原型指向构造函数的 prototype。
3.执行构造函数:将构造函数的 this 指向新创建的对象,并执行构造函数。
4.返回新对象:返回新创建的对象。
以下是实现 new 操作符的代码:

function myNew(constructor, ...args) {
    // 创建一个空对象,并将其原型指向构造函数的 prototype
    const obj = Object.create(constructor.prototype);

    // 将构造函数的 this 指向新创建的对象,并执行构造函数
    const result = constructor.apply(obj, args);

    // 如果构造函数返回一个对象,则返回该对象;否则返回新创建的对象
    return result instanceof Object ? result : obj;
}

// 测试用例
function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.sayHello = function () {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};

const person1 = myNew(Person, "Alice", 25);
console.log(person1); // 输出: Person { name: 'Alice', age: 25 }
person1.sayHello(); // 输出: Hello, my name is Alice and I am 25 years old.

const person2 = myNew(Person, "Bob", 30);
console.log(person2); // 输出: Person { name: 'Bob', age: 30 }
person2.sayHello(); // 输出: Hello, my name is Bob and I am 30 years old.

十八、对版本号数组进行排序,比如[0.1.2.3, 1.2.1.0, 4.2.1.0, 0.1.2.0]

function compareVersions(a, b) {
  // 将版本号按点号分割成数组
  var aParts = a.split('.');
  var bParts = b.split('.');

  // 比较每个部分
  for (var i = 0; i < Math.max(aParts.length, bParts.length); i++) {
    // 如果一个版本号较短,我们可以认为缺失的部分为0
    var aPart = aParts[i] || 0;
    var bPart = bParts[i] || 0;

    // 将部分转换为数字并进行比较
    var aNum = parseInt(aPart, 10);
    var bNum = parseInt(bPart, 10);

    if (aNum > bNum) {
      return 1;
    } else if (aNum < bNum) {
      return -1;
    }
  }

  // 如果所有部分都相等,则版本号相等
  return 0;
}

// 使用比较函数对版本号数组进行排序
var versions = ['0.1.2.3', '1.2.1.0', '4.2.1.0', '0.1.2.0'];
versions.sort(compareVersions);

console.log(versions); // 输出排序后的数组

十九、手写 call 和 apply

Function.prototype.call2 = function (context) {
  var context = context || window
  context.fn = this

  var args = []
  for (var i = 1, len = arguments.length; i < len; i++) {
    args.push('arguments[' + i + ']')
  }

  var result = eval('context.fn(' + args + ')')

  delete context.fn
  return result
}

Function.prototype.apply = function (context, arr) {
  var context = Object(context) || window
  context.fn = this

  var result
  if (!arr) {
    result = context.fn()
  } else {
    var args = []
    for (var i = 0, len = arr.length; i < len; i++) {
      args.push('arr[' + i + ']')
    }
    result = eval('context.fn(' + args + ')')
  }

  delete context.fn
  return result
}

二十、手写 EventBus 自定义事件

class EventBus {
  constructor() {
    this.eventObj = {}
    this.callbcakId = 0
  }

  $on(name, callbcak) {
    if (!this.eventObj[name]) {
      this.eventObj[name] = {}
    }
    const id = this.callbcakId++
    this.eventObj[name][id] = callbcak
    return id
  }
  $emit(name, ...args) {
    const eventList = this.eventObj[name]
    for (const id in eventList) {
      eventList[id](...args)
      if (id.indexOf('D') !== -1) {
        delete eventList[id]
      }
    }
  }
  $off(name, id) {
    delete this.eventObj[name][id]
    if (!Object.keys(this.eventObj[name]).length) {
      delete this.eventObj[name]
    }
  }
  $once(name, callbcak) {
    if (!this.eventObj[name]) {
      this.eventObj[name] = {}
    }
    const id = 'D' + this.callbcakId++
    this.eventObj[name][id] = callbcak
    return id
  }
}

二十一、怎么实现将一个图标拖拽到另一个地方?

实现图标拖拽功能可以通过 HTML5 的拖放 API(Drag and Drop API)来完成。以下是一个简单的示例,展示如何将一个图标拖拽到另一个地方。
示例代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>拖拽示例</title>
  <style>
    .draggable {
      width: 50px;
      height: 50px;
      background-color: blue;
      color: white;
      text-align: center;
      line-height: 50px;
      cursor: pointer;
      margin: 10px;
    }
    .dropzone {
      width: 200px;
      height: 200px;
      background-color: lightgray;
      margin: 10px;
      padding: 10px;
      border: 2px dashed black;
    }
  </style>
</head>
<body>
  <div class="draggable" draggable="true" id="draggable">拖拽我</div>
  <div class="dropzone" id="dropzone">拖到这里</div>

  <script src="script.js"></script>
</body>
</html>

script.js

document.addEventListener("DOMContentLoaded", () => {
  const draggable = document.getElementById("draggable");
  const dropzone = document.getElementById("dropzone");

  // 拖拽开始时触发
  draggable.addEventListener("dragstart", (event) => {
    event.dataTransfer.setData("text/plain", event.target.id); // 传递被拖拽元素的 ID
  });

  // 拖拽进入目标区域时触发
  dropzone.addEventListener("dragover", (event) => {
    event.preventDefault(); // 阻止默认行为,允许放置
  });

  // 拖拽离开目标区域时触发
  dropzone.addEventListener("dragleave", (event) => {
    console.log("离开目标区域");
  });

  // 拖拽结束时触发
  dropzone.addEventListener("drop", (event) => {
    event.preventDefault(); // 阻止默认行为
    const draggableId = event.dataTransfer.getData("text/plain"); // 获取被拖拽元素的 ID
    const draggableElement = document.getElementById(draggableId); // 获取被拖拽的元素
    dropzone.appendChild(draggableElement); // 将被拖拽的元素移动到目标区域
  });
});

二十二、手写实现sleep

用setTimeout

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// 使用示例
async function example() {
  console.log("Hello");
  await sleep(2000); // 等待2秒
  console.log("World");
}

example();

二十三、数组转树

const arr = [
  { id: "01", name: "张大大", pid: "", job: "项目经理" },
  { id: "02", name: "小亮", pid: "01", job: "产品leader" },
  { id: "03", name: "小美", pid: "01", job: "UIleader" },
  { id: "04", name: "老马", pid: "01", job: "技术leader" },
  { id: "05", name: "老王", pid: "01", job: "测试leader" },
  { id: "06", name: "老李", pid: "01", job: "运维leader" },
  { id: "07", name: "小丽", pid: "02", job: "产品经理" },
  { id: "08", name: "大光", pid: "02", job: "产品经理" },
  { id: "09", name: "小高", pid: "03", job: "UI设计师" },
  { id: "10", name: "小刘", pid: "04", job: "前端工程师" },
  { id: "11", name: "小华", pid: "04", job: "后端工程师" },
  { id: "12", name: "小李", pid: "04", job: "后端工程师" },
  { id: "13", name: "小赵", pid: "05", job: "测试工程师" },
  { id: "14", name: "小强", pid: "05", job: "测试工程师" },
  { id: "15", name: "小涛", pid: "06", job: "运维工程师" },
];

// 数组转树

function toTree(list, parId){
    let len=list.length;
    function loop(parId){
        let res=[];
        for(let i=0;i<len;i++){
            let item=list[i];
            if(item.pid==parId){
                item.children=loop(item.id);
                res.push(item);
            }
        }
        return res;
    }
    return loop(parId);
}

let result=toTree(arr,"");
console.log(result)


//hash
function toTreeHash(list){
    const res = [];
    const map = {};
    // 1. 先遍历一遍数组,把每个元素的 id 映射为节点对象
    for(const item of list){
        map[item.id]={...item,children:[]};
    }
    // 2. 再遍历数组,将节点放入其父节点的 children 中
    for(const item of list){
        const node = map[item.id];
        if(item.pid){
            if(!map[item.pid])continue; // 防御性处理,避免找不到父节点报错
            map[item.pid].children.push(node);
        }else{
            res.push(node);// 没有 pid 的就是根节点
        }
    }
    return res;
}

const tree = toTreeHash(arr);
console.log(tree);

二十四、数组API实现

Array.prototype.map = function(fn) {
  const res = []
  for(let i = 0; i < this.length; i++) {
    res.push(fn(this[i], i, this)) 
  }
  return res
}

Array.prototype.filter = function(fn) {
  const res = []
  for(let i = 0; i < this.length; i++) {
    if(fn(this[i], i, this)) {
      res.push(this[i])
    }
  }
  return res
}

Array.prototype.reduce = function(fn, initValue) {
  let res, start = 0
  if(arguments.length !== 1) {
    res = initValue
  } else {
    res = this[0]
    start = 1
  }
  for(let i = start; i < this.length; i++) {
    res = fn(res, this[i], i, this)
  }
  return res
}

二十五、发布订阅模式

class EventEmitter {
  constructor() {
    this.arrayList = {}
  }
  on(name, fn) {
    if(this.arrayList[name] && !this.arrayList[name].include(fn)) {
      this.arrayList[name].push(fn)
    } else {
      this.arrayList[name] = [fn]
    }
  }
  off(name, fn) {
    if(this.arrayList[name]) {
      let idx = this.arrayList.indexOf(name)
      this.arrayList[name].splice(idx, 1)
    }
  }
  emit(name, ...args) {
    if(this.arrayList[name]) {
      let task = [...arrayList[name]]
      for(const fn of task) {
        fn.call(this, ...args)
      }
    }
  }
}

二十六、url解析

const parseUrl = (url) => {
  const tmpUrl = url.split("?")[1]
  const resObj = {}
  for(const str of tmpUrl.split("&")) {
    let [key, value] = str.split("=")
    value = decodeURIComponent(value)
    if(resObj.hasOwnProperty(key)) {
      resObj[key] = [].concat(resObj[key], value)
    } else if(value == "undefined") { // !!!
      resObj[key] = true
    } else {
      resObj[key] = value 
    }
  }
  return resObj
}

二十七、对象比较


function isEqual (obj1, obj2) {
  //不是对象,直接返回比较结果
  if (typeof obj1 !== 'object' || typeof obj2 !== 'object') {
    return obj1 === obj2
  }
  //都是对象,且地址相同,返回true
  if (obj2 === obj1) return true;
  //是对象或数组
  let keys1 = Object.keys(obj1)
  let keys2 = Object.keys(obj2)
  //比较keys的个数,若不同,肯定不相等
  if (keys1.length !== keys2.length) return false;
  for (let k of keys1) {
    //递归比较键值对
    if (!isEqual(obj1[k], obj2[k])) {
      return false
    }
  }
  return true;
}

const obj1 = {
  a: 100,
  b: {
    x: 100,
    y: 200
  }
}
const obj2 = {
  a: 200,
  b: {
    x: 100,
    y: 200
  }
}
console.log(isEqual(obj1, obj2)) //false

二十八、实现一个类,可以监听属性值的变化

实现思路
1.核心机制:使用 Proxy 或 Object.defineProperty。这里我使用功能更强大的 Proxy,因为它能更好地拦截数组操作和动态新增的属性。
2.递归代理:在 get 拦截中,如果获取的属性值是对象(包括数组),则递归地将其也转换为可监听的 Observer 实例,确保嵌套属性的变化也能被捕获。
3.发布-订阅模式:每个被监听的对象实例内部维护一个事件中心(Map),用于存储每个属性对应的回调函数集合。当属性变化时,在 set 拦截中触发对应的所有回调。

class Observer {
  constructor(data) {
    this._events = new Map(); // 存储事件监听器:{ key: [callback1, callback2, ...] }
    
    // 如果传入的不是对象,则直接返回
    if (typeof data !== 'object' || data === null) {
      return data;
    }
    
    // 创建代理对象
    return this._createProxy(data);
  }

  /**
   * 创建 Proxy 代理对象
   */
  _createProxy(target) {
    const self = this;
    
    const proxy = new Proxy(target, {
      get(obj, key) {
        // 排除原型方法和内部属性
        if (key in self) {
          return self[key];
        }
        
        const value = obj[key];
        
        // 如果值是对象,递归创建代理
        if (typeof value === 'object' && value !== null) {
          return self._createProxy(value);
        }
        
        return value;
      },

      set(obj, key, newValue, receiver) {
        const oldValue = obj[key];
        
        // 如果新值是对象,先创建代理
        let processedValue = newValue;
        if (typeof newValue === 'object' && newValue !== null) {
          processedValue = self._createProxy(newValue);
        }
        
        // 设置属性值
        const success = Reflect.set(obj, key, processedValue, receiver);
        
        if (success) {
          // 触发属性变化监听
          self._emit(key, processedValue, oldValue);
        }
        
        return success;
      }
    });

    return proxy;
  }

  /**
   * 监听属性变化
   */
  $on(key, callback) {
    if (!this._events.has(key)) {
      this._events.set(key, []);
    }
    this._events.get(key).push(callback);
  }

  /**
   * 取消监听
   */
  $off(key, callback) {
    const callbacks = this._events.get(key);
    if (callbacks) {
      const index = callbacks.indexOf(callback);
      if (index > -1) {
        callbacks.splice(index, 1);
      }
    }
  }

  /**
   * 触发属性变化事件
   */
  _emit(key, newValue, oldValue) {
    const callbacks = this._events.get(key);
    if (callbacks) {
      // 使用 setTimeout 确保在同步代码执行完后触发回调
      setTimeout(() => {
        callbacks.forEach(callback => {
          try {
            // 确保回调中的 this 指向代理对象
            callback.call(this, newValue, oldValue);
          } catch (error) {
            console.error(`Error in observer callback for property "${key}":`, error);
          }
        });
      }, 0);
    }
  }
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值