关于javascript:几个常见的js手写题你能写出来几道

35次阅读

共计 10996 个字符,预计需要花费 28 分钟才能阅读完成。

实现 new 过程:

要点:

  1. 函数第一个参数是构造函数
  2. 实例的__proto__指向构造函数的原型属性 prototype
  3. 函数残余参数要挂载到一个实例对象上
  4. 构造函数有返回值时,就返回这个返回值
const createObj = function () {let obj = {}
  let Constructor = [].shift.call(arguments) // 1
  obj.__proto__ = Constructor.prototype // 2
  let ret = Constructor.apply(obj, arguments) // 3
  return typeof ret === 'object' ? ret: obj // 4
}

// 应用
const Fun = function (name) {this.name = name}
Fun.prototype.getName = function() {alert(this.name)
}
let fun = createObj(Fun, 'gim')
fun.getName() // gim

值得注意的是,es6 的 class 必须用 new 调用,否则会报错,如下:

class Fun {constructor(name) {this.name = name}
  getName() {alert(this.name)
  }
}
let fun = createObj(Fun, 'gim')
fun.getName() // Uncaught TypeError: Class constructor Fun cannot be invoked without 'new'

手写 call、apply 及 bind 函数

共同点:

  1. 第一个参数是要绑定的 this
  2. 函数外部的 this 其实是要执行绑定的函数(因为三者都是点调用)

bind

这里实现简略版本(new 调用后果不一样)

  1. bind 函数执行后,要返回一个原函数的拷贝
  2. 给返回函数外部的 fn 绑定传入的 context
Function.prototype.myBind = function(context, ...args) {if (typeof this !== 'function') throw 'caller must be a function'
  const fn = this
  return function() {return fn.call(context, ...args, ...arguments)
  }
}

callapply 函数的实现其实都借助了点调用。利用第一个参数做个直达,调用完之后删除。

call

Function.prototype.myCall = function(context = windows, ...args) {
  context._fn = this
  const result = context._fn(...args)
  delete context._fn
  return result
}

apply

Function.prototype.myApply = function(context = windows, args) {
  context._fn = this
  const result = context._fn(args)
  delete context._fn
  return result
}
参考面试题解答参见 前端手写面试题具体解答

节流和防抖

刚开始接触这俩概念的时候傻傻分不清楚。

浏览器的一些事件,如:resize,scroll,keydown,keyup,keypress,mousemove 等。这些事件触发频率太过频繁,绑定在这些事件上的回调函数会不停的被调用。会减轻浏览器的累赘,导致用户体验十分蹩脚。

节流防抖次要是利用了闭包。

节流

节流函数来让函数每隔 n 毫秒触发一次。

// 节流
function throttle (f, wait = 200) {
  let last = 0
  return function (...args) { // 以下 外部匿名函数 均是指这个匿名函数
    let now = Date.now()
    if (now - last > wait) {
      last = now
      f.apply(this, args) // 留神此时 f 函数的 this 被绑定成了外部匿名函数的 this,这是很有用的
    }
  }
}
// 未节流
input.onkeyup = funciton () {$.ajax(url, this.value)
}
// 节流
input.onkeyup = throttle(function () {// throttle() 返回外部匿名函数,所以 input 被绑定到了外部匿名函数的 this 上
  $.ajax(url, this.value) // 留神这个 this 在执行时被 apply 到了外部匿名函数上的 this,也就是 input
})

防抖

防抖函数让函数在 n 毫秒内只触发最初一次。

// 防抖
function debounce (f, wait = 200) {
  let timer = 0
  return function (...args) {clearTimeout(timer)
    timer = setTimeout(() => {f.apply(this, args)
    }, wait)
  }
}
// 未防抖
input.onkeyup = funciton () {$.ajax(url, this.value)
}
// 防抖
input.onkeyup = debounce(function () {// debounce() 返回外部匿名函数,所以 input 被绑定到了外部匿名函数的 this 上
  $.ajax(url, this.value) // 留神这个 this 在执行时被 apply 到了外部匿名函数上的 this,也就是 input
})

柯里化函数

柯里化能够利用函数和不同的参数形成性能更加专一的函数。

柯里化其实就是利用闭包的技术将函数和参数一次次缓存起来,等到参数凑够了就执行函数。

function curry(fn, ...rest) {
  const length = fn.length
  return function() {const args = [...rest, ...arguments]
    if (args.length < length) {return curry.call(this, fn, ...args)
    } else {return fn.apply(this, args)
    }
  }
}
function add(m, n) {return m + n}
const add5 = curry(add, 5)

Promise

要点:

  1. 三种状态的扭转:pending fulfilled rejected
  2. resolve() reject() 函数的实现
  3. 关键点 then 链式调用的实现
class MyPromise {constructor(fn) {
    this.status = 'pending'
    this.value = null
    this.resolve = this._resolve.bind(this)
    this.reject = this._reject.bind(this)
    this.resolvedFns = []
    this.rejectedFns = []
    try {fn(this.resolve, this.reject)
    } catch (e) {this.catch(e)
    }
  }
  _resolve(res) {setTimeout(() => {
      this.status = 'fulfilled'
      this.value = res
      this.resolvedFns.forEach(fn => {fn(res)
      })
    })
  }
  _reject(res) {setTimeout(() => {
      this.status = 'rejected'
      this.value = res
      this.rejectedFns.forEach(fn => {fn(res)
      })
    })
  }
  then(resolvedFn, rejecetedFn) {return new MyPromise(function(resolve, reject) {this.resolveFns.push(function(value) {
        try {const res = resolvedFn(value)
          if (res instanceof MyPromise) {res.then(resolve, reject)
          } else {resolve(res)
          }
        } catch (err) {reject(err)
        }
      })
      this.rejectedFns.push(function(value){
        try {const res = rejectedFn(value)
          if (res instanceof MyPromise) {res.then(resolve, reject)
          } else {reject(res)
          }
        } catch (err) {reject(err)
        }
      })
    })
  }
  catch(rejectedFn) {return this.then(null, rejectedFn)
  }
}

this.resolvedFnsthis.rejectedFns 中寄存着 then 函数的参数的解决逻辑,待 Promise 操作有了后果就会执行。

then函数返回一个 Promise 实现链式调用。

其实面试的时候次要靠死记硬背,因为有一次 20 分钟让我写 5 个实现(包含 promise),,,谁给你思考的工夫。。。

深拷贝

乞丐版的

function deepCopy(obj) {
  // 判断是否是简略数据类型,if (typeof obj == "object") {
    // 简单数据类型
    var result = obj.constructor == Array ? [] : {};
    for (let i in obj) {result[i] = typeof obj[i] == "object" ? deepCopy(obj[i]) : obj[i];
    }
  } else {
    // 简略数据类型 间接 == 赋值
    var result = obj;
  }
  return result;
}

观察者模式和公布订阅模式

观察者模式观察者 Observer 和主体 Subject 都比拟清晰,而公布订阅模式的公布和订阅都由一个调度核心来解决,发布者和订阅者界线含糊。

观察者模式存在耦合,主体中存储的是观察者实例,而 notify 办法遍历时调用了观察者的 update 办法。而公布订阅模式是齐全解耦的,因为调度核心中存的间接就是逻辑处理函数。

要点:都要实现增加 / 删除 / 派发更新三个事件。

观察者模式

class Subject {constructor() {this.observers = []
  }
  add(observer) {this.observers.push(observer)
    this.observers = [...new Set(this.observers)]
  }
  notify(...args) {this.observers.forEach(observer => observer.update(...args))
  }
  remove(observer) {
    let observers = this.observers
    for (let i = 0, len = observers.length; i < len; i++) {if (observers[i] === observer) observers.splice(i, 1)
    }
  }
}

class Observer {update(...args) {console.log(...args)
  }
}

let observer_1 = new Observer() // 创立观察者 1
let observer_2 = new Observer()
let sub = new Subject() // 创立主体
sub.add(observer_1) // 增加观察者 1
sub.add(observer_2)
sub.notify('I changed !')

公布订阅模式

这里应用了还在提案阶段的 class 的公有属性 #handlers,然而支流浏览器已反对。

class Event {
  // 首先定义一个事件容器,用来装事件数组(因为订阅者能够是多个)#handlers = {}

  // 事件增加办法,参数有事件名和事件办法
  addEventListener(type, handler) {
    // 首先判断 handlers 内有没有 type 事件容器,没有则创立一个新数组容器
    if (!(type in this.#handlers)) {this.#handlers[type] = []}
    // 将事件存入
    this.#handlers[type].push(handler)
  }

  // 触发事件两个参数(事件名,参数)dispatchEvent(type, ...params) {
    // 若没有注册该事件则抛出谬误
    if (!(type in this.#handlers)) {return new Error('未注册该事件')
    }
    // 便当触发
    this.#handlers[type].forEach(handler => {handler(...params)
    })
  }

  // 事件移除参数(事件名,删除的事件,若无第二个参数则删除该事件的订阅和公布)removeEventListener(type, handler) {
    // 有效事件抛出
    if (!(type in this.#handlers)) {return new Error('有效事件')
    }
    if (!handler) {
      // 间接移除事件
      delete this.#handlers[type]
    } else {const idx = this.#handlers[type].findIndex(ele => ele === handler)
      // 抛出异样事件
      if (idx === -1) {return new Error('无该绑定事件')
      }
      // 移除事件
      this.#handlers[type].splice(idx, 1)
      if (this.#handlers[type].length === 0) {delete this.#handlers[type]
      }
    }
  }
}

要点:

  1. 函数第一个参数是构造函数
  2. 实例的__proto__指向构造函数的原型属性 prototype
  3. 函数残余参数要挂载到一个实例对象上
  4. 构造函数有返回值时,就返回这个返回值
const createObj = function () {let obj = {}
  let Constructor = [].shift.call(arguments) // 1
  obj.__proto__ = Constructor.prototype // 2
  let ret = Constructor.apply(obj, arguments) // 3
  return typeof ret === 'object' ? ret: obj // 4
}

// 应用
const Fun = function (name) {this.name = name}
Fun.prototype.getName = function() {alert(this.name)
}
let fun = createObj(Fun, 'gim')
fun.getName() // gim

值得注意的是,es6 的 class 必须用 new 调用,否则会报错,如下:

class Fun {constructor(name) {this.name = name}
  getName() {alert(this.name)
  }
}
let fun = createObj(Fun, 'gim')
fun.getName() // Uncaught TypeError: Class constructor Fun cannot be invoked without 'new'

手写 call、apply 及 bind 函数

共同点:

  1. 第一个参数是要绑定的 this
  2. 函数外部的 this 其实是要执行绑定的函数(因为三者都是点调用)

bind

这里实现简略版本(new 调用后果不一样)

  1. bind 函数执行后,要返回一个原函数的拷贝
  2. 给返回函数外部的 fn 绑定传入的 context
Function.prototype.myBind = function(context, ...args) {if (typeof this !== 'function') throw 'caller must be a function'
  const fn = this
  return function() {return fn.call(context, ...args, ...arguments)
  }
}

callapply 函数的实现其实都借助了点调用。利用第一个参数做个直达,调用完之后删除。

call

Function.prototype.myCall = function(context = windows, ...args) {
  context._fn = this
  const result = context._fn(...args)
  delete context._fn
  return result
}

apply

Function.prototype.myApply = function(context = windows, args) {
  context._fn = this
  const result = context._fn(args)
  delete context._fn
  return result
}
参考面试题解答参见 前端手写面试题具体解答

节流和防抖

刚开始接触这俩概念的时候傻傻分不清楚。

浏览器的一些事件,如:resize,scroll,keydown,keyup,keypress,mousemove 等。这些事件触发频率太过频繁,绑定在这些事件上的回调函数会不停的被调用。会减轻浏览器的累赘,导致用户体验十分蹩脚。

节流防抖次要是利用了闭包。

节流

节流函数来让函数每隔 n 毫秒触发一次。

// 节流
function throttle (f, wait = 200) {
  let last = 0
  return function (...args) { // 以下 外部匿名函数 均是指这个匿名函数
    let now = Date.now()
    if (now - last > wait) {
      last = now
      f.apply(this, args) // 留神此时 f 函数的 this 被绑定成了外部匿名函数的 this,这是很有用的
    }
  }
}
// 未节流
input.onkeyup = funciton () {$.ajax(url, this.value)
}
// 节流
input.onkeyup = throttle(function () {// throttle() 返回外部匿名函数,所以 input 被绑定到了外部匿名函数的 this 上
  $.ajax(url, this.value) // 留神这个 this 在执行时被 apply 到了外部匿名函数上的 this,也就是 input
})

防抖

防抖函数让函数在 n 毫秒内只触发最初一次。

// 防抖
function debounce (f, wait = 200) {
  let timer = 0
  return function (...args) {clearTimeout(timer)
    timer = setTimeout(() => {f.apply(this, args)
    }, wait)
  }
}
// 未防抖
input.onkeyup = funciton () {$.ajax(url, this.value)
}
// 防抖
input.onkeyup = debounce(function () {// debounce() 返回外部匿名函数,所以 input 被绑定到了外部匿名函数的 this 上
  $.ajax(url, this.value) // 留神这个 this 在执行时被 apply 到了外部匿名函数上的 this,也就是 input
})

柯里化函数

柯里化能够利用函数和不同的参数形成性能更加专一的函数。

柯里化其实就是利用闭包的技术将函数和参数一次次缓存起来,等到参数凑够了就执行函数。

function curry(fn, ...rest) {
  const length = fn.length
  return function() {const args = [...rest, ...arguments]
    if (args.length < length) {return curry.call(this, fn, ...args)
    } else {return fn.apply(this, args)
    }
  }
}
function add(m, n) {return m + n}
const add5 = curry(add, 5)

Promise

要点:

  1. 三种状态的扭转:pending fulfilled rejected
  2. resolve() reject() 函数的实现
  3. 关键点 then 链式调用的实现
class MyPromise {constructor(fn) {
    this.status = 'pending'
    this.value = null
    this.resolve = this._resolve.bind(this)
    this.reject = this._reject.bind(this)
    this.resolvedFns = []
    this.rejectedFns = []
    try {fn(this.resolve, this.reject)
    } catch (e) {this.catch(e)
    }
  }
  _resolve(res) {setTimeout(() => {
      this.status = 'fulfilled'
      this.value = res
      this.resolvedFns.forEach(fn => {fn(res)
      })
    })
  }
  _reject(res) {setTimeout(() => {
      this.status = 'rejected'
      this.value = res
      this.rejectedFns.forEach(fn => {fn(res)
      })
    })
  }
  then(resolvedFn, rejecetedFn) {return new MyPromise(function(resolve, reject) {this.resolveFns.push(function(value) {
        try {const res = resolvedFn(value)
          if (res instanceof MyPromise) {res.then(resolve, reject)
          } else {resolve(res)
          }
        } catch (err) {reject(err)
        }
      })
      this.rejectedFns.push(function(value){
        try {const res = rejectedFn(value)
          if (res instanceof MyPromise) {res.then(resolve, reject)
          } else {reject(res)
          }
        } catch (err) {reject(err)
        }
      })
    })
  }
  catch(rejectedFn) {return this.then(null, rejectedFn)
  }
}

this.resolvedFnsthis.rejectedFns 中寄存着 then 函数的参数的解决逻辑,待 Promise 操作有了后果就会执行。

then函数返回一个 Promise 实现链式调用。

其实面试的时候次要靠死记硬背,因为有一次 20 分钟让我写 5 个实现(包含 promise),,,谁给你思考的工夫。。。

深拷贝

乞丐版的

function deepCopy(obj) {
  // 判断是否是简略数据类型,if (typeof obj == "object") {
    // 简单数据类型
    var result = obj.constructor == Array ? [] : {};
    for (let i in obj) {result[i] = typeof obj[i] == "object" ? deepCopy(obj[i]) : obj[i];
    }
  } else {
    // 简略数据类型 间接 == 赋值
    var result = obj;
  }
  return result;
}

观察者模式和公布订阅模式

观察者模式观察者 Observer 和主体 Subject 都比拟清晰,而公布订阅模式的公布和订阅都由一个调度核心来解决,发布者和订阅者界线含糊。

观察者模式存在耦合,主体中存储的是观察者实例,而 notify 办法遍历时调用了观察者的 update 办法。而公布订阅模式是齐全解耦的,因为调度核心中存的间接就是逻辑处理函数。

要点:都要实现增加 / 删除 / 派发更新三个事件。

观察者模式

class Subject {constructor() {this.observers = []
  }
  add(observer) {this.observers.push(observer)
    this.observers = [...new Set(this.observers)]
  }
  notify(...args) {this.observers.forEach(observer => observer.update(...args))
  }
  remove(observer) {
    let observers = this.observers
    for (let i = 0, len = observers.length; i < len; i++) {if (observers[i] === observer) observers.splice(i, 1)
    }
  }
}

class Observer {update(...args) {console.log(...args)
  }
}

let observer_1 = new Observer() // 创立观察者 1
let observer_2 = new Observer()
let sub = new Subject() // 创立主体
sub.add(observer_1) // 增加观察者 1
sub.add(observer_2)
sub.notify('I changed !')

公布订阅模式

这里应用了还在提案阶段的 class 的公有属性 #handlers,然而支流浏览器已反对。

class Event {
  // 首先定义一个事件容器,用来装事件数组(因为订阅者能够是多个)#handlers = {}

  // 事件增加办法,参数有事件名和事件办法
  addEventListener(type, handler) {
    // 首先判断 handlers 内有没有 type 事件容器,没有则创立一个新数组容器
    if (!(type in this.#handlers)) {this.#handlers[type] = []}
    // 将事件存入
    this.#handlers[type].push(handler)
  }

  // 触发事件两个参数(事件名,参数)dispatchEvent(type, ...params) {
    // 若没有注册该事件则抛出谬误
    if (!(type in this.#handlers)) {return new Error('未注册该事件')
    }
    // 便当触发
    this.#handlers[type].forEach(handler => {handler(...params)
    })
  }

  // 事件移除参数(事件名,删除的事件,若无第二个参数则删除该事件的订阅和公布)removeEventListener(type, handler) {
    // 有效事件抛出
    if (!(type in this.#handlers)) {return new Error('有效事件')
    }
    if (!handler) {
      // 间接移除事件
      delete this.#handlers[type]
    } else {const idx = this.#handlers[type].findIndex(ele => ele === handler)
      // 抛出异样事件
      if (idx === -1) {return new Error('无该绑定事件')
      }
      // 移除事件
      this.#handlers[type].splice(idx, 1)
      if (this.#handlers[type].length === 0) {delete this.#handlers[type]
      }
    }
  }
}

正文完
 0