【前端手写】全面考察基础的23道经典笔试题

695 阅读6分钟

常见功能实现

本文整理了前端面试中比较常考、有代表性的笔试题(题目按照考察频率做了一定排序),包含解题思路。题目全面考察前端基础,不面试的同学也可以看看是否命中你不熟悉的知识点啦。欢迎收藏以及起留言补充~

1. 异步并发数限制 (考察频率高)

题目内容

有很多个并发任务,控制同一时间内执行的任务数不超过N个,并尽快执行完。

关键实现步骤

  1. 并发控制: 通过计数器来确保不超过限制的并发数。
  2. 任务调度: 合理分配和移除任务,实现高效调度。
  3. 异步操作: 利用Promise处理异步任务,确保流程连续性。
  4. 完成通知:所有任务完成后通知调用者,提升用户体验。

知识点

  1. 递归:一有异步任务结束 => 马上判断是否有todo任务可执行
  2. Promise.resolve使用: 把所有任务都转化为异步,可以在任务完成后引立刻执行其他任务
  3. Promise finally使用: 确保任务完成后逻辑总是执行。
  4. 边缘情况: 处理诸如limit为0等特殊情况。

完整过程实现解释

  1. 并发限制: 通过设置并发限制数量limit,来控制同时执行的任务数。

  2. 任务队列管理: 使用todoTasks数组来管理待执行的任务,使用count来跟踪当前正在执行的任务数量。

  3. 任务执行: 在handleFn函数中处理每个任务的执行,当当前执行的任务数量小于限制时,立即执行任务,并更新计数。

  4. Promise结构: 使用Promise.resolve().finally来确保任务完成后的逻辑执行,无论任务是成功还是失败。

  5. 添加新任务: 当一个任务完成时(不论成功或失败),从待执行任务队列中取出一个新任务并执行。

  6. 任务完成处理: 通过finishCount来跟踪已完成的任务数量,当所有任务都完成时,调用resolve来标记整体任务完成。

  7. 控制流: 通过递归调用handleFn来确保任务按照最大并发数进行执行,并在一个任务完成时继续添加新任务。

  8. 错误处理和边缘情况: 代码应考虑异常处理和边缘情况(例如array为空或limit为0等),确保代码的健壮性。

function limit(limit, array) {
  let todoTasks = [];
  let count = 0;
  let finishCount = 0;

  function handleFn(fn) {
    if (count < limit) {
      count++;
      Promise.resolve(fn()).finally(() => {
        finishCount++;
        if (finishCount === array.length) {
          resolve();
        }
        count--;
        let newFn = todoTasks.splice(0, 1)[0];
        if (newFn) handleFn(newFn);
      });
    } else {
      todoTasks.push(fn);
    }
  }

  return new Promise((resolve) => {
    for (let i = 0; i < array.length; i++) {
      handleFn(array[i]);
    }
  });
}

// test
可参考目录[[异步并发数限制.js]]

2. deepCopy

考察的前端基础知识点

  1. 对象引用与拷贝: 深拷贝与浅拷贝之间的区别,以及如何实现对象的深拷贝。
  2. 递归: 通过递归调用解决多层嵌套对象的拷贝问题。
  3. 数据类型检测: 理解JavaScript中的不同数据类型,包括函数、日期和正则表达式,并能够针对不同类型进行特定处理。
  4. 循环引用处理: 了解并处理对象之间的循环引用问题,避免无限递归的问题

运用到的关键语法/方法

  1. instanceof 关键字: 判断引用类型
  2. WeakMap: 使用WeakMap来存储已经拷贝的对象,解决循环引用的问题。
function deepCopy(obj, cache = new WeakMap()) {
  if (!(obj instanceof Object)) return obj; // 判断是否为对象
  if (cache.get(obj)) return cache.get(obj); // 处理循环引用
  
  // 针对特殊对象和类型进行处理
  if (obj instanceof Function) return () => obj.apply(this, arguments);
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
  // 可以增加其他对象,比如Map, Set等

  const res = Array.isArray(obj) ? [] : {}; // 判断对象类型
  cache.set(obj, res); // 缓存拷贝对象

  Object.keys(obj).forEach((key) => {
    res[key] = obj[key] instanceof Object ? deepCopy(obj[key], cache) : obj[key];
  });

  return res;
}

// 测试
const source = {
  name: 'Jack',
  meta: {
    age: 12,
    birth: new Date('1997-10-10'),
    ary: [1, 2, { a: 1 }],
    say() {
      console.log('Hello');
    }
  }
};
source.source = source; // 创建循环引用
const newObj = deepCopy(source);
console.log(newObj.meta.ary[2] === source.meta.ary[2]); // false
console.log(newObj.meta.birth === source.meta.birth); // false

3. 柯里化:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数

关键知识点

  1. function.length 用来获取函数的形参个数
  2. 注意调用curried的时候也要去绑定this的指向
  3. 递归(递归的关键是找函数的出口)
function curry(func){
  return function curried(...args){
    if(args.length<func.length){
     return function (...args2){
      // 注意:这里return curreied的时候也要去绑定this的指向
      curried.apply(this,args.concat(args2))
     }
    }else{
       return func.apply(this,args)
    }
  }
}


// 测试
function sum (a, b, c) {
  return a + b + c
}
const curriedSum = curry(sum)
console.log(curriedSum(1, 2, 3))
console.log(curriedSum(1)(2,3))
console.log(curriedSum(1)(2)(3))

4. 洋葱模型(Node基础)

考察点

洋葱模型的含义

  1. 洋葱模型是一种处理请求和响应的方法。洋葱模型的处理方式类似于剥洋葱一样的层层处理。
  2. 在洋葱模型中,请求(request)和响应(response)的处理过程被看作是穿过一层层的中间件,每个中间件都可以在请求传递过程中对其进行某些处理,然后传递给下一个中间件。

洋葱模型的用法

洋葱模型被用于构建中间件。洋葱模型允许开发者在请求和响应的处理过程中插入自定义的处理逻辑。开发者可以通过编写中间件,然后将这些中间件按照处理顺序组合起来,构成一个处理请求和响应的管道。

洋葱模型的实现原理

洋葱模型的实现依赖于 JavaScript 的 async/await 语法和函数递归。

  1. 洋葱模型的实现使用了递归。
    • 在递归调用中,每个中间件函数都会调用一个名为 next 的函数,该函数会触发下一个中间件的执行。
  2. 洋葱模型的实现使用了 async/await 语法。
    • async/await 语法允许我们在函数执行的过程中暂停和恢复执行上下文,这是实现洋葱模型处理流程的关键。

代码

// 中间件数组
let middlewareArr = [
    async function(ctx, next) {
        console.log('Middleware 1 Start');
        await next();
        console.log('Middleware 1 End');
    },
    async function(ctx, next) {
        console.log('Middleware 2 Start');
        await next();
        console.log('Middleware 2 End');
    },
    async function(ctx, next) {
        console.log('Middleware 3 Start');
        await next();
        console.log('Middleware 3 End');
    }
];

// 实现洋葱模型
function compose(middlewareArr) {
    return function(ctx) {
        return dispatch(0);
        function dispatch(i) {
            let fn = middlewareArr[i];
            if (!fn) {
                return Promise.resolve();
            }
            return Promise.resolve(
                fn(ctx, function next() {
                    return dispatch(i + 1);
                })
            );
        }
    };
}

// 使用洋葱模型
let finalFn = compose(middlewareArr);
finalFn({});

代码过程解释

  1. 们声明一个名为 middlewareArr 的数组,它包含三个异步中间件函数。每个函数都接收两个参数,ctxnextctx 是一个对象,用于在中间件之间传递信息。next 是一个函数,当调用它时,就会触发下一个中间件。

  2. 接着,我们定义了 compose 函数。这个函数接收一个中间件数组,并返回一个新的函数。这个新的函数接收一个 ctx 参数,并开始执行中间件数组中的第一个中间件。

  3. 在这个新的函数中,我们定义了 dispatch 函数。dispatch 函数接收一个索引 i,并尝试获取中间件数组中的第 i 个函数。如果存在这个函数,就使用 Promise.resolve 将其包装成一个 Promise,并传入两个参数:ctxnext 函数。这里的 next 函数其实就是调用 dispatch(i + 1),也就是触发下一个中间件的执行。

  4. 最后,我们创建了一个 finalFn 函数,这个函数是 compose(middlewareArr) 的返回结果。当我们执行 finalFn({}) 时,就会启动这个洋葱模型,并按照预定的顺序开始执行中间件。

在这个代码中,核心的思想是利用递归和 Promise 把异步函数按照特定的顺序串联起来,从而实现类似洋葱一样的处理流程。每个中间件都可以在进入和退出时执行特定的代码,通过这种方式,我们可以非常灵活地处理请求和响应。

5. new

考察知识点

主要考察new的实现原理:

  • 创建一个全新的对象。
  • 这个新对象会被执行[[Prototype]]链接。
  • 这个新对象会绑定到函数调用的this。
  • 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

关键语法

  1. Object.create: 用于创建一个新对象,并继承另一个对象的原型。
  2. apply方法: 用于调用一个具有给定this值及作为一个数组(或类数组对象)提供的参数的函数。
function myNew(Func, ...args) {
  const instance = Object.create(Func.prototype); // 使用Object.create链接原型
  const res = Func.apply(instance, args);
  return (typeof res === 'object' && res !== null) ? res : instance; // 判断res是否为非空对象
}

// 测试
function Person(name) {
  this.name = name;
}
Person.prototype.sayName = function () {
  console.log(`My name is ${this.name}`);
};
const me = myNew(Person, 'Jack');
me.sayName();
console.log(me);

yuya模拟实现

6. instanceof

考察点

考察的是原型链。

  1. 实例的__ptoto__指向构造函数的prototype
  2. 再当前__proto__层级找不到,会向上逐层查找__proto__,直到到达原型链顶端Null
// 目前已经不推荐直接访问__proto__ ,而是通过Object.getPrototypeOf来访问
// obj.__proto__ === Object.getPrototypeOf(obj)
function isInstanceOf(instance, myClass) {
    let proto = Object.getPrototypeOf(instance);
    const prototype = myClass.prototype;
    while (proto) {
        if (proto === prototype) return true;
        proto = Object.getPrototypeOf(proto);
    }

    return false;
}

// 测试
class Parent {}
class Child extends Parent {}
const child = new Child();
console.log(isInstanceOf(child, Parent), isInstanceOf(child, Child), isInstanceOf(child, Array));

7. 事件总线 | 发布订阅模式

关注点

  1. 实现的话: 就是通过一个对象,对象key存储事件名,对象value是回调事件的数组
  2. emit触发的时候要注意,需要先拷贝副本出来,因为如果callback中又往这个name上注册了事件,那遍历这个数组就会出现死循环的情况(数组不断被新增)
class myEventEmitter(){
  this.cache = {}

  on(name,fn){
    if(this.cache[name]){
      this.cache[name].push(fn)
    }else{
      this.cache[name] = [fn]
    }
  }

  off(name,fn){
     if(this.cache[name]){
      let index = this.cache[name].indexOf(fn)
      this.cache[name].splice(index,1)
    }
  }

  emit(name){
    if(this.cache[name]){
            // 创建副本,如果回调函数内又在name下面注册了事件,会造成死循环
      let callbackFn = this.cache[name].slice();
      callBackFn.forEach(fn=>{
        fn()
      })
    }
  }

 // 触发某个时间,只触发一次
  emit(name,once){
    if(this.cache[name]){
            // 创建副本,如果回调函数内又在name下面注册了事件,会造成死循环
      let callbackFn = this.cache[name].slice();
      callBackFn.forEach(fn=>{
        fn()
      })
    }
    if(once){
       delete this.cache[name]
      }
    }
  }

// 测试
const eventBus = new EventEmitter()
const task1 = () => { console.log('task1'); }
const task2 = () => { console.log('task2'); }
eventBus.on('task', task1)
eventBus.on('task', task2)

setTimeout(() => {
  eventBus.emit('task')
}, 1000)

8. call

考察点

  1. call\apply\bind区别
  2. 三者原理 (call\apply再对象上创建属性方法,然后删除方法。bind同样,不同bind返回函数)

PS: 有些面试官可能会说用es6的语法来实现call这一些原始函数不太合适,这种情况下的话获取参数可以通过arguments来获取函数的实参

const arguments = arguments[0]
const otherArguments = Array.from(arguments).slice(1)

写法

Function.prototype.myCall = function(context,...args){
  const fn = Symbol('fn')
  context[fn] = this;
  const result = context[fn](...args)
  delete context[fn]
  return result;
}

// 测试
const me = { name: 'Jack' }
function say() {
  console.log(`My name is ${this.name || 'default'}`);
}
say.myCall(me)

9. apply


Function.prototype.myApply = function (context = globalThis) {
  // 关键步骤,在 context 上调用方法,触发 this 绑定为 context,使用 Symbol 防止原有属性的覆盖
  const key = Symbol('key')
  context[key] = this
  let res
  if (arguments[1]) {
    res = context[key](...arguments[1])
  } else {
    res = context[key]()
  }
  delete context[key]
  return res
}

// 测试
const me = { name: 'Jack' }
function say() {
  console.log(`My name is ${this.name || 'default'}`);
}
say.myApply(me)

10. bind

这里要特别注意的是最终的this指向

  • 如果bind返回的方法被new调用,this指向new 实例出来的对象
  • 如果没有New,就是当做普通方法调用,this指向的context 注意点2
  • 支持new调用,还要处理返回的函数的原型

参考yayu的实现

Function.prototype.bind(context,...args){
  let fn = this;
   function binded(...args2){
    if(this instanceof binded){
      // new出来的,binded会是New出来的实例的原型
      // TODO: 处理返回值
      fn.apply(this,args.concat(args2))
    }else{
      fn.apply(context,args.concat(args2))
    }
  }
  // 注意这里要有Object.create包一层,如果不包的话,修改binded的原型也会造成fn的原型被改变,这样的话是一个New出来的新对象
  binded.prototype = Object.create(fn.prototype)
  // ps: 如果没有Object.create可以使用的话,我们可以通过创建一个空函数作为中介的方式
  /**
   * function Noop(){
   * }
   * Noop.prototype = fn.prototype
   * binded.prototype = new Noop()
  */
  return binded
}



// 测试
const me = { name: 'Jack' }
const other = { name: 'Jackson' }
function say() {
  console.log(`My name is ${this.name || 'default'}`);
}
const meSay = say.myBind(me)
meSay()
const otherSay = say.myBind(other)
otherSay()

11. 链式调用考察

题目

实现一个 arrange 函数,可以进行时间和工作调度 [ > … ] 表示调用函数后的打印内容

arrange('William').execute(); // > William is notified 
arrange('William').do('commit').execute(); 
// > William is notified 
// > Start to commit 

// arrange('William').wait(5).do('commit').execute();
// > William is notified 
// 等待 5 秒 
// > Start to commit // 

arrange('William').waitFirst(5).do('push').execute(); 
// 等待 5 秒 
// > William is notified 
// > Start to push

关键点

  1. 理解链式调用的实现原理:通过返回函数this
  2. 观察用例,waitFirst需要先等待: => 要控制函数的执行顺序,可以通过回调数组来存放回调函数,从而控制顺序。

代码实现

function arrange(name) {
 let tasks = [() => console.log(`${name} is notified`)];
 let initialWait = 0;

 return {
   do: function(action) {
     tasks.push(() => console.log(`Start to ${action}`));
     return this;
   },
   wait: function(seconds) {
     tasks.push(() => setTimeout(() => console.log(`等待 ${seconds} 秒`), seconds * 1000));
     return this;
   },
   waitFirst: function(seconds) {
     initialWait = seconds * 1000;
     return this;
   },
   execute: function() {
     const runTasks = () => tasks.forEach(task => task());
     
     if (initialWait > 0) {
       setTimeout(runTasks, initialWait);
     } else {
       runTasks();
     }
   }
 };
}

// 测试用例
arrange('William').execute();                             // > William is notified
arrange('William').do('commit').execute();               // > William is notified
                                                        // > Start to commit
arrange('William').wait(5).do('commit').execute();       // > William is notified
                                                        // 等待 5 秒
                                                        // > Start to commit
arrange('William').waitFirst(5).do('push').execute();    // 等待 5 秒
                                                        // > William is notified
                                                        // > Start to push

12. 防抖

function debounce(func, ms = 1000) {
  let timer;
  return function debounced(...args) 
  {
   const context = this;
    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      timer = null;
      return func.apply(context, args)
    }, ms)
  }
}


// 测试
const task = () => { console.log('run task') }
const debounceTask = debounce(task, 1000)
window.addEventListener('scroll', debounceTask)

12. 节流

function throttleOfSimple(fn, wait) {
    let timer = null;

    function _throttle() {
        /** 保持上下文 */
        const context = this;
        const args = Array.prototype.slice(arguments);
        if (!timer) {
            timer = setTimeout(()=> {
                fn.apply(context, args);
                timer = null;
            }, wait);
        }
    }
    return _throttle;
}

// 测试
const task = () => { console.log('run task') }
const throttleTask = throttle(task, 1000)
window.addEventListener('scroll', throttleTask)

13. promise.all

思路

利用数组 + 最外层包一个Promise来实现


  function promiseAll(array){
    return new Promise((resolve,reject)=>{
      const result = [];
      let finishCount = 0;
      array.forEach((fn,index)=>{
        Promise.resolve(fn).then(res=>{
           result[index] = res; 
            finishCount++;
            if(finishCount === array.length){
              resolve(result)
            }
        }
        ).catch(err=>{
            reject(err)
        })
        // result.push
      })
    })
  }

14. promise.race

function promiseRace(promises){
  return new Promise((resolve,reject)=>{
    if(promise.length === 0){
      return;
    }

    for(let i=0;i<promises.length;i++){
      Promise.resolve(promises[i])
      .then(value=>{
        resolve(value)
      })
      .catch(err=>{
        reject(err);
      })
    }
  })
}

15. 异步串行 | 异步并行

// 字节面试题,实现一个异步加法
// 异步加法
function asyncAdd(a,b,cb){
  setTimeout(() => {
    cb(null, a + b)
  }, Math.random() * 1000)
}

async function total(){
  const res1 = await sum(1,2,3,4,5,6,4)
  const res2 = await sum(1,2,3,4,5,6,4)
  return [res1, res2]
}

total()

// 实现下 sum 函数。注意不能使用加法,在 sum 中借助 asyncAdd 完成加法。尽可能的优化这个方法的时间。
function sum(){

}

解决方案

  1. 把add函数promisify化
  2. 串行处理的话可以用reduce来实现
  3. 并行处理的话可以用
const promiseAdd = (a, b) => new Promise((resolve, reject) => {
  asyncAdd(a, b, (err, res) => {
    if (err) {
      reject(err)
    } else {
      resolve(res)
    }
  })
})
async function serialSum(...args) {
  // PS: args.reduce(callbackFn,initialValue) initialValue的默认值是数组第一个元素
  let initialValue = Promise.resolve(0);
  let callBack = (prevCallTask, now) => {
    prevCallTask.then(res => promiseAdd(res, now))
    }
  return args.reduce(callBack,initialValue)
}
// 3. 并行处理
async function parallelSum(...args) {
  if (args.length === 1) return args[0]
  const tasks = []
  for (let i = 0; i < args.length; i += 2) {
    tasks.push(promiseAdd(args[i], args[i + 1] || 0))
  }
  const results = await Promise.all(tasks)
  return parallelSum(...results)
}

// 测试
(async () => {
  console.log('Running...');
  const res1 = await serialSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)
  console.log(res1)
  const res2 = await parallelSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)
  console.log(res2)
  console.log('Done');
})()

16. es5 实现继承

function create(proto) {
  const type = typeof proto
  const isObject = type === 'function' || type === 'object' && !!proto
  if (!isObject) return {}
  function F() {}
  F.prototype = proto;
  return new F();
}

// Parent
function Parent(name) {
  this.name = name
}

Parent.prototype.sayName = function () {
  console.log(this.name)
};

// Child
function Child(age, name) {
  Parent.call(this, name)
  this.age = age
}
Child.prototype = create(Parent.prototype)
Child.prototype.constructor = Child

Child.prototype.sayAge = function () {
  console.log(this.age)
}

// 测试
const child = new Child(18, 'Jack')
child.sayName()
child.sayAge()

17. 数组扁平化

原理和知识点

  1. 递归调用: 将多层嵌套数组转化为一维数组,通过递归进行扁平化。

  2. Array.prototype.reduce方法(数组递归的一种优雅写法): 累加器,将数组中的每个值(从左到右)开始缩减,最终计算为一个值。

// 方案 1
function recursionFlat(ary = []) {
  const res = []
  ary.forEach(item => {
    if (Array.isArray(item)) {
      res.push(...recursionFlat(item))
    } else {
      res.push(item)
    }
  })
  return res
}
// 方案 2
function reduceFlat(ary = []) {
  return ary.reduce((res, item) => res.concat(Array.isArray(item) ? reduceFlat(item) : item), [])
}

// 测试
const source = [1, 2, [3, 4, [5, 6]], '7']
console.log(recursionFlat(source))
console.log(reduceFlat(source))

18. 对象扁平化

function objectFlat(obj = {}) {
  const res = {}
  function flat(item, preKey = '') {
    Object.entries(item).forEach(([key, val]) => {
      const newKey = preKey ? `${preKey}.${key}` : key
      if (val && typeof val === 'object') {
        flat(val, newKey)
      } else {
        res[newKey] = val
      }
    })
  }
  flat(obj)
  return res
}

// 测试
const source = { a: { b: { c: 1, d: 2 }, e: 3 }, f: { g: 2 } }
console.log(objectFlat(source));

19. vue reactive

// Dep module
class Dep {
  static stack = []
  static target = null
  deps = null
  
  constructor() {
    this.deps = new Set()
  }

  depend() {
    if (Dep.target) {
      this.deps.add(Dep.target)
    }
  }

  notify() {
    this.deps.forEach(w => w.update())
  }

  static pushTarget(t) {
    if (this.target) {
      this.stack.push(this.target)
    }
    this.target = t
  }

  static popTarget() {
    this.target = this.stack.pop()
  }
}

// reactive
function reactive(o) {
  if (o && typeof o === 'object') {
    Object.keys(o).forEach(k => {
      defineReactive(o, k, o[k])
    })
  }
  return o
}

function defineReactive(obj, k, val) {
  let dep = new Dep()
  Object.defineProperty(obj, k, {
    get() {
      dep.depend()
      return val
    },
    set(newVal) {
      val = newVal
      dep.notify()
    }
  })
  if (val && typeof val === 'object') {
    reactive(val)
  }
}

// watcher
class Watcher {
  constructor(effect) {
    this.effect = effect
    this.update()
  }

  update() {
    Dep.pushTarget(this)
    this.value = this.effect()
    Dep.popTarget()
    return this.value
  }
}

// 测试代码
const data = reactive({
  msg: 'aaa'
})

new Watcher(() => {
  console.log('===> effect', data.msg);
})

setTimeout(() => {
  data.msg = 'hello'
}, 1000)

20. promise

// 建议阅读 [Promises/A+ 标准](https://promisesaplus.com/)
class MyPromise {
  constructor(func) {
    this.status = 'pending'
    this.value = null
    this.resolvedTasks = []
    this.rejectedTasks = []
    this._resolve = this._resolve.bind(this)
    this._reject = this._reject.bind(this)
    try {
      func(this._resolve, this._reject)
    } catch (error) {
      this._reject(error)
    }
  }

  _resolve(value) {
    setTimeout(() => {
      this.status = 'fulfilled'
      this.value = value
      this.resolvedTasks.forEach(t => t(value))
    })
  }

  _reject(reason) {
    setTimeout(() => {
      this.status = 'reject'
      this.value = reason
      this.rejectedTasks.forEach(t => t(reason))
    })
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      this.resolvedTasks.push((value) => {
        try {
          const res = onFulfilled(value)
          if (res instanceof MyPromise) {
            res.then(resolve, reject)
          } else {
            resolve(res)
          }
        } catch (error) {
          reject(error)
        }
      })
      this.rejectedTasks.push((value) => {
        try {
          const res = onRejected(value)
          if (res instanceof MyPromise) {
            res.then(resolve, reject)
          } else {
            reject(res)
          }
        } catch (error) {
          reject(error)
        }
      })
    })
  }

  catch(onRejected) {
    return this.then(null, onRejected);
  }
}

// 测试
new MyPromise((resolve) => {
  setTimeout(() => {
    resolve(1);
  }, 500);
}).then((res) => {
    console.log(res);
    return new MyPromise((resolve) => {
      setTimeout(() => {
        resolve(2);
      }, 500);
    });
  }).then((res) => {
    console.log(res);
    throw new Error('a error')
  }).catch((err) => {
    console.log('==>', err);
  })

21. 图片懒加载

// <img src="default.png" data-src="https://xxxx/real.png">
function isVisible(el) {
  const position = el.getBoundingClientRect()
  const windowHeight = document.documentElement.clientHeight
  // 顶部边缘可见
  const topVisible = position.top > 0 && position.top < windowHeight;
  // 底部边缘可见
  const bottomVisible = position.bottom < windowHeight && position.bottom > 0;
  return topVisible || bottomVisible;
}

function imageLazyLoad() {
  const images = document.querySelectorAll('img')
  for (let img of images) {
    const realSrc = img.dataset.src
    if (!realSrc) continue
    if (isVisible(img)) {
      img.src = realSrc
      img.dataset.src = ''
    }
  }
}

// 测试
window.addEventListener('load', imageLazyLoad)
window.addEventListener('scroll', imageLazyLoad)
// or
window.addEventListener('scroll', throttle(imageLazyLoad, 1000))

22. 匹配url中的参数和Hash

解题技巧与思路

  1. 一般我们对url处理可以直接用原生api window.location.xxx 参数从window.location.search中取,hash从window.location.hash中取
  2. 通过Js的a标签我们可以把一个url地址处理陈类似 window.location.xxx中这种格式。具体原理:

JavaScript的DOM接口为<a>元素提供了一系列属性,这些属性可以用来解析和操作URL。这是一个在浏览器环境下的特性,这里我们借助这个特性来解析URL。

以下是一些主要的属性:

  • href: 这是<a>元素的href属性,可以获取或设置完整的URL。
  • protocol: 这是URL的协议部分(例如,http:,https:)。
  • hostname: 这是URL的主机名部分(例如,www.example.com)。
  • port: 这是URL的端口号部分。
  • pathname: 这是URL的路径部分(例如,/path/to/page)。
  • search: 这是URL的查询参数部分(例如,?key=value&name=123)。
  • hash: 这是URL的hash部分(例如,#section)。

当你创建一个新的<a>元素,并设置其href属性为一个URL时,浏览器会自动解析这个URL,并将各个部分设置到这些属性上。然后你就可以方便地获取这些部分。

需要注意的是,这种方法只在浏览器环境下有效,在非浏览器环境(例如Node.js)下,你需要使用其他的方法来解析URL,例如使用url模块的parse方法。

具体代码

// 函数获取URL参数
function getUrlParams(url) {
    var params = {};
    var parser = document.createElement('a');
    parser.href = url;
    var query = parser.search.substring(1);
    var vars = query.split('&');
    for (var i = 0; i < vars.length; i++) {
        var pair = vars[i].split('=');
        params[pair[0]] = decodeURIComponent(pair[1]);
    }
    return params;
}

// 函数获取URL的hash值
function getUrlHash(url) {
    var parser = document.createElement('a');
    parser.href = url;
    return parser.hash.substring(1); // 去掉前面的 '#'
}

// 函数判断URL参数值是否和hash值匹配
function isParamsMatchHash(url) {
    var params = getUrlParams(url);
    var hash = getUrlHash(url);
    for (var key in params) {
        if (params[key] === hash) {
            return true;
        }
    }
    return false;
}

// 测试用例
var url = "http://example.com/?param1=value1&param2=value2#value1";
console.log(isParamsMatchHash(url)); // 输出: true

23. 利用SetTimeout实现SetInterval、ClearInterval

本题易错点

  1. 返回的timer没有返回对象、而是返回了基本类型
  2. 如果返回的是基本类型,那后续拿到的timer都是旧的
  3. 通过返回引用类型,来保证可以正常清理
function mySetInterval(callback,time){
  const myTimer = {}
  function handleDiff(){
    myTimer.timer = setTimeout(()=>{
      callback()
      handleDiff();
    },time)
  }
  handleDiff()
  return myTimer;
}

function myClearInterval(timer){
  clearTimeout(timer?.timer)
}

参考系列

讶羽专题系列