写点什么

几个常见的 js 手写题,你能写出来几道

  • 2022 年 10 月 04 日
    浙江
  • 本文字数:8278 字

    阅读完需:约 27 分钟

实现 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() // 创建观察者1let observer_2 = new Observer()let sub = new Subject() // 创建主体sub.add(observer_1) // 添加观察者1sub.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() // 创建观察者1let observer_2 = new Observer()let sub = new Subject() // 创建主体sub.add(observer_1) // 添加观察者1sub.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] } } }}
复制代码


用户头像

还未添加个人签名 2022.07.31 加入

还未添加个人简介

评论

发布
暂无评论
几个常见的js手写题,你能写出来几道_JavaScript_helloworld1024fd_InfoQ写作社区