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
}
                    
