1. 防抖
防抖:在事件被触发的 n 秒后再执行,如果这 n 秒内又被触发,则重新计时。
要求写一个可以实现首次立即执行的防抖函数,缺少返回值
/**
* 防抖函数,可立即执行,可传参,但无返回值
* @param {Function} fn 需要防抖的函数
* @param {Number} delay 防抖的时间
* @param {Boolean} immediate 是否立即执行
* @returns
*/
const debounce = (fn, delay = 800, immediate = false) => {
let timer = null;
return function(...args) {
if(timer !== null) clearTimeout(timer);
if(immediate && timer === null) {
fn.apply(this, args);
timer = setTimeout(()=> timer = null, delay);
return;
}
timer = setTimeout(()=>{
fn.apply(this, args);
clearTimeout(timer);
timer = null;
}, delay)
}
}
debounce 内部通过 apply 或 call 方式来调用原函数,可以使原函数 this 指向不变,前提是新函数必须挂载到原函数的对象上,比如
o.b = debounce(o.a)
使用场景:
- 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
- 输入场景:搜索联想词功能类似
2. 节流
节流:在规定单位时间内,只能触发一次函数。如果多次触发则只生效一次。
/**
* 节流函数
* @param {Function} fn 需要节流的函数
* @param {Number} delay 节流的时间
* @returns
*/
const throttle = (fn, delay) => {
let previous = 0;
return function (...args) {
const now = Date.now();
if (now - previous > delay) {
previous = now;
return fn.apply(this, args);
}
}
}
3. 深拷贝
简单版
const cloneDeep = (source) => JSON.parse(JSON.stringify(source));
局限性:
- 时间对象会变成字符串
- 函数或者 undefined 会丢失
- RegExp、NodeList 等特殊对象会变成空对象
- NaN、 Infinity、 -Infinity 会变成 null
- 会抛弃对象的 constructor,所有的构造函数会指向 Object
对象有循环引用,会报错
const a = {}
a.next = a;
cloneDeep(a);
面试版
支持 Date、RegExp、Function、Symbol、NaN、无穷值、循环引用的深拷贝 ```javascript /**
- 数据类型:
- 可以直接返回的:
- Number、String、Boolean、undefined、Symbol、BigInt、Function
- null(需要特殊判断) *
- 不可以直接返回的:
- Object、Array
- Object 的特殊细分类型:Date、RegExp *
- 避免循环引用 */ const deepClone = (obj, hash = new WeakMap()) => { if (obj === null) return obj if (obj instanceof Date) return new Date(obj) if (obj instanceof RegExp) return new RegExp(obj) if (typeof obj !== ‘object’) return obj
if (hash.has(obj)) return hash.get(obj)
const target = Array.isArray(obj) ? [] : {} hash.set(obj, target)
Reflect.ownKeys(obj).forEach(key => { target[key] = deepClone(obj[key], hash) })
return target }
// 测试用例 const a = {}
const obj = { name: ‘foo’, reg: /456/, date: new Date(), add: () => { }, goods: [1, 2, 3, 4, 5], circleReference: a, symbol: Symbol(), NaN: NaN, Null: null, Undefined: undefined }
a.circleReference = obj
const newObj = deepClone(obj) console.log(newObj) console.log(obj.symbol === newObj.symbol)
<a name="kDUEb"></a>
# 4. Promise
<a name="Wd7XK"></a>
## 简单版
```javascript
function MyPromise(executor) {
this.status = 'pending';
const resolve = (result) => {
if (this.status === 'pending') {
this.status = 'fulfilled';
this.result = result;
}
}
const reject = (err) => {
if (this.status === 'pending') {
this.status = 'rejected';
this.result = err;
}
}
try {
executor(resolve, reject);
} catch(err) {
reject(err);
}
}
Mypromise.prototype.then = function (onFulfilled, onRejected) {
if(this.status === 'fulfilled') onFulfilled && onFulfilled(this.result);
if(this.status === 'rejected') onRejected && onRejected(this.result);
}
5. 柯里化 (curry)
把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术
const curry = function (fn, ...firstArgs) {
return function (...args) {
// 首次柯里化时,若未提供firstArgs,则不拼接进args
if (firstArgs.length) args = firstArgs.concat(args);
// 递归调用,若 args 参数长度不满足函数 fn 的参数要求,则将参数传入,并柯里化并返回
if (args.length < fn.length) return curry(fn, ...args);
// 递归出口,执行函数
return fn.apply(null, args)
}
}
// 应用
function multiFn(a, b, c) {
return a * b * c;
}
var multi = curry(multiFn);
multi(2)(3)(4);
multi(2,3,4);
multi(2)(3,4);
multi(2,3)(4)
6. 函数组合(compose)
function fn1(x) {
return x + 1;
}
function fn2(x) {
return x - 2;
}
function fn3(x) {
return x * 3;
}
function fn4(x) {
return x + 4;
}
// 相当于 fn1(fn2(fn3(fn4(x))))
const a = compose(fn1, fn2, fn3, fn4);
console.log(a(1)); // 14
// 经过 fn4 = 1 + 4 = 5
// 经过 fn3 = 5 * 3 = 15
// 经过 fn2 = 15 - 2 = 13
// 经过 fn1 = 13 + 1 = 14
const compose = (...fn) => {
if (fn.length === 0) return a => a
if (fn.length === 1) return fn[0]
return fn.reduce((pre, cur) => (...args) => pre(cur(...args)))
}
7. 并发的调度器
JS 实现一个带并发限制的异步调度器 Scheduler,保证同时运行的任务最多有两个。完善代码中Scheduler类,使得以下程序能正确输出
// 实现这个类
class Scheduler {
}
const timeout = (time) => new Promise(resolve => {
setTimeout(resolve, time)
})
const scheduler = new Scheduler();
const addTask = (time, order) => {
scheduler.add(() => timeout(time))
.then(() => console.log(order))
}
addTask(1000, '1')
addTask(500, '2')
addTask(300, '3')
addTask(400, '4')
// output: 2 3 1 4
// 一开始,1、2两个任务进入队列
// 500ms时,2完成,输出2,任务3进队
// 800ms时,3完成,输出3,任务4进队
// 1000ms时,1完成,输出1
// 1200ms时,4完成,输出4
class Scheduler {
constructor(max) {
this.max = max || 2;
this.queue = []; // 等待执行的任务队列
this.tasks = []; // 正在执行的任务
}
add(task) {
return new Promise((resolve, reject) => {
try {
task.resolve = resolve; // 存放 resolve 函数,当 task 执行后可以使用
if (this.tasks.length < this.max) this.run(task);
else this.queue.push(task);
} catch (error) {
reject(error);
}
})
}
run(task) {
this.tasks.push(task);
task().then((res) => {
// 异步任务执行完,resolve 执行完的值
task.resolve(res);
this.clearTask(task);
})
}
// 清除执行完的任务,并把缓存的任务加入任务队列
clearTask(task) {
const index = this.tasks.indexOf(task);
this.tasks.splice(index, 1);
if (this.queue.length) this.run(this.queue.shift())
}
}
8. 订阅者模式(EventEmitter)
EventEmiiter 既是 node 中各个模块的基石,又是前端组件通信的依赖手段之一,同时涉及了订阅-发布设计模式,是非常重要的基础。
题目描述:实现一个发布订阅模式拥有 on emit once off 方法
// 实现的使用如下
const event = new EventEmitter();
const handle = (...rest) => {
console.log(rest);
};
event.on("click", handle);
event.emit("click", 1, 2, 3, 4);
event.off("click", handle);
event.emit("click", 1, 2);
event.once("dbClick", () => {
console.log(123456);
});
event.emit("dbClick");
event.emit("dbClick");
class EventEmitter {
construtor() {
this.events = {}
}
// 订阅类型为 type 的事件,callback 是回调函数
on(type, callback) {
if (this.events[type]) this.events[type].push(callback)
else this.events[type] = [callback]
}
// 取消订阅类型为 type 的事件,原本的回调函数是 callback
off(type, callback) {
if (this.events[type]) return new Error('不存在此类型');
this.events[type] = this.events[type].filter(f => f !== callback);
}
// 发布订阅,触发事件
emit(type, ...args) {
if (this.events[type]) return new Error('不存在此类型');
this.events[type].forEach(f => f.apply(this, args))
}
// 添加只执行一次的订阅事件
once(type, callback) {
function onceFn(...args) {
callback.apply(this, args);
this.off(type, onceFn);
}
this.on(type, onceFn);
}
}
9. Function.prototype.call
Function.prototype.Call = function (context, ...args) {
// 函数使用该方法时,this 指向该函数
context = context || window;
// 利用隐式绑定的规则,谁调用,函数上下文就属于谁
const fnSym = Symbol();
context[fnSym] = this // 把函数添加到该对象上
const res = context[fnSym](...args);
delete context[fnSym]; // 把添加到对象上的函数删除
return res;
}
10. Function.prototype.apply
Function.prototype.Apply = function (context, args) {
if (args !== undefined && !Array.isArray(args)) return new Error('Apply 第二个参数应该是数组');
args = args || [];
const fnSym = Symbol();
context[fnSym] = this;
const res = context[fnSym](...args);
delete context[fnSym];
return res;
}
11. Function.prototype.bind
对于直接调用来说,因为 bind 可以实现类似于
fn.bind(obj, 1)(2)
,所以需要将两边的参数拼起来。 对于通过 new 调用来说,函数不会被任何方式改变 this,所以要忽略传入的 this。
// 测试用例
function Person(name, age) {
console.log(name); //'我是参数传进来的name'
console.log(age); //'我是参数传进来的age'
console.log(this); // 构造函数 this 指向实例对象
}
// 构造函数原型的方法
Person.prototype.say = function() {
console.log(123);
}
let obj = {
objName: '我是obj传进来的name',
objAge: '我是obj传进来的age'
}
// 普通函数
function normalFun(name, age) {
console.log(name); //'我是参数传进来的name'
console.log(age); //'我是参数传进来的age'
console.log(this); //普通函数this指向绑定bind的第一个参数 也就是例子中的obj
console.log(this.objName); //'我是obj传进来的name'
console.log(this.objAge); //'我是obj传进来的age'
}
// 先测试作为构造函数调用
let bindFun = Person.Bind(obj, '我是参数传进来的name')
let a = new bindFun('我是参数传进来的age')
a.say() //123
// 再测试作为普通函数调用
let bindFun = normalFun.Bind(obj, '我是参数传进来的name')
bindFun('我是参数传进来的age')
Function.prototype.Bind = function (context, ...firstrgs) {
// 把函数暂存起来
const fn = this;
const fnSym = Symbol();
context[fnSym] = fn;
return function F(...args) {
// 判断是否是 new 操作
if (this instanceof F) {
const res = new context[fnSym](...firstrgs, ...args);
delete context[fnSym]
return res;
} else {
const res = context[fnSym](...firstrgs, ...args);
delete context[fnSym]
return res;
}
}
}
12. JSON.stringify
JSON.stringify(value[, replacer [, space]])
- value:将要序列化成一个 JSON 字符串的值
- replacer:
- 如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性,都会经过该函数的转换和处理;
- 如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;
- 如果该参数为 null 或者未提供,则对象所有的属性都会被序列化
- space:指定缩进用的空白字符串,用于美化输出(pretty-print);
- 如果参数是个数字,它代表有多少的空格;上限为10。该值若小于1,则意味着没有空格;
- 如果该参数为字符串(当字符串长度超过10个字母,取其前10个字母),该字符串将被作为空格;
- 如果该参数没有提供(或者为 null),将没有空格
- 返回值:一个表示给定值的 JSON 字符串
几个注意的点:
- 要转换的对象中存在 BigInt 格式时无法转换,需抛出 TypeError
- 如果对象的值为 undefined,则该对象会被忽略
- 数组中所有无法转换的值都会被转换为 null
const jsonStringify = source => {
const sourceType = Object.prototype.toString.call(source).slice(8, -1);
// 无法转换 BigInt 类型
if (sourceType === 'BigInt') return new TypeError('Invalid JSON');
const targetResult = {
Null: 'null',
Undefined: undefined,
Function: undefined,
Symbol: undefined,
// 下面需要在前面加一层判断是为了防止:
// 1. 不是 Date 的对象执行 toISOString
// 2. 数组中的 Symbol 被转为字符串(会报错)
Date: `"${sourceType === 'Date' && source.toISOString()}"`,
String: `"${sourceType === 'String' && source}"`,
Boolean: `${sourceType === 'Boolean' && source}`,
Number: `${sourceType === 'Number' && source}`,
}
if (Object.keys(targetResult).includes(sourceType)) return targetResult[sourceType];
let target = ''
// 剩下数组 和 对象的情况
if (sourceType === 'Object') {
Object.keys(source).forEach(key => {
const res = jsonStringify(source[key]);
// 如果 value 为 undefined 就不存入字符串
if (source[key] !== undefined && res !== undefined) {
target += `"${key}":res,`
}
})
return `{${target.substring(0, target.length - 1)}}`
} else {
// 不用 forEach 因为会忽略 undefined 的值
// JSON.stringify 会将数组 undefined 的值转为 null
for (let item of source) target += `${jsonStringify(item) || 'null'},`
return `[${target.substring(0, target.length - 1)}]`
}
}
13. JSON.parse
JSON.parse(text[, reviver])
- text:要被解析成 JavaScript 值的字符串
- reviver:转换器, 如果传入该参数(函数),可以用来修改解析生成的原始值,调用时机在 parse 函数返回之前
- 返回值:object 类型, 对应给定 JSON 文本的对象/值
使用 eval
JSON.Parse = source => eval(`(${source})`);
eval()
执行的代码拥有着执行者的权利。如果其运行的字符串代码被恶意方操控修改,最终可能会在用户计算机上运行恶意代码。它会执行 JS 代码,有 XSS 漏洞
如果想用 eval,必须对参数进行 json 校验
JSON.Parse = source => {
const rx_one = /^[\],:{}\s]*$/
const rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g
const rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g
const rx_four = /(?:^|:|,)(?:\s*\[)+/g
if (
rx_one.test(
source
.replace(rx_two, "@")
.replace(rx_three, "]")
.replace(rx_four, "")
)
) {
return eval("(" + source + ")")
}
}
使用 Function
JSON.Parse = source => new Function(`return ${source}`)()
eval 与 Function 都有着动态编译 js 代码的作用,但是在实际的编程中并不推荐使用
14. 列表转树结构
// 题目描述
[
{
id: 1,
text: '节点1',
parentId: 0 //这里用0表示为顶级节点
},
{
id: 2,
text: '节点1_1',
parentId: 1 //通过这个字段来确定子父级
}
...
]
转成
[
{
id: 1,
text: '节点1',
parentId: 0,
children: [
{
id:2,
text: '节点1_1',
parentId:1
}
]
}
]
const listToTree = (lists) => {
if(!Array.isArray(lists)) return new Error('输入不是数组');
const map = {}, tree = [];
lists = JSON.parse(JSON.stringify(lists))
for (let list of lists) {
list.children = [];
map[list.id] = list;
}
for (let list of lists) {
if (list.parentId === 0) tree.push(list);
else map[list.parentId].children.push(list);
}
return tree;
}
15. 树结构转列表
const treeToList = (tree) => {
const res = [];
const dfs = (tree) => {
tree.forEach(item => {
if (item.children && item.children.length) dfs(tree.children);
const tmp = JSON.parse(JSON.stringify(item));
delete tmp.children;
res.push(tmp);
})
}
dfs(tree);
return res;
}
16. 数组去重
普通版
const unique = (arr) => {
arr.sort();
let last = null;
const res = [];
arr.forEach(item => {
if (item !== last) {
res.push(item);
last = item;
}
})
return res;
}
使用 Set
const unique = (arr) => Array.from(new Set(arr));
对象数组去重
根据每个对象的某一个具体属性来进行去重
const uniqBy = (arr, property) => {
const propertyArr = [];
const res = [];
arr.forEach(item => {
if (!propertyArr.includes(item[property])) {
res.push(item);
propertyArr.push(item[property]);
}
})
return res;
}
17. Array 的方法
Array.prototype.map
Array<any>
.map<U>(
callbackfn: (value: any, index: number, array: any[]) => U,
thisArg?: any
): U[]
Array.prototyep.Map = function(callbackFn, thisArg) {
const res = [];
for(let i = 0;i < this.length;i++) {
res.push(callbackFn.call(thisArg, this[i], i, this));
}
return res;
}
Array.prototype.reduce
Array<any>
.reduce(
callbackfn: (previousValue: any, currentValue: any,currentIndex: number,array: any[] => any
initialValue?: any
): any
Array.prototype.Reduce = function (fn, initialVal) {
if (this.length === 0) return new Error('数组不能为空')
let res = initialVal || this[0]
let i = initialVal ? 0 : 1
for (; i < this.length; i++) {
res = fn.call(this, res, this[i])
}
return res
}
Array.prototype.flat
使用 JSON 方法
无法控制扁平化的深度:depth
不起作用
Array.prototype.Flat = function(depth) {
const reg = /(\[)|(\])/g;
const strArr = JSON.stringify(this)
return `[${strArr.replace(reg, '')}]`;
}
使用 reduce + concat
Array.prototype.Flat = function(depth) {
let res = this;
// 判断 this 指向的数组中是否有元素 为 数组
// 判断还未减少的层数 depth
while(res.some(Array.isArray) && depth-- > 0) {
// 第二个参数传初始值,既防止数组长度为 1 而出错,又可以用 concat 方法
res = res.reduce((pre, cur) => pre.concat(cur) ,[])
}
return res;
}
18. 实现双向绑定
使用 defineProperty
const span = document.getElementById('span');
const input = document.getElementById('input');
const data = { val: 'default' };
Object.defineProperty(data, 'val', {
set(value) {
// 数据改变 ——> 视图改变
span.innerHTML = value;
input.value = value;
}
})
input.addEventListener('change', (e) => {
// 视图变化 ——> 数据变化
data.val = e.target.value
})
使用 Proxy
const span = document.getElementById('span');
const input = document.getElementById('input');
const data = { val: 'default' };
const handler = {
set(target, key, value) {
target[key] = value; // 修改源对象的属性值
// 数据改变 ——> 视图改变
span.innerHTML = value;
input.value = value;
return value
}
}
const proxy = new Proxy(data, handler);
input.addEventListener('change', (e) => {
// 视图变化 ——> 数据变化
proxy.val = e.target.value
})
// 存储被监听对象 与 其监听回调函数 的映射
const ObserveStore = new Map()
/**
* 基于 proxy 代理 target,监听 target 的数据变化
* @param {Object} target 监听对象
* @param {Boolean} deepObservable 是否深度监听
*/
const makeObservable = (target, deepObservable = false) => {
const handlerName = Symbol('observeHandler')
ObserveStore.set(handlerName, [])
// 给监听的对象 target 增加用于添加监听回调的方法
target.observe = (handler) => ObserveStore.get(handlerName).push(handler)
if (deepObservable) {
const keys = Reflect.ownKeys(target)
for (let key of keys) {
if (typeof target[key] === 'object' && target[key] !== null) {
target[key] = makeObservable(target[key], deepObservable)
}
}
}
const proxyHandler = {
get(target, property, receiver) {
const result = Reflect.get(target, property, receiver)
if (result) {
const handlers = ObserveStore.get(handlerName)
handlers.forEach(handler => handler('get', property, target[property]))
}
return result
},
set(target, property, value, receiver) {
const result = Reflect.set(target, property, value, receiver)
if (result) {
const handlers = ObserveStore.get(handlerName)
handlers.forEach(handler => handler('set', property, value))
}
return result
},
deleteProperty(target, property) {
const success = Reflect.delete(target, property)
if (success) {
const handlers = ObserveStore.get(handlerName)
handlers.forEach(handler => handler('delete', property))
}
return success
}
}
return new Proxy(target, proxyHandler)
}
// 使用方式
let user = {
name: 55,
house: {
has: false,
}
};
user = makeObservable(user, true)
user.observe((method, key, value) => {
console.log('user', method, key, value)
})
user.house.observe((method, key, value) => {
console.log('house', method, key, value)
})
user.house.has = true;
19. Object.create
Object.create(proto,[propertiesObject])
- proto:新创建对象的原型对象
- propertiesObject:将为新创建的对象添加指定的属性值和对应的属性描述
Object.Create = function (proto, properties = {}) { if (/null|object/.test(typeof proto) === false) { throw new TypeError('Object prototype may only be an Object or null'); } const res = { __proto__: proto }; Object.defineProperties(res, properties); return res; }
20. maxRequest
实现 maxRequest,成功后 resolve 结果,失败后重试,尝试超过一定次数才真正的 reject
```javascript // 测试用例 function getData() { return new Promise(async (resolve, reject) => {function maxRequest(fn, maxNum = 1) { return new Promise((resolve, reject) => { if (maxNum === 0) { reject('maxRequest') return } Promise.resolve(fn()) .then(res => { resolve(res); }) .catch(err => { return maxRequest(fn, maxNum - 1); }) .catch(err => reject(err)) }) }
}) }setTimeout(() => { reject('err') }, 2000);
maxRequest(getData, 5).then(res => { console.log(‘maxRequest res = ‘, res); }).catch(err => { console.log(err); })
<a name="h4SND"></a>
# 21. 延迟任务队列
实现一个 Queue,task 方法可以添加任务,在一段延迟后执行下一个任务,start 方法开始这个任务队列<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/1174243/1646147604585-b70f5167-6cf6-4b5e-a683-2b26f07bf6f3.png#clientId=u2cc8f693-7dc3-4&crop=0.0091&crop=0.007&crop=1&crop=1&from=paste&height=282&id=u0bbc26f5&margin=%5Bobject%20Object%5D&name=image.png&originHeight=356&originWidth=820&originalType=binary&ratio=1&rotation=0&showTitle=false&size=77862&status=done&style=none&taskId=ue5df9f0b-4109-4c6e-ac17-89a441c2544&title=&width=650)
```javascript
class Queue {
constructor() {
this.queue = []
}
task(delay, fn) {
this.queue.push({ delay, fn });
return this;
}
circle() {
const { queue } = this
const node = queue.shift()
if (node) {
const { delay, fn } = node
const timer = setTimeout(() => {
fn();
clearTimeout(timer)
this.circle()
}, delay)
}
}
start() {
this.circle()
}
}
22. 模拟 instanceof
const InstanceOf = (left, right) => {
if (typeof left !== 'object' || left === null) return false
let proto = Object.getPrototypeOf(left);
while (proto && proto !== right) {
if (proto === right.prototype) return true
proto = Object.getPrototypeOf(proto)
}
return false
}
23. 解析 URL
let url = "http://www.baidu.com?name=elephant&age=25&sex=male&num=100"
使用正则表达式
const queryURLParams = (url) => {
const result = {}
const pattern = /(\w+)=(\w+)/ig
const queryArray = url.match(pattern)
queryArray.forEach(query => {
const [key, val] = query.split("=")
result[key] = val
})
return result
}
使用 a 标签
node 环境不可用
const queryURLParams = (url) => {
const result = {}
const a = document.createElement("a")
a.href = url
const queryArray = a.search.replace('?', '').split('&')
queryArray.forEach(query => {
const [key,val] = query.split('=')
result[key] = val
})
return result
}
使用 URLSearchParams
const queryURLParams = (url) => {
const result = {}
const queryUrl = url.split("?")[1]
const searchParams = new URLSearchParams(queryUrl)
for (let [key, val] of searchParams.entries()) {
result[key] = val
}
return result
}