前端面试-js-手写代码原理

533 阅读15分钟

1. 推平数组

1.1.1. 方法一:ES6 的 flat 方法

const arr = [1,[2,[3,[4,5]]],6]
//  方法一:数组自带的扁平化方法,flat的参数代表的是需要展开几层,如果是Infinity的话,就是不管嵌套几层,全部都展开
console.log(arr.flat(Infinity))

1.1.2. 方法二:使用正则

const arr = [1,[2,[3,[4,5]]],6]
const res = JSON.stringify(arr).replace(/[|]/g,'')
const res2 = JSON.parse('[' + res + ']')
console.log(res2)

此方法 可以延伸, 从 res

1.1.3. 方法三:使用递归

const array = []
const  fn = (arr)=>{
    for(let i = 0;i<arr.length; i++){
        if(Array.isArray(arr[i])){
            fn(arr[i])
        }
        else {
            array.push(arr[i])
        }
    }
}
fn(arr)
console.log(array)

1.1.4. 方法四:使用 reduce

本质上还是递归

const newArr = (arr)=>{
            return arr.reduce((pre,cur)=>{
                return pre.concat(Array.isArray(cur) ? newArr(cur) : cur)
            },[])
        }
console.log(newArr(arr),"reduce方法")

1.1.5. 方法五:使用栈的思想实现 flat 函数

// 栈思想
function flat(arr) {
  const newArr = [];
  const stack = [].concat(arr);  // 将数组元素拷贝至栈,直接赋值会改变原数组//如果栈不为空,则循环遍历while (stack.length !== 0) {
    const val = stack.pop(); // 删除数组最后一个元素,并获取它if (Array.isArray(val)) {
      stack.push(...val); // 如果是数组再次入栈,并且展开了一层
    } else {
      newArr.unshift(val); // 如果不是数组就将其取出来放入结果数组中
    }
  }
  return newArr;
}

let arr = [12, 23, [34, 56, [78, 90, 100, [110, 120, 130, 140]]]];
console.log(flat(arr));
// [12, 23, 34, 56, 78, 90, 100, 110, 120, 130, 140]

最常见的递归版本如下:

function flatter(arr) {
  if (!arr.length) return;
  return arr.reduce(
    (pre, cur) =>
      Array.isArray(cur) ? [...pre, ...flatter(cur)] : [...pre, cur],
    []
  );
}
// console.log(flatter([1, 2, [1, [2, 3, [4, 5, [6]]]]]));

扩展思考:能用迭代的思路去实现吗?

实现代码如下:

function flatter(arr) {
  if (!arr.length) return;
  while (arr.some((item) => Array.isArray(item))) {
    arr = [].concat(...arr);
  }
  return arr;
}
// console.log(flatter([1, 2, [1, [2, 3, [4, 5, [6]]]]]));

2. 对象的 扁平化flatten 方法

题目描述:

const obj = {
 a: {
        b: 1,
        c: 2,
        d: {e: 5}
    },
 b: [1, 3, {a: 2, b: 3}],
 c: 3
}

flatten(obj) 结果返回如下
// {
//  'a.b': 1,
//  'a.c': 2,
//  'a.d.e': 5,
//  'b[0]': 1,
//  'b[1]': 3,
//  'b[2].a': 2,
//  'b[2].b': 3
//   c: 3
// }

实现代码如下:

function isObject(val) {
  return typeof val === "object" && val !== null;
}

function flatten(obj) {
  if (!isObject(obj)) {
    return;
  }
  let res = {};
  const dfs = (cur, prefix) => {
    if (!isObject(cur)) {
      res[prefix] = cur;//  这里负责组装任务
      return 
    }
    if (Array.isArray(cur)) {
      cur.forEach((item, index) => {
        dfs(item, `${prefix}[${index}]`);
      });
      return 
    } 
    for (let k in cur) {
      dfs(cur[k], `${prefix}${prefix ? "." : ""}${k}`);
    }
  };
  dfs(obj, "");
  return res;
}

3. 类数组转化为数组的方法

题目描述:类数组拥有 length 属性 可以使用下标来访问元素 但是不能使用数组的方法 如何把类数组转化为数组?

实现代码如下:

const arrayLike=document.querySelectorAll('div')

// 1.扩展运算符
[...arrayLike]
// 2.Array.from
Array.from(arrayLike)
// 3.Array.prototype.slice
Array.prototype.slice.call(arrayLike)
// 4.Array.apply
Array.apply(null, arrayLike)
// 5.Array.prototype.concat
Array.prototype.concat.apply([], arrayLike)

复制代码

4. 深拷贝和浅拷贝

4.1. 实现浅拷贝

var obj1 ={
 name:'张三',
 age:8,
 pal:['王五','王六','王七']
}
var obj3 = shallowCopy(obj1)
function shallowCopy(obj) {  
    if (typeof obj !== 'object' || obj === null) {  
        return obj;  
    }  
  
    return Object.assign({}, obj);  
}
 obj3.name = '李四'
 obj3.pal[0] = '王麻子'
   
console.log("obj1", obj1); //{age: 8, name: "张三", pal: ['王麻子', '王六', '王七']}
console.log("obj3", obj3); //{age: 8, name: "李四", pal: ['王麻子', '王六', '王七']}

4.2. 递归实现深拷贝

1、不是对象直接return

2、是对对象

  • 是否复制过
  • 是否数组 不是则创建新的对象
  • 存入 hash
  • 使用 for in 复制所有属性
function deepCopy(target, hash = new WeakMap()) {  
  // 如果不是对象或null,则直接返回(因为null也是typeof 'object')  
  if (target == null || typeof target !== 'object') {  
    return target;  
  }  
  // 如果已经复制过,返回缓存的对象  
  if (hash.has(target)) {  
    return hash.get(target);  
  }  
  // 根据目标类型创建新的对象或数组  
  const newTarget = Array.isArray(target) ? [] : Object.create(Object.getPrototypeOf(target));  
  // 将新对象存入hash中,之后再设置属性,以防止属性本身是个对象且引用了原对象导致的问题  
  hash.set(target, newTarget);  
  // 复制所有属性  
  for (const key in target) {  
    if (target.hasOwnProperty(key)) { // 确保不会复制原型链上的属性  
      newTarget[key] = deepCopy(target[key], hash);  
    }  
  }  
  return newTarget;  
}
  1. 基本类型数据是否能拷贝?
  2. 键和值都是基本类型的普通对象是否能拷贝?
  3. Symbol作为对象的key是否能拷贝?
  4. Date和RegExp对象类型是否能拷贝?
  5. Map和Set对象类型是否能拷贝?
  6. Function对象类型是否能拷贝?(函数我们一般不用深拷贝)
  7. 对象的原型是否能拷贝?
  8. 不可枚举属性是否能拷贝?
  9. 循环引用是否能拷贝?
function deepClone(target) {
    const map = new WeakMap()
    
    function isObject(target) {
        return (typeof target === 'object' && target ) || typeof target === 'function'
    }

    function clone(data) {
        if (!isObject(data)) {
            return data
        }
        if ([Date, RegExp].includes(data.constructor)) {
            return new data.constructor(data)
        }
        if (typeof data === 'function') {
            return new Function('return ' + data.toString())()
        }
        const exist = map.get(data)
        if (exist) {
            return exist
        }
        if (data instanceof Map) {
            const result = new Map()
            map.set(data, result)
            data.forEach((val, key) => {
                if (isObject(val)) {
                    result.set(key, clone(val))
                } else {
                    result.set(key, val)
                }
            })
            return result
        }
        if (data instanceof Set) {
            const result = new Set()
            map.set(data, result)
            data.forEach(val => {
                if (isObject(val)) {
                    result.add(clone(val))
                } else {
                    result.add(val)
                }
            })
            return result
        }
        const keys = Reflect.ownKeys(data)
        const allDesc = Object.getOwnPropertyDescriptors(data)
        const result = Object.create(Object.getPrototypeOf(data), allDesc)
        map.set(data, result)
        keys.forEach(key => {
            const val = data[key]
            if (isObject(val)) {
                result[key] = clone(val)
            } else {
                result[key] = val
            }
        })
        return result
    }
    return clone(target)
}

5. 将函数的实际参数转换成数组的方法

arguments 是一个类数组对象,用来存储实际传递给函数的参, arguments访问单个参数的方式与访问数组元素的方式相同。例如arguments[0]

借用arguments.length可以来查看实参和形参的个数是否一致

借用arguments.callee来让匿名函数实现递归

利用for of 遍历arguments

如何将其转换为数组:
方法一:var args = Array.prototype.slice.call(arguments);

方法二:var args = [].slice.call(arguments, 0);

方法三:

var args = []; 
for (var i = 1; i < arguments.length; i++) { 
    args.push(arguments[i]);
}

6. 获取随机数

Math.floor(Math.random()*10)

7. 数组去重

// 方法1
Array.from(new Set(arr))
// 方法2
[...new Set(arr)]
// 方法3
let res = [],
      obj = {}
  for (let i = 0; i < arr.length; i++) {
      if (!obj[arr[i]]) {
          res.push(arr[i])
          obj[arr[i]] = 1
      } else {
          obj[arr[i]]++
      }
  }
  return res
// 方法4
arr.filter((item, index)=> arr.indexOf(item) === index;);
// 方法5
    let res = []
    for (let i = 0; i < arr.length; i++) {
        if (res.indexOf(arr[i]) === -1) {
            res.push(arr[i])
        }
    }

// 方法6

arr.reduce((a, c) => a.includes(c) ? a : a.concat(c), []);

8. 判断类型

const isType= (target,type)=>`[object ${type}]` === Object.prototype.toString.call(target)

9. isPromise

  • 不为空
  • 是对象 或函数
  • 其下的 then 是函数
function isPromise(obj) {
  return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
}

// 有值 是对象或者函数、且挂载then方法
//  ts
declare function isPromise<T, S>(obj: PromiseLike<T> | S): obj is PromiseLike<T>;

10. 防抖和节流

/*
防抖原理
在事件被触发n秒后,再去执行回调函数。如果n秒内该事件被重新触发,则重新计时。结果就是将频繁触发的事件合并为一次,且在最后执行。
例如
电梯5秒后会关门开始运作,如果有人进来,等待5秒,5秒之内又有人进来,5秒等待重新计时...直至超过5秒,电梯才开始运作。
使用场景
input输入数据时请求服务器等。
* */

// 防抖防止输入抖动 就是 input 输入时执行搜索,要求最后一次输入后 0.5s 出发搜索,子啊 0.5s 内再次输入则重新计时
function debounce(fn, time) {  
  //        let timer = null
  return function (...args) {  
    clearTimeout(fn.timer);  
    fn.timer = setTimeout(() => {  
      fn.apply(this, args);  
    }, time);  
  };  
}
// debounce(con,1)(111)
// debounce(con,1)(334)


// 节流 节制的流动,防止过度操作 比如拖拽,和窗口大小调整
/*
电梯等第一个人进来之后,5秒后准时运作,不等待,若5秒内还有人进来,也不重置。
使用场景
resize,touchmove移动DOM,上拉列表加载数据等。
func (Function): 要节流的函数。
[wait=0] (number): 需要节流的毫秒。
*/

// 定时器时间戳

function throttle(fn,time){
  let lastTime = 0;
  return function (...args) {
    const nowTime = +new Date();
    const self = this;
    if(nowTime - lastTime >= time){
      fn.apply(self,args);
      lastTime = nowTime
    }
  }
}

11. 柯里化

参数不定长的柯里化

  • 函数柯里化是指将使用多个参数的函数转化成一系列使用一个参数的函数的技术, 它返回一个新的函数, 这个新函数去处理剩余的参数
    • 柯里化第一个接受的参数是函数
    • add(1,2,3,4) => addCurry (1)(2)(3)

参数复用: 本质上来说就是降低通用性, 提高适用性


假如一个函数需要两个参数, 其中一个参数可能多次调用并不会发生更改, 比如商品打折的力度, 此时可以根据折扣进行封装

提前返回


经典实例: 元素绑定事件监听器, 区分 IE 浏览器的 attachEvent 方法

延迟计算: 柯里化函数不会立即执行计算,第一次只是返回一个函数,后面的调用才会进行计算

function fn() {
    return [...arguments].reduce((prev, curr) => {
        return prev + curr
    }, 0)
}
//  args1 主要是兼容 curry 自己调用的场景
function curry(fn,...args1){
    return  function (...args2){
        const allArgs = [...args1,...args2];
        if(args2.length === 0){
            return fn(...args1)
        }
        return curry(fn,...allArgs)
    }
}
var curried = curry(fn);
console.log(curried(1, 2, 3)()); // 6
console.log(curried(1, 2)(3)()); // 6
console.log(curried(1)(2, 6)()); // 6
console.log(curried(122)(2)(3)()); // 6
// 由于最新的浏览器修改了console.log()规则,导致curried(1, 2, 3)  输出函数,最好的方式就是最后带上()  获取值输出
  • 函数柯里化是指将使用多个参数的函数转化成一系列使用一个参数的函数的技术, 它返回一个新的函数, 这个新函数去处理剩余的参数

12. 链式调用加减乘除

plus\minus\multi\divi 分别代表加\减\乘\除
const count = new Count(5)
count.plus(5)
     .multi(2)
     .minus(3)
     .plusRightNow(10)// 加法优先执行 先执行 5+10 然后再从头开始计算
     .divi(2)
class Count {
    constructor(value) {
        console.log(`初始值是=>${value}`)
        this.baseCount = value // 初始值
        this.taskQueue = [] // 任务队列
    }
    // 加法
    plus(val){}
    // 减法
    minus(val){}
    // 乘法
    multi(val){}
    // 除法
    divi(val){}
    // 优先执行的加法
    plusRightNow(val){}
}
class Count {
  constructor(value) {
      console.log(`初始值是=>${value}`)
      this.baseCount = value // 初始值
      this.taskQueue = [] // 任务队列
      this.init()
  }
  // 加法
  plus(val){
    this.taskQueue.push(()=>{
      this.baseCount=this.baseCount+val
      console.log(`+${val}=${this.baseCount}`)
    })
    return this;
  }
  // 减法
  minus(val){
    this.taskQueue.push(()=>{
      this.baseCount=this.baseCount-val
      console.log(`-${val}=${this.baseCount}`)
    })
    return this;
  }
  // 乘法
  multi(val){
    this.taskQueue.push(()=>{
      this.baseCount=this.baseCount*val
      console.log(`*${val}=${this.baseCount}`)
    })
    return this;

  }
  // 除法
  divi(val){
    this.taskQueue.push(()=>{
      this.baseCount=this.baseCount/val
      console.log(`/${val}=${this.baseCount}`)
    })
    return this;

  }
  // 优先执行的加法
  plusRightNow(val){
    this.taskQueue.unshift(()=>{
      this.baseCount=this.baseCount+val
      console.log(`优先执行+${val}=${this.baseCount}`)
    })
    return this;

  }
  init(){
    setTimeout(() => {
      this.taskQueue.forEach(element => {
        element()
      });
    }, 0);
  }
}

13. 发布订阅模式

核心是取消事件 传入 fn 然后 filter fn

题目描述:实现一个发布订阅模式拥有 on emit once off 方法

实现代码如下:

function EventBus() {
  // 存储事件及其对应的处理函数
  const events = {};//  对象中存订阅者的回调函数的形式

  // 订阅事件
  this.subscribe = function(eventName, callback) {
    // 如果事件尚未存在,则创建一个新的事件数组
    if (!events[eventName]) {
      events[eventName] = [];
    }
    // 将处理函数添加到事件数组中
    events[eventName].push(callback);
  };

  // 发布事件
  this.publish = function(eventName, data) {
    // 如果事件存在,则遍历执行所有处理函数
    if (events[eventName]) {
      events[eventName].forEach(callback => {
        callback(data);
      });
    }
  };

  // 取消订阅事件
  this.unsubscribe = function(eventName, callback) {
    // 如果事件存在,则移除相应的处理函数
    if (events[eventName]) {
      events[eventName] = events[eventName].filter(cb => cb !== callback);
    }
  };
}

// 使用示例
const eventBus = new EventBus();

// 订阅事件
const subscription1 = eventBus.subscribe('event1', data => {
  console.log('Event 1 occurred with data:', data);
});

const subscription2 = eventBus.subscribe('event2', data => {
  console.log('Event 2 occurred with data:', data);
});

// 发布事件
eventBus.publish('event1', { message: 'Hello from event1!' });
eventBus.publish('event2', { message: 'Hello from event2!' });

// 取消订阅事件
eventBus.unsubscribe('event1', subscription1);

14. 观察者模式

  • 发布订阅模式:一对多事件(通过eventType区分) → 多对多订阅(订阅者通过eventType订阅)
  • 观察者模式:一对多状态变化(被视为单一“事件类型”) → 一对多观察者(直接注册到被观察者上,关注全局状态变化)

源码区分 :

  • 发布订阅这 events 是对象 name 的 value 是数组 也就是 {eventType:[fn,fn]}
  • 观察者 events 是数组直接存储数组值 [fn,fn]
function Subject() {
  // 存储观察者对象
  this.observers = [];

  // 添加观察者
  this.addObserver = function(observer) {
    this.observers.push(observer);
  };

  // 移除观察者
  this.removeObserver = function(observer) {
    const index = this.observers.indexOf(observer);
    if (index !== -1) {
      this.observers.splice(index, 1);
    }
  };

  // 通知观察者
  this.notifyObservers = function(data) {
    this.observers.forEach(observer => {
      observer.update(data);
    });
  };
}

function Observer(name) {
  // 观察者的名称
  this.name = name;

  // 观察者的更新方法
  this.update = function(data) {
    console.log(`${this.name} received data:`, data);
  };
}

// 使用示例
const subject = new Subject();

// 创建观察者对象
const observer1 = new Observer('Observer 1');
const observer2 = new Observer('Observer 2');
const observer3 = new Observer('Observer 3');

// 将观察者添加到主题中
subject.addObserver(observer1);
subject.addObserver(observer2);
subject.addObserver(observer3);

// 通知所有观察者
subject.notifyObservers({ message: 'Hello observers!' });

// 将观察者从主题中移除
subject.removeObserver(observer2);

// 再次通知观察者
subject.notifyObservers({ message: 'Hello again!' });

15. 模拟new

function myNew(fn,...restArgs){
  const ins = new Object()
  ins.__proto__ = fn.prototype;
  let res = fn.apply(ins, restArgs)
  return typeof res === 'object' ? res : ins
}
  • 创建一个空对象
  • 获取构造函数
  • 设置空对象的原型
  • 绑定 this 并执行构造函数
  • 确保返回值为对象

16. apply、call、bind

手写本质上,当前环境的 this 就是函数 将其复制在 context 下 一般使用context[Symbol()]==this然后再删除context[Symbol()]

  • call 接收多个参数,第一个为函数上下文也就是this,后边参数为函数本身的参数。
    • allName.call(obj, '我是', '前端')
  • apply接收两个参数,第一个参数为函数上下文this,第二个参数为函数参数只不过是通过一个数组的形式传入的
    • allName.apply(obj, ['我是', '前端'])//我的全名是“我是一个前端” this指向obj
  • bind 接收多个参数,第一个是bind返回值返回值是一个函数上下文的this,不会立即执行。
    • fn = allName.bind(obj, "你是")
    • fn('前端', '好好学习天天向上')

实现函数 Apply方法

Function.prototype.myCall = function (context, ...args) {
  context = context || globalThis;
  // 用Symbol来创建唯一的fn,防止名字冲突
  let fn = Symbol("key");
  // this是调用myCall的函数,将函数绑定到上下文对象的新属性上
  context[fn] = this;
  const result = context[fn](args);
  // 将增加的fn方法删除
  delete context[fn];
  return result;
};

实现函数 call 方法

原理就是将函数作为传入的上下文参数(context)的属性执行,这里为了防止属性冲突使用了 ES6 的 Symbol 类型

Function.prototype.myApply = function (context, args) {
  context = context || globalThis;
  // 用Symbol来创建唯一的fn,防止名字冲突
  let fn = Symbol("key");
  // this是调用myCall的函数,将函数绑定到上下文对象的新属性上
  context[fn] = this;
  const result = context[fn](args);
  // 将增加的fn方法删除
  delete context[fn];
  return result;
};

实现函数 Bind方法

Function.prototype.myBind = function (context, ...args) {
  context = context || globalThis;
  // 保存原始函数的引用,this就是要绑定的函数
  const _this = this;
  // 返回一个新的函数作为绑定函数
  return function fn(...innerArgs) {
    // 判断返回出去的函数有没有被new
    if (this instanceof fn) {
      return new _this(...args, ...innerArgs);
    }
    // 使用apply方法将原函数绑定到指定的上下文对象上
    return _this.apply(context,[...args,...innerArgs]);
  };
};

17. 实现 keyBy、groupBy

const users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
  { id: 3, name: 'Charlie' },
];

console.log(keyBy(users, 'id')); // 输出: {1: {id: 1, name: 'Alice'}, 2: {id: 2, name: 'Bob'}, 3: {id: 3, name: 'Charlie'}}

function keyBy(array, iteratee) {
  if (!Array.isArray(array)) {
    throw new TypeError('Expected an array for the first argument');
  }

  // 如果iteratee是一个字符串,则假设它是对象属性名
  if (typeof iteratee === 'string') {
    iteratee = function(item) {
      return item[iteratee];
    };
  }

  // 确保iteratee是一个函数
  if (typeof iteratee !== 'function') {
    throw new TypeError('Expected a function or string for the second argument');
  }

  const result = {};
  
  for (let i = 0; i < array.length; i++) {
    const item = array[i];
    const key = iteratee(item);
    
    // 如果iteratee返回的key已存在,则覆盖原值
    result[key] = item;
  }

  return result;
}

// 示例:
const users = [
  { id: 1, age: 25, name: 'Alice' },
  { id: 2, age: 30, name: 'Bob' },
  { id: 3, age: 25, name: 'Charlie' },
];

console.log(groupBy(users, 'age')); 
// 输出: {25: [{id: 1, age: 25, name: 'Alice'}, {id: 3, age: 25, name: 'Charlie'}], 30: [{id: 2, age: 30, name: 'Bob'}]}

// 或者使用迭代器函数
console.log(groupBy(users, user => Math.floor(user.age / 10) * 10));
// 输出: {20: [{id: 1, age: 25, name: 'Alice'}, {id: 3, age: 25, name: 'Charlie'}], 30: [{id: 2, age: 30, name: 'Bob'}]}

function groupBy(collection, iteratee) {
  if (!Array.isArray(collection) && typeof collection !== 'object') {
    throw new TypeError('Expected an array or object as the first argument');
  }

  const result = {};

  const iterateeFn = typeof iteratee === 'function'
    ? iteratee
    : (item) => item[iteratee];

  // 遍历集合
  for (const item of Object.values(collection)) {
    const key = iterateeFn(item);

    // 如果键不存在于结果对象中,则初始化为空数组
    if (!result.hasOwnProperty(key)) {
      result[key] = [];
    }

    // 将当前元素添加到对应键的数组中
    result[key].push(item);
  }

  return result;
}

18. 实现一个Promise

const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'

function MyPromise(fn) {
  const that = this
  that.state = PENDING
  that.value = null
  that.resolvedCallbacks = []
  that.rejectedCallbacks = []
  // 待完善 resolve 和 reject 函数
  // 待完善执行 fn 函数
}
function resolve(value) {
  if (that.state === PENDING) {
    that.state = RESOLVED
    that.value = value
    that.resolvedCallbacks.map(cb => cb(that.value))
  }
}

function reject(value) {
  if (that.state === PENDING) {
    that.state = REJECTED
    that.value = value
    that.rejectedCallbacks.map(cb => cb(that.value))
  }
}
// 测试
try {
  fn(resolve, reject)
} catch (e) {
  reject(e)
}
new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(1)
  }, 0)
}).then(value => {
  console.log(value)
})
  • 首先我们创建了三个常量用于表示状态,对于经常使用的一些值都应该通过常量来管理,便于开发及后期维护
  • 在函数体内部首先创建了常量 that,因为代码可能会异步执行,用于获取正确的 this 对象
  • 一开始 Promise 的状态应该是 pending
  • value 变量用于保存 resolve 或者 reject 中传入的值
  • resolvedCallbacks 和 rejectedCallbacks 用于保存 then 中的回调,因为当执行完 Promise 时状态可能还是等待中,这时候应该把 then 中的回调保存起来用于状态改变时使用

实现一个符合 Promise/A+ 规范的 Promise

function resolve(value) {
  if (value instanceof MyPromise) {
    return value.then(resolve, reject)
  }
  setTimeout(() => {
    if (that.state === PENDING) {
      that.state = RESOLVED
      that.value = value
      that.resolvedCallbacks.map(cb => cb(that.value))
    }
  }, 0)
}
function reject(value) {
  setTimeout(() => {
    if (that.state === PENDING) {
      that.state = REJECTED
      that.value = value
      that.rejectedCallbacks.map(cb => cb(that.value))
    }
  }, 0)
}
  • 对于 resolve 函数来说,首先需要判断传入的值是否为 Promise 类型
  • 为了保证函数执行顺序,需要将两个函数体代码使用 setTimeout 包裹起来

接下来继续改造 then 函数中的代码,首先我们需要新增一个变量 promise2,因为每个 then 函数都需要返回一个新的 Promise 对象,该变量用于保存新的返回对象,然后我们先来改造判断等待态的逻辑

if (that.state === PENDING) {
  return (promise2 = new MyPromise((resolve, reject) => {
    that.resolvedCallbacks.push(() => {
      try {
        const x = onFulfilled(that.value)
        resolutionProcedure(promise2, x, resolve, reject)
      } catch (r) {
        reject(r)
      }
    })

    that.rejectedCallbacks.push(() => {
      try {
        const x = onRejected(that.value)
        resolutionProcedure(promise2, x, resolve, reject)
      } catch (r) {
        reject(r)
      }
    })
  }))
}
if (that.state === RESOLVED) {
  return (promise2 = new MyPromise((resolve, reject) => {
    setTimeout(() => {
      try {
        const x = onFulfilled(that.value)
        resolutionProcedure(promise2, x, resolve, reject)
      } catch (reason) {
        reject(reason)
      }
    })
  }))
}

19. Promise.all Promise.race Promise.any

Promise.myAll =function (arr){
  let count = 0;
  let result = []
  return new Promise((resolve,reject)=>{
      for(let i = 0;i<arr.length;i++){
        Promise.resolve(arr[i]).then(v=>{
          result[i] = v;
          count++;
          if(count === arr.length){
            resolve(result)
          }
        }).catch((e)=>{
          reject(e)
        })
      } 
  })
}

// 第一个成功则返回
function promiseRace(promises) {
     if (!Array.isArray(promises)) {
         throw new Error("promises must be an array")
     }
    return new Promise(function (resolve, reject) {
         promises.forEach(p =>
             Promise.resolve(p).then(data => {
                 resolve(data)
                 }, err => {
                 reject(err)
             })
         )
     })
 }
// 只要有一个成功则成功
function myPromiseAny(arr) {
    if (!Array.isArray(arr)) {
        throw new Error('arguments must be a array');
    }
    let rejectCount = 0;
    return new Promise((resolve, reject) => {
        for (let i = 0; i < arr.length; i++) {
            arr[i].then(data => {
                resolve(data);
            }, () => {
                rejectCount++;
                if (rejectCount === arr.length) {
                    reject(new AggregateError('All promises were rejected'))
                }
            });
        }
    })
}

20. settimeout 模拟实现 setinterval(带清除定时器的版本)

题目描述:setinterval 用来实现循环定时调用 可能会存在一定的问题 能用 settimeout 解决吗

实现代码如下:

function mySettimeout(fn, t) {
  let timer = null;
  function interval() {
    fn();
    timer = setTimeout(interval, t);
  }
  interval();
  return {
    cancel:()=>{
      clearTimeout(timer)
    }
  }
}
// let a=mySettimeout(()=>{
//   console.log(111);
// },1000)
// let b=mySettimeout(() => {
//   console.log(222)
// }, 1000)
复制代码

扩展:我们能反过来使用 setinterval 模拟实现 settimeout 吗?

const mySetTimeout = (fn, time) => {
  const timer = setInterval(() => {
    clearInterval(timer);
    fn();
  }, time);
};
// mySetTimeout(()=>{
//   console.log(1);
// },1000)
复制代码

扩展思考:为什么要用 settimeout 模拟实现 setinterval?setinterval 的缺陷是什么?

答案请自行百度哈 这个其实面试官问的也挺多的 小编这里就不展开了

21. 二分查找--时间复杂度 log2(n)

题目描述:如何确定一个数在一个有序数组中的位置

实现代码如下:

function search(arr, target, start, end) {
  let targetIndex = -1;

  let mid = Math.floor((start + end) / 2);

  if (arr[mid] === target) {
    targetIndex = mid;
    return targetIndex;
  }

  if (start >= end) {
    return targetIndex;
  }

  if (arr[mid] < target) {
    return search(arr, target, mid + 1, end);
  } else {
    return search(arr, target, start, mid - 1);
  }
}
// const dataArr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
// const position = search(dataArr, 6, 0, dataArr.length - 1);
// if (position !== -1) {
//   console.log(`目标元素在数组中的位置:${position}`);
// } else {
//   console.log("目标元素不在数组中");
// }
复制代码

22. LRU 算法

题目描述:

实现代码如下:

//  一个Map对象在迭代时会根据对象中元素的插入顺序来进行
// 新添加的元素会被插入到map的末尾,整个栈倒序查看
class LRUCache {
  constructor(capacity) {
    this.secretKey = new Map();
    this.capacity = capacity;
  }
  get(key) {
    if (this.secretKey.has(key)) {
      let tempValue = this.secretKey.get(key);
      this.secretKey.delete(key);
      this.secretKey.set(key, tempValue);
      return tempValue;
    } else return -1;
  }
  put(key, value) {
    // key存在,仅修改值
    if (this.secretKey.has(key)) {
      this.secretKey.delete(key);
      this.secretKey.set(key, value);
    }
    // key不存在,cache未满
    else if (this.secretKey.size < this.capacity) {
      this.secretKey.set(key, value);
    }
    // 添加新key,删除旧key
    else {
      this.secretKey.set(key, value);
      // 删除map的第一个元素,即为最长未使用的
      this.secretKey.delete(this.secretKey.keys().next().value);
    }
  }
}
// let cache = new LRUCache(2);
// cache.put(1, 1);
// cache.put(2, 2);
// console.log("cache.get(1)", cache.get(1))// 返回  1
// cache.put(3, 3);// 该操作会使得密钥 2 作废
// console.log("cache.get(2)", cache.get(2))// 返回 -1 (未找到)
// cache.put(4, 4);// 该操作会使得密钥 1 作废
// console.log("cache.get(1)", cache.get(1))// 返回 -1 (未找到)
// console.log("cache.get(3)", cache.get(3))// 返回  3
// console.log("cache.get(4)", cache.get(4))// 返回  4
复制代码

23. 分片思想解决大数据量渲染问题

题目描述:渲染百万条结构简单的大数据时 怎么使用分片思想优化渲染

实现代码如下:

let ul = document.getElementById("container");
// 插入十万条数据
let total = 100000;
// 一次插入 20 条
let once = 20;
//总页数
let page = total / once;
//每条记录的索引
let index = 0;
//循环加载数据
function loop(curTotal, curIndex) {
  if (curTotal <= 0) {
    return false;
  }
  //每页多少条
  let pageCount = Math.min(curTotal, once);
  window.requestAnimationFrame(function () {
    for (let i = 0; i < pageCount; i++) {
      let li = document.createElement("li");
      li.innerText = curIndex + i + " : " + ~~(Math.random() * total);
      ul.appendChild(li);
    }
    loop(curTotal - pageCount, curIndex + pageCount);
  });
}
loop(total, index);

扩展思考:对于大数据量的简单 dom 结构渲染可以用分片思想解决 如果是复杂的 dom 结构渲染如何处理?

24. 列表转成树形结构

[
  {
    id: 1,
    text: '节点1',
    pid: 0 //这里用0表示为顶级节点
  },
  {
    id: 2,
    text: '节点1_1',
    pid: 1 //通过这个字段来确定子父级
  }
  ...
]

转成
[
  {
    id: 1,
    text: '节点1',
    parentId: 0,
    children: [
      {
        id:2,
        text: '节点1_1',
        parentId:1
      }
    ]
  }
]
function listToTree(data, pid) {
  let tree = [];
  for (let i = 0; i < data.length; i++) {
    if (data[i].pid == pid) {
      tree.push({
        ...data[i],
        children : listToTree(data, data[i].id)
      });
    }
  }
  return tree;
 }

25. 树形结构转成列表

[
    {
        id: 1,
        text: '节点1',
        parentId: 0,
        children: [
            {
                id:2,
                text: '节点1_1',
                parentId:1
            }
        ]
    }
]
转成
[
    {
        id: 1,
        text: '节点1',
        parentId: 0 //这里用0表示为顶级节点
    },
    {
        id: 2,
        text: '节点1_1',
        parentId: 1 //通过这个字段来确定子父级
    }
    ...
]
function treeToList(data) {
  let res = [];
  const dfs = (tree) => {
    tree.forEach((item) => {
      if (item.children) {
        dfs(item.children);
        delete item.children;
      }
      res.push(item);
    });
  };
  dfs(data);
  return res;
}

26. 大数相加

题目描述:实现一个add方法完成两个大数相加

let a = "9007199254740991";
let b = "1234567899999999999";

function add(a ,b){
   //...
}
function add(a ,b){
   //取两个数字的最大长度
   let maxLength = Math.max(a.length, b.length);
   //用0去补齐长度
   a = a.padStart(maxLength , 0);//"0009007199254740991"
   b = b.padStart(maxLength , 0);//"1234567899999999999"
   //定义加法过程中需要用到的变量
   let t = 0;
   let f = 0;   //"进位"
   let sum = "";
   for(let i=maxLength-1 ; i>=0 ; i--){
      t = parseInt(a[i]) + parseInt(b[i]) + f;
      f = Math.floor(t/10);
      sum = t%10 + sum;
   }
   if(f!==0){
      sum = '' + f + sum;
   }
   return sum;
}

27. url 参数复杂参数的拼接

function parseUrlParams(url) {
  const params = {};
  const queryString = url.split('?')[1];

  if (queryString) {
    const pairs = queryString.split('&');

    pairs.forEach(pair => {
      const [key, value] = pair.split('=');
      params[key] = decodeURIComponent(value);
    });
  }

  return params;
}

// Example usage:
const url = 'https://example.com/path?foo=bar&baz=qux';
const params = parseUrlParams(url);
console.log(params); // Output: { foo: 'bar', baz: 'qux' }

28. 手写proxy defineProperty

juejin.cn/post/706939…

//  Object.defineProperty
<input type="text" id="ipt">
<p id="op"></p>
<script>
	const obj = {};
	Object.defineProperty(obj, 'text', {
		get: function() {
			console.log('get val');
		},
		set: function(newVal) {
			console.log('set val:' + newVal);
			ipt.value = newVal;
			op.innerHTML = newVal;
		}
	});

	addEventListener('input', function(e){
		obj.text = e.target.value;
	})

</script>

<input type="text" id="ipt">
<p id="op"></p>
<script>
	let obj = { txtVal: '输入框的默认值' }

	let newObj = new Proxy(obj, {
		get(target, property) {
			console.log("target", target);
			console.log("property", property);
			return target[property]
		}
	})

	// 核心是op和输入框的值  双向绑定
	ipt.value = newObj.txtVal
	op.innerHTML = newObj.txtVal
	addEventListener('input', (e) => {
		console.log(e.target.value);
		op.innerHTML = e.target.value
	})
</script>

29. 实现 loadsh.get 方法

  • 问题的关键是将 path 统一改为数组处理
  • 每次出来数组一个然后 截短一点
function get(obj,path='',defaultValue){
  if(!path || !obj){
    return defaultValue
  }
  if(typeof path === 'string'){
    path = path.split('.')
  }
  function fn(value,arr){
    if(arr.length<1){
      return value
    }
    if(!value || !value[arr[0]]){
      return defaultValue
    }
    return fn(value[arr[0]],arr.splice(1,arr.length))
  }
  return fn(obj,path)
}

// 使用示例:
const obj = { a: { b: { c: 'value' } } };
console.log(get(obj, 'a.b.c')); // 输出: 'value'
console.log(get(obj, ['a', 'b', 'c'])); // 输出: 'value'
console.log(get(obj, 'a.d.e', 'default')); // 输出: 'default'

30. 实现 LazyMan

  • 将所有任务 promise 化
  • 写一个 对数组函数 同步执行的函数
  • 所有方法挂载在 函数的 this 上 然后直接输出 this
实现一个LazyMan,可以按照以下方式调用:
LazyMan(“Hank”)输出:
Hi! This is Hank!

LazyMan(“Hank”).sleep(10).eat(“dinner”)输出
Hi! This is Hank!
//等待10秒..
Wake up after 10
Eat dinner~

LazyMan(“Hank”).eat(“dinner”).eat(“supper”)输出
Hi This is Hank!
Eat dinner~
Eat supper~

LazyMan(“Hank”).eat(“supper”).sleepFirst(5)输出
//等待5秒
Wake up after 5
Hi This is Hank!
Eat supper

实现代码如下:

function executeFunctionsSequentially(tasks) {
  if(!tasks.length){
    return 
  }
  const task = tasks.shift();
    task().then((v)=>{
      executeFunctionsSequentially(tasks)
    })
}
function LazyMan(name){
  this.time;
  this.task = [()=>{
    return new Promise((resolve)=>{
      console.log(`Hi! This is ${name}!`);
      resolve(true)
    })
  }];
  setTimeout(()=>{
    executeFunctionsSequentially(this.task)
  },0) 
  this.sleep=(time)=>{
    this.task.push(()=> {
      return new Promise((resolve)=>{
        console.log(`Wake up after ${time}`);
        setTimeout(() => {
          resolve(true)
        }, time*1000) 
      })
    }) 
    return this;
  }
  this.eat = (food)=>{
    this.task.push(()=>{
      return new Promise((resolve)=>{
        console.log(`Eat ${food}~`);
        resolve(true)
      })
    }) 
    return this;
  }
  this.sleepFirst=(time)=>{
    this.task.unshift(()=> {
      console.log(`Wake up after ${time}`);
      return new Promise((resolve)=>{
        setTimeout(() => {
          resolve(true)
        }, time*1000) 
      })
      
    })
    return this
  }
  return this
}

31. 实现一个EatMan

实现一个EatMan
说明:实现一个EatMan,EatMan可以有以下一些行为
示例:
   1. EatMan('Hank')输出:
    Hi! This is Hank!
   2. EatMan('Hank').eat('dinner').eat('supper')输出
    Hi! This is Hank!
    Eat dinner~
    Eat supper~
   3. EatMan('Hank').eat('dinner').eatFirst('lunch')输出
    Eat lunch~
    Hi! This is Hank!
    Eat dinner~
   4. EatMan('Hank').eat('dinner').eatFirst('lunch').eatFirst('breakfast')输出
    Eat breakfast~
    Eat lunch~
    Hi! This is Hank!
    Eat dinner~
function EatMan(name){
  const sayList  = [];
  const man = {
    eat:function(value){
      sayList.push(`Eat ${value}~`);
      return this;
    },
    eatFirst:function(value){
      sayList.unshift(`Eat ${value}~`);
      return this;
    }
  }
  sayList.push(`Hi This is ${name}~`);
  Promise.resolve().then(()=>{
    for(let i =0;i<sayList.length;i++){
      console.log(sayList[i])
    }
  })
  return man
}

32. 设计任务队列,并发数量为 n,按顺序输出任务结果

设计一个函数 schedule,schedule 接收一个数量 n,返回一个新函数,新函数接受一个返回 promise 的函数数组,会按照顺序执行函数,并且同时执行的函数数量不超过 n。并且该函数的返回值是一个 promsie,该 promise 会在所有函数执行完后 resolve, 其值是函数数组的返回值按顺序构成的数组。看描述比较容易犯迷糊,直接看模版代码和输出示例:

  • 将所有任务看出一个队列
  • 依次执行队头并将其从任务列表移除
  • 直到达到限制则暂停,在输出结果位置 判断是否要 重新执行一次任务
function schedule(n) {
    // 在此处写下你的实现
}

const runTask = schedule(4);

const tasks = new Array(10).fill(0).map((x, i) => () => new Promise(resolve => {
    setTimeout(() => {
        console.log(`task${i} complete`);
        resolve(`task${i}`);
    }, 2000);
}));

runTask(tasks).then(console.log);

// 预期输出
// 2s 后
// task0 complete
// task1 complete
// task2 complete
// task3 complete
// 再2s 后
// task4 complete
// task5 complete
// task6 complete
// task7 complete
// 再2s 后
// task8 complete
// task9 complete
// ['task0', 'task1', ..., 'task9']
  • 有执行中任务数,已完成任务数,保存结果数 新的 task 队列 等字段支持
  • 使用while 操作 只有任务队列有 且还能继续 执行才进入循环
  • new Promise 里面套一个 run 函数 然后使用 run 来调用函数
function schedule(n) {
  return function (tasks) {
    const res = [];
    let queue = [];
    let cur = 0;    // 记录当前执行中的任务数量
    let finished = 0; // 记录执行完成的任务数量
    queue = tasks.map((v, index) => ({
      task: v,
      index,
    }));
    return new Promise((resolve) => {
      function runTask() {
        while (cur < n && queue[0]) {// 判断当前执行中的任务数量和待执行的数量
          const item = queue.shift();// 取出第一个任务
         
          item.task().then((result) => {
            res[item.index] = result;
            cur--;  // 执行中的任务数量--
            finished++;            // 完成的任务数量增加
            if (finished === tasks.length) {
              resolve(res);
            } else {
              runTask(resolve);// 否则继续执行
            }
          });
          cur++;  // 增加执行中的任务数量
        }
      }
      runTask();
    });
  };
}

另一种写法

function taskPool(max=2) {
  this.tasks = [];
  this.pool = [];
  this.max = 2;
}

taskPool.prototype.addTask = function(task) {
  this.tasks.push(task);
  this.run();
}

taskPool.prototype.run = function() {
  if(this.tasks.length === 0) {
    return;
  }
  let min = Math.min(this.tasks.length, this.max - this.pool.length);
  for(let i = 0; i<min;i++) {
    const currTask = this.tasks.shift();
    this.pool.push(currTask);
    currTask().finally(() => {
      this.pool.splice(this.pool.indexOf(currTask), 1);
      this.run();
    })
  }
}

33. 手写 hook 实现对 div 窗口宽高变化的监控

import { useState, useEffect } from 'react';

function useResizeObserver(ref) {
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });

  useEffect(() => {
    const observer = new ResizeObserver(entries => {
      const { width, height } = entries[0].contentRect;
      setDimensions({ width, height });
    });

    if (ref.current) {
      observer.observe(ref.current);
    }

    return () => {
      observer.disconnect();
    };
  }, [ref]);

  return dimensions;
}

// Example usage:
function MyComponent() {
  const divRef = useRef(null);
  const { width, height } = useResizeObserver(divRef);

  return (
    <div ref={divRef}>
      <h1>Resizable Div</h1>
      <p>Width: {width}px</p>
      <p>Height: {height}px</p>
    </div>
  );
}

34. 判断对象子集

需考虑对象嵌套的问题

function isSubset(obj1, obj2) {
  // 判断是否 obj2 是 obj1 的子集
  function isSubsetRecursive(obj1, obj2) {
    // 遍历 obj2 的所有属性
    for (let key in obj2) {
      // 如果 obj2 的属性是对象,则递归调用 isSubsetRecursive 判断子对象是否是 obj1 中的子集
      if (typeof obj2[key] === 'object' && obj2[key] !== null) {
        if (typeof obj1[key] !== 'object' || obj1[key] === null) {
          return false; // 如果 obj1 中相应的属性不是对象,则 obj2 不是 obj1 的子集
        }
        if (!isSubsetRecursive(obj1[key], obj2[key])) {
          return false; // 如果子对象不是 obj1 中相应的子集,则 obj2 不是 obj1 的子集
        }
      } else {
        // 检查 obj2 的属性是否都存在于 obj1 中
        if (!(key in obj1)) {
          return false;
        }
        // 检查 obj2 的属性值是否与 obj1 中相应属性值相等
        if (obj1[key] !== obj2[key]) {
          return false;
        }
      }
    }
    return true;
  }

  // 调用递归函数开始检查
  return isSubsetRecursive(obj1, obj2);
}

// 使用示例
const obj1 = { a: 1, b: { c: 2, d: 3 }, e: { f: { g: 4 } } };
const obj2 = { f: { g: 4 } };
console.log(isSubset(obj1, obj2)); // true

const obj3 = { g: 4 };
console.log(isSubset(obj1, obj3)); // true

const obj4 = { g: 34 };
console.log(isSubset(obj1, obj4)); // false

const obj5 = { c: 2, d: 3 };
console.log(isSubset(obj1, obj5)); // true

const obj6 = { c: 2 };
console.log(isSubset(obj1, obj6)); // false

const obj7 = { a: 1 };
console.log(isSubset(obj1, obj7)); // true

35. 手写 hook

let state = [];
let currentIdx = 0;
let effectCleanupFuncs = [];

function useState(initialValue) {
    const currentIndex = currentIdx;
    state[currentIndex] = state[currentIndex] || initialValue;
    const setState = (newValue) => {
        state[currentIndex] = newValue;
        render();
    };
    currentIdx++;
    return [state[currentIndex], setState];
}

function useEffect(callback, dependencies) {
    const currentIndex = currentIdx;
    if (!effectCleanupFuncs[currentIndex]) {
        effectCleanupFuncs[currentIndex] = () => {};
    }

    const cleanup = effectCleanupFuncs[currentIndex];
    const effect = () => {
        cleanup();
        effectCleanupFuncs[currentIndex] = callback();
    };

    if (!dependencies || dependencies.some((dep, index) => dep !== state[currentIndex])) {
        effect();
    }

    currentIdx++;
}

function useMemo(callback, dependencies) {
    const currentIndex = currentIdx;
    const result = callback();
    state[currentIndex] = state[currentIndex] || {};
    const prevDeps = state[currentIndex].deps;
    if (prevDeps) {
        const hasChanged = dependencies.some((dep, index) => dep !== prevDeps[index]);
        if (!hasChanged) {
            currentIdx++;
            return state[currentIndex].value;
        }
    }
    state[currentIndex] = { value: result, deps: dependencies };
    currentIdx++;
    return result;
}

function useCallback(callback, dependencies) {
    return useMemo(() => callback, dependencies);
}

// Example usage
let count = 0;

function Counter() {
    const [value, setValue] = useState(count);

    useEffect(() => {
        console.log('Effect: Count updated to', value);
        return () => {
            console.log('Cleanup: Count updated from', count, 'to', value);
        };
    }, [value]);

    return {
        value,
        increment: () => setValue(value + 1),
        decrement: () => setValue(value - 1)
    };
}

function App() {
    const { value, increment, decrement } = Counter();

    return `
        <div>
            <h1>Count: ${value}</h1>
            <button onclick="(${increment})()">Increment</button>
            <button onclick="(${decrement})()">Decrement</button>
        </div>
    `;
}

function render() {
    const appDiv = document.getElementById('app');
    appDiv.innerHTML = App();
}

render();

36. 用 hook 手写全局状态管理

import React from 'react';  
  
export const GlobalStateContext = React.createContext();
/**************/
import React, { useState, createContext, useContext } from 'react';  
  
export const GlobalStateContext = createContext();  
  
export const GlobalStateProvider = ({ children }) => {  
  const [globalState, setGlobalState] = useState(initialState);  
  
  const setValue = (key, value) => {  
    setGlobalState((prevState) => ({  
      ...prevState,  
      [key]: value,  
    }));  
  };  
  
  return (  
    <GlobalStateContext.Provider value={{ globalState, setValue }}>  
      {children}  
    </GlobalStateContext.Provider>  
  );  
};  
  
// 你可以根据需要定义初始状态  
const initialState = {  
  count: 0,  
  // 其他全局状态...  
};

使用

import React from 'react';  
import { GlobalStateProvider } from './GlobalState';  
  
function App() {  
  return (  
    <GlobalStateProvider>  
      {/* 你的应用组件 */}  
    </GlobalStateProvider>  
  );  
}  
  
export default App;

37. 如何使用 Proxy 实现链式调用?

目前已知在 window 上挂载了 3 个方法,如下所示

function increase(value) {
  return value + 1;
}

function decrease(value) {
  return value - 1;
}

function double(value) {
  return value * 2;
}

希望达到的目标是实现一个 chain 函数,借助 chain 函数可以实现下述功能:

console.log(chain(3).increase().double().value); // 8
console.log(chain(3).decrease().double().value); // 4

那么 chain函数该如何实现呢?
链接:juejin.cn/post/728966…

function chain(initialValue) {
    const proxy = new Proxy({ value: initialValue }, {
        get(target, property) {
            if (property === 'value') {
                return target.value;
            }
            if (window[property] && typeof window[property] === 'function') {
                // 如果属性存在于 window 对象且是函数
                return function (...args) {
                    // 返回一个函数,该函数将在链式调用时执行
                    target.value = window[property](target.value, ...args);
                    return proxy; // 返回代理对象以支持链式调用
                };
            }
        },
    });

    return proxy;
}

38. 数组分组改成减法运算

这个题的意思就是 [5, [[4, 3], 2, 1]] 变成 (5 - ((4 - 3) - 2 - 1)) 并执行。 且不能使用eval()

var newArr = [5, [[4, 3], 2, 1]]

    // 1. 取巧
    // 转为字符串
    let newStringArr = `${JSON.stringify(newArr)}`
    // 循环修改括号和减号
    let fn = newStringArr.split("").map((el) => {
      switch (el) {
        case "[":
          return '('
        case "]":
          return ')'
        case ",":
          return '-'
        default:
          return el
      }
    }).join("")
    // 最终通过new Function 调用可以了!
    new Function("return " + fn)()
    
    
    // 2. 方法二 
    function run(arr) {
      return arr.reduce((pre, cur) => {
        let first = Array.isArray(pre) ? run(pre) : pre
        let last = Array.isArray(cur) ? run(cur) : cur
        return first - last
      })
    }
    run(newArr)

    // 使用 promise 配合await的异步方法来实现 sleep
    {
      (async () => {
        console.log('start');
        await sleep(3000)
        console.log('end');

        function sleep(timer) {
          return new Promise(res => {
            setTimeout(() => {
              res()
            }, timer);
          })
        }
      })();
    }

    // 方法二 这是完全堵塞进程来达到sleep
    {
      (async () => {
        console.log('start');
        await sleep(3000)
        console.log('end');

        function sleep(delay) {
          let t = Date.now();
          while (Date.now() - t <= delay) {
            continue;
          }
        };
      })()
    }

39. 如何拦截全局Promise reject

,但并没有设定 reject处理器 时候的错误

// 使用Try catch 只能拦截try语句块里面的
try {
  new Promise((resolve, reject) => {
    reject("WTF 123");
  });
} catch (e) {
  console.log("e", e);
  throw e;
}

// 使用 unhandledrejection 来拦截全局错误  (这个是对的)
window.addEventListener("unhandledrejection", (event) => {
  event && event.preventDefault();
  console.log("event", event);
});

40. 不使用循环删除数组中指定位置的元素


var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

// 方法一 : splice 操作数组 会改变原数组 
arr.splice(2, 1)


// 方法二 : slice 截取选中元素 返回新数组 不改变原数组
arr.slice(0, 2).concat(arr.slice(3,))

// 方法三 delete数组中的元素 再把这个元素给剔除掉
delete arr[2]
arr.join(" ").replaceAll(/\s{1,2}/g," ").split(" ")

41. 简单实现async/await中的async函数

async/await语法糖就是使用Generator函数+自动执行器来运作的,注意只要要实现async函数就是实现一个generate函数+执行器的语法糖

// 定义了一个promise,用来模拟异步请求,作用是传入参数++
function getNum(num){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(num+1)
        }, 1000)
    })
}

//自动执行器,如果一个Generator函数没有执行完,则递归调用
function asyncFun(func){
  var gen = func();

  function next(data){
    var result = gen.next(data);
    if (result.done) return result.value;
    result.value.then(function(data){
      next(data);
    });
  }

  next();
}

// 所需要执行的Generator函数,内部的数据在执行完成一步的promise之后,再调用下一步
var func = function* (){
  var f1 = yield getNum(1);
  var f2 = yield getNum(f1);
  console.log(f2) ;
};
asyncFun(func);

42. 实现一个只执行一次的函数

// 闭包
function once(fn) {
  let called = false;
  return function _once() {
    if (called) {
      return _once.value;
    }
    called = true;
    _once.value = fn.apply(this, arguments);
  }
}

//ES6 的元编程 Reflect API 将其定义为函数的行为
Reflect.defineProperty(Function.prototype, 'once', {
  value () {
    return once(this);
  },
  configurable: true,
})

43. js下划线转驼峰处理

function camelCase(str) {
  return str.replace(/_([a-z])/g, function(match, group1) {
    return group1.toUpperCase();
  });
}

console.log(camelCase("some_string"));  // "someString"

44. 对象扁平化flatObj

/* 题目*/
var entryObj = {
    a: {
        b: {
            c: {
                    dd: 'abcdd'
            }
        },
        d: {
            xx: 'adxx'
        },
        e: 'ae'
    }
}

// 要求转换成如下对象
var outputObj = {
	'a.b.c.dd': 'abcdd',
	'a.d.xx': 'adxx',
	'a.e': 'ae'
}

function flat(obj, path = '', res = {}, isArray) {
  for (let [k, v] of Object.entries(obj)) {
    if (Array.isArray(v)) {
      let _k = isArray ? `${path}[${k}]` : `${path}${k}`;
      flat(v, _k, res, true);
    } else if (typeof v === 'object') {
      let _k = isArray ? `${path}[${k}].` : `${path}${k}.`;
      flat(v, _k, res, false);
    } else {
      let _k = isArray ? `${path}[${k}]` : `${path}${k}`;
      res[_k] = v;
    }
  }
  return res;
}

console.log(flat({ a: { aa: [{ aa1: 1 }] } }))

45. 数组乱序

// 取巧的一种算法,但是每个位置乱序的概率不同
function mixArr(arr){
    return arr.sort(() => {
        return Math.random() - 0.5;
    })
}

// 著名的Fisher–Yates shuffle 洗牌算法
function shuffle(arr){
    let m = arr.length;
    while(m > 1){
        let index = parseInt(Math.random() * m--);
        [arr[index],arr[m]] = [arr[m],arr[index]];
    }
    return arr;
}

46. 获取对象、数组深度

	function getDeep(obj) {
      let res = 1
      // 递归函数
      function fn(obj) {
        let arr = []
        let arrLength = arr.length
        // 先遍历是不是里面还有对象类型,是就计数
        for (let item in obj) {
          if (typeof (obj[item]) === 'object') {
            // 然后把这个再存到数组中下次遍历
            // arr.push([...obj[item]])
            // 要分别存入,否则下次死循环
            for (let key in obj[item]) {
              arr.push(obj[item][key])
            }
          }
        }
        // 循环结束后判断arr有没有增加
        if (arr.length > arrLength) {
          res++
          arrLength = arr.length
          // 继续递归
          return fn(arr)
        } else return res
      }
      return fn(obj)
    }

47. 拼手气红包

提供了一个RedPackage的类初始化时传入红包金额和个数,需要实现一个openRedPackage方法,每调一次都进行一次“抢红包”,并以console.log的形式输出抢到的红包金额。

class RedPackage {
    money = 0;
    count = 0;
    _remain = 0;
    
    constructor(money, count) {
        this.money = money;
        this.count = count;
        this._remain = money;
    }
    
    openRedPackge() {
        //  已经抢完了
        if (this.count <= 0) {
            console.log('红包已经被抢完啦~');
            return;
        }
        
        //  只剩一个红包
        if (this.count === 1) {
            this.count--;
            console.log(this._remain);
            return;
        }
        
        const ratio = Math.random() * (this._remain / this.money);
        //  这里会涉及到一个JS计算精度的问题
        //  正常应该用第三方库或者字符串算法实现一个精准的加减乘除
        //  这里为了简单就这么直接做了
        let youGet = (this.money * ratio).toFixed(2);
        const tempRemain = +(this._remain - youGet).toFixed(2);
        const allLeast = this.count * 0.01;
        
        //  如果剩余的金额不够每人一分钱,那么需要减少本次获得的金额
        if (tempRemain < allLeast) {
            youGet = +(this._remain - allLeast).toFixed(2);
            this._remain = allLeast;
        } else {
            this._remain = tempRemain;
        }
        console.log(youGet);
        this.count--;
    }
}

48. 手写Event事件,包括on、off、once、trigger

1、on(event,fn):监听event事件,事件触发时调用fn函数;

2、once(event,fn):为指定事件注册一个单次监听器,单次监听器最多只触发一次,触发后立即解除监听器;

3、emit(event,arg1,arg2,arg3...):触发event事件,并把参数arg1,arg2,arg3....传给事件处理函数;

4、off(event,fn):停止监听某个事件。

class EventEmitter{
        constructor(){
            this._envents = {}
        }

        on(event,callback){  //监听event事件,触发时调用callback函数
            let callbacks = this._events[event] || []
            callbacks.push(callback)
            this._events[event] = callbacks
            return this
        }
        off(event,callback){  //停止监听event事件
            let callbacks = this._events[event]
            this._events[event] = callbacks && callbacks.filter(fn => fn !== callback)
            return this
        }
        emit(...args){ //触发事件,并把参数传给事件的处理函数
            const event = args[0]
            const params = [].slice.call(args,1)
            const callbacks = this._events[event]
            callbacks.forEach(fn => fn.apply(this.params))
            return this
        }
        once(event,callback){ //为事件注册单次监听器
            let wrapFanc = (...args) => {
                callback.apply(this.args)
                this.off(event,wrapFanc)
            }
            this.on(event,wrapFanc)
            return this
        }

    }

49. 利用Promise实现控制并发数量

const fn = url => {
    // 实际场景这里用axios等请求库 发请求即可 也不用设置延时
    return new Promise(resolve => {
        setTimeout(() => {
            console.log('完成一个任务', url, new Date());
            resolve({ url, date: new Date() });
        }, 0);
    })
};

function limitQueue(urls, limit) {
    // 完成任务数
    let count = 0;
    // 填充满执行队列
    for (let i = 0; i < limit; i++) {
        run();
    }
    // 执行一个任务
    function run() {
        // 构造待执行任务 当该任务完成后 如果还有待完成的任务 继续执行任务
        new Promise((resolve, reject) => {
            const url = urls[count];
            count++;
            resolve(fn(url))
        }).then(() => {
            if (count < urls.length) run()   // 这一句才是核心,假如执行完还没limit则继续循环执行
        })
    }
};

const urls = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15];

(async _ => {
    await limitQueue(urls, 4);
})()