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

尾调极昼
• 阅读 441

实现 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]
      }
    }
  }
}
点赞
收藏
评论区
推荐文章
菜园前端 菜园前端
2年前
前端学习重点-原型与原型链
原文链接:什么是原型?每一个函数都包含一个prototype属性,这是一个指针指向"原型对象"(prototypeobject),也就是我们平时说的原型。每一个函数都包含不同的原型对象。当将函数用作构造函数的时候,新创建的对象(实例)会从原型对象上继承属性
Chase620 Chase620
4年前
Promise从入门到拿Offer之手写Promise
1、Promise构造函数的实现Promise构造函数用来声明示例对象,需要传入一个执行器函数。其中包括resolve函数和reject函数,以及几个重要的属性:状态属性、结果属性和回调函数队列。构造函数的基本框架resolve函数用于异步处理成功后调用的函数。其中包括验证对象状态修改次数,修改promise实例对象状态,异步调用成功的回调函数
明月 明月
2年前
JS数据类型的几种判断方法
1.typeof(常用)typeof是一个运算符,返回值是一个字符串,用来说明变量的数据类型,可以用来判断number,string,object,boolean,function,undefined,symbol这七种类型.2.instanceof(知道即可)instanceof运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上,返回值为布尔值,用于指示一个变量是否属于某个对象的实例3.Object.prototype.to
Easter79 Easter79
3年前
SpringIOC官方文档解读
IoC容器本章介绍了Spring的控制反转(IoC)容器。1.1。SpringIoC容器和Bean简介本章介绍了反转控制(IoC)原则的Spring框架实现。IoC也称为依赖注入(DI)。在此过程中,对象可以通过①构造函数参数(),②工厂方法的参数③或在构造或从工厂方法返回后在对象实例上设置的属性来定义其依
可莉 可莉
3年前
23.JavaScript原型和原型链
1.原型:prototype,所有的函数都有一个属性prototype,称之为函数原型默认情况下,prototype是一个普通的Object对象默认情况下,prototype中有一个属性constructor,它也是一个对象,它指向构造函数本身functionTest(){}Test.prototype:函数Test的原型Test.pr
Wesley13 Wesley13
3年前
JS函数高级
原型与原型链所有函数都有一个特别的属性:prototype:显式原型属性所有实例对象都有一个特别的属性:__proto__:隐式原型属性显式原型与隐式原型的关系函数的prototype:定义函数时被自动赋值,值默认为{},即用为原型对象
Wesley13 Wesley13
3年前
C++中构造函数和析构函数
构造函数定义它是一种特殊的方法。主要用来在创建对象时初始化对象,即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。另外,一个类可以有多个构造函数,我们可以根据其参数个数的不同或参数类型的不同来区分它们(这就是构造函数的重载)特点1.构造函数的命名必须和类名完全相同;2.构造函数的功能主要用于在类的对象创建时定义
Stella981 Stella981
3年前
Javascript 构造函数和类
1.构造函数构造函数的名称一般都是首字母大写挂载在this上面的属性为实例属性,实例属性再每个实例间都是独立的原型链属性通过prototype添加,他是所有实例共享的类方法/静态属性只能由构造函数本身访问当实例属性和原型链上的属性重名时,优先访问实例属性,没有实例属性再访问原型属性大多数浏览器的ES5实现之中,每一个对象都有\_\_pr
Stella981 Stella981
3年前
Python中函数装饰器及练习
1)装饰器的理解:1、作用:在不改变原函数的基础上,给函数增加功能   2、返回值:把一个函数当作参数,返回一个替代版的函数3、本质:返回函数的函数4、应用场景:计时器、记录日志、用户登陆认证、函数参数认证2)无参函数装饰器  实例:被装饰的函数没有参数     执行结果为:  
小万哥 小万哥
1年前
Java 构造函数与修饰符详解:初始化对象与控制权限
Java构造函数Java构造函数是一种特殊的类方法,用于在创建对象时初始化对象的属性。它与类名相同,并且没有返回值类型。构造函数的作用:为对象的属性设置初始值执行必要的初始化操作提供创建对象的多种方式构造函数的类型:默认构造函数:无参数的构造函数,如果用户
小万哥 小万哥
1年前
C++ 构造函数实战指南:默认构造、带参数构造、拷贝构造与移动构造
C构造函数构造函数是C中一种特殊的成员函数,当创建类对象时自动调用。它用于初始化对象的状态,例如为属性分配初始值。构造函数与类同名,且没有返回值类型。构造函数类型C支持多种类型的构造函数,用于满足不同的初始化需求:默认构造函数:不带参数的构造函
尾调极昼
尾调极昼
Lv1
纵然万劫不复纵然相思入骨我也待你眉眼如初岁月如故.
文章
5
粉丝
0
获赞
0