前端面试题之手写代码

422 阅读19分钟

在前端开发的广阔领域中,手写代码是面试过程中不可或缺的一环。它不仅考察了你对基础知识的掌握程度,还体现了你的逻辑思维能力和问题解决能力。正好笔者最近面试,所以也整理出来了一些常见的手写题。

基础篇

实现Object.create

Object.create()  静态方法以一个现有对象作为原型,创建一个新对象。

function create(obj){
  function Fn() {}
  Fn.prototype = obj
  Fn.prototype.contructor = Fn
  return new Fn()
}

实现instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

function myInstanceOf(obj, cons) {
  if (!obj || typeof obj !== 'object') {
    return false
  }
  let proto = Object.getPrototypeOf(obj)
  while (proto) {
    if (proto === cons.prototype) {
      return true
    }
    proto = Object.getPrototypeOf(proto)
  }
  return false
}

实现new关键字

在调用new之后会发生这几个步骤

  1. 创建一个空对象
  2. 设置原型:将空白对象的原型设置为函数的prototype对象
  3. 让函数的this指向这个对象,执行构造函数的代码(为空白对象添加属性)
  4. 判断函数的返回值
    • 如果是引用类型,直接返回,比如构造函数主动返回了一个对象:function fn(){return {x: 1}}
    • 如果不是引用类型,返回空白对象; 比如构造函数返回一个数字:function fn(){return 1}
function objectFactory() {
  let newObj = null
  let constructor = Array.prototype.shift.call(arguments)
  let res = null
  if (typeof constructor !== 'function') {
    console.log('type error');
    return
  }
  newObj = Object.create(constructor.prototype)
  res = constructor.apply(newObj, arguments)
  let flag = res && (typeof res === 'object' || typeof res === 'function')
  return flag ? res : newObj
}

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

console.log(objectFactory(Person, 'zl', 18));

拦截构造函数调用

function Person(name) {
  if (new.target !== undefined) {
    this.name = name
  } else {
    throw new Error("必须使用new关键字调用构造函数")
  }
}

实现继承

组合式继承

function Father(name, age) {
  this.name = name
  this.age = age
  this.hobby = ['玩游戏', '游泳', '跑步']
}

Father.prototype.sayName = function () {
  console.log(this.name)
}
Father.prototype.x = 1

function Son(name, age) {
  Father.call(this, name, age) // 调用父类的构造函数 (继承父类的属性)
  this.a = 1
}

Son.prototype = new Father()
Son.prototype.constructor = Son

类继承

class Father {
  constructor(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }

  getAge() {
    return 18;
  }
}

class Son extends Father {
  constructor(name, age) {
    super(name);
    this.age = age;
  }
}

const son = new Son('zhangsan', 18);
console.log(son.getAge());

实现Promise

const PENDING = 'pending';
const RESOLVED = 'resolved';
const REJECTED = 'rejected';

function MyPromise(fn) {
  const self = this;
  this.state = PENDING;
  this.value = null;
  this.reason = null;
  this.resolvedCallbacks = [];
  this.rejectedCallbacks = [];

  function resolve(value) {
    if (value instanceof MyPromise) {
      value.then(resolve, reject)
    }
    setTimeout(() => {
      if (self.state === PENDING) {
        self.state = RESOLVED;
        self.value = value;
        self.resolvedCallbacks.forEach(cb => cb(value));
      }
    }, 0)
  }

  function reject(reason) {
    setTimeout(() => {
      if (self.state === PENDING) {
        self.state = REJECTED;
        self.reason = reason;
        self.rejectedCallbacks.forEach(cb => cb(reason));
      }
    }, 0)
  }
  try {
    fn(resolve, reject);
  } catch (e) {
    reject(e);
  }
}

MyPromise.prototype.then = function (onFulfilled, onReject) {
  const self = this;
  return new MyPromise((resolve, reject) => {
    let fulfilled = () => {
      try {
        const result = onFulfilled(self.value);
        return result instanceof MyPromise ? result.then(result) : resolve(result);
      } catch (e) {
        reject(e);
      }
    };
    let rejected = () => {
      try {
        const result = onReject(self.reason);
        return result instanceof MyPromise ? result.then(resolve, reject) : reject(result);
      } catch (e) {
        reject(e);
      }
    }
    switch (self.state) {
      case PENDING:
      case RESOLVED:
      case RESOLVED:

    }
  })
}

实现Promise.all

function myPromiseAll(promises) {
  if (!Array.isArray(promises)) {
    return Promise.reject(new Error('promises must be an array'))
  }
  let cnt = 0, res = []
  return new Promise((resolve, reject) => {
    for (let i = 0; i < promises.length; i++) {
      // 使用 `Promise.resolve` 包装它,以确保即使传入的是非 `Promise` 值(如普通值),它也会被转换成一个已经解决的 `Promise`。
      Promise.resolve(promises[i]).then((result) => {
        // 如果成功,将结果存入 res 数组,并增加计数器  
        cnt++
        res[i] = result
        // 如果所有 promise 都已解决,则调用 resolve
        if (cnt === promises.length) {
          resolve(res)
        }
      },
        // 如果失败,则立即调用 reject   
        (error) => {
          reject(error)
        })
    };
  })
}

防抖

防抖就是触发一个事件,设置一个定时器,在定时器计时期间,如果再次触发该事件,则重置定时器。只有在一段时间内没有再次触发该事件,定时器才会执行相应的回调函数。

function debounce(fn, delay) {
  let timer = null
  return function () {
    const args = arguments
    clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}

节流

节流是在规定的时间间隔内,无论事件被触发多少次,它只执行一次。

function throttle(fn, delay) {
  let timer = 0
  return function () {
    const args = arguments
    const now = Date.now()
    if (now - timer > delay) {
      fn.apply(this, args)
      timer = now
    }
  }
}

实现类型判断函数

function getType(value){
  if(value === null){
    return 'null'
  }
  else if(typeof value === 'object') {
    return Object.prototype.toString.call(value).slice(8,-1).toLowerCase()
  }
  else {
    return typeof value
  } 
}

实现call函数

Function 实例的 call()  方法会以给定的 this 值和逐个提供的参数调用该函数。

call 函数的实现步骤:

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
  3. 处理传入的参数,截取第一个参数后的所有参数。
  4. 将函数作为上下文对象的一个属性。
  5. 使用上下文对象来调用这个方法,并保存返回结果。
  6. 删除刚才新增的属性。
  7. 返回结果。
function myCall(context) {
  if (typeof this !== 'function') {
    throw new Error('this is not function')
  }
  let args = arguments.slice(1)
  let res = null

  context = context || window
  context.fn = this

  res = context.fn(...args)
  delete context.fn
  return res
}

实现apply函数

Function 实例的 apply()  方法会以给定的 this 值和作为数组(或类数组对象)提供的 arguments 调用该函数。

与call基本一致,只是第二个参数是数组而已

function myApply(context) {
  if (typeof this !== 'function') {
    throw new Error('this is not function')
  }
  let args = arguments[1]
  let res = null

  context = context || window
  context.fn = this

  res = context.fn(...args)
  delete context.fn
  return res
}

实现bind函数

Function 实例的 bind()  方法创建一个新函数,当调用该新函数时,它会调用原始函数并将其 this 关键字设置为给定的值,同时,还可以传入一系列指定的参数,这些参数会插入到调用新函数时传入的参数的前面。

bind 函数的实现步骤:

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  2. 保存当前函数的引用,获取其余传入参数值。
  3. 创建一个函数返回
  4. 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。
function myBind(context) {
  if (typeof this !== 'function') {
    throw new Error('this is not function')
  }
  let args = arguments.slice(1)
  let fn = this

  return function F() {
    return fn.apply(
      this instanceof F ? this : context,
      args.concat(...arguments)
    )
  }
}

实现浅拷贝

function shallowCopy(obj) {
  if (!obj || typeof obj !== 'object') {
    throw new Error('obj is not an object')
  }
  let newObj = Array.isArray(obj) ? [] : {}
  for (let key in obj) {
    newObj[key] = obj[key]
  }
  return newObj
}

实现深拷贝

function deepClone(obj, map = new WeakMap()) {
  if (!obj || typeof obj !== 'object') {
    return obj;
  }
  if (map.has(obj)) {
    return map.get(obj)
  }
  let newObj = Array.isArray(obj) ? [] : {}
  map.set(obj, newObj)
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = deepClone(obj[key], map)
    }
  }
  return newObj
}

实现Object.assign

Object.assign()  静态方法将一个或者多个源对象中所有可枚举自有属性复制到目标对象,并返回修改后的目标对象。

hasOwnProperty()  方法返回一个布尔值,表示对象自有属性(而不是继承来的属性)中是否具有指定的属性。

function myAssign(target, ...sources) {
  if (target === null) {
    throw new Error('Cannot convert undefined or null to object')
  }
  let res = Object(target)
  sources.forEach((obj) => {
    if (Object.keys(obj)?.length) {
      for (let key in obj) {
        if(obj.hasOwnProperty(key)){
          res[key] = obj[key]
        }
      }
    }
  })
  return res
}

实现async/await中的async函数

function getNum(num) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(num + 1)
    }, 1000)
  })
}

function myAsync(fn) {
  let gen = fn()

  function step() {
    let { value, done } = gen.next()
    if (done) return value
    value.then((result) => {
      step(result)
    })
  }

  step()
}

function* fn() {
  let num1 = yield getNum(1)
  let num2 = yield getNum(2)
  console.log(num1 + num2);
}

myAsync(fn)

实现Object.freeze

Object.freeze() 静态方法可以使一个对象被冻结。冻结对象可以防止扩展,并使现有的属性不可写入和不可配置。被冻结的对象不能再被更改:不能添加新的属性,不能移除现有的属性,不能更改它们的可枚举性、可配置性、可写性或值,对象的原型也不能被重新指定。freeze() 返回与传入的对象相同的对象。

function myFreeze(obj) {
  if (!obj || typeof obj !== 'object') {
    return obj;
  }
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      Object.defineProperty(obj, key, {
        value: obj[key],
        writable: false,
        configurable: false
      })
    }
  }
  return obj;
}

实现map

map()  方法创建一个新数组,这个新数组由原数组中的每个元素都调用一次提供的函数后的返回值组成。

function myMap(fn) {
  if (typeof fn !== 'function') {
    throw new Error('参数必须是一个函数')
  }
  const res = []
  for (let v of this) {
    res.push(fn(v))
  }
  return res
}

实现reduce

reduce()  方法对数组中的每个元素按序执行一个提供的 reducer 函数,每一次运行 reducer 会将先前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值。

第一次执行回调函数时,不存在“上一次的计算结果”。如果需要回调函数从数组索引为 0 的元素开始执行,则需要传递初始值。否则,数组索引为 0 的元素将被用作初始值,迭代器将从第二个元素开始执行(即从索引为 1 而不是 0 的位置开始)。

Array.prototype.myReduce = (fn, initialValue) => {
  var arr = Array.prototype.slice.call(this);
  var res, startIndex;
  res = initialValue ? initialValue : arr[0];
  startIndex = initialValue ? 0 : 1;
  for (let i = startIndex; i < arr.length; i++) {
    res = fn.call(null, res, arr[i], i, this);
  }
  return res;
}

函数柯里化

function curry(fn) {
  return function curried(...args) {
    if (fn.length === args.length) {
      return fn.apply(this, args)
    } else {
      return function (...args2) {
        return curried.apply(this, args.concat(args2))
      }
    }
  }
}

// 使用示例  
function sum(a, b, c) {  
  return a + b + c;  
}  

const curriedSum = curry(sum);  
console.log(curriedSum(1)(2, 3)); // 输出: 6  
console.log(curriedSum(1, 2)(3)); // 输出: 6

场景篇

LRU

var LRUCache = function (capacity) {
  this.capacity = capacity
  this.m = new Map()
};

LRUCache.prototype.get = function (key) {
  if (this.m.has(key)) {
    let value = this.m.get(key)
    this.m.delete(key)
    this.m.set(key, value)
    return value
  }
  return -1
};

LRUCache.prototype.put = function (key, value) {
  if (this.m.has(key)) {
    this.m.delete(key)
  }
  this.m.set(key, value)
  if (this.m.size > this.capacity) {
    this.m.delete(this.m.keys().next().value)
  }
};

下划线转驼峰处理

function camelCase(str) {
  return str.replace(/[-_]([a-z])/g, function(match, group1) {
    return group1.toUpperCase();
  });
}

console.log(camelCase("some-string_with-underscores"));  // "someString"

Hex转RGB的方法

function hexToRgb(hex) {
  // 移除可能存在的 "#" 符号
  hex = hex.replace(/^#/, '');

  // 将十六进制颜色值拆分为红、绿、蓝三个分量
  const r = parseInt(hex.substring(0, 2), 16);
  const g = parseInt(hex.substring(2, 4), 16);
  const b = parseInt(hex.substring(4, 6), 16);

  return `rgb(${r}, ${g}, ${b})`;
}

// 测试示例
const hexColor = '#FF0000';
const rgbColor = hexToRgb(hexColor);
console.log(rgbColor);

数字千分位分割

function formatNumberWithCommas(number) {
  // 将数字转为字符串并分割整数部分和小数部分
  const parts = number.toString().split('.');
  const integerPart = parts[0];
  const decimalPart = parts.length > 1? '.' + parts[1] : '';

  // 对整数部分进行千分位分割
  let formattedIntegerPart = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',');

  return formattedIntegerPart + decimalPart;
}

// 测试示例
const number = 1234567.89;
const formattedNumber = formatNumberWithCommas(number);
console.log(formattedNumber);

对象扁平化flatObj

function flatObj(obj, parentKey = '', flattened = {}) {
  for (let key in obj) {
    const newKey = parentKey? `${parentKey}.${key}` : key;
    if (typeof obj[key] === 'object' && obj[key]) {
      flatObj(obj[key], newKey, flattened);
    } else {
      flattened[newKey] = obj[key];
    }
  }
  return flattened;
}

// 测试示例
const obj = {
  a: {
    b: {
      c: 1,
      d: 2
    },
    e: 3
  },
  f: 4
};

const flattenedObj = flatObj(obj);
console.log(flattenedObj);

对象扁平化flattenObject(带对象和数组)

function flattenObject(input) {
  const flattened = {};
  function process(obj, prefix = '') {
    for (const key in obj) {
      const value = obj[key];
      const newPrefix = prefix ? `${prefix}.${key}` : key;
      if (Array.isArray(value)) {
        value.forEach((item, index) => {
          const arrayPrefix = `${newPrefix}[${index}]`;
          if (typeof item === 'object' && item) {
            process(item, arrayPrefix);
          } else {
            flattened[arrayPrefix] = item;
          }
        });
      } else if (typeof value === 'object' && value !== null) {
        process(value, newPrefix);
      } else {
        flattened[newPrefix] = value;
      }
    }
  }
  process(input);
  return flattened;
}

// 示例用法
const complexObject = {
  a: 1,
  b: {
    c: 2,
    d: [3, { e: 4 }]
  },
  f: [5, { g: 6 }]
};
console.log(flattenObject(complexObject));
// { a: 1, 'b.c': 2, 'b.d[0]': 3, 'b.d[1].e': 4, 'f[0]': 5, 'f[1].g': 6 }

数组转树形结构

function arrayToTree(arr) {
  const map = {};
  const roots = [];

  arr.forEach(item => {
    map[item.id] = {...item, children: [] };
  });

  arr.forEach(item => {
    if (item.parentId) {
      if (!map[item.parentId]['children']) {
        map[item.parentId]['children'] = []
      }
      map[item.parentId]['children'].push(map[item.id]);
    } else {
      roots.push(map[item.id]);
    }
  });

  return roots;
}

// 测试示例
const data = [
  { id: 1, name: 'Node 1', parentId: null },
  { id: 2, name: 'Node 2', parentId: 1 },
  { id: 3, name: 'Node 3', parentId: 1 },
  { id: 4, name: 'Node 4', parentId: 2 }
];

const tree = arrayToTree(data);
console.log(tree);

获取URL中的参数

function getUrlParams(url) {
  const params = {};
  const urlParts = url.split('?');
  if (urlParts.length > 1) {
    const queryString = urlParts[1];
    const paramPairs = queryString.split('&');
    paramPairs.forEach(pair => {
      const [key, value] = pair.split('=');
      if (key) {
        params[key] = decodeURIComponent(value);
      }
    });
  }
  return params;
}

// 测试示例
const url = 'https://example.com/page?param1=value1&param2=value2';
const params = getUrlParams(url);
console.log(params);

执行队列call("Jack").wait(5).go("home").waitFirst(3).execute()

function call(name) {
  let tasks = []
  tasks.push(() => {
    console.log(`${name} is waitting`);
  })
  function wait(delay) {
    tasks.push(() => {
      return new Promise((resolve, reject) => {
        setTimeout(resolve, delay * 1000)
      })
    })
    return this
  }
  function waitFirst(delay) {
    tasks.unshift(() => {
      return new Promise((resolve, reject) => {
        setTimeout(resolve, delay * 1000)
      })
    })
    return this
  }
  function go(tasksName) {
    tasks.push(() => {
      console.log(tasksName);
    })
    return this
  }
  async function execute() {
    for (let i = 0; i < tasks.length; i++) {
      await tasks[i]()
    }
    return this
  }
  return {
    wait,
    go,
    waitFirst,
    execute
  }
}
// 前3秒打印Jack is waitting,后5秒打印出home
call("Jack").wait(5).go("home").waitFirst(3).execute()

实现链式调用const task = new Task() task.add(task1).add(task2, 42) .add(task3).run();

class Task {
  constructor() {
    this.tasks = [];
  }

  add(task, ...args) {
    // 包装任务函数,将参数一并传递  
    const wrappedTask = (next) => task(next, ...args);
    this.tasks.push(wrappedTask);
    // 返回当前 Task 实例以支持链式调用  
    return this;
  }

  run() {
    // 创建一个执行函数队列  
    const executeTasks = (index = 0) => {
      if (index >= this.tasks.length) {
        return; // 所有任务执行完毕  
      }
      const currentTask = this.tasks[index];
      // 创建一个回调函数,用于执行下一个任务  
      const next = () => executeTasks(index + 1);
      // 执行当前任务  
      currentTask(next);
    };
    // 开始执行任务队列  
    executeTasks();
  }
}

function task1(next) {
  setTimeout(() => {
    console.log('red');
    next();
  }, 3000);
}

function task2(next, b) {
  setTimeout(() => {
    console.log(b);
    next();
  }, 1000);
}

function task3(next) {
  setTimeout(() => {
    console.log('yellow');
    next();
  }, 2000);
}

// 创建 Task 实例
const task = new Task()
// 3秒后打印出red,1秒后打印出42,2秒后打印出yellow
task.add(task1).add(task2, 42) .add(task3).run();

实现一个异步任务调度器Scheduler,最多可并发执行2个任务

class Scheduler {
  constructor(maxNum = 2) {
    // 保存待执行任务的队列
    this.tasks = [];
    // 当前正在执行的任务数量
    this.running = 0;
    // 最大并发数
    this.maxNum = maxNum;
  }

  // 添加任务到调度器
  add(task) {
    return new Promise(resolve => {
      // 定义一个运行任务的函数
      const run = async () => {
        this.running++; // 增加正在运行的任务数
        let result = await task(); // 执行任务
        resolve(result); // 任务完成后,返回结果
        this.running--; // 任务完成后减少运行计数
        this.next(); // 检查是否有其他任务可以运行
      };

      // 如果当前运行任务数小于最大并发数,直接运行
      if (this.running < this.maxNum) {
        run();
      } else {
        // 否则,将任务包装成函数,存入等待队列 
        this.tasks.push(() => run());
      }
    });
  }

  // 执行下一个任务
  next() {
    // 检查任务队列中是否还有任务,并且运行的任务数没有达到最大并发数
    if (this.tasks.length > 0 && this.running < this.maxNum) {
      const nextTask = this.tasks.shift(); // 从队列中取出下一个任务
      nextTask(); // 运行该任务
    }
  }
}

const timeout = (ms, data) => new Promise(resolve => setTimeout(resolve, ms, data));

const scheduler = new Scheduler();
function addTask(promise) {
  scheduler.add(() => promise.then(data => console.log(data)));
}

// 添加任务到调度器
addTask(timeout(1000, 1));
addTask(timeout(300, 2));
addTask(timeout(500, 3));
addTask(timeout(2000, 4));

请求并发控制

// url请求地址,limit限制并发数
function trave(urls, limit) {
  return new Promise((resolve, reject) => {
    let num = 0
    let tasks = urls
    let res = []
    function go() {
      if (tasks.length === 0 && num === 0) {
        resolve(res)
        return
      }
      while (num < limit && tasks.length > 0) {
        let url = tasks.shift()
        num = num + 1
        axios.get(url).then((result) => {
          num = num - 1
          res.push(result.data)
          go()
        }).catch((err) => {
          num = num - 1
          console.log(err);
          go()
        });
      }
    }
    go()
  })
}

实现异步累加函数:从1累加到n,每次累加后检查是否超过15ms,若超时则暂停并继续异步执行

async function asyncSum(n) {
  let total = 0;      // 累加总和(类似线程局部存储)
  let i = 1;          // 当前处理数值(类似迭代器指针)
  const batchSize = 1; // 强制单步执行模式(每次累加后触发时间检查)

  const processBatch = async () => {
    // 高精度计时器(单位:毫秒,精度优于Date.now())
    const startTime = performance.now();
    // 主处理循环(持续执行直到完成或超时)
    while (i <= n) {
      // 单次累加操作(同步代码段)
      total += i;
      i++;
      // 每次累加后触发检查(batchSize=1的等效实现)
      if (i % batchSize === 0) {
        // 计算当前代码段执行耗时
        const elapsed = performance.now() - startTime;
        // 超时处理机制(RAIL模型的时间敏感阈值)
        if (elapsed > 15) {
          /**
           * 通过setTimeout移交控制权(事件循环分帧)
           * - 将后续任务放入宏任务队列
           * - 允许浏览器处理渲染/用户交互等任务
           */
          await new Promise(resolve => setTimeout(resolve, 0));
          // 递归调用开启新处理帧(保持闭包状态连续性)
          return processBatch();
        }
      }
    }
    // 完成时返回最终结果(触发Promise链的resolve)
    return total;
  };

  // 启动首次处理批次(立即执行同步代码段)
  return processBatch();
}

// 使用示例(10,000,000次累加)
asyncSum(10000000).then(finalTotal => {
  console.log("最终结果:", finalTotal); // 输出50000005000000
});

实现高阶函数promisify,将普通函数转化为支持 Promise 的函数

function promisify(fn) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      // 判断是否需要回调
      const hasCallback = (fn.length === args.length + 1); // 判断是否有回调函数作为参数传入
      if (hasCallback) {
        fn(...args, (err, ...results) => {
          if (err) {
            reject(err);
          } else {
            resolve(results.length === 1 ? results[0] : results);
          }
        });
      } else {
        try {
          const result = fn(...args);
          resolve(result);
        } catch (err) {
          reject(err);
        }
      }
    });
  };
}

// 示例:带回调的异步函数
function asyncAdd(a, b, callback) {
  setTimeout(() => {
    callback(null, a + b);
  }, 1000);
}

// 示例:不带回调的同步函数
function syncAdd(a, b) {
  return a + b;
}

// 测试异步函数
const addPromise = promisify(asyncAdd);

addPromise(1, 2).then((data) => {
  console.log('res', data); // 输出 'res 3'
}).catch((err) => {
  console.error('Error:', err);
});

// 测试同步函数
const syncAddPromise = promisify(syncAdd);
syncAddPromise(1, 2).then((data) => {
  console.log('res', data); // 输出 'res 3'
}).catch((err) => {
  console.error('Error:', err);
});

自定义hook实现loading,data,error

import React, { useEffect, useState } from 'react'

function useAsyncFn(fn) {
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState(null)
  const [data, setData] = useState(null)

  useEffect(() => {
    const fentchData = async () => {
      setIsLoading(true)
      setError(null)
      try {
        let data = await fn()
        setData(data)
      } catch (error) {
        setError(error)
      } finally {
        setIsLoading(false)
      }
    }
    fentchData()
  }, [])

  return [isLoading, error, data]
}

export default function App() {

  const fetchData = async () => {
    // 模拟 API 请求  
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        const success = Math.random() > 0.5;
        if (success) {
          resolve('Data fetched successfully');
        } else {
          reject('Failed to fetch data');
        }
      }, 1000);
    });
  };

  const [isLoading, error, data] = useAsyncFn(fetchData)

  return (
    isLoading ? <div>Loading...</div> : error ? <div>Error: {error}</div> : <div>Data: {data}</div>
  )
}

自定义hook实现倒计时

import { useState, useEffect, useCallback } from 'react';

function useCountdown(initialSeconds) {
  const [remaining, setRemaining] = useState(initialSeconds);
  const [isComplete, setIsComplete] = useState(false);

  useEffect(() => {
    if (remaining <= 0) {
      setIsComplete(true);
      return;
    }

    const timer = setInterval(() => {
      setRemaining((prev) => prev - 1);
    }, 1000);

    return () => clearInterval(timer);
  }, [remaining]);

  useEffect(() => {
    setRemaining(initialSeconds);
    setIsComplete(false);
  }, [initialSeconds]);

  const reset = useCallback(() => {
    setRemaining(initialSeconds);
    setIsComplete(false);
  }, [initialSeconds]);

  return { remaining, isComplete, reset };
}

export default useCountdown;

useMemoizedFn:使用useMemo实现

function useMemoizedFn(func){
  if(typeof func !== 'function') return

  // 通过 useRef 保持其引用地址不变,并且值能够保持值最新
  const funcRef = useRef(func)
  funcRef.current = useMemo(()=>{
    return func
  }, [func])

  const memoizedFn = useRef();

  if (!memoizedFn.current) {
    // 返回的持久化函数,调用该函数的时候,调用原始的函数
    memoizedFn.current = function(...args){
      return funcRef.current.apply(this, args)
    }
  }

  return memoizedFn.current
}

发布订阅

class EventEmitter {
  constructor() {
    this.list = {}
  }
  // 订阅事件
  on(event, callback) {
    if (!this.list[event]) {
      this.list[event] = []
    }
    this.list[event].push(callback)
  }
  // 触发事件
  emit(event, ...args) {
    if (this.list[event]) {
      this.list[event].forEach(fn => {
        fn.apply(this, args)
      });
    }
  }
  // 取消订阅指定事件的指定回调函数
  off(event, callback) {
    if (this.list[event] && callback) {
      this.list[event] = this.list[event].filter(fn => fn !== callback)
    }
  }
  // once方法用于仅订阅一次事件,即回调函数只会被执行一次
  once(event, callback) {
    const onceCallback = (...args) => {
      callback.apply(this, args)
      this.off(event, onceCallback)
    }
    this.on(event, onceCallback)
  }
}

function hello(...data) {
  console.log('hello' + data);
}

const emitter = new EventEmitter()
// 使用 once 方法订阅事件,回调函数只会被执行一次
emitter.once('onSell', hello)
// 触发事件,只会执行一次回调函数
emitter.emit('onSell', '1', '2', '3')
emitter.emit('onSell', '1', '2', '3') 

有一种花、两种鸟,花定时开放,鸟看到花开会叫,鸟的叫声不一样。(发布订阅应用)

class EventBus {
  constructor() {
    this.list = {};
  }
  on(fnName, fn) {
    if (!this.list[fnName]) this.list[fnName] = [];
    this.list[fnName].push(fn);
  }
  emit(fnName, ...args) {
    if (this.list[fnName]) {
      this.list[fnName].forEach(fn => {
        fn.apply(this, args);
      });
    }
  }
}

const eventBus = new EventBus();

// 发布者  
class Flower {
  constructor() { }
  open(delay) {
    setTimeout(() => {
      eventBus.emit('flowerOpen'); // 不需要传递 delay 给订阅者  
    }, delay * 1000);
  }
}

// 订阅者  
class Bird {
  constructor(name, sound) {
    this.name = name;
    this.sound = sound;
    eventBus.on('flowerOpen', this.makeSound.bind(this)); // 修正方法名  
  }
  makeSound() { // 移除了不必要的 delay 参数  
    console.log(`${this.name} ${this.sound}`);
  }
}

// 实例化并测试  
const myFlower = new Flower();
const bird1 = new Bird('鸟A', '叽叽喳喳');
const bird2 = new Bird('鸟B', '咕咕咕');

// 模拟花开  
myFlower.open(1); // 1秒后花开

查询参数匹配的路径匹配

特殊字符需要转义,例如c: 'm=1'

function appendQueryParams(url, params) {
  let res = Object.entries(params).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&')
  if(url.includes('?')) {
    return url + '&' + res
  }
  return url + '?' + res
}
let url = 'https://example.com/path'
let obj = { a: 1, b: 2, c: 'm=1' }
console.log(appendQueryParams(url,obj));

判断两个变量是否相等

function areEqual(a, b) {
  if (typeof a !== typeof b) {
    return false
  }
  if (a === null || typeof a !== 'object') {
    return a === b
  }
  if (Array.isArray(a) && Array.isArray(b)) {
    if (a.length !== b.length) {
      return false
    }
    for (let i = 0; i < a.length; i++) {
      if (!areEqual(a[i], b[i])) {
        return false
      }
    }
    return true
  }
  const k1 = Object.keys(a)
  const k2 = Object.keys(b)
  if(k1.length !== k2.length) {
    return false
  }
  for (let key of k1) {
    if (!areEqual(a[key], b[key])) {
      return false
    }
  }
  return true
}

// 测试用例  
console.log(areEqual(1, 1)); // true  
console.log(areEqual(1, '1')); // false  
console.log(areEqual([1, 2, 3], [1, 2, 3])); // true  
console.log(areEqual({a: 1, b: 2}, {b: 2, a: 1})); // true  
console.log(areEqual({a: 1, b: [1, 2]}, {a: 1, b: [1, 2]})); // true,注意这不会检查数组内部的顺序

图片懒加载

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
</head>

<body>
  <img data-price="20" data-src="https://img.36krcdn.com/20190808/v2_1565254363234_img_jpg">
  <img data-src="https://img.36krcdn.com/20190905/v2_1567641293753_img_png">
  <img data-src="https://img.36krcdn.com/20190905/v2_1567640518658_img_png">
  <img data-src="https://img.36krcdn.com/20190905/v2_1567642423719_img_000">
  <img data-src="https://img.36krcdn.com/20190905/v2_1567642425030_img_000">
  <img data-src="https://img.36krcdn.com/20190905/v2_1567642425101_img_000">
  <img data-src="https://img.36krcdn.com/20190905/v2_1567642425061_img_000">
  <img data-src="https://img.36krcdn.com/20190904/v2_1567591358070_img_jpg">
  <img data-src="https://img.36krcdn.com/20190905/v2_1567641974410_img_000">
  <img data-src="https://img.36krcdn.com/20190905/v2_1567641974454_img_000">

  <script>
    // 获取页面中所有的img元素
    const imgs = document.getElementsByTagName('img');
    // 获取img元素的数量
    const num = imgs.length;
    // 记录当前加载的是第几张图片,从0开始
    let n = 0;

    // 当DOM完全加载后,执行loadImage函数
    document.addEventListener('DOMContentLoaded', () => {
      loadImage();
    });

    // 定义loadImage函数
    function loadImage() {
      console.log('121122');
      // 获取可视区域的高度,兼容不同浏览器
      let screenHeight = document.documentElement.clientHeight;
      // 获取滚动条的位置,兼容不同浏览器
      let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;

      // 遍历所有图片
      for (let i = 0; i < num; i++) {
        // 如果图片的顶部位置在可视区内
        if (imgs[i].offsetTop < screenHeight + scrollTop) {
          // 设置图片的src属性
          imgs[i].src = imgs[i].getAttribute('data-src');
          // 更新加载的图片索引
          n = i + 1;
          // 如果所有图片都已加载,则移除scroll事件的监听器,进行性能优化
          if (n === num) {
            // console.log('加载完成');
            // 所有图片加载完成后,移除功能函数
            window.removeEventListener('scroll', throttleLazyLoad);
          }
        }
      }
    }

    // 使用Lodash的throttle函数来延迟loadImage的执行,减少性能开销
    const throttleLazyLoad = _.throttle(loadImage, 300);
    // 添加scroll事件监听器,在滚动时执行throttleLazyLoad
    window.addEventListener('scroll', throttleLazyLoad);

  </script>
</body>

</html>

图片懒加载二

// 监听 DOMContentLoaded 事件,当 HTML 文档被完全加载和解析完成后触发该事件,而无需等待样式表、图像和子框架的加载完成
document.addEventListener("DOMContentLoaded", function () {
  // 使用 document.querySelectorAll 方法选取所有带有 'lazy' 类名的元素,这些元素通常是需要进行懒加载的图片
  let lazyImages = document.querySelectorAll('.lazy');

  // 创建一个 IntersectionObserver 实例,用于观察元素是否进入浏览器的可视区域
  // IntersectionObserver 是一个浏览器 API,它会在目标元素与视口(或指定的根元素)交叉时触发回调函数
  let lazyImageObserver = new IntersectionObserver(function (entries, observer) {
    // 遍历 entries 数组,entries 数组包含了所有被观察元素的交叉信息
    entries.forEach(function (entry) {
      // 检查当前元素是否进入了可视区域,isIntersecting 是一个布尔值,表示元素是否与视口交叉
      if (entry.isIntersecting) {
        // 获取当前进入可视区域的元素,即目标元素
        let lazyImage = entry.target;

        // 将图片的 src 属性设置为其 data-src 属性的值,data-src 属性通常用于存储图片的真实地址
        // 这样就触发了图片的加载
        lazyImage.src = lazyImage.getAttribute('data-src');

        // 为图片的 onload 事件添加一个回调函数,当图片成功加载完成后执行
        lazyImage.onload = function () {
          // 移除图片的 'lazy' 类名,表示该图片已经不再是待加载状态
          lazyImage.classList.remove('lazy');
          // 添加 'loaded' 类名,表示该图片已经成功加载
          lazyImage.classList.add('loaded');
        };

        // 为图片的 onerror 事件添加一个回调函数,当图片加载失败时执行
        lazyImage.onerror = function () {
          // 添加 'error' 类名,表示该图片加载失败
          lazyImage.classList.add('error');
        };

        // 停止对当前图片的观察,因为图片已经开始加载,不需要再继续观察它是否进入可视区域
        lazyImageObserver.unobserve(lazyImage);
      }
    });
  });

  // 遍历所有带有 'lazy' 类名的图片元素
  lazyImages.forEach(function (lazyImage) {
    // 开始观察每个图片元素,当这些元素进入可视区域时,IntersectionObserver 的回调函数会被触发
    lazyImageObserver.observe(lazyImage);
  });
});

实现模版字符串解析

function templateEngine(template, data) {
  let result = template
  for (let key in data) {
    const regex = new RegExp(`{{${key}}}`, 'g')
    result = result.replace(regex, data[key])
  }
  return result
}
// 使用示例
const template = 'The value of {{name}} is {{age}}.';
const data = { name: 'John', age: 30 };
const rendered = templateEngine(template, data);
console.log(rendered);

实现lodash.get

function customGet(obj, path, defaultValue = undefined) {
  let keys = Array.isArray(path) ? path : path.split(/[\.\[\]]/).filter(item => item.trim() !== '');
  let current = obj;
  for (const key of keys) {
    if (current && typeof current === 'object') {
      if (Array.isArray(current) && !isNaN(key)) {
        current = current[Number(key)];
      } else if (current.hasOwnProperty(key)) {
        current = current[key];
      } else {
        return defaultValue;
      }
    } else {
      return defaultValue;
    }
  }
  return current;
}

const object = {
  a: [
    { b: { c: 3 } }
  ]
};

console.log(customGet(object, 'a[0].b.c')); // 3
console.log(customGet(object, 'a.0.b.c')); // 3
console.log(customGet(object, ['a', '0', 'b', 'c'])); // 3
console.log(customGet(object, 'a[1].b.c', undefined)); // undefined

实现数据双向绑定

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>双向数据绑定示例</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      padding: 20px;
    }

    input {
      padding: 5px;
      font-size: 16px;
    }

    span {
      font-weight: bold;
    }
  </style>
</head>

<body>

  <h1>双向数据绑定示例</h1>

  <!-- 输入框 -->
  <input id="inputElement" type="text" placeholder="输入内容...">

  <!-- 显示输入内容 -->
  <p>你输入的内容是:<span id="displayText"></span></p>

  <script>
    // 双向数据绑定函数
    function bindData(model, element) {
      // 初始化 UI 与模型的绑定
      element.value = model.value.toString();  // 强制将数字转为字符串
      document.getElementById('displayText').textContent = model.value; // 显示模型的初始值

      // 使用 Object.defineProperty 为 model 对象的 value 属性定义 getter 和 setter
      Object.defineProperty(model, 'value', {
        get() {
          return this._value;
        },
        set(newValue) {
          this._value = newValue;
          // 数据更新时同步更新 UI
          element.value = newValue;
          document.getElementById('displayText').textContent = newValue; // 更新展示文本
        }
      });

      // 监听 UI 的 input 事件,数据变化时更新 model
      element.addEventListener('input', (e) => {
        model.value = e.target.value;  // 更新 model 的值
      });

      return model;
    }

    // 初始化数据模型
    const model = { value: 1 };

    // 获取 DOM 元素
    const inputElement = document.getElementById('inputElement');

    // 将模型与页面元素进行双向绑定
    const boundModel = bindData(model, inputElement);
  </script>

</body>

</html>

一棵二叉树,它的每一个节点,它都有一个指针指向它的父节点。现在寻找两颗叶子节点的,最近公共祖先,空间复杂度O(1)。

function findLowestCommonAncestor(root, node1, node2) {
  // 计算节点的深度
  function getDepth(node) {
    let depth = 0;
    while (node !== null) {
      node = node.parent;
      depth++;
    }
    return depth;
  }

  // 获取两个节点的深度
  const depth1 = getDepth(node1);
  const depth2 = getDepth(node2);

  // 调整两个节点,使它们位于同一深度
  while (depth1 > depth2) {
    node1 = node1.parent;
  }
  while (depth2 > depth1) {
    node2 = node2.parent;
  }

  // 同步向上移动,直到找到最近公共祖先
  while (node1 !== node2) {
    node1 = node1.parent;
    node2 = node2.parent;
  }

  return node1; // 返回最近公共祖先
}

根据数组构建一颗二叉树

const arr = [1, 2, 3, 4, null, null, 5]

class Tree {
  constructor(val) {
    this.val = val
    this.left = null
    this.right = null
  }
}

function buildTree(arr, index = 0) {
  if (index >= arr.length || arr[index] === null) {
    return null;
  }

  const node = new Tree(arr[index]);
  node.left = buildTree(arr, 2 * index + 1);
  node.right = buildTree(arr, 2 * index + 2);

  return node;
}

const root = buildTree(arr)
console.log(root);