JavaScript———原理题

发布时间 2023-05-29 00:00:51作者: CD、小月

@

前言

本文针对目前常见的面试题,实现了相应方法的核心原理,部分边界细节未处理。

实现一个call函数

// 通过this获取指定方法,然后挂载在传入的上下文
Function.prototype.myCall = function (context) {
  // 判断指定方法是否为函数
  if (typeof this !== 'function') {
    throw new TypeError('not funciton')
  }

  // 检测上下文是否为空
  context = context || window

  // 上下文保存指定方法
  context.myFun = this

  // 获取其余参数
  let args = [...arguments].slice(1)
  let result = context.myFun(args)

  // 删除该属性
  delete context.myFun
  return result
}

实现一个apply函数

Function.prototype.myApply = function (context) {
  // 判断指定方法是否为函数
  if (typeof this !== 'function') {
    throw new TypeError('not funciton')
  }

  // 检测上下文是否为空
  context = context || window

  // 上下文保存指定方法
  context.myFun = this

  let result
  // 参数判断 其余参数是否为一个数组
  if (arguments[1] && Array instanceof arguments[1]) {
    result = context.myFun(...arguments[1])
  } else {
    result = context.myFun()
  }
  // 删除该属性
  delete context.myFun
  return result
}

实现一个bind函数

Function.prototype.myBind = function (context) {
  // 判断指定方法是否为函数
  if (typeof this !== 'function') {
    throw new TypeError('not funciton')
  }

  // 保存指定方法
  let _this = this

  // 获取初始参数
  let arg = [...arguments].slice(1)

  // 返回绑定了上下文的函数
  return function F(...args) {
    // 处理函数被使用new的情况
    if (this instanceof F) {
      return new _this(...arg, ...args)
    } else {
      return _this.apply(context, arg.concat(...args))
    }
  }
}

instanceof的原理


// 可通过constructor属性比较,但是constructor属性具有被改写的风险
// 用原型和原型对象进行比较
function myInstanceof(obj, cons) {
  let leftVal = obj.__proto__
  let rigthVal = cons.prototype

  while (true) {
    if (leftVal) {
      return false
    }

    if (leftVal === rigthVal) {
      return true
    }

    leftVal = leftVal.__proto__
  }
}

Object.create的基本实现原理


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

new本质

function myNew (fun) {
  return function () {
    // 创建一个新对象且将其隐式原型指向构造函数原型
    let obj = {
      __proto__ : fun.prototype
    }
    // 执行构造函数
    fun.call(obj, ...arguments)
    // 返回该对象
    return obj
  }
}

function person(name, age) {
  this.name = name
  this.age = age
}
let obj = myNew(person)('chen', 18) // {name: "chen", age: 18}

实现一个基本的Promise

  // 判断变量否为function
  const isFunction = variable => typeof variable === 'function'
  // 定义Promise的三种状态常量
  const PENDING = 'PENDING'
  const FULFILLED = 'FULFILLED'
  const REJECTED = 'REJECTED'

  class MyPromise {
    constructor(handle) {
      if (!isFunction(handle)) {
        throw new Error('MyPromise must accept a function as a parameter')
      }
      // 添加状态
      this._status = PENDING
      // 添加状态
      this._value = undefined
      // 添加成功回调函数队列
      this._fulfilledQueues = []
      // 添加失败回调函数队列
      this._rejectedQueues = []
      // 执行handle
      try {
        handle(this._resolve.bind(this), this._reject.bind(this))
      } catch (err) {
        this._reject(err)
      }
    }
    // 添加resovle时执行的函数
    _resolve(val) {
      const run = () => {
        if (this._status !== PENDING) return
        this._status = FULFILLED
        // 依次执行成功队列中的函数,并清空队列
        const runFulfilled = (value) => {
          let cb;
          while (cb = this._fulfilledQueues.shift()) {
            cb(value)
          }
        }
        // 依次执行失败队列中的函数,并清空队列
        const runRejected = (error) => {
          let cb;
          while (cb = this._rejectedQueues.shift()) {
            cb(error)
          }
        }
        /* 如果resolve的参数为Promise对象,则必须等待该Promise对象状态改变后,
          当前Promsie的状态才会改变,且状态取决于参数Promsie对象的状态
        */
        if (val instanceof MyPromise) {
          val.then(value => {
            this._value = value
            runFulfilled(value)
          }, err => {
            this._value = err
            runRejected(err)
          })
        } else {
          this._value = val
          runFulfilled(val)
        }
      }
      // 为了支持同步的Promise,这里采用异步调用
      setTimeout(run, 0)
    }
    // 添加reject时执行的函数
    _reject(err) {
      if (this._status !== PENDING) return
      // 依次执行失败队列中的函数,并清空队列
      const run = () => {
        this._status = REJECTED
        this._value = err
        let cb;
        while (cb = this._rejectedQueues.shift()) {
          cb(err)
        }
      }
      // 为了支持同步的Promise,这里采用异步调用
      setTimeout(run, 0)
    }
    // 添加then方法
    then(onFulfilled, onRejected) {
      const {
        _value,
        _status
      } = this
      // 返回一个新的Promise对象
      return new MyPromise((onFulfilledNext, onRejectedNext) => {
        // 封装一个成功时执行的函数
        let fulfilled = value => {
          try {
            if (!isFunction(onFulfilled)) {
              onFulfilledNext(value)
            } else {
              let res = onFulfilled(value);
              if (res instanceof MyPromise) {
                // 如果当前回调函数返回MyPromise对象,必须等待其状态改变后在执行下一个回调
                res.then(onFulfilledNext, onRejectedNext)
              } else {
                //否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
                onFulfilledNext(res)
              }
            }
          } catch (err) {
            // 如果函数执行出错,新的Promise对象的状态为失败
            onRejectedNext(err)
          }
        }
        // 封装一个失败时执行的函数
        let rejected = error => {
          try {
            if (!isFunction(onRejected)) {
              onRejectedNext(error)
            } else {
              let res = onRejected(error);
              if (res instanceof MyPromise) {
                // 如果当前回调函数返回MyPromise对象,必须等待其状态改变后在执行下一个回调
                res.then(onFulfilledNext, onRejectedNext)
              } else {
                //否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
                onFulfilledNext(res)
              }
            }
          } catch (err) {
            // 如果函数执行出错,新的Promise对象的状态为失败
            onRejectedNext(err)
          }
        }
        switch (_status) {
          // 当状态为pending时,将then方法回调函数加入执行队列等待执行
          case PENDING:
            this._fulfilledQueues.push(fulfilled)
            this._rejectedQueues.push(rejected)
            break
            // 当状态已经改变时,立即执行对应的回调函数
          case FULFILLED:
            fulfilled(_value)
            break
          case REJECTED:
            rejected(_value)
            break
        }
      })
    }
    // 添加catch方法
    catch (onRejected) {
      return this.then(undefined, onRejected)
    }
    // 添加静态resolve方法
    static resolve(value) {
      // 如果参数是MyPromise实例,直接返回这个实例
      if (value instanceof MyPromise) return value
      return new MyPromise(resolve => resolve(value))
    }
    // 添加静态reject方法
    static reject(value) {
      return new MyPromise((resolve, reject) => reject(value))
    }
    // 添加静态all方法
    static all(list) {
      return new MyPromise((resolve, reject) => {
        /**
         * 返回值的集合
         */
        let values = []
        let count = 0
        for (let [i, p] of list.entries()) {
          // 数组参数如果不是MyPromise实例,先调用MyPromise.resolve
          this.resolve(p).then(res => {
            values[i] = res
            count++
            // 所有状态都变成fulfilled时返回的MyPromise状态就变成fulfilled
            if (count === list.length) resolve(values)
          }, err => {
            // 有一个被rejected时返回的MyPromise状态就变成rejected
            reject(err)
          })
        }
      })
    }
    // 添加静态race方法
    static race(list) {
      return new MyPromise((resolve, reject) => {
        for (let p of list) {
          // 只要有一个实例率先改变状态,新的MyPromise的状态就跟着改变
          this.resolve(p).then(res => {
            resolve(res)
          }, err => {
            reject(err)
          })
        }
      })
    }
    finally(cb) {
      return this.then(
        value => MyPromise.resolve(cb()).then(() => value),
        reason => MyPromise.resolve(cb()).then(() => {
          throw reason
        })
      );
    }
  }

参考
Promise实现原理(附源码)
Promise的运行原理以及重写Promise内置类

实现浅拷贝


// 1. ...实现
let copy1 = {...{x:1}}

// 2. Object.assign实现

let copy2 = Object.assign({}, {x:1})

实现一个基本的深拷贝


// 1. JOSN.stringify()/JSON.parse()
let obj = {a: 1, b: {x: 3}}
JSON.parse(JSON.stringify(obj))

// 2. 递归拷贝
function deepClone(obj) {
  let copy = obj instanceof Array ? [] : {}
  for (let i in obj) {
    if (obj.hasOwnProperty(i)) {
      copy[i] = typeof obj[i] === 'object' ? deepClone(obj[i]) : obj[i]
    }
  }
  return copy
}

使用setTimeout模拟setInterval


// 可避免setInterval因执行时间导致的间隔执行时间不一致
setTimeout (function () {
  // do something
  setTimeout (arguments.callee, 500)
}, 500)

实现一个基本的Event Bus

class EventEmitter {
    constructor() {
        this._event = this._event || new Map() // 储存事件
        this._maxListeners = this._maxListeners || 10 // 设置监听上限
    }
}

EventEmitter.prototype.emit = function(type, ...args) {
    let handler = null
    handler = this._event.get(type)

    if (Array.isArray(handler)) {
        // 有多个监听者,需要依次触发
        for (let i = 0; i < handler.length; i++) {
            if (args.length > 0) {
                handler[i].apply(this, args)
            } else {
                handler[i].call(this)
            }
        }
    } else if (handler && typeof handler === 'function') {
        if (args.length > 0) {
            handler.apply(this, args)
        } else {
            handler.call(this)
        }
    }

    return true
}

EventEmitter.prototype.addListener = function(type, fn) {
    let handler = this._event.get(type)

    if (!handler) {
        this._event.set(type, fn)
    } else if (handler && typeof handler === 'function') {
        // 如果handler是函数,说明目前已经存在一个监听者
        this._event.set(type, [handler, fn])
    } else {
        // 已经有多个监听者,直接push
        handler.push(fn)
    }
}

EventEmitter.prototype.removeListener = function(type, fn) {
    let handler = this._event.get(type)

    if (handler && typeof handler === 'function') {
        // 只有一个监听者,直接删除
        this._event.delete(type, fn)
    } else if (Array.isArray(handler)) {
        // 是数组,说明被监听多次,要找到对应的函数
        let position = -1

        for (let i = 0; i < handler.length; i++) {
            if (handler[i] === fn) {
                position = i
            }
        }

        // 如果匹配,从数组中移除
        if (position !== -1) {
            handler.splice(position, 1)

            // 移除后,如果监听只剩一个,那么取消数组,以函数形式保存
            if (handler.length === 1) {
                this._event.set(type, handler[0])
            }
        } else {
            return this
        }
    }
}

实现一个双向数据绑定

let obj = {}
let input = document.getElementById('input')
let span = document.getElementById('span')
Object.defineProperty(obj, 'text', {
    configurable: true,
    enumerable: true,
    get() {
        console.log('获取数据了')
        return obj.text
    },
    set(newVal) {
        console.log('数据更新了')
        input.value = newVal
        span.innerHTML = newVal
    }
})
input.addEventListener('keyup', function(e) {
    obj.text = e.target.value
})

实现一个简单路由


class Route {
    constructor() {
        // 保存所有路由
        this.routes = {}

        // 当前hash
        this.currentHash = ''

        // 绑定this,避免监听时this指向改变
        this.freshRoute = this.freshRoute.bind(this)

        // 初始化监听函数
        window.addEventListener('load', this.freshRoute, false)
        window.addEventListener('hash', this.freshRoute, false)
    }

    // 注册并存储路由
    storeRoute(path, fn) {
        let callback = fn || function() {}
        this.routes[path] = callback
    }

    // hash值改变
    freshRoute() {
        this.currentHash = location.hash.slice(1) || '/'
        this.routes[this.currentHash]()
    }
}

实现懒加载

// html:
// <ul>
//   <li><img src="./imgs/default.png" data="./imgs/1.png" alt=""></li>
//   <li><img src="./imgs/default.png" data="./imgs/2.png" alt=""></li>
//   <li><img src="./imgs/default.png" data="./imgs/3.png" alt=""></li>
//   <li><img src="./imgs/default.png" data="./imgs/4.png" alt=""></li>
//   <li><img src="./imgs/default.png" data="./imgs/5.png" alt=""></li>
//   <li><img src="./imgs/default.png" data="./imgs/6.png" alt=""></li>
//   <li><img src="./imgs/default.png" data="./imgs/7.png" alt=""></li>
//   <li><img src="./imgs/default.png" data="./imgs/8.png" alt=""></li>
//   <li><img src="./imgs/default.png" data="./imgs/9.png" alt=""></li>
//   <li><img src="./imgs/default.png" data="./imgs/10.png" alt=""></li>
// </ul>

let imgs = document.querySelectorAll('img')

// 获取窗口的显示高度
let clientHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight

// 获取滚动条滚动的高度
let scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop

// 懒加载函数
function lazyLoad() {

    // 获取元素到文档顶部的距离
    function getTop(e) {
        let h = e.offsetTop
        while (e = e.offsetParent) {
            h += e.offsetTop
        }
        return h
    }

    imgs.forEach(ele => {
        // 元素顶部到视窗底部的距离差
        let x = scrollTop + clientHeight - getTop(ele)
        if (x > 0 && x < clientHeight + ele.height) {
            ele.src = ele.getAttribute('data')
        }
    })
}


// 也可直接通过 e.getBoundingClientRect 获取元素相对视窗的距离做判断
// function lazyLoad() {

//     function isIn(e) {
//         // 返回元素的大小及其相对于视口的位置。
//         let bound = e.getBoundingClientRect();
//         let is = (bound.top <= clientHeight) && (bound.top > 0)
//         return is
//     }
//     imgs.forEach(ele => {
//         if (is) {
//             ele.src = ele.getAttribute('data')
//         }
//     })
// }

// 定时操作
setInterval(lazyLoad, 1000)

rem实现原理

function setRem () {
  let doc = document.documentElement
  let width = doc.getBoundingClientRect().width
  // 假设设计稿为宽750,则rem为10px
  let rem = width / 75
  doc.style.fontSize = rem + 'px'
}

手写实现AJAX

// 手写实现AJAX

// xhr 简单实现

let xhr = new XMLHttpRequest()

// 初始化
xhr.open(method, url, async)

// 状态检测
// readyState 属性,该属性表示请求/响应过程的当前活动阶段。
// 0:未初始化。尚未调用 open()方法。
// 1:启动。已经调用 open()方法,但尚未调用 send()方法。
// 2:发送。已经调用 send()方法,但尚未接收到响应。
// 3:接收。已经接收到部分响应数据。
// 4:完成。已经接收到全部响应数据,而且已经可以在客户端使用了。
// 只要 readyState 属性的值由一个值变成另一个值,都会触发一次 readystatechange 事件。

// status:响应的 HTTP 状态。
xhr.onreadystatechange = function () {
  if (xhr.readyStatus === 4 && xhr.status === 200) {
    console.log(xhr.responseText)
  }
}

// 发送
xhr.send(data)


// 基于promise
function ajax(options) {
  // 初始化参数
  let url = options.url
  let method = options.method.toLocaleLowerCase() || 'get'
  let async = options.async
  let data = options.data

  let xhr = new XMLHttpRequest()
  if (options.timeout && options.timeout > 0) {
    xhr.timeout = options.timeout
  }

  // 返回Promise结果
  return new Promise((resolve, reject) => {
    // 必须在调用 open()之前指定 onreadystatechange事件处理程序才能确保跨浏览器兼容性。
    xhr.onreadystatechange = function () {
      if (xhr.readyStatus === 4) {
        try {
          if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
            alert(xhr.responseText)
            // 决议成功
            resolve && resolve(xhr.responseText)
          } else {
            // alert('Request was unsuccessful: ' + xhr.status)
            // reject && reject(e)
            resolve && resolve()
          }
        } catch (e) {
          // 决议失败
          reject && reject(e)
          console.log('异常处理')
        }
      }
    }

    // XMLHttpRequest事件
    // loadstart:在接收到响应数据的第一个字节时触发。
    // progress:在接收响应期间持续不断地触发。
    // error:在请求发生错误时触发。
    // abort:在因为调用 abort()方法而终止连接时触发。
    // load:在接收到完整的响应数据时触发。
    // loadend:在通信完成或者触发 error、 abort 或 load 事件后触发。
    // 每个请求都从触发 loadstart 事件开始,接下来是一或多个 progress 事件,
    // 然后触发 error、abort 或 load 事件中的一个,最后以触发 loadend 事件结束。

    // 请求发生错误监听
    xhr.onerror = (err) => reject && reject(err)

    // 超时监听
    xhr.ontimeout = (err) => reject && reject(err)

    // 判断请求方法
    // get方法 拼接查询字符串
    if (method === 'get') {
      let paramArray = []
      let encodeParam

      if (typeof data === 'object') {
        for (let key in data) {
          paramArray.push(
            encodeURIComponent(key) + '=' + encodeURIComponent(data[key])
          )
        }
        encodeParam = paramArray.join('&')
      }
      // 判断是否已有查询字符串 没有添加?号 有则添加&
      url += url.indexOf('?') == '-1' ? '?' : '&'
      // 拼接查询字符串
      url += encodeParam

      xhr.open(method, url, async)
    }

    if (method === 'post') {
      let dataForm = new FormData(data)
      xhr.send(dataForm)
    } else {
      xhr.send(null)
    }
  })
}

参考:
XMLHttpRequest—必知必会
你不知道的 XMLHttpRequest

实现一个节流(throttle)函数

函数频繁操作,在规定时间内只执行一次,在大于等于执行周期时才执行,周期内调用不执行。

// 时间戳版本 比较时间
function throttle(fn, delay) {
  // 利用闭包保存时间
  let prev = Date.now()
  return function () {
    let context = this
    let arg = arguments
    let now = Date.now()
    if (now - prev >= delay) {
      fn.apply(context, arg)
      prev = Date.now()
    }
  }
}


// 定时器版本
function throttle(fn, delay) {
  let time
  return function () {
    let context = this
    let arg = arguments
    if (!time) {
      time = setTimeout(() => {
        time = null
        fn.apply(context, args)
      }, delay)
    }
  }
}


// 精简版

function throttle(fn, delay) {
  let bool = true
  return function () {
    let context = this
    let arg = arguments
    if (bool) {
      fn.apply(context, args)
      bool = false
      setTimeout(() => {
        bool = true
      }, 2000)
    }
  }
}

实现一个防抖(debounce)函数

在函数需要频繁触发时,只有当有足够空闲的时间时,才执行一次。
和节流函数的区别:在发生持续触发事件时,防抖设置事件延迟并在空闲时间去触发事件,而节流则是隔一定的时间触发一次。

function debounce(fn, time) {
  // 利用闭包保存定时器
  let delay
  return function () {
    let context = this
    let arg = arguments
    // 清除定时器
    clearTimeout(delay)
    // 重新指定定时器
    delay = setTimeout(() => {
      fn.apply(context, arg)
    }, time)
  }
}