携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情
输出内容才能更好的理解输入的知识
前言🎀
手写代码是对过往知识的总结,实现的过程即是对知识的复习 绝不是为了面试什么的🙈
希望结合自身的理解能帮到更多的同学
如果觉得有收获还望大家点个赞🌹
题目📖
1. 防抖函数 debounce
规定时间后执行回调,多次调用重新计时,只触发最后一次。一般用 定时器 + 闭包 实现
场景:用户搜索联想,防止点击按钮重复提交,表单校验的触发时机
function debounce(fn, delay = 300) {
let timer = null
return function(...args) {
clearTimeOut(timer)
timer = setTimeOut(() => {
fn.apply(null, [...args])
}, delay)
}
}
2. 节流函数 throttle
持续触发的事件,间隔时间只能生效一次,其余调用无效。通常用 定时器 + 闭包 实现
场景:滚动窗口,搜索联想,resize窗口
function throttle(fn, delay = 300) {
let flag = true
return function(...args) {
if (flag) {
flag = false
fn.apply(null, [...args])
setTimeOut(() => {
flag = true
}, delay)
}
}
}
3. 深拷贝 deepClone
引用类型对象保存的是指向数据地址的指针。
如果使用 =赋值 浅拷贝的对象在修改时会影响到原数据
如果使用 JSON序列化+反序列化 会丢失 方法属性/循环引用/Date、RegExp等特殊对象。
使用 递归 + 条件判断 实现
// 基础版
function deepClone(obj) {
if (typeof obj !== 'object') return obj
const result = Array.isArray(obj) ? [] : {}
for (let [key,value] of Object.entries(obj)) {
if (typeof value !== 'object') {
result[key] = value
} else {
result[key] = deepClone(value)
}
}
return result
}
// 加强版 判断循环引用 null
function deepClone(obj, map = new WeakMap()) {
if (typeof obj !== 'object' || obj === null) return obj
if (map.has(obj)) return map.get(obj)
const result = Array.isArray(obj) ? [] : {}
map.set(obj, result)
for (let [key,value] of Object.entries(obj)) {
if (typeof value !== 'object' || value === null) {
result[key] = value
} else {
result[key] = deepClone(value)
}
}
return result
}
4. new 操作符
new通过构造函数创建了一个实例对象,需要理解的是创建对象时做了哪些操作
1.基于原型新创建一个对象
2.构造函数的this绑定新建的对象和构建参数
3.如果fn有执行结果 且为对象类型返回fn的结果,否则返回新创建的对象
function myNew(fn, ...args) {
let obj = Object.create(fn.prototype)
const res = fn.apply(obj, [...args])
return res && typeof res === 'object' ? res : obj
}
// 测试
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHi = function() { console.log('Hi I am ' + this.name) }
let p1 = new Person('西维', 18)
p1.sayHi()
let p2 = myNew(Person, '青羽', 24)
p2.sayHi()
5. 继承 class
ES6的class其实是 寄生式继承 + 组合式继承 的语法糖
过程中抓住一个重点——JS中继承的核心思想是对原型、属性和方法的拷贝
理解 寄生组合式继承 即可扩散到 寄生式继承 和 组合式继承
原型式继承:利用空构造函数继承目标原型,缺点 被其他实例影响原型 无法传递参数
等同于Object.create()
寄生式继承:在 原型式继承 的基础上增强对象,缺点 同上
// 原型式
function object(obj) {
function F() {}
F.prototype = obj
return new F()
}
// 寄生式
function objectEx(obj) {
let clone = Object.create(obj)
// 增强对象
clone.sayHi = () => console.log('Hi I'm ' + clone.name)
return clone
}
组合式继承:借用构造函数 组合原型+属性&方法,缺点 实例对象和原型中存在相同的属性和方法
// 父类构造函数
function SuperType(name){
// some code
}
// 第一次调用 SuperType() 继承原型
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
function SubType(name, age){
// 第二次调用SuperType() 继承属性
SuperType.call(this, name);
this.age = age;
}
var instance1 = new SubType("Nicholas", 29);
寄生组合式继承:顾名思义 整体思路是借用构造参数+寄生式继承,分别实现拷贝属性、方法+原型
function Parent(name) { //... }
Parent.prototype.sayHi = function() { //... }
......
// 继承原型
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
// 继承属性和方法
function Child(name, age) {
Parent.call(this, name)
this.age = age
}
6. 函数柯里化 currying
接收多个参数的函数 转换为 接收单一参数并多次执行的函数
主要作用:收集状态,延迟执行,参数复用
核心思想:利用闭包记录状态,拆分接收多参数的原函数 在函数内部返回调用下一个单参数的函数
例:sum(1)(2)(3)() = 6 支持多个数据 如sum(1)(2,3)() = 6
第一次容易写卡在判断返回函数还是返回结果,漏掉第一份数据的初始化(就是我🙈)
还有种重写toString的,感兴趣可以自己了解
function sum() {
const nums = [...arguments]
return function add() {
if (arguments.length) {
nums.push(...arguments)
return add
} else {
return nums.reduce((pre, cur) => pre + cur)
}
}
}
可总结出模板:
function currying(fn, ...args) {
const length = fn.length
let allArgs = [...args]
const res = (...newArgs) => {
allArgs = [...allArgs, ...newArgs];
// 判断 函数的参数数量 和 实际接收的参数数量
// 相等 执行原函数 否则 返回函数接收剩余参数
if (allArgs.length === length) {
return fn(...allArgs);
} else {
return res;
}
}
return res;
}
const add = (a, b, c) => a + b + c
const a = currying(add, 1);
console.log(a(2,3))
// currying可以再接收一个参数len代表原函数需要的参数数量,具体了解 函数.length 会返回多少
7. 作用域绑定 call/apply/bind
call、apply
作用:替换目标函数的作用域并执行函数(修改this指向 并传入参数)
区别:call接收多个参数项,apply接收一个参数数组
实现:主要实现方法为将函数赋值给对象的内部属性,利用函数的作用域是在声明时创建的特性
Function.prototype.myCall = function(context, ...args) {
if (typeof this !== 'function') return new Error('type error') // 确保this是函数
context = context || window // 默认指向window
const sym = Symbol() // 生成唯一值
context[sym] = this // this指向调用call的函数
const res = context[sym](...args) // 传入参数并执行
delete context[sym]
return res
}
Function.prototype.myApply(context, args) {
// ....同上
// ...
const res = context[sym](...args) // 传入参数并执行
// ...
// ....同上
}
bind
作用:替换目标函数的作用域并返回函数(修改this指向 并传入参数)
Function.prototype.myBind(context, ...args) {
if (typeof this !== 'function') return new Error('type error')
if (context === null || context === undefined) context = window
else context = Object(context) // 确保context是对象
const self = this
return function Fn(...newArgs) {
// 如果当前函数执行中的this是Fn的实例,说明是new执行的,那么当前this就是函数的实例,否则是context
if (this instanceof Fn) {
return new selft(...args, ...newArgs)
} else {
return self.apply(context, [...args, ...newArgs])
}
}
}
8. 期约对象 Promise
Promise 常见的是对.all/.race原理的考查和对同步异步的理解
理清思路,先简单总结下Promise的特性:
- Promise 译为 期约,它是一个容器 里面装着某个未来才会结束的事件的结果,从它可以获取异步操作的消息
- Promise 有三种状态 pending, fulfilled, rejected,分别代表等待,执行,拒绝
初始状态为pending,状态一经改变无法回退 - Promise 构造函数接收一个函数A作为参数 函数A的两个参数为函数resolve和reject 分别代表Promise进行 执行、拒绝 操作
Promise 创建时调用了构造参数 传入的函数被立即执行, - Promise .then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行
- Promise 的调用流程其实是 观察者模式
then收集依赖 -> resolve/reject变更触发通知 -> 取出对应依赖执行
暂不详述,更多知识建议看大佬写的关于Promise的文章
//promise有3种状态 (pending/Fulfilled/rejected) 默认pending 状态一旦变化不可再逆转
class Promise {
constructor(executor) { //构造器 接收一个回调函数
this.state = 'pending' //默认状态
this.value = undefined; //Fulfilled成功时的值
this.reason = undefined; //Rejected失败时的原因
//使用队列是因为要满足then方法可以被一个promise调用多次 xx.then().then()
this.resolveQueue = [] // then收集的执行成功的回调队列
this.rejectQueue = [] // then收集的执行失败的回调队列
//promise对象内可使用resolve Fulfilled成功promise对象 并返回一个promise对象
let resolve = value => {
if (this.state === 'pending') {
this.state = 'fulfilled'
this.value = value
//成功时调用成功回调队列
while (this.resolveQueue.length) {
const callback = this.resolveQueue.shift()
callback()
}
}
}
//也可使用reject Rejected成功promise对象 并返回一个promise对象
let reject = reason => {
if (this.state === 'pending') {
this.state = 'rejected'
this.reason = reason
while (this.rejectQueue.length) {
const callback = this.rejectQueue.shift()
callback(reason)
}
}
}
//如果 执行器函数 执行报错,直接reject
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
//then方法可以链式调用并需要拿到上一个then返回的值 所以then方法返回一个Promise
//then方法顺序执行 then方法接收一个成功回调和一个失败回调
then(onFulfilled, onRejected) {
//静默处理非函数参数
typeof onFulfilled !== 'function' ? onFulfilled = value => value : null
typeof onRejected !== 'function' ? onRejected = reason => {
throw new Error(reason instanceof Error ? reason.message : reason)
} : null
return new Promise((resolve, reject) => {
//包装 成功回调函数和失败回调函数 再push进resolve队列 是为了能够获取回调的返回值进行分类操作
const fulfilledFn = value => {
try {
//执行第一个Promise的成功回调,并获取返回值
let x = onFulfilled(value)
//分类返回值 如果是Promise 等待Promise状态变更 否则直接resolve
//假设then方法返回Promise(A) 如果then方法的回调函数的返回值是普通值直接resolve(x)返回即可
//但返回值也可能是个Promise(B) 实际应使用Promise(B)返回的值
//因此需要传值 即Promise(B).state ===> Promise(A)
//所以用then去收集他的状态改变,再把Promise(A)的解决函数(resolve/reject)传给它
//这样在Promise(B)解决(resolve)时Promise(B)就解决了 同时它的值就传回给Promise(A)
x instanceof Promise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
//失败回调包装同理
const rejectedFn = error => {
try {
let x = onRejected(error)
x instanceof Promise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
switch (this.state) {
// 当状态为pending时,把then回调push进resolve/reject执行队列,等待执行
case 'pending':
this.resolveQueue.push(onFulfilled)
this.rejectQueue.push(onRejected)
break;
// 当状态已经变为resolve/reject时,直接执行then回调
case 'fulfilled':
fulfilledFn(this.value) // this._value是上一个then回调return的值(见完整版代码)
break;
case 'rejected':
rejectedFn(this.reason)
break;
}
})
}
//finally方法
//在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数 还可以继续then
finally(callback) {
return this.then(
value => MyPromise.resolve(callback()).then(() => value), //执行回调,并returnvalue传递给后面的then
reason => MyPromise.resolve(callback()).then(() => {
throw reason
}) //reject同理
)
}
//静态的resolve方法
//返回一个以给定值解析后的Promise 对象
static resolve(value) {
if (value instanceof MyPromise) return value //根据规范, 如果参数是Promise实例, 直接return这个实例
// 如果是 thenable
if (val && val.then && typeof val.then === 'function') {
const promise = new MyPromise((resolve, reject) => {
try {
val.then(resolve, reject)
} catch (error) {
reject(error)
}
})
return promise
}
return new MyPromise(resolve => resolve(value))
}
//静态的reject方法
//返回一个带有拒绝原因的Promise对象
static reject(reason) {
return new MyPromise((resolve, reject) => reject(reason))
}
//静态的all方法 返回一个 Promise 实例
//iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve)
//如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败原因的是第一个失败 promise 的结果。
static all(promiseArr) {
let index = 0
let result = []
return new MyPromise((resolve, reject) => {
promiseArr.forEach((p, i) => {
//Promise.resolve(p)用于处理传入值不为Promise的情况
MyPromise.resolve(p).then(
val => {
index++
result[i] = val
if (index === promiseArr.length) {
resolve(result)
}
},
err => {
reject(err)
}
)
})
})
}
//静态的race方法 返回一个 promise
//一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝
static race(promiseArr) {
return new MyPromise((resolve, reject) => {
//同时执行Promise,如果有一个Promise的状态发生改变,就变更新MyPromise的状态
for (let p of promiseArr) {
MyPromise.resolve(p).then(
//Promise.resolve(p)用于处理传入值不为Promise的情况
value => {
resolve(value)
//注意这个resolve是上边new MyPromise的
},
err => {
reject(err)
}
)
}
})
}
}
无注释版
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise {
constructor(executor) {
this.status = PENDING
this.value = undefined
this.fulfilledQueue = []
this.rejectedQueue = []
let resolve = value => {
const run = () => {
if (this.status !== PENDING) return
this.status = FULFILLED
this.value = value
while (this.fulfilledQueue.length) {
const callback = this.fulfilledQueue.shift()
callback()
}
}
setTimeout(run)
}
let reject = reason => {
//same as resolve
}
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
then(resolveFn, rejectFn) {
if (typeof resolveFn !== 'function') resolveFn = value => value
if (typeof rejectFn !== 'function') rejectFn = error => {
throw new Error( error instanceof Error ? error.message : error )
}
return new MyPromise((resolve, reject) => {
const fulfilledFn = value => {
let x = resolveFn(value)
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
}
const rejectedFn = reason => {
//same as fulfilledFn
}
switch (this.status) {
case PENDING:
this.fulfilledQueue.push(fulfilledFn)
this.rejectedQueue.push(rejectedFn)
break;
case FULFILLED:
this.rejectedQueue.push(fulfilledFn)
break;
case REJECTED:
this.rejectedQueue.push(rejectedFn)
break;
}
})
}
static resolve(value) {
if (value instanceof MyPromise) return value
return new MyPromise((resolve) => resolve(value))
}
static reject(value) {
return new MyPromise((resolve, reject) => reject(value))
}
catch(rejectFn) {
return this.then(undefined, rejectFn)
}
finally(fn) {
return this.then(
value => MyPromise.resolve(fn()).then(() => value),
reason => MyPromise.resolve(fn()).then(() => { throw reason })
)
}
static all(promiseArr) {
let count = 0
let result = []
return new MyPromise((resolve, reject) => {
promiseArr.forEach((item, idx) => {
MyPromise.resolve(item).then(value => {
count += 1
result[idx] = value
if (count === promiseArr.length) resolve(result)
}).catch(reject)
})
})
}
static race(promiseArr) {
return new MyPromise((resolve, reject) => {
promiseArr.forEach((item, idx) => {
MyPromise.resolve(item).then(value => resolve(value)).catch(reject)
})
})
}
}
9. 数组扁平化 flat
将多维数组转为一维数组,实现方式为 递归 + 判断
还有更好的ES6 数组API reduce 在数组降维和去重中都有不错的表现
function flat(arr) {
if (!arr.length) return
const res = []
for (let item of arr) {
Array.isArray(item) ? res.push(...flat(item)) : res.push(item)
}
return res
}
// reduce
function flat(arr) {
if (!arr.length) return
return arr.reduce((pre, cur) => {
return pre.concat(cur.constructor === Array ? flat(cur) : cur)
}, [])
}
console.log(flat([1,[2,3],[[4,5]]))
10. 检查原型 instanceof
每个对象都有__proto__属性指向它的原型对象(prototype)
而原型对象也是对象也有__proto__属性指向它的原型对象(prototype),这样形成一条原型链
而 instanceof 的原理是 检查左对象的原型链上是否有右对象的原型,实现方式为 循环 + 判断
function _instanceof(leftObj, rightObj) {
let rightProto = rightObj.prototype
let leftProto = leftObj.__proto__
// 迭代左对象原型链上的所有原型对象
while (true) {
if (leftProto === null) return false
if (leftProto === rightProto) return true
leftProto = leftProto.__proto__
}
}
11. 数组去重
需要判断元素是否重复,实现方式有多种 set、reduce、map、filter...
// set
let uniqueArr = arr => [...new Set(arr)]
// reduce
function uniqueArr(arr) {
return arr.reduce((pre, cur) => {
pre.includes(cur) && pre.push(cur)
return pre
}, [])
}
// map
function uniqueArr(arr) {
let map = new Map()
let res = []
for (let item of arr) {
if (map.has(item)) continue
map.set(item, 1)
res.push(item)
}
return res
}
// filter
let uniqueArr = arr => arr.filter((item, index) => arr.indexOf(item) === index)
12. 发布订阅模式 EventEmeitter
发布订阅模式是node事件驱动的核心,也是Vue框架响应式更新的重点
它属于设计模式中三大类型之一的行为型模式
行为型模式关注对象之间的交互,研究运行时对象之间相互通信和协作,进一步明确对象职责
与 观察者模式 的区别
观察者模式 定义了对象之间的一对多的依赖,当一个对象改变状态时,它的所有依赖者都会收到通知并更新 —— Head First设计模式
发布订阅模式 则是在 订阅者Watcher、观察者Observer 两者之间增加了事件中心Dep
Watcher与Observer之间没有直接的关系。发布者无需关注订阅者,当对象改变时,会直接通知到Dep
class EventEmeitter {
constructor() {
// 准备一个数据结构缓存订阅者信息
this._events = Object.create(null)
}
// 添加订阅方法
on(type, callback) {
// 判断当前事件是否已存在,决定如何缓存
if (this._events[type]) {
this._events[type].push(callback)
} else {
this._events[type] = [callback]
}
}
// 移除订阅方法
off(type, callback) {
if (!this._events[type]) return
this._events[type] = this._events[type].filter(item => item !== callback)
}
// 触发订阅更新
emit(type, ...args) {
if (this._events[type]?.length) {
// 获取对应type缓存的回调函数执行
this._events[type].forEach(fn => fn.call(this, ...args))
}
}
// 只触发一次的订阅事件
once(type, callback) {
function fn() {
callback()
this.off(type, fn)
}
this.on(type, fn)
}
}
let event = new EventEmeitter()
let outParam = (...args) => console.log(...args)
event.on('事件1', outParam)
event.on('事件1', () => console.log('事件1-2'))
event.emit('事件1', { name: '西维', age: '18' })
event.off('事件1', outParam)
event.emit('事件1', { name: '青羽', age: '24' })
event.once('事件2', () => console.log('delete after emit'))
event.emit('事件2')
event.emit('事件2')
13. 睡眠函数 sleep
阻塞代码执行流程指定时间,原理是用 async/await + 定时器 + 期约 在某个时间过后恢复执行流程
function sleep(delay = 100) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(), delay)
})
}
console.log('start,' + Date.now())
await sleep(3000)
console.log('end,' + Date.now())
14. Array 与 Tree 互相转换
偏算法,但确实是实际开发中经常遇到的问题,有很多种实现方式
数组转树
采用 map结构,迭代数组所有元素item
- 判断item是否有pid(=0) 且 map中是否存在key为pid的元素
- 没有pid(=0)则将其放到结果数组中
const arr = [
{id: 1, pid: 0, content: 'xx1'},
{id: 2, pid: 1, content: 'xx1-xx2'},
{id: 3, pid: 1, content: 'xx1-xx3'},
{id: 4, pid: 2, content: 'xx1-xx2-xx4'},
{id: 5, pid: 2, content: 'xx1-xx2-xx5'},
{id: 6, pid: 4, content: 'xx1-xx2-xx4-xx6'},
{id: 7, pid: 0, content: 'xx7'},
{id: 8, pid: 7, content: 'xx7-xx8'},
{id: 9, pid: 0, content: 'xx9'},
]
function arrToTree(arr) {
let map = new Map()
let res = []
for (let item of arr) {
let param = { children: [], ...item }
map.set(item.id, param)
if (item.pid && map.has(item.pid)) {
map.get(item.pid).children.push(param)
} else if (!item.pid) {
res.push(param)
}
}
return res
}
arrToTree(arr)
树转数组
采用 bfs/dfs 都行,具体原理偏算法 不懂的同学可以去了解下 或者等我有空了写篇算法文章🙈
function treeToArr(tree) {
let res = []
let stack = []
for (let item of tree) {
stack.push(item)
}
while (stack.length) {
let node = stack.pop()
res.push(node)
if (node.children?.length) stack.push(...node.children)
}
return res
}
treeToArr(arrToTree(arr))
15. 块级作用域 let、const
块级作用域的变量只在当前作用域及子作用域可以被访问, 通过 立即执行函数 模拟实现
// let
(function() {
var a = 1
consoloe.log(a)
})()
// const
var f = Object.freeze({'name':'admin'});
结语🎉
不要光看不实践哦,后续会持续更新前端和算法相关的知识
写作不易,如果觉得有收获欢迎大家点个赞谢谢🌹
顺便给自己写的动态规划引流一下😆感兴趣的同学看看
才疏学浅,如果文章有什么问题欢迎大家指教