Skip to main content

1.js高级手撕题

1实现防抖函数(debounce)

防抖动是将多次执行变为最后一次执行

const debounce = (func, wait = 50) => {
let timer = 0
return function (...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}

适用场景:

  • 文本输入的验证,连续输入文字后发送 AJAX 请求进行验证,验证一次就好
  • 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
  • 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似

2 实现节流函数(throttle)

节流是将多次执行变成每隔一段时间执行

时间戳方式:

使用时间戳的节流函数会在第一次触发事件时立即执行,以后每过 wait 秒之后才执行一次,并且最后一次触发事件不会被执行

const throttle = (func, wait = 50) => {
let lastTime = 0
return function (...args) {
let now = +new Date()
if (now - lastTime > wait) {
lastTime = now
func.apply(this, args)
}
}
}

定时器方式:

使用定时器的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,当最后一次停止触发后,还会再执行一次函数

const throttle => (func, delay){
var timer = 0;
return function(...args){
if(timer) return // 当前有任务了,直接返回
timer = setTimeout(()=>{
func.apply(this, args);
timer = 0;
},delay);
}
}

适用场景:

  • 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动。DOM 元素的拖拽功能实现(mousemove
  • 缩放场景:监控浏览器resize
  • 滚动场景:监听滚动scroll事件判断是否到页面底部自动加载更多
  • 动画场景:避免短时间内多次触发动画引起性能问题

3. 实现instanceOf

思路:

  • 步骤 1:先取得当前类的原型,当前实例对象的原型链
  • 步骤 2:一直循环(执行原型链的查找机制)
    • 取得当前实例对象原型链的原型链(proto = proto.__proto__,沿着原型链一直向上查找)
    • 如果 当前实例的原型链__proto__上找到了当前类的原型prototype,则返回 true
    • 如果 一直找到Object.prototype.__proto__ == nullObject的基类(null)上面都没找到,则返回 false
function _instanceof(instance, classOrFunc) {
// 由于instance要检测的是某对象,需要有一个前置判断条件
//基本数据类型直接返回false
if (typeof instance !== "object" || instance == null) return false
let proto = Object.getPrototypeOf(instance)
while (proto) {
if (proto == classOrFunc.prototype) return true
proto = Object.getPrototypeof(proto)
}
return false
}

console.log("test", _instanceof(null, Array)) // false
console.log("test", _instanceof([], Array)) // true
console.log("test", _instanceof("", Array)) // false
console.log("test", _instanceof({}, Object)) // true

4 实现 new 的过程

new 操作符做了这些事:

  • 创建一个全新的对象obj,继承构造函数的原型:这个对象的__proto__要指向构造函数的原型prototype
  • 执行构造函数,使用 call/apply 改变 this 的指向(将obj作为this
  • 返回值为object类型则作为new方法的返回值返回,否则返回上述全新对象obj
function myNew(constructor, ...args) {
let newObj = Object.create(constructor.prototype)
let res = constructor.apply(newObj, args)
return typeof res === "object" ? res : newObj
}

function Person(name, age) {
this.name = name
this.age = age
}
let p1 = myNew(Person, "poety", 18)

5 实现 call 方法

call 做了什么:

  • 将函数设为对象的属性
  • 执行和删除这个函数
  • 指定this到函数并传入给定参数执行函数
  • 如果不传入参数,默认指向 window
Function.prototype.call = function (context, ...args) {
context = !context
? window
: typeof context !== "object"
? new Object(context)
: context
let fnKey = Symbol()
context[fnKey] = this
let result = context[fnKey](...args)
delete context[fnKey]
return result
}

6 实现 apply 方法

Function.prototype.apply = function (context, args = []) {
context = !context
? window
: typeof context !== "object"
? new Object(context)
: context
const fnKey = Symbol()
context[fnKey] = this
const result = context[fnKey](...args)
delete context[fnKey]
return result
}

7 实现 bind 方法

  • bind 返回了一个函数,对于函数来说有两种方式调用,一种是直接调用,一种是通过 new 的方式

  • 对于直接调用来说,这里选择了 apply 的方式实现,因为 bind 可以实现类似这样的代码 f.bind(obj, 1)(2),所以我们需要将两边的参数拼接起来

  • 最后来说通过 new 的方式,对于 new 的情况来说,不会被任何方式改变 this,所以对于这种情况我们需要忽略传入的 this

  • 箭头函数的底层是bind,无法改变this,只能改变参数

Function.prototype.bind = function (context, ...args) {
let that = this //// fn.bind(obj) that就是fn
function fBound(...innerArgs) {
//this instanceof fBound为true表示构造函数的情况。如new func.bind(obj)
// 当作为构造函数时,this 指向实例,此时 this instanceof fBound 结果为 true,可以让实例获得来自绑定函数的值
// 当作为普通函数时,this 默认指向 window,此时结果为 false,将绑定函数的 this 指向 context
return that.apply(
this instanceof fBound ? this : context,
args.concat(innerArgs)
)
}
fBound.prototype = Object.create(this.prototype)
return fBound
}
//测试用例
var obj = {
name: "zs",
}
function normalFun(name) {
console.log(this.name)
}
var bindNormalFun = normalFun.bind(obj, "ls")
bindNormalFun()

function Person(name, age) {
console.log("Person name:", name)
console.log("Person age:", age)
console.log("Person this:", this) // 构造函数this指向实例对象
}
Person.prototype.say = function () {
console.log("person say")
}
var bindFun = Person.myBind(obj, "poetry1") // undefined
var a = new bindFun(10) // Person name: poetry1、Person age: 10、Person this: fBound {}
a.say() // person say

8 实现深拷贝

浅拷贝

let obj1 = {
name: "zs",
other: {
age: 15,
},
}
const obj2 = { ...obj1 }
//------------------
const arr = [[1, 5, 6]]
const arr2 = arr.slice()

深拷贝简单版

const newObj = JSON.parse(JSON.stringify(oldObj))

局限性:

  • 无法解决循环引用的问题,拷贝会出现系统栈溢出,因为出现了无限递归的情况。

  • 无法拷贝一些特殊的对象,诸如 RegExp, Date, Set, Map

  • 无法拷贝函数。

  • 会丢失 undefiend

深拷贝进阶版

  • 解决拷贝循环引用问题
  • 解决拷贝对应原型问题
// 递归拷贝 (类型判断)
function deepClone(oldObj, hash = new WeakMap()) {
// 弱引用,不用map,weakMap更合适一点
// null 和 undefiend 是不需要拷贝的
if (oldObj == null) {
return oldObj
}
if (oldObj instanceof RegExp) {
return new RegExp(oldObj)
}
if (oldObj instanceof Date) {
return new Date(oldObj)
}
// 函数是不需要拷贝
if (typeof oldObj != "object") return oldObj
let newObj = new oldObj.constructor() // [] {}
// 说明是一个对象类型
if (hash.get(oldObj)) {
return hash.get(oldObj)
}
hash.set(oldObj, newObj)
for (let key in oldObj) {
// in 会遍历当前对象上的属性 和 __proto__指代的属性
// 补拷贝 对象的__proto__上的属性
if (oldObj.hasOwnProperty(key)) {
// 如果值还有可能是对象 就继续拷贝
newObj[key] = deepClone(oldObj[key], hash)
}
}
return newObj
// 区分对象和数组 Object.prototype.toString.call
}
// test

var o = {}
o.x = o
var o1 = deepClone(o) // 如果这个对象拷贝过了 就返回那个拷贝的结果就可以了
console.log(o1)

深拷贝完整版

const getType = (obj) => Object.prototype.toString.call(obj)

const isObject = (target) =>
(typeof target === "object" || typeof target === "function") &&
target !== null

const canTraverse = {
"[object Map]": true,
"[object Set]": true,
"[object Array]": true,
"[object Object]": true,
"[object Arguments]": true,
}
const mapTag = "[object Map]"
const setTag = "[object Set]"
const boolTag = "[object Boolean]"
const numberTag = "[object Number]"
const stringTag = "[object String]"
const symbolTag = "[object Symbol]"
const dateTag = "[object Date]"
const errorTag = "[object Error]"
const regexpTag = "[object RegExp]"
const funcTag = "[object Function]"

const handleRegExp = (target) => {
const { source, flags } = target
return new target.constructor(source, flags)
}

const handleFunc = (func) => {
// 箭头函数直接返回自身
if (!func.prototype) return func
const bodyReg = /(?<={)(.|\n)+(?=})/m
const paramReg = /(?<=\().+(?=\)\s+{)/
const funcString = func.toString()
// 分别匹配 函数参数 和 函数体
const param = paramReg.exec(funcString)
const body = bodyReg.exec(funcString)
if (!body) return null
if (param) {
const paramArr = param[0].split(",")
return new Function(...paramArr, body[0])
} else {
return new Function(body[0])
}
}

const handleNotTraverse = (target, tag) => {
const Ctor = target.constructor
switch (tag) {
case boolTag:
return new Object(Boolean.prototype.valueOf.call(target))
case numberTag:
return new Object(Number.prototype.valueOf.call(target))
case stringTag:
return new Object(String.prototype.valueOf.call(target))
case symbolTag:
return new Object(Symbol.prototype.valueOf.call(target))
case errorTag:
case dateTag:
return new Ctor(target)
case regexpTag:
return handleRegExp(target)
case funcTag:
return handleFunc(target)
default:
return new Ctor(target)
}
}

const deepClone = (target, map = new WeakMap()) => {
if (!isObject(target)) return target
let type = getType(target)
let cloneTarget
if (!canTraverse[type]) {
// 处理不能遍历的对象
return handleNotTraverse(target, type)
} else {
// 这波操作相当关键,可以保证对象的原型不丢失!
let ctor = target.constructor
cloneTarget = new ctor()
}

if (map.get(target)) return target
map.set(target, true)

if (type === mapTag) {
//处理Map
target.forEach((item, key) => {
cloneTarget.set(deepClone(key, map), deepClone(item, map))
})
}

if (type === setTag) {
//处理Set
target.forEach((item) => {
cloneTarget.add(deepClone(item, map))
})
}

// 处理数组和对象
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = deepClone(target[prop], map)
}
}
return cloneTarget
}