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

前言

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

实现 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)) 
console.log(curriedAdd(1, 2)(3)) 
console.log(curriedAdd(1)(2, 3)) 

获取 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){
  
  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() {
  
  let req = new XMLHTTPRequest()
  if (req) {
    
    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个测试用例[1]

手写 instanceof

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

function myInstanceof(obj, constructor) {
  while (obj !== null) {
    
    if (obj.__proto__ === constructor.prototype) {
      return true 
    }
    obj = obj.__proto__ 
  }
  return false 
}

手写 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)
  
  const key = Symbol()
  ctx[key] = this
  
  const res = ctx[key](...args)
  
  delete ctx[key]
  
  return res
}

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

foo._call(undefined) 
foo._call(null) 
foo._call(1) 
foo._call('11') 
foo._call(true) 
foo._call(obj) 
console.log(foo._call(obj)) 

  • apply

apply 传参为数组形式

Function.prototype._apply = function (ctx, args = []) {
  if (ctx == null) ctx = globalThis
  if (typeof ctx !== 'object') ctx = new Object(ctx)
  
  const key = Symbol()
  ctx[key] = this
  
  const res = ctx[key](...args)
  
  delete ctx[key]
  
  return res
}

手写 bind

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

Function.prototype.myBind = function (context, ...args1) {
  const self = this 
  return function (...args2) {
    return self.apply(context, [...args1, ...args2]) 
  }
}

手写一个深度比较 isEqual

function isEqual(obj1, obj2) {
  
  if (typeof obj1 !== 'object' || typeof obj2 !== 'object') {
    return obj1 === obj2
  }
  
  if (obj2 === obj1) return true
  
  let keys1 = Object.keys(obj1)
  let keys2 = Object.keys(obj2)
  
  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)) 

实现 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)) 

console.log(_flat(list)) 

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

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
}


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
}


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)) 
console.log(list.myFilter((i) => i > 3)) 
console.log(list.myMap((i) => i + 1)) 

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) 

观察者模式

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()



发布订阅模式

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) {
    
    const wrapper = (...args) => {
      callback.apply(this, args)
      this.off(eventName, wrapper)
    }
    
    this.on(eventName, wrapper)
  }
}

const emitter = new EventEmitter()


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


emitter.on('event2', function () {
  console.log('event2 is triggered')
})


emitter.emit('event1', 'hello world')


emitter.off('event1')


emitter.emit('event1', 'foo bar')


emitter.emit('event2')

const emitter = new EventEmitter()


emitter.once('event1', function (data) {
  console.log(`event1 is triggered with data: ${data}`)
})


emitter.emit('event1', 'hello world')


emitter.emit('event1', 'foo bar')

最后

如果你现在正在找工作,可以私信“web”进群领取前端面试小册、简历优化修改、大厂内推以及更多阿里、字节大厂面试真题合集,和p8大佬一起交流。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Web面试那些事儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值