2023高频前端面试之手写代码篇

3,563 阅读9分钟

前言

本文主要整理了一些前端面试中常见的手写题,分享给大家一起来学习。如有错误的地方,也欢迎指正!

实现 sleep 函数

作用:暂停 JavaScript 的执行一段时间后再继续执行

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

// 使用
async function test() {
  console.log('start')
  await sleep(2000)
  console.log('end')
}
test()

函数柯里化

柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术

函数柯里化定义:接收一部分参数,返回一个函数接收剩余参数,接收足够参数后,执行原函数。

实现:

const myCurried = (fn, ...args) => {
  if (args.length < fn.length) {
    // 未接受完参数
    return (..._args) => myCurried(fn, ...args, ..._args)
  } else {
    // 接受完所有参数,直接执行
    return fn(...args)
  }
}

function add(a, b, c) {
  return a + b + c
}


const curriedAdd = myCurried(add)

console.log(curriedAdd(1)(2)(3)) // 输出 6
console.log(curriedAdd(1, 2)(3)) // 输出 6
console.log(curriedAdd(1)(2, 3)) // 输出 6

获取 URL 参数

  • 字符串分割
function getParams() {
  const params = {};
  const search = location.search.substring(1); // 去掉问号
  const pairs = search.split('&'); // 按 & 分割参数

  for (let i = 0; i < pairs.length; i++) {
    const pair = pairs[i].split('=');
    const key = decodeURIComponent(pair[0]); // 解码参数名
    const value = decodeURIComponent(pair[1] || ''); // 解码参数值(如果没有值,则默认为 "")
    params[key] = value; // 存储为对象属性
  }

  return params;
}
  • 使用 URLSearchParams
const getSearchParams = () => {
  const search = new URLSearchParams(window.location.search)
  const paramsObj = {}
  for (const [key, value] of search.entries()) {
    paramsObj[key] = value
  }
  return paramsObj
}

手写 new 的执行过程

首先我们知道 new 的执行过程如

  1. 创建一个空对象
  2. 将对象的 __proto__ 指向构造函数的 prototype
  3. 将这个对象作为构造函数的 this
  4. 确保返回值为对象,如果构造函数返回了一个对象,则返回该对象;否则返回步骤 1 中创建的空对象。
function myNew(Con, ...arg) {
  let obj = Object.create(Con.prototype)
  let result = Con.apply(obj, arg)
  return typeof result === 'object' ? result : obj
}

手写实现 Object.create()

Object.create() 是创建一个新对象并将其原型设置为指定对象的方法

function(obj){
  // 参数必须是一个对象或 null
  if (typeof obj !== "object" && typeof obj !== "function") {
    throw new TypeError("Object prototype may only be an Object or null.");
  }
  // 创建一个空的构造函数
  function F(){}
  // 将构造函数的原型指向传入的对象
  F.prototype = obj
  // 返回一个新的实例对象,该对象的原型为传入的对象
  return new F()
}

首先检查参数是否是一个对象或 null,因为只有这两种情况才能作为对象的原型。然后它创建一个空函数 F,并将其原型设置为传入的参数对象,最后返回用 F 创建的新对象,它的原型是传入的参数对象。

防抖

使用场景:input 搜索

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

节流

使用场景:滚动页面 scroll 函数

function throttle(fn, delay = 500) {
  let timer
  return function (...args) {
    if (timer) return
    timer = setTimeout(() => {
      fn.apply(this, args)
      timer = null
    }, delay)
  }
}

// 时间戳实现
function throttle(fn, delay = 500) {
  let start = +Date.now()
  return function (...args) {
    const now = +Date.now()
    if (now - start >= delay) {
      fn.apply(this, ...args)
      start = now
    }
  }
}

实现深拷贝

  • 简单版
function deepClone(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }
  const newObj = Array.isArray(obj) ? [] : {};
  for (const key in obj) {
    if (Object.prototype.hasOwnProperty(key)) {
      newObj[key] = deepClone(obj[key]);
    }
  }
  return newObj;
}

  • 考虑更多情况
function deepClone(obj, hash = new WeakMap()) {
  if (Object(obj) !== obj) {
    return obj; // 基本数据类型直接返回
  }

  if (hash.has(obj)) {
    return hash.get(obj); // 避免循环引用
  }

  let cloneObj;
  const Constructor = obj.constructor;

  switch (Constructor) {
    case RegExp:
      cloneObj = new Constructor(obj);
      break;
    case Date:
      cloneObj = new Constructor(obj.getTime());
      break;
    default:
      cloneObj = new Constructor();
  }

  hash.set(obj, cloneObj); // 存储克隆对象,用于避免循环引用

  // 遍历对象并克隆属性
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }

  return cloneObj;
}

手写 ajax

可能算一个老面试题了,近几年面试可能比较少遇到

function get() {
  //创建ajax实例
  let req = new XMLHTTPRequest()
  if (req) {
    //执行open 确定要访问的链接 以及同步异步
    req.open('GET', 'http://test.com/?keywords=手机', true)
    //监听请求状态
    req.onreadystatechange = function () {
      if (req.readystate === 4) {
        if (req.statue === 200) {
          console.log('success')
        } else {
          console.log('error')
        }
      }
    }
    //发送请求
    req.send()
  }
}

手写 Promise

要实现一个符合 Promises/A+ 规范的 Promise,说实话真挺难的(非常难),这里附一个参考手把手一行一行代码教你“手写Promise“,完美通过 Promises/A+ 官方872个测试用例

手写 instanceof

instanceof 用于判断一个对象是否是某个构造函数(或者其原型链上)的实例

function myInstanceof(obj, constructor) {
  while (obj !== null) {
    // 直到 obj === null (原型链底端)时停止循环
    if (obj.__proto__ === constructor.prototype) {
      return true // obj 的原型是构造函数的原型,它是构造函数的实例
    }
    obj = obj.__proto__ // 沿着原型链向上查找
  }
  return false // obj 不是构造函数的实例
}

手写 apply call

两者区别:传参不同,apply 第二个参数为数组,a 开头,参数也是 arrry 形式,call 后边参数为函数本身的参数,一个个传

在非严格模式下使用 call 或者 apply 时,如果第一个参数被指定为 null 或 undefined,那么函数执行时的 this 指向全局对象(浏览器环境中是 window);如果第一个参数被指定为原始值,该原始值会被包装。

  • call
/**
 *
 * 如果传入值类型,返回对应类型构造函数创建的实例
 * 如果传入对象,则返回对象本身
 * 如果传入 undefined 或者 null 会返回空对象
 */
Function.prototype._call = function (ctx, ...args) {
  if (ctx == null) ctx = globalThis
  if (typeof ctx !== 'object') ctx = new Object(ctx)
  //给context新增一个独一无二的属性以免覆盖原有属性
  const key = Symbol()
  ctx[key] = this
  // 立即执行一次
  const res = ctx[key](...args)
  // 删除这个属性,防止污染
  delete ctx[key]
  // 把函数的返回值赋值给_call的返回值
  return res
}

let name = '树哥'
const obj = {
  name: 'shuge',
}
function foo() {
  console.dir(this)
  return 'success'
}

foo._call(undefined) // window
foo._call(null) // window
foo._call(1) // Number
foo._call('11') // String
foo._call(true) // Boolean
foo._call(obj) // {name: 'shuge'}
console.log(foo._call(obj)) // success
  • apply

apply 传参为数组形式

Function.prototype._apply = function (ctx, args = []) {
  if (ctx == null) ctx = globalThis
  if (typeof ctx !== 'object') ctx = new Object(ctx)
  //给context新增一个独一无二的属性以免覆盖原有属性
  const key = Symbol()
  ctx[key] = this
  // 立即执行一次
  const res = ctx[key](...args)
  // 删除这个属性
  delete ctx[key]
  // 把函数的返回值赋值给_apply的返回值
  return res
}

手写 bind

bind() 方法会创建一个新函数,当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一系列参数将会在传递的实参前传入作为它的参数。

Function.prototype.myBind = function (context, ...args1) {
  const self = this // 当前的函数本身
  return function (...args2) {
    return self.apply(context, [...args1, ...args2]) // apply 参数数组
  }
}

手写一个深度比较 isEqual

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) {
    //递归比较键值对
    let res = isEqual(obj1[k], obj2[k])
    if (!res) 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

实现 jsonp

其原理是通过动态创建<script>标签,给该标签设置 src 属性,以达到跨域请求数据的目的。服务端应返回一段 JavaScript 代码,并在其中调用回调函数,将请求到的数据作为参数传入回调函数中,从而实现数据的传递。

function jsonp(url, callbackName, success) {
  const script = document.createElement('script')
  script.src = `${url}?callback=${callbackName}`
  document.body.appendChild(script)
  window['callbackName'] = (response) => {
    success(response)
    document.body.removeChild(script)
  }
}

jsonp(url, 'handleResponse', (response) => console.log(response))

// 服务端将返回响应
handleResponse({
  name: '树哥',
  age: 18,
})

使用 setTimeout 实现 setInterval

function mySetinterval(callback, interval) {
  let timeoutId = null
  function repeat() {
    callback()
    timeoutId = setTimeout(repeat, interval)
  }

  repeat()

  return {
    cancel: () => clearTimeout(timeoutId),
  }
}

将 timeoutId 设置为 null,并在 repeat 函数内用 setTimeout 更新它的值,以确保始终拥有最新的 ID。在返回时,返回一个包含 cancel() 方法的对象,该方法使用 clearTimeout 函数取消定时器。

使用:

const interval = mySetinterval(() => console.log('Hello'), 1000)

setTimeout(() => {
  interval.cancel()
}, 5000)

实现数组扁平化

function _flat(arr, depth) {
  if (!Array.isArray(arr) || depth <= 0) {
    return arr
  }
  return arr.reduce((prev, cur) => {
    if (Array.isArray(cur)) {
      return prev.concat(_flat(cur, depth - 1))
    } else {
      return prev.concat(cur)
    }
  }, [])
}

使用:

const list = [1, 2, [3, 4, [5]], 6, 7]

console.log(_flat(list, 1)) // [1, 2, 3, 4, [5], 6, 7]

console.log(_flat(list)) // [1, 2, 3, 4, 5, 6, 7]

实现数组的 push、filter、map 方法

// push
Array.prototype.myPush = function (...args) {
  const length = this.length
  for (let i = 0; i < args.length; i++) {
    this[this.length + i] = args[i]
  }
  return this.length
}

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

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

const list = [1, 2, 3, 4, 5]

console.log(list.myPush(6)) // 6
console.log(list.myFilter((i) => i > 3)) //[ 4, 5, 6 ]
console.log(list.myMap((i) => i + 1)) // [ 2, 3, 4, 5, 6, 7 ]

js 实现红黄绿循环打印

  • 用 promise 实现
const task = (timer, light) =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      if (light === 'red') {
        red()
      } else if (light === 'green') {
        green()
      } else if (light === 'yellow') {
        yellow()
      }
      resolve()
    }, timer)
  })
const step = () => {
  task(1000, 'red')
    .then(() => task(2000, 'green'))
    .then(() => task(3000, 'yellow'))
    .then(step)
}
step()
  • 用 async/await 实现
const task = (light, timeout) => {
  return new Promise((resolve) => {
    setTimeout(() => resolve(console.log(light)), timeout)
  })
}

const taskRunner = async () => {
  await task('red', 1000)
  await task('green', 2000)
  await task('yellow', 3000)
  taskRunner()
}

taskRunner()

用 promise 如何实现异步加载图片

  1. 创建一个 Promise 对象,该对象包含一个异步操作,例如加载图片。
  2. 在异步操作中,使用 Image 对象加载图片,并监听其 onload 和 onerror 事件。
  3. 如果图片加载成功,调用 resolve 方法,并将 Image 对象作为参数传递给 resolve 方法。
  4. 如果图片加载失败,调用 reject 方法,并将错误信息作为参数传递给 reject 方法。
  5. 在使用异步操作时,调用 Promise 对象的 then 方法,如果图片加载成功,则在 then 方法中获取 Image 对象并使用它;如果图片加载失败,则在 catch 方法中处理错误。
function loadImg(url) {
  return new Promise((resolve, reject) => {
    const img = new Image()
    img.url = url
    img.onload = () => {
      resolve(img)
    }
    img.onerror = () => {
      reject(`图片加载失败-${url}`)
    }
  })
}

// 使用
loadImg('xxx.png')
  .then((res) => {
    console.log(res)
  })
  .catch((error) => {
    console.log(error)
  })

设计一个图片懒加载 SDK

const throttle = (fn, delay) => {
  let timer = null
  return function (...args) {
    if (!timer) {
      setTimeout(() => {
        fn.apply(this, args)
        timer = null
      }, delay)
    }
  }
}

const lazyLoadImages = () => {
  const images = document.querySelectorAll('img[data-src]')
  images.forEach((img) => {
    const rect = img.getBoundingClientRect()
    if (rect.top < window.innerHeight) {
      img.src = img.dataSet.src
      img.removeAttribute('data-src')
    }
  })
}

const throttledLazyLoad = throttle(lazyLoadImages, 100)

window.addEventListener('scroll', throttledLazyLoad)

实现单例模式

确保一个类只有一个实例

场景:Redux/Vuex 中的 store、JQ 的$

let cache
class A {
  // ...
}

function getInstance() {
  if (cache) return cache
  return (cache = new A())
}

const x = getInstance()
const y = getInstance()
console.log(x === y) // true

观察者模式

class Subject {
  constructor() {
    this.observers = []
  }

  addObserver(observer) {
    this.observers.push(observer)
  }
  removeObserver(observer) {
    this.observers = this.observers.filter((item) => item !== observer)
  }

  notify() {
    this.observers.forEach((observer) => observer.update())
  }
}

class Observer {
  constructor(data) {
    this.data = data
  }
  update() {
    console.log('data:', this.data)
  }
}

// 创建主题对象
const subject = new Subject()

// 创建两个观察者对象
const observer1 = new Observer('Hello啊,树哥!')
const observer2 = new Observer('Hello')

// 将观察者添加到主题对象中
subject.addObserver(observer1)
subject.addObserver(observer2)

// 通知观察者
subject.notify()
// data: Hello啊,树哥!
// data: Hello

发布订阅模式

class EventEmitter {
  constructor() {
    this.events = {}
  }

  on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = []
    }
    this.events[eventName].push(callback)
  }

  emit(eventName, ...args) {
    const callbacks = this.events[eventName] || []
    callbacks.forEach((cb) => cb.apply(this, args))
  }

  off(eventName, callback) {
    const callbacks = this.events[eventName] || []
    const index = callbacks.indexOf(callback)
    if (index !== -1) {
      callbacks.splice(index, 1)
    }
  }

  // 只监听一次事件
  once(eventName, callback) {
    // 定义一个新函数 wrapper,它接收任意数量的参数,并在调用原始回调函数后通过 off() 方法将自己从订阅者集合中移除。
    const wrapper = (...args) => {
      callback.apply(this, args)
      this.off(eventName, wrapper)
    }
    // 传入的是封装后的 wrapper 函数
    this.on(eventName, wrapper)
  }
}

const emitter = new EventEmitter()

// 订阅 'event1' 事件
emitter.on('event1', function (data) {
  console.log(`event1 is triggered with data: ${data}`)
})

// 订阅 'event2' 事件
emitter.on('event2', function () {
  console.log('event2 is triggered')
})

// 触发 'event1' 事件
emitter.emit('event1', 'hello world')

// 移除订阅的 'event1' 事件
emitter.off('event1')

// 再次触发 'event1' 事件,但不会执行任何回调函数
emitter.emit('event1', 'foo bar')

// 再次触发 'event2' 事件
emitter.emit('event2')

const emitter = new EventEmitter()

// 只监听一次 'event1' 事件
emitter.once('event1', function (data) {
  console.log(`event1 is triggered with data: ${data}`)
})

// 触发 'event1' 事件
emitter.emit('event1', 'hello world')

// 再次触发 'event1' 事件,但不会执行任何回调函数
emitter.emit('event1', 'foo bar')

往期回顾

15分钟学会 pnpm monorepo+vue3+vite 搭建组件库并发布到私有仓库(人人都能学会)
用微前端 qiankun 接入十几个子应用后,我遇到了这些问题
vue3 正式发布两年后,我才开始学 — vue3+setup+ts 🔥
2022年了,我才开始学 typescript ,晚吗?(7.5k字总结)
当我们对组件二次封装时我们在封装什么
vue 项目开发,我遇到了这些问题
关于首屏优化,我做了哪些