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>## 简单版```javascriptfunction 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,则不拼接进argsif (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 => aif (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 的事件,原本的回调函数是 callbackoff(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的第一个参数 也就是例子中的objconsole.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 />
```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
}
