阿里前端二面经典手写面试题汇总

news2024/11/18 11:39:58

实现类的继承

实现类的继承-简版

类的继承在几年前是重点内容,有n种继承方式各有优劣,es6普及后越来越不重要,那么多种写法有点『回字有四样写法』的意思,如果还想深入理解的去看红宝书即可,我们目前只实现一种最理想的继承方式。

// 寄生组合继承
function Parent(name) {
  this.name = name
}
Parent.prototype.say = function() {
  console.log(this.name + ` say`);
}
Parent.prototype.play = function() {
  console.log(this.name + ` play`);
}

function Child(name, parent) {
  // 将父类的构造函数绑定在子类上
  Parent.call(this, parent)
  this.name = name
}

/** 
 1. 这一步不用Child.prototype = Parent.prototype的原因是怕共享内存,修改父类原型对象就会影响子类
 2. 不用Child.prototype = new Parent()的原因是会调用2次父类的构造方法(另一次是call),会存在一份多余的父类实例属性
3. Object.create是创建了父类原型的副本,与父类原型完全隔离
*/
Child.prototype = Object.create(Parent.prototype);
Child.prototype.say = function() {
  console.log(this.name + ` say`);
}

// 注意记得把子类的构造指向子类本身
Child.prototype.constructor = Child;
// 测试
var parent = new Parent('parent');
parent.say() 

var child = new Child('child');
child.say() 
child.play(); // 继承父类的方法

ES5实现继承-详细

第一种方式是借助call实现继承

function Parent1(){
    this.name = 'parent1';
}
function Child1(){
    Parent1.call(this);
    this.type = 'child1'    
}
console.log(new Child1);

这样写的时候子类虽然能够拿到父类的属性值,但是问题是父类中一旦存在方法那么子类无法继承。那么引出下面的方法

第二种方式借助原型链实现继承:

function Parent2() {
    this.name = 'parent2';
    this.play = [1, 2, 3]
  }
  function Child2() {
    this.type = 'child2';
  }
  Child2.prototype = new Parent2();

  console.log(new Child2());

看似没有问题,父类的方法和属性都能够访问,但实际上有一个潜在的不足。举个例子:

var s1 = new Child2();
  var s2 = new Child2();
  s1.play.push(4);
  console.log(s1.play, s2.play); // [1,2,3,4] [1,2,3,4]

明明我只改变了s1的play属性,为什么s2也跟着变了呢?很简单,因为两个实例使用的是同一个原型对象

第三种方式:将前两种组合:

function Parent3 () {
    this.name = 'parent3';
    this.play = [1, 2, 3];
  }
  function Child3() {
    Parent3.call(this);
    this.type = 'child3';
  }
  Child3.prototype = new Parent3();
  var s3 = new Child3();
  var s4 = new Child3();
  s3.play.push(4);
  console.log(s3.play, s4.play); // [1,2,3,4] [1,2,3]

之前的问题都得以解决。但是这里又徒增了一个新问题,那就是Parent3的构造函数会多执行了一次(Child3.prototype = new Parent3();)。这是我们不愿看到的。那么如何解决这个问题?

第四种方式: 组合继承的优化1

function Parent4 () {
    this.name = 'parent4';
    this.play = [1, 2, 3];
  }
  function Child4() {
    Parent4.call(this);
    this.type = 'child4';
  }
  Child4.prototype = Parent4.prototype;

这里让将父类原型对象直接给到子类,父类构造函数只执行一次,而且父类属性和方法均能访问,但是我们来测试一下

var s3 = new Child4();
  var s4 = new Child4();
  console.log(s3)

子类实例的构造函数是Parent4,显然这是不对的,应该是Child4。

第五种方式(最推荐使用):优化2

function Parent5 () {
    this.name = 'parent5';
    this.play = [1, 2, 3];
  }
  function Child5() {
    Parent5.call(this);
    this.type = 'child5';
  }
  Child5.prototype = Object.create(Parent5.prototype);
  Child5.prototype.constructor = Child5;

这是最推荐的一种方式,接近完美的继承。

实现instanceOf

思路:

  • 步骤1:先取得当前类的原型,当前实例对象的原型链
  • ​步骤2:一直循环(执行原型链的查找机制)
    • 取得当前实例对象原型链的原型链(proto = proto.__proto__,沿着原型链一直向上查找)
    • 如果 当前实例的原型链__proto__上找到了当前类的原型prototype,则返回 true
    • 如果 一直找到Object.prototype.__proto__ == nullObject的基类(null)上面都没找到,则返回 false
// 实例.__ptoto__ === 类.prototype
function _instanceof(example, classFunc) {
    // 由于instance要检测的是某对象,需要有一个前置判断条件
    //基本数据类型直接返回false
    if(typeof example !== 'object' || example === null) return false;

    let proto = Object.getPrototypeOf(example);
    while(true) {
        if(proto == null) return false;

        // 在当前实例对象的原型链上,找到了当前类
        if(proto == classFunc.prototype) return true;
        // 沿着原型链__ptoto__一层一层向上查
        proto = Object.getPrototypeof(proto); // 等于proto.__ptoto__
    }
}

console.log('test', _instanceof(null, Array)) // false
console.log('test', _instanceof([], Array)) // true
console.log('test', _instanceof('', Array)) // false
console.log('test', _instanceof({}, Object)) // true

实现一个队列

基于链表结构实现队列

const LinkedList = require('./实现一个链表结构')

// 用链表默认使用数组来模拟队列,性能更佳
class Queue {
  constructor() {
    this.ll = new LinkedList()
  }
  // 向队列中添加
  offer(elem) {
    this.ll.add(elem)
  }
  // 查看第一个
  peek() {
    return this.ll.get(0)
  }
  // 队列只能从头部删除
  remove() {
    return this.ll.remove(0)
  }
}

var queue = new Queue()

queue.offer(1)
queue.offer(2)
queue.offer(3)
var removeVal = queue.remove(3)

console.log(queue.ll,'queue.ll')
console.log(removeVal,'queue.remove')
console.log(queue.peek(),'queue.peek')

实现every方法

Array.prototype.myEvery=function(callback, context = window){
    var len=this.length,
        flag=true,
        i = 0;

    for(;i < len; i++){
      if(!callback.apply(context,[this[i], i , this])){
        flag=false;
        break;
      } 
    }
    return flag;
  }


  // var obj = {num: 1}
  // var aa=arr.myEvery(function(v,index,arr){
  //     return v.num>=12;
  // },obj)
  // console.log(aa)

实现LRU淘汰算法

LRU 缓存算法是一个非常经典的算法,在很多面试中经常问道,不仅仅包括前端面试

LRU 英文全称是 Least Recently Used,英译过来就是” 最近最少使用 “的意思。LRU 是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰

通俗的解释:

假如我们有一块内存,专门用来缓存我们最近发访问的网页,访问一个新网页,我们就会往内存中添加一个网页地址,随着网页的不断增加,内存存满了,这个时候我们就需要考虑删除一些网页了。这个时候我们找到内存中最早访问的那个网页地址,然后把它删掉。这一整个过程就可以称之为 LRU 算法

上图就很好的解释了 LRU 算法在干嘛了,其实非常简单,无非就是我们往内存里面添加或者删除元素的时候,遵循最近最少使用原则

使用场景

LRU 算法使用的场景非常多,这里简单举几个例子即可:

  • 我们操作系统底层的内存管理,其中就包括有 LRU 算法
  • 我们常见的缓存服务,比如 redis 等等
  • 比如浏览器的最近浏览记录存储
  • vue中的keep-alive组件使用了LRU算法

梳理实现 LRU 思路

  • 特点分析:
    • 我们需要一块有限的存储空间,因为无限的化就没必要使用LRU算发删除数据了。
    • 我们这块存储空间里面存储的数据需要是有序的,因为我们必须要顺序来删除数据,所以可以考虑使用 ArrayMap 数据结构来存储,不能使用 Object,因为它是无序的。
    • 我们能够删除或者添加以及获取到这块存储空间中的指定数据。
    • 存储空间存满之后,在添加数据时,会自动删除时间最久远的那条数据。
  • 实现需求:
    • 实现一个 LRUCache 类型,用来充当存储空间
    • 采用 Map 数据结构存储数据,因为它的存取时间复杂度为 O(1),数组为 O(n)
    • 实现 getset 方法,用来获取和添加数据
    • 我们的存储空间有长度限制,所以无需提供删除方法,存储满之后,自动删除最久远的那条数据
    • 当使用 get 获取数据后,该条数据需要更新到最前面

具体实现

class LRUCache {
  constructor(length) {
    this.length = length; // 存储长度
    this.data = new Map(); // 存储数据
  }
  // 存储数据,通过键值对的方式
  set(key, value) {
    const data = this.data;
    if (data.has(key)) {
      data.delete(key)
    }

    data.set(key, value);

    // 如果超出了容量,则需要删除最久的数据
    if (data.size > this.length) {
      const delKey = data.keys().next().value;
      data.delete(delKey);
    }
  }
  // 获取数据
  get(key) {
    const data = this.data;
    // 未找到
    if (!data.has(key)) {
      return null;
    }
    const value = data.get(key); // 获取元素
    data.delete(key); // 删除元素
    data.set(key, value); // 重新插入元素

    return value // 返回获取的值
  }
}
var lruCache = new LRUCache(5);
  • set 方法:往 map 里面添加新数据,如果添加的数据存在了,则先删除该条数据,然后再添加。如果添加数据后超长了,则需要删除最久远的一条数据。data.keys().next().value 便是获取最后一条数据的意思。
  • get 方法:首先从 map 对象中拿出该条数据,然后删除该条数据,最后再重新插入该条数据,确保将该条数据移动到最前面
// 测试

// 存储数据 set:

lruCache.set('name', 'test');
lruCache.set('age', 10);
lruCache.set('sex', '男');
lruCache.set('height', 180);
lruCache.set('weight', '120');
console.log(lruCache);

继续插入数据,此时会超长,代码如下:

lruCache.set('grade', '100');
console.log(lruCache);

此时我们发现存储时间最久的 name 已经被移除了,新插入的数据变为了最前面的一个。

我们使用 get 获取数据,代码如下:

我们发现此时 sex 字段已经跑到最前面去了

总结

LRU 算法其实逻辑非常的简单,明白了原理之后实现起来非常的简单。最主要的是我们需要使用什么数据结构来存储数据,因为 map 的存取非常快,所以我们采用了它,当然数组其实也可以实现的。还有一些小伙伴使用链表来实现 LRU,这当然也是可以的。

实现Promise相关方法

实现Promise的resolve

实现 resolve 静态方法有三个要点:

  • 传参为一个 Promise, 则直接返回它。
  • 传参为一个 thenable 对象,返回的 Promise 会跟随这个对象,采用它的最终状态作为自己的状态。
  • 其他情况,直接返回以该值为成功状态的promise对象。
Promise.resolve = (param) => {
  if(param instanceof Promise) return param;
  return new Promise((resolve, reject) => {
    if(param && param.then && typeof param.then === 'function') {
      // param 状态变为成功会调用resolve,将新 Promise 的状态变为成功,反之亦然
      param.then(resolve, reject);
    }else {
      resolve(param);
    }
  })
}

实现 Promise.reject

Promise.reject 中传入的参数会作为一个 reason 原封不动地往下传, 实现如下:

Promise.reject = function (reason) {
    return new Promise((resolve, reject) => {
        reject(reason);
    });
}

实现 Promise.prototype.finally

前面的promise不管成功还是失败,都会走到finally中,并且finally之后,还可以继续then(说明它还是一个then方法是关键),并且会将初始的promise值原封不动的传递给后面的then.

Promise.prototype.finally最大的作用

  • finally里的函数,无论如何都会执行,并会把前面的值原封不动传递给下一个then方法中
  • 如果finally函数中有promise等异步任务,会等它们全部执行完毕,再结合之前的成功与否状态,返回值

Promise.prototype.finally六大情况用法

// 情况1
Promise.resolve(123).finally((data) => { // 这里传入的函数,无论如何都会执行
  console.log(data); // undefined
})

// 情况2 (这里,finally方法相当于做了中间处理,起一个过渡的作用)
Promise.resolve(123).finally((data) => {
  console.log(data); // undefined
}).then(data => {
  console.log(data); // 123
})

// 情况3 (这里只要reject,都会走到下一个then的err中)
Promise.reject(123).finally((data) => {
  console.log(data); // undefined
}).then(data => {
  console.log(data);
}, err => {
  console.log(err, 'err'); // 123 err
})

// 情况4 (一开始就成功之后,会等待finally里的promise执行完毕后,再把前面的data传递到下一个then中)
Promise.resolve(123).finally((data) => {
  console.log(data); // undefined
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('ok');
    }, 3000)
  })
}).then(data => {
  console.log(data, 'success'); // 123 success
}, err => {
  console.log(err, 'err');
})

// 情况5 (虽然一开始成功,但是只要finally函数中的promise失败了,就会把其失败的值传递到下一个then的err中)
Promise.resolve(123).finally((data) => {
  console.log(data); // undefined
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('rejected');
    }, 3000)
  })
}).then(data => {
  console.log(data, 'success');
}, err => {
  console.log(err, 'err'); // rejected err
})

// 情况6 (虽然一开始失败,但是也要等finally中的promise执行完,才能把一开始的err传递到err的回调中)
Promise.reject(123).finally((data) => {
  console.log(data); // undefined
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('resolve');
    }, 3000)
  })
}).then(data => {
  console.log(data, 'success');
}, err => {
  console.log(err, 'err'); // 123 err
})

源码实现

Promise.prototype.finally = function (callback) {
  return this.then((data) => {
    // 让函数执行 内部会调用方法,如果方法是promise,需要等待它完成
    // 如果当前promise执行时失败了,会把err传递到,err的回调函数中
    return Promise.resolve(callback()).then(() => data); // data 上一个promise的成功态
  }, err => {
    return Promise.resolve(callback()).then(() => {
      throw err; // 把之前的失败的err,抛出去
    });
  })
}

实现 Promise.all

对于 all 方法而言,需要完成下面的核心功能:

  • 传入参数为一个空的可迭代对象,则直接进行resolve
  • 如果参数中有一个promise失败,那么Promise.all返回的promise对象失败。
  • 在任何情况下,Promise.all 返回的 promise 的完成状态的结果都是一个数组
Promise.all = function(promises) {
  return new Promise((resolve, reject) => {
    let result = [];
    let index = 0;
    let len = promises.length;
    if(len === 0) {
      resolve(result);
      return;
    }

    for(let i = 0; i < len; i++) {
      // 为什么不直接 promise[i].then, 因为promise[i]可能不是一个promise
      Promise.resolve(promise[i]).then(data => {
        result[i] = data;
        index++;
        if(index === len) resolve(result);
      }).catch(err => {
        reject(err);
      })
    }
  })
}

实现promise.allsettle

MDN: Promise.allSettled()方法返回一个在所有给定的promise已经fulfilledrejected后的promise,并带有一个对象数组,每个对象表示对应的promise`结果

当您有多个彼此不依赖的异步任务成功完成时,或者您总是想知道每个promise的结果时,通常使用它。

【译】Promise.allSettledPromise.all 类似, 其参数接受一个Promise的数组, 返回一个新的Promise, 唯一的不同在于, 其不会进行短路, 也就是说当Promise全部处理完成后我们可以拿到每个Promise的状态, 而不管其是否处理成功。

用法 | 测试用例

let fs = require('fs').promises;

let getName = fs.readFile('./name.txt', 'utf8'); // 读取文件成功
let getAge = fs.readFile('./age.txt', 'utf8');

Promise.allSettled([1, getName, getAge, 2]).then(data => {
    console.log(data);
});
// 输出结果
/*
    [
    { status: 'fulfilled', value: 1 },
    { status: 'fulfilled', value: 'zf' },
    { status: 'fulfilled', value: '11' },
    { status: 'fulfilled', value: 2 }
    ]
*/

let getName = fs.readFile('./name123.txt', 'utf8'); // 读取文件失败
let getAge = fs.readFile('./age.txt', 'utf8');
// 输出结果
/*
    [
    { status: 'fulfilled', value: 1 },
    {
      status: 'rejected',
      value: [Error: ENOENT: no such file or directory, open './name123.txt'] {
        errno: -2,
        code: 'ENOENT',
        syscall: 'open',
        path: './name123.txt'
      }
    },
    { status: 'fulfilled', value: '11' },
    { status: 'fulfilled', value: 2 }
  ]
*/

实现

function isPromise (val) {
  return typeof val.then === 'function'; // (123).then => undefined
}

Promise.allSettled = function(promises) {
  return new Promise((resolve, reject) => {
    let arr = [];
    let times = 0;
    const setData = (index, data) => {
      arr[index] = data;
      if (++times === promises.length) {
        resolve(arr);
      }
      console.log('times', times)
    }

    for (let i = 0; i < promises.length; i++) {
      let current = promises[i];
      if (isPromise(current)) {
        current.then((data) => {
          setData(i, { status: 'fulfilled', value: data });
        }, err => {
          setData(i, { status: 'rejected', value: err })
        })
      } else {
        setData(i, { status: 'fulfilled', value: current })
      }
    }
  })
}

实现 Promise.race

race 的实现相比之下就简单一些,只要有一个 promise 执行完,直接 resolve 并停止执行

Promise.race = function(promises) {
  return new Promise((resolve, reject) => {
    let len = promises.length;
    if(len === 0) return;
    for(let i = 0; i < len; i++) {
      Promise.resolve(promise[i]).then(data => {
        resolve(data);
        return;
      }).catch(err => {
        reject(err);
        return;
      })
    }
  })
}

实现一个简版Promise

// 使用
var promise = new Promise((resolve,reject) => {
    if (操作成功) {
        resolve(value)
    } else {
        reject(error)
    }
})
promise.then(function (value) {
    // success
},function (value) {
    // failure
})
function myPromise(constructor) {
    let self = this;
    self.status = "pending"   // 定义状态改变前的初始状态
    self.value = undefined;   // 定义状态为resolved的时候的状态
    self.reason = undefined;  // 定义状态为rejected的时候的状态
    function resolve(value) {
       if(self.status === "pending") {
          self.value = value;
          self.status = "resolved";
       }
    }
    function reject(reason) {
       if(self.status === "pending") {
          self.reason = reason;
          self.status = "rejected";
       }
    }
    // 捕获构造异常
    try {
       constructor(resolve,reject);
    } catch(e) {
       reject(e);
    }
}
// 添加 then 方法
myPromise.prototype.then = function(onFullfilled,onRejected) {
   let self = this;
   switch(self.status) {
      case "resolved":
        onFullfilled(self.value);
        break;
      case "rejected":
        onRejected(self.reason);
        break;
      default:       
   }
}

var p = new myPromise(function(resolve,reject) {
    resolve(1)
});
p.then(function(x) {
    console.log(x) // 1
})

使用class实现

class MyPromise {
  constructor(fn) {
    this.resolvedCallbacks = [];
    this.rejectedCallbacks = [];

    this.state = 'PENDING';
    this.value = '';

    fn(this.resolve.bind(this), this.reject.bind(this));

  }

  resolve(value) {
    if (this.state === 'PENDING') {
      this.state = 'RESOLVED';
      this.value = value;

      this.resolvedCallbacks.map(cb => cb(value));   
    }
  }

  reject(value) {
    if (this.state === 'PENDING') {
      this.state = 'REJECTED';
      this.value = value;

      this.rejectedCallbacks.map(cb => cb(value));
    }
  }

  then(onFulfilled, onRejected) {
    if (this.state === 'PENDING') {
      this.resolvedCallbacks.push(onFulfilled);
      this.rejectedCallbacks.push(onRejected);

    }

    if (this.state === 'RESOLVED') {
      onFulfilled(this.value);
    }

    if (this.state === 'REJECTED') {
      onRejected(this.value);
    }
  }
}

Promise 实现-详细

  • 可以把 Promise 看成一个状态机。初始是 pending 状态,可以通过函数 resolvereject ,将状态转变为 resolved或者 rejected 状态,状态一旦改变就不能再次变化。
  • then 函数会返回一个 Promise 实例,并且该返回值是一个新的实例而不是之前的实例。因为 Promise 规范规定除了 pending 状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个 then 调用就失去意义了。
  • 对于 then来说,本质上可以把它看成是 flatMap
// 三种状态
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
// promise 接收一个函数参数,该函数会立即执行
function MyPromise(fn) {
  let _this = this;
  _this.currentState = PENDING;
  _this.value = undefined;
  // 用于保存 then 中的回调,只有当 promise
  // 状态为 pending 时才会缓存,并且每个实例至多缓存一个
  _this.resolvedCallbacks = [];
  _this.rejectedCallbacks = [];

  _this.resolve = function (value) {
    if (value instanceof MyPromise) {
      // 如果 value 是个 Promise,递归执行
      return value.then(_this.resolve, _this.reject)
    }
    setTimeout(() => { // 异步执行,保证执行顺序
      if (_this.currentState === PENDING) {
        _this.currentState = RESOLVED;
        _this.value = value;
        _this.resolvedCallbacks.forEach(cb => cb());
      }
    })
  };

  _this.reject = function (reason) {
    setTimeout(() => { // 异步执行,保证执行顺序
      if (_this.currentState === PENDING) {
        _this.currentState = REJECTED;
        _this.value = reason;
        _this.rejectedCallbacks.forEach(cb => cb());
      }
    })
  }
  // 用于解决以下问题
  // new Promise(() => throw Error('error))
  try {
    fn(_this.resolve, _this.reject);
  } catch (e) {
    _this.reject(e);
  }
}

MyPromise.prototype.then = function (onResolved, onRejected) {
  var self = this;
  // 规范 2.2.7,then 必须返回一个新的 promise
  var promise2;
  // 规范 2.2.onResolved 和 onRejected 都为可选参数
  // 如果类型不是函数需要忽略,同时也实现了透传
  // Promise.resolve(4).then().then((value) => console.log(value))
  onResolved = typeof onResolved === 'function' ? onResolved : v => v;
  onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;

  if (self.currentState === RESOLVED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      // 规范 2.2.4,保证 onFulfilled,onRjected 异步执行
      // 所以用了 setTimeout 包裹下
      setTimeout(function () {
        try {
          var x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }

  if (self.currentState === REJECTED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      setTimeout(function () {
        // 异步执行onRejected
        try {
          var x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }

  if (self.currentState === PENDING) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      self.resolvedCallbacks.push(function () {
        // 考虑到可能会有报错,所以使用 try/catch 包裹
        try {
          var x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });

      self.rejectedCallbacks.push(function () {
        try {
          var x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });
    }));
  }
};
// 规范 2.3
function resolutionProcedure(promise2, x, resolve, reject) {
  // 规范 2.3.1,x 不能和 promise2 相同,避免循环引用
  if (promise2 === x) {
    return reject(new TypeError("Error"));
  }
  // 规范 2.3.2
  // 如果 x 为 Promise,状态为 pending 需要继续等待否则执行
  if (x instanceof MyPromise) {
    if (x.currentState === PENDING) {
      x.then(function (value) {
        // 再次调用该函数是为了确认 x resolve 的
        // 参数是什么类型,如果是基本类型就再次 resolve
        // 把值传给下个 then
        resolutionProcedure(promise2, value, resolve, reject);
      }, reject);
    } else {
      x.then(resolve, reject);
    }
    return;
  }
  // 规范 2.3.3.3.3
  // reject 或者 resolve 其中一个执行过得话,忽略其他的
  let called = false;
  // 规范 2.3.3,判断 x 是否为对象或者函数
  if (x !== null && (typeof x === "object" || typeof x === "function")) {
    // 规范 2.3.3.2,如果不能取出 then,就 reject
    try {
      // 规范 2.3.3.1
      let then = x.then;
      // 如果 then 是函数,调用 x.then
      if (typeof then === "function") {
        // 规范 2.3.3.3
        then.call(
          x,
          y => {
            if (called) return;
            called = true;
            // 规范 2.3.3.3.1
            resolutionProcedure(promise2, y, resolve, reject);
          },
          e => {
            if (called) return;
            called = true;
            reject(e);
          }
        );
      } else {
        // 规范 2.3.3.4
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    // 规范 2.3.4,x 为基本类型
    resolve(x);
  }
}

实现Promisify

const fs = require('fs')
const path = require('path')

// node中使用
// const fs = require('fs').promises 12.18版
// const promisify = require('util').promisify

// 包装node api promise化 典型的高级函数
const promisify = fn=>{
  return (...args)=>{
    return new Promise((resolve,reject)=>{
      fn(...args, (err,data)=>{
        if(err) {
          reject(err)
        } 
        resolve(data)
      })
    })
  }
}

// const read = promisify(fs.readFile)

// read(path.join(__dirname, './promise.js'), 'utf8').then(d=>{
//   console.log(d)
// })

// promise化node所有api
const promisifyAll = target=>{
  Reflect.ownKeys(target).forEach(key=>{
    if(typeof target[key] === 'function') {
      target[key+'Async'] = promisify(target[key])
    }
  })
  return target
}

// promise化fs下的函数
const promisifyNew = promisifyAll(fs)

promisifyNew.readFileAsync(path.join(__dirname, './promise.js'), 'utf8').then(d=>{
  console.log(d)
})

module.exports = {
  promisify,
  promisifyAll
}

完整实现Promises/A+规范

/**
 * Promises/A+规范 实现一个promise
 * https://promisesaplus.com/
*/

const EMUM = {
  PENDING: 'PENDING',
  FULFILLED: 'FULFILLED',
  REJECTED: 'REJECTED'
}

// x 返回值
// promise2 then的时候new的promise
// promise2的resolve, reject
const resolvePromise = (x, promise2, resolve, reject)=>{
  // 解析promise的值解析promise2是成功还是失败 传递到下层then
  if(x === promise2) {
    reject(new TypeError('类型错误'))
  }
  // 这里的x如果是一个promise的话 可能是其他的promise,可能调用了成功 又调用了失败
  // 防止resolve的时候 又throw err抛出异常到reject了
  let called
  // 如果x是promise 那么就采用他的状态
  // 有then方法是promise
  if(typeof x === 'object' && typeof x!== null || typeof x === 'function') {
    // x是对象或函数
    try {
      let then = x.then // 缓存,不用多次取值
      if(typeof then === 'function') {
        // 是promise,调用then方法里面有this,需要传入this为x才能取到then方法里面的值this.value
        then.call(x, y=>{// 成功
          // y值可能也是一个promise 如resolve(new Promise()) 此时的y==new Promise()
          // 递归解析y,直到拿到普通的值resolve(x出去)
          if(called) return;
          called = true;

          resolvePromise(y, promise2, resolve, reject)
        },r=>{// 一旦失败直接失败
          if(called) return;
          called = true;
          reject(r)
        })
      } else {
        // 普通对象不是promise
        resolve(x)
      }
    } catch (e) {
      // 对象取值可能报错,用defineProperty定义get 抛出异常
      if(called) return;
      called = true;
      reject(e)
    }
  } else {
    // x是普通值
    resolve(x) // 直接成功
  }

}
class myPromise {
  constructor(executor) {
    this.status = EMUM.PENDING // 当前状态
    this.value = undefined // resolve接收值
    this.reason = undefined // reject失败返回值

    /**
     * 同一个promise可以then多次(发布订阅模式)
     * 调用then时 当前状态是等待态,需要将当前成功或失败的回调存放起来(订阅)
     * 调用resolve时 将订阅函数进行执行(发布)
    */
    // 成功队列
    this.onResolvedCallbacks = []
    // 失败队列
    this.onRejectedCallbacks = []
    const resolve = value =>{
      // 如果value是一个promise,需要递归解析
      // 如 myPromise.resolve(new myPromise()) 需要解析value
      if(value instanceof myPromise) {
        // 不停的解析 直到值不是promise
        return value.then(resolve,reject)
      }

      if(this.status === EMUM.PENDING) {
        this.status = EMUM.FULFILLED
        this.value = value

        this.onResolvedCallbacks.forEach(fn=>fn())
      }
    }
    const reject = reason =>{
      if(this.status === EMUM.PENDING) {
        this.status = EMUM.REJECTED
        this.reason = reason

        this.onRejectedCallbacks.forEach(fn=>fn())
      }
    }
    try {
      executor(resolve,reject)
    } catch(e) {
      reject(e)
    }
  }
  then(onFulFilled, onRejected) {
    // 透传 处理默认不传的情况
    // new Promise((resolve,reject)=>{
    //   resolve(1)
    // }).then().then().then(d=>{})
    // new Promise((resolve,reject)=>{
    //   resolve(1)
    // }).then(v=>v).then(v=>v).then(d=>{})
    // new Promise((resolve,reject)=>{
    //   reject(1)
    // }).then().then().then(null, e=>{console.log(e)})
    // new Promise((resolve,reject)=>{
    //   reject(1)
    // }).then(null,e=>{throw e}).then(null,e=>{throw e}).then(null,e=>{console.log(e)})
    onFulFilled = typeof onFulFilled === 'function' ? onFulFilled : v => v
    onRejected = typeof onRejected === 'function' ? onRejected : err => {throw err}

    // 调用then 创建一个新的promise
    let promise2 = new myPromise((resolve,reject)=>{
      // 根据value判断是resolve 还是reject value也可能是promise
      if(this.status === EMUM.FULFILLED) {
        setTimeout(() => {
          try {
            // 成功回调结果
            let x = onFulFilled(this.value)
            // 解析promise
            resolvePromise(x, promise2,resolve,reject)
          } catch (error) {
            reject(error)
          }
        }, 0);
      }
      if(this.status === EMUM.REJECTED) {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason)
            // 解析promise
            resolvePromise(x, promise2,resolve,reject)
          } catch (error) {
            reject(error)
          }
        }, 0);
      }
      // 用户还未调用resolve或reject方法
      if(this.status === EMUM.PENDING) {
        this.onResolvedCallbacks.push(()=>{
          try {
            let x = onFulFilled(this.value)
            // 解析promise
            resolvePromise(x, promise2,resolve,reject)
          } catch (error) {
            reject(error)
          }
        })
        this.onRejectedCallbacks.push(()=>{
          try {
            let x = onRejected(this.reason)
            // 解析promise
            resolvePromise(x, promise2,resolve,reject)
          } catch (error) {
            reject(error)
          }
        })
      }
    })

    return promise2
  }
  catch(errCallback) {
    // 等同于没有成功,把失败放进去而已
    return this.then(null, errCallback)
  }
  // myPromise.resolve 具备等待功能的 如果参数的promise会等待promise解析完毕在向下执行
  static resolve(val) {
    return new myPromise((resolve,reject)=>{
      resolve(val)
    })
  }
  // myPromise.reject 直接将值返回
  static reject(reason) {
    return new myPromise((resolve,reject)=>{
      reject(reason)
    })
  }
  // finally传入的函数 无论成功或失败都执行
  // Promise.reject(100).finally(()=>{console.log(1)}).then(d=>console.log('success',d)).catch(er=>console.log('faild',er))
  // Promise.reject(100).finally(()=>new Promise()).then(d=>console.log(d)).catch(er=>)
  finally(callback) {
    return this.then((val)=>{
      return myPromise.resolve(callback()).then(()=>val)
    },(err)=>{
      return myPromise.resolve(callback()).then(()=>{throw err})
    })
  }
  // Promise.all
  static all(values) {
    return new myPromise((resolve,reject)=>{
      let resultArr = []
      let orderIndex = 0
      const processResultByKey = (value,index)=>{
        resultArr[index] = value 
        // 处理完全部
        if(++orderIndex === values.length) {
          resolve(resultArr) // 处理完成的结果返回去
        }
      }
      for (let i = 0; i < values.length; i++) {
        const value = values[i];
        // 是promise
        if(value && typeof value.then === 'function') {
          value.then((val)=>{
            processResultByKey(val,i)
          },reject)
        } else {
          // 不是promise情况
          processResultByKey(value,i)
        }
      }
    })
  }
  static race(promises) {
    // 采用最新成功或失败的作为结果
    return new myPromise((resolve,reject)=>{
      for (let i = 0; i < promises.length; i++) {
        let val = promises[i]
        if(val && typeof val.then === 'function') {
          // 任何一个promise先调用resolve或reject就返回结果了 也就是返回执行最快的那个promise的结果
          val.then(resolve,reject)
        }else{
          // 普通值
          resolve(val)
        }
      }
    })
  }
}


/**
 * =====测试用例-====
 */
// let promise1 = new myPromise((resolve,reject)=>{
//   setTimeout(() => {
//     resolve('成功')
//   }, 900);
// })

// promise1.then(val=>{
//   console.log('success', val)
// },reason=>{
//   console.log('fail', reason)
// })

/**
 * then的使用方式 普通值意味不是promise
 * 
 * 1、then中的回调有两个方法 成功或失败 他们的结果返回(普通值)会传递给外层的下一个then中
 * 2、可以在成功或失败中抛出异常,走到下一次then的失败中
 * 3、返回的是一个promsie,那么会用这个promise的状态作为结果,会用promise的结果向下传递
 * 4、错误处理,会默认先找离自己最新的错误处理,找不到就向下查找,找打了就执行
 */

// read('./name.txt').then(data=>{
//   return '123'
// }).then(data=>{

// }).then(null,err=>{

// })
// // .catch(err=>{ // catch就是没有成功的promise

// // })

/**
 * promise.then实现原理:通过每次返回一个新的promise来实现(promise一旦成功就不能失败,失败就不能成功)
 * 
 */

// function read(data) {
//   return new myPromise((resolve,reject)=>{
//     setTimeout(() => {
//       resolve(new myPromise((resolve,reject)=>resolve(data)))
//     }, 1000);
//   })
// }

// let promise2 = read({name: 'poetry'}).then(data=>{
//   return data
// }).then().then().then(data=>{
//   console.log(data,'-data-')
// },(err)=>{
//   console.log(err,'-err-')
// })

// finally测试
// myPromise
//   .resolve(100)
//   .finally(()=>{
//     return new myPromise((resolve,reject)=>setTimeout(() => {
//       resolve(100)
//     }, 100))
//   })
//   .then(d=>console.log('finally success',d))
//   .catch(er=>console.log(er, 'finally err'))


/**
 * promise.all 测试
 * 
 * myPromise.all 解决并发问题 多个异步并发获取最终的结果
*/

// myPromise.all([1,2,3,4,new myPromise((resolve,reject)=>{
//   setTimeout(() => {
//     resolve('ok1')
//   }, 1000);
// }),new myPromise((resolve,reject)=>{
//   setTimeout(() => {
//     resolve('ok2')
//   }, 1000);
// })]).then(d=>{
//   console.log(d,'myPromise.all.resolve')
// }).catch(err=>{
//   console.log(err,'myPromise.all.reject')
// })


// 实现promise中断请求
let promise = new Promise((resolve,reject)=>{
  setTimeout(() => {
    // 模拟接口调用 ajax调用超时
    resolve('成功') 
  }, 10000);
})

function promiseWrap(promise) {
  // 包装一个promise 可以控制原来的promise是成功 还是失败
  let abort
  let newPromsie = new myPromise((resolve,reject)=>{
    abort = reject
  })
  // 只要控制newPromsie失败,就可以控制被包装的promise走向失败
  // Promise.race 任何一个先成功或者失败 就可以获得结果
  let p = myPromise.race([promise, newPromsie])
  p.abort = abort

  return p
}

let newPromise = promiseWrap(promise)

setTimeout(() => {
  // 超过3秒超时
  newPromise.abort('请求超时')
}, 3000);

newPromise.then(d=>{
  console.log('d',d)
}).catch(err=>{
  console.log('err',err)
})


// 使用promises-aplus-tests 测试写的promise是否规范
// 全局安装 cnpm i -g promises-aplus-tests
// 命令行执行 promises-aplus-tests promise.js
// 测试入口 产生延迟对象
myPromise.defer = myPromise.deferred = function () {
  let dfd = {}
  dfd.promise = new myPromise((resolve,reject)=>{
    dfd.resolve = resolve
    dfd.reject = reject
  })
  return dfd
}

// 延迟对象用户
// ![](http://img-repo.poetries.top/images/20210509172817.png)
// promise解决嵌套问题
// function readData(url) {
//   let dfd = myPromise.defer()
//   fs.readFile(url, 'utf8', function (err,data) {
//     if(err) {
//       dfd.reject()
//     }
//     dfd.resolve(data)
//   })
//   return dfd.promise
// }
// readData().then(d=>{
//   return d
// })

module.exports = myPromise

参考 前端进阶面试题详细解答

实现一个简易的MVVM

实现一个简易的MVVM我会分为这么几步来:

  1. 首先我会定义一个类Vue,这个类接收的是一个options,那么其中可能有需要挂载的根元素的id,也就是el属性;然后应该还有一个data属性,表示需要双向绑定的数据
  2. 其次我会定义一个Dep类,这个类产生的实例对象中会定义一个subs数组用来存放所依赖这个属性的依赖,已经添加依赖的方法addSub,删除方法removeSub,还有一个notify方法用来遍历更新它subs中的所有依赖,同时Dep类有一个静态属性target它用来表示当前的观察者,当后续进行依赖收集的时候可以将它添加到dep.subs中。
  3. 然后设计一个observe方法,这个方法接收的是传进来的data,也就是options.data,里面会遍历data中的每一个属性,并使用Object.defineProperty()来重写它的getset,那么这里面呢可以使用new Dep()实例化一个dep对象,在get的时候调用其addSub方法添加当前的观察者Dep.target完成依赖收集,并且在set的时候调用dep.notify方法来通知每一个依赖它的观察者进行更新
  4. 完成这些之后,我们还需要一个compile方法来将HTML模版和数据结合起来。在这个方法中首先传入的是一个node节点,然后遍历它的所有子级,判断是否有firstElmentChild,有的话则进行递归调用compile方法,没有firstElementChild的话且该child.innderHTML用正则匹配满足有/\{\{(.*)\}\}/项的话则表示有需要双向绑定的数据,那么就将用正则new Reg('\\{\\{\\s*' + key + '\\s*\\}\\}', 'gm')替换掉是其为msg变量。
  5. 完成变量替换的同时,还需要将Dep.target指向当前的这个child,且调用一下this.opt.data[key],也就是为了触发这个数据的get来对当前的child进行依赖收集,这样下次数据变化的时候就能通知child进行视图更新了,不过在最后要记得将Dep.target指为null哦(其实在Vue中是有一个targetStack栈用来存放target的指向的)
  6. 那么最后我们只需要监听documentDOMContentLoaded然后在回调函数中实例化这个Vue对象就可以了

coding :

需要注意的点:

  • childNodes会获取到所有的子节点以及文本节点(包括元素标签中的空白节点)
  • firstElementChild表示获取元素的第一个字元素节点,以此来区分是不是元素节点,如果是的话则调用compile进行递归调用,否则用正则匹配
  • 这里面的正则真的不难,大家可以看一下

完整代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>MVVM</title>
  </head>
  <body>
    <div id="app">
      <h3>姓名</h3>
      <p>{{name}}</p>
      <h3>年龄</h3>
      <p>{{age}}</p>
    </div>
  </body>
</html>
<script>
  document.addEventListener(
    "DOMContentLoaded",
    function () {
      let opt = { el: "#app", data: { name: "等待修改...", age: 20 } };
      let vm = new Vue(opt);
      setTimeout(() => {
        opt.data.name = "jing";
      }, 2000);
    },
    false
  );
  class Vue {
    constructor(opt) {
      this.opt = opt;
      this.observer(opt.data);
      let root = document.querySelector(opt.el);
      this.compile(root);
    }
    observer(data) {
      Object.keys(data).forEach((key) => {
        let obv = new Dep();
        data["_" + key] = data[key];

        Object.defineProperty(data, key, {
          get() {
            Dep.target && obv.addSubNode(Dep.target);
            return data["_" + key];
          },
          set(newVal) {
            obv.update(newVal);
            data["_" + key] = newVal;
          },
        });
      });
    }
    compile(node) {
      [].forEach.call(node.childNodes, (child) => {
        if (!child.firstElementChild && /\{\{(.*)\}\}/.test(child.innerHTML)) {
          let key = RegExp.$1.trim();
          child.innerHTML = child.innerHTML.replace(
            new RegExp("\\{\\{\\s*" + key + "\\s*\\}\\}", "gm"),
            this.opt.data[key]
          );
          Dep.target = child;
          this.opt.data[key];
          Dep.target = null;
        } else if (child.firstElementChild) this.compile(child);
      });
    }
  }

  class Dep {
    constructor() {
      this.subNode = [];
    }
    addSubNode(node) {
      this.subNode.push(node);
    }
    update(newVal) {
      this.subNode.forEach((node) => {
        node.innerHTML = newVal;
      });
    }
  }
</script>

简化版2

function update(){
  console.log('数据变化~~~ mock update view')
}
let obj = [1,2,3]
// 变异方法 push shift unshfit reverse sort splice pop
// Object.defineProperty
let oldProto = Array.prototype;
let proto = Object.create(oldProto); // 克隆了一分
['push','shift'].forEach(item=>{
  proto[item] = function(){
    update();
    oldProto[item].apply(this,arguments);
  }
})
function observer(value){ // proxy reflect
  if(Array.isArray(value)){
    // AOP
    return value.__proto__ = proto;
    // 重写 这个数组里的push shift unshfit reverse sort splice pop
  }
  if(typeof value !== 'object'){
    return value;
  }
  for(let key in value){
    defineReactive(value,key,value[key]);
  }
}
function defineReactive(obj,key,value){
  observer(value); // 如果是对象 继续增加getter和setter
  Object.defineProperty(obj,key,{
    get(){
        return value;
    },
    set(newValue){
        if(newValue !== value){
            observer(newValue);
            value = newValue;
            update();
        }
    }
  })
}
observer(obj); 
// AOP
// obj.name = {n:200}; // 数据变了 需要更新视图 深度监控
// obj.name.n = 100;
obj.push(123);
obj.push(456);
console.log(obj);

实现JSONP方法

利用<script>标签不受跨域限制的特点,缺点是只能支持 get 请求

  • 创建script标签
  • 设置script标签的src属性,以问号传递参数,设置好回调函数callback名称
  • 插入到html文本中
  • 调用回调函数,res参数就是获取的数据
function jsonp({url,params,callback}) {
  return new Promise((resolve,reject)=>{
  let script = document.createElement('script')

    window[callback] = function (data) {
      resolve(data)
      document.body.removeChild(script)
    }
    var arr = []
    for(var key in params) {
      arr.push(`${key}=${params[key]}`)
    }
    script.type = 'text/javascript'
    script.src = `${url}?callback=${callback}&${arr.join('&')}`
    document.body.appendChild(script)
  })
}
// 测试用例
jsonp({
  url: 'http://suggest.taobao.com/sug',
  callback: 'getData',
  params: {
    q: 'iphone手机',
    code: 'utf-8'
  },
}).then(data=>{console.log(data)})
  • 设置 CORS: Access-Control-Allow-Origin:*
  • postMessage

实现Ajax

步骤

  • 创建 XMLHttpRequest 实例
  • 发出 HTTP 请求
  • 服务器返回 XML 格式的字符串
  • JS 解析 XML,并更新局部页面
  • 不过随着历史进程的推进,XML 已经被淘汰,取而代之的是 JSON。

了解了属性和方法之后,根据 AJAX 的步骤,手写最简单的 GET 请求。

对象数组列表转成树形结构(处理菜单)

[
    {
        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
            }
        ]
    }
]

实现代码如下:

function listToTree(data) {
  let temp = {};
  let treeData = [];
  for (let i = 0; i < data.length; i++) {
    temp[data[i].id] = data[i];
  }
  for (let i in temp) {
    if (+temp[i].parentId != 0) {
      if (!temp[temp[i].parentId].children) {
        temp[temp[i].parentId].children = [];
      }
      temp[temp[i].parentId].children.push(temp[i]);
    } else {
      treeData.push(temp[i]);
    }
  }
  return treeData;
}

异步串行 | 异步并行

// 字节面试题,实现一个异步加法
function asyncAdd(a, b, callback) {
  setTimeout(function () {
    callback(null, a + b);
  }, 500);
}

// 解决方案
// 1. promisify
const promiseAdd = (a, b) => new Promise((resolve, reject) => {
  asyncAdd(a, b, (err, res) => {
    if (err) {
      reject(err)
    } else {
      resolve(res)
    }
  })
})

// 2. 串行处理
async function serialSum(...args) {
  return args.reduce((task, now) => task.then(res => promiseAdd(res, now)), Promise.resolve(0))
}

// 3. 并行处理
async function parallelSum(...args) {
  if (args.length === 1) return args[0]
  const tasks = []
  for (let i = 0; i < args.length; i += 2) {
    tasks.push(promiseAdd(args[i], args[i + 1] || 0))
  }
  const results = await Promise.all(tasks)
  return parallelSum(...results)
}

// 测试
(async () => {
  console.log('Running...');
  const res1 = await serialSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)
  console.log(res1)
  const res2 = await parallelSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)
  console.log(res2)
  console.log('Done');
})()

手写深度比较isEqual

思路:深度比较两个对象,就是要深度比较对象的每一个元素。=> 递归

  • 递归退出条件:
    • 被比较的是两个值类型变量,直接用“===”判断
    • 被比较的两个变量之一为null,直接判断另一个元素是否也为null
  • 提前结束递推:
    • 两个变量keys数量不同
    • 传入的两个参数是同一个变量
  • 递推工作:深度比较每一个key
function isEqual(obj1, obj2){
    //其中一个为值类型或null
    if(!isObject(obj1) || !isObject(obj2)){
        return obj1 === obj2;
    }

    //判断是否两个参数是同一个变量
    if(obj1 === obj2){
        return true;
    }

    //判断keys数是否相等
    const obj1Keys = Object.keys(obj1);
    const obj2Keys = Object.keys(obj2);
    if(obj1Keys.length !== obj2Keys.length){
        return false;
    }

    //深度比较每一个key
    for(let key in obj1){
        if(!isEqual(obj1[key], obj2[key])){
            return false;
        }
    }

    return true;
}

数组中的数据根据key去重

给定一个任意数组,实现一个通用函数,让数组中的数据根据 key 排重:

const dedup = (data, getKey = () => {} ) => {
  // todo
}
let data = [
  { id: 1, v: 1 },
  { id: 2, v: 2 },
  { id: 1, v: 1 },
];

// 以 id 作为排重 key,执行函数得到结果
// data = [
//   { id: 1, v: 1 },
//   { id: 2, v: 2 },
// ];

实现

const dedup = (data, getKey = () => { }) => {
    const dateMap = data.reduce((pre, cur) => {
        const key = getKey(cur)
        if (!pre[key]) {
            pre[key] = cur
        }
        return pre
    }, {})
    return Object.values(dateMap)
}

使用

let data = [
    { id: 1, v: 1 },
    { id: 2, v: 2 },
    { id: 1, v: 1 },
];
console.log(dedup(data, (item) => item.id))

// 以 id 作为排重 key,执行函数得到结果
// data = [
//   { id: 1, v: 1 },
//   { id: 2, v: 2 },
// ];

实现节流函数(throttle)

节流函数原理:指频繁触发事件时,只会在指定的时间段内执行事件回调,即触发事件间隔大于等于指定的时间才会执行回调函数。总结起来就是: 事件,按照一段时间的间隔来进行触发

像dom的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多

手写简版

使用时间戳的节流函数会在第一次触发事件时立即执行,以后每过 wait 秒之后才执行一次,并且最后一次触发事件不会被执行

时间戳方式:

// func是用户传入需要防抖的函数
// wait是等待时间
const throttle = (func, wait = 50) => {
  // 上一次执行该函数的时间
  let lastTime = 0
  return function(...args) {
    // 当前时间
    let now = +new Date()
    // 将当前时间和上一次执行函数时间对比
    // 如果差值大于设置的等待时间就执行函数
    if (now - lastTime > wait) {
      lastTime = now
      func.apply(this, args)
    }
  }
}

setInterval(
  throttle(() => {
    console.log(1)
  }, 500),
  1
)

定时器方式:

使用定时器的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,当最后一次停止触发后,还会再执行一次函数

function throttle(func, delay){
  var timer = null;
  returnfunction(){
    var context = this;
    var args = arguments;
    if(!timer){
      timer = setTimeout(function(){
        func.apply(context, args);
        timer = null;
      },delay);
    }
  }
}

适用场景:

  • DOM 元素的拖拽功能实现(mousemove
  • 搜索联想(keyup
  • 计算鼠标移动的距离(mousemove
  • Canvas 模拟画板功能(mousemove
  • 监听滚动事件判断是否到页面底部自动加载更多
  • 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
  • 缩放场景:监控浏览器resize
  • 动画场景:避免短时间内多次触发动画引起性能问题

总结

  • 函数防抖 :将几次操作合并为一次操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
  • 函数节流 :使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。

实现一个 sleep 函数,比如 sleep(1000) 意味着等待1000毫秒

// 使用 promise来实现 sleep
const sleep = (time) => {
  return new Promise(resolve => setTimeout(resolve, time))
}

sleep(1000).then(() => {
  // 这里写你的骚操作
})

树形结构转成列表(处理菜单)

[
    {
        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;
}

实现lodash的chunk方法–数组按指定长度拆分

题目

/**
 * @param input
 * @param size
 * @returns {Array}
 */
_.chunk(['a', 'b', 'c', 'd'], 2)
// => [['a', 'b'], ['c', 'd']]

_.chunk(['a', 'b', 'c', 'd'], 3)
// => [['a', 'b', 'c'], ['d']]

_.chunk(['a', 'b', 'c', 'd'], 5)
// => [['a', 'b', 'c', 'd']]

_.chunk(['a', 'b', 'c', 'd'], 0)
// => []

实现

function chunk(arr, length) {
  let newArr = [];
  for (let i = 0; i < arr.length; i += length) {
    newArr.push(arr.slice(i, i + length));
  }
  return newArr;
}

设计一个方法提取对象中所有value大于2的键值对并返回最新的对象

实现:

var obj = { a: 1, b: 3, c: 4 }
foo(obj) // { b: 3, c: 4 }

方法有很多种,这里提供一种比较简洁的写法,用到了ES10Object.fromEntries()

var obj = { a: 1, b: 3, c: 4 }
function foo (obj) {
  return Object.fromEntries(
    Object.entries(obj).filter(([key, value]) => value > 2)
  )
}
var obj2 = foo(obj) // { b: 3, c: 4 }
console.log(obj2)
// ES8中 Object.entries()的作用:
var obj = { a: 1, b: 2 }
var entries = Object.entries(obj); // [['a', 1], ['b', 2]]
// ES10中 Object.fromEntries()的作用:
Object.fromEntries(entries); // { a: 1, b: 2 }

实现一个链表结构

链表结构

看图理解next层级

// 链表 从头尾删除、增加 性能比较好
// 分为很多类 常用单向链表、双向链表

// js模拟链表结构:增删改查

// node节点
class Node {
  constructor(element,next) {
    this.element = element
    this.next = next
  } 
}

class LinkedList {
 constructor() {
   this.head = null // 默认应该指向第一个节点
   this.size = 0 // 通过这个长度可以遍历这个链表
 }
 // 增加O(n)
 add(index,element) {
   if(arguments.length === 1) {
     // 向末尾添加
     element = index // 当前元素等于传递的第一项
     index = this.size // 索引指向最后一个元素
   }
  if(index < 0 || index > this.size) {
    throw new Error('添加的索引不正常')
  }
  if(index === 0) {
    // 直接找到头部 把头部改掉 性能更好
    let head = this.head
    this.head = new Node(element,head)
  } else {
    // 获取当前头指针
    let current = this.head
    // 不停遍历 直到找到最后一项 添加的索引是1就找到第0个的next赋值
    for (let i = 0; i < index-1; i++) { // 找到它的前一个
      current = current.next
    }
    // 让创建的元素指向上一个元素的下一个
    // 看图理解next层级
    current.next = new Node(element,current.next) // 让当前元素指向下一个元素的next
  }

  this.size++;
 }
 // 删除O(n)
 remove(index) {
  if(index < 0 || index >= this.size) {
    throw new Error('删除的索引不正常')
  }
  this.size--
  if(index === 0) {
    let head = this.head
    this.head = this.head.next // 移动指针位置

    return head // 返回删除的元素
  }else {
    let current = this.head
    for (let i = 0; i < index-1; i++) { // index-1找到它的前一个
      current = current.next
    }
    let returnVal = current.next // 返回删除的元素
    // 找到待删除的指针的上一个 current.next.next 
    // 如删除200, 100=>200=>300 找到200的上一个100的next的next为300,把300赋值给100的next即可
    current.next = current.next.next 

    return returnVal
  }
 }
 // 查找O(n)
 get(index) {
  if(index < 0 || index >= this.size) {
    throw new Error('查找的索引不正常')
  }
  let current = this.head
  for (let i = 0; i < index; i++) {
    current = current.next
  }
  return current
 }
}


var ll = new LinkedList()

ll.add(0,100) // Node { ellement: 100, next: null }
ll.add(0,200) // Node { element: 200, next: Node { element: 100, next: null } }
ll.add(1,500) // Node {element: 200,next: Node { element: 100, next: Node { element: 500, next: null } } }
ll.add(300)
ll.remove(0)

console.log(ll.get(2),'get')
console.log(ll.head)

module.exports = LinkedList

数组去重方法汇总

首先:我知道多少种去重方式

1. 双层 for 循环

function distinct(arr) {
    for (let i=0, len=arr.length; i<len; i++) {
        for (let j=i+1; j<len; j++) {
            if (arr[i] == arr[j]) {
                arr.splice(j, 1);
                // splice 会改变数组长度,所以要将数组长度 len 和下标 j 减一
                len--;
                j--;
            }
        }
    }
    return arr;
}

思想: 双重 for 循环是比较笨拙的方法,它实现的原理很简单:先定义一个包含原始数组第一个元素的数组,然后遍历原始数组,将原始数组中的每个元素与新数组中的每个元素进行比对,如果不重复则添加到新数组中,最后返回新数组;因为它的时间复杂度是O(n^2),如果数组长度很大,效率会很低

2. Array.filter() 加 indexOf/includes

function distinct(a, b) {
    let arr = a.concat(b);
    return arr.filter((item, index)=> {
        //return arr.indexOf(item) === index
        return arr.includes(item)
    })
}

思想: 利用indexOf检测元素在数组中第一次出现的位置是否和元素现在的位置相等,如果不等则说明该元素是重复元素

3. ES6 中的 Set 去重

function distinct(array) {
   return Array.from(new Set(array));
}

思想: ES6 提供了新的数据结构 Set,Set 结构的一个特性就是成员值都是唯一的,没有重复的值。

4. reduce 实现对象数组去重复

var resources = [
    { name: "张三", age: "18" },
    { name: "张三", age: "19" },
    { name: "张三", age: "20" },
    { name: "李四", age: "19" },
    { name: "王五", age: "20" },
    { name: "赵六", age: "21" }
]
var temp = {};
resources = resources.reduce((prev, curv) => {
 // 如果临时对象中有这个名字,什么都不做
 if (temp[curv.name]) {

 }else {
    // 如果临时对象没有就把这个名字加进去,同时把当前的这个对象加入到prev中
    temp[curv.name] = true;
    prev.push(curv);
 }
 return prev
}, []);
console.log("结果", resources);

这种方法是利用高阶函数 reduce 进行去重, 这里只需要注意initialValue得放一个空数组[],不然没法push

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/374955.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

rollup环境配置

VUE2.x源码学习笔记 1. rollup环境配置 首先在VScode中新建文件夹vue_sc&#xff0c;然后终端打开定位到打开的文件夹&#xff0c;输入“npm init -y”初始化配置项&#xff0c;运行成功之后文件夹新增package.json文件 继续在终端运行"npm install babel/preset-env ba…

浅析Tomcat架构上的Valve内存马(内存马系列篇十一)

写在前面 这篇也是在Tomcat容器上面构造的内存马(收回之前说的不搞Tomcat了)&#xff0c;这是建立在Tomcat的管道上面做文章的一个内存马的实现方式。这是内存马系列的第十一篇文章了。 前置 什么是Pipeline-Valve管道&#xff1f; 根据前面Tomcat架构的相关知识&#xff0…

腾讯云卖向“有币”区块链

曾经坚决“不涉币”的腾讯云将业务延伸向“有币区块链”。 在首届 Web3 全球峰会“腾讯云Web3构建日”上&#xff0c;腾讯云宣布进军Web3&#xff0c;并公开了与Ankr、Avalanche、Scroll和Sui 四个原生区块链项目的合作&#xff0c;其中前两个项目都发行了加密货币&#xff0c…

关于Java中的静态块讲解

文章目录类的加载特性与时机类加载的特性类加载的时机static的三个常用地方什么是静态块?特点写法静态块 static怎么用?类的加载特性与时机 在介绍static之前可以先看看类的相关 类加载的特性 在JVM的生命周期里&#xff0c;每个类只会被加载一次。 类加载的原则&#xf…

老板让我在Linux中使用traceroute排查服务器网络问题,幸好我收藏了这篇文章!

一、前言 作为网络工程师或者运维工程师&#xff0c;traceroute命令不会陌生&#xff0c;它的作用类似于ping命令&#xff0c;用于诊断网络的连通性&#xff0c;不过traceroute命令输出的命令会比ping命令丰富的多&#xff0c;可以跟踪从源系统到目标系统的路径。 很多工程师…

jsp城乡信息管理系统

技术&#xff1a;Java、JSP等摘要&#xff1a;管理信息系统&#xff08;Management System&#xff0c;简称MS&#xff09;是一个由管理人员和计算机组成的用以进行信息的收集、传输、加工、存储、维护和使用的系统。它是一门综合了管理科学&#xff0c;系统理论&#xff0c;计…

WSL2:开发环境安装

写在前面 主要是记录一下如何安装和搭建基于WSL2的开发环境&#xff1b;参考博文&#xff1a;搭建优雅的Windows终端 Windows terminalscoopstarship&#xff1b; 一、安装WSL2 以管理员身份运行CMD&#xff0c;执行以下命令即可WSL和Linux的默认Ubuntu发行版&#xff1a; …

【聚类】谱聚类解读、代码示例

【聚类】谱聚类详解、代码示例 文章目录【聚类】谱聚类详解、代码示例1. 介绍2. 方法解读2.1 先验知识2.1.1 无向权重图2.1.2 拉普拉斯矩阵2.2 构建图&#xff08;第一步&#xff09;2.2.1 ϵ\epsilonϵ 邻近法2.2.2 k 近邻法2.2.3 全连接法2.3 切图&#xff08;第二步&#xf…

基于jeecgboot的flowable的H5版本在演示系统发布

目前在NBCIO 亿事达企业管理平台上发布了H5的在线演示系统&#xff0c;欢迎大家批评指正。 在nbcio-vue nbcio-vue: NBCIO 亿事达企业管理平台前端代码&#xff0c;基于ant-design-vue-jeecg的前端版本&#xff1a; 3.0.0代码和和flowable6.7.2&#xff0c;初步完成了集流程设…

【Linux】安装MySQL

目录 1.检测当前系统是否安装过MySQL相关数据库 2. 卸载现有的MySQL数据库 3.上传解压 4.顺序安装rpm包 5.启动MySQL 6.查看临时密码 7.登录MySQL 8.开放端口 1.检测当前系统是否安装过MySQL相关数据库 需要通过rpm相关指令&#xff0c;来查询当前系统中是否存在已安…

无法将“django-admin”项识别为cmdlet,函数,脚本文件或可运行程序的名称问题

无法将“django admin”项识别为cmdlet&#xff0c;函数&#xff0c;脚本文件或可运行程序的名称问题 小提示&#xff1a;首先检查一下有没有拼写错误&#xff01;&#xff01;&#xff01;没有的话请继续 我们要知道django装到哪里去了 pip show django 注意&#xff1a;3.0…

Flutter(二)第一个Flutter应用

1.默认应用 在Android Studio中创建好项目以后&#xff0c;项目的入口即是lib下的main.dart import package:flutter/material.dart;void main() {runApp(const MyApp()); } //无状态的组件&#xff08;Stateless widget&#xff09; class MyApp extends StatelessWidget {}…

基于paddlex的C#环境配置及其部署【附带安装包】

前言 最近应老师要求部署一个基于paddlex的C#环境&#xff0c;踩了一些坑&#xff0c;经过几个版本的安装测试&#xff0c;最终成功&#xff0c;这里记录一下。此次用到的所有软件的安装包如下&#xff1a; 补充的vs 2019安装包&#xff1a; 官方参考链接&#xff08;没有环…

Meta利用视觉信息来优化3D音频模型,未来将用于AR/VR

我们知道&#xff0c;Meta为了给AR眼镜打造智能助手&#xff0c;专门开发了第一人称视觉模型和数据集。与此同时&#xff0c;该公司也在探索一种将视觉和语音融合的AI感知方案。相比于单纯的语音助手&#xff0c;同时结合视觉和声音数据来感知环境&#xff0c;可进一步增强智能…

ERD Online 4.0.9 在线数据库建模、元数据管理平台(免费、私有部署)

ERD Online 是全球第一个开源、免费在线数据建模、元数据管理平台。提供简单易用的元数据设计、关系图设计、SQL查询等功能&#xff0c;辅以版本、导入、导出、数据源、SQL解析、审计、团队协作等功能、方便我们快速、安全的管理数据库中的元数据。 4.0.9 ❝ feat(erd): 主键生…

Opencv项目实战:21 美国ASL手势识别

0、项目介绍 首先&#xff0c;我可以保证在这里&#xff0c;你并不需要多么了解深的机器学习算法&#xff0c;我的初衷是通过本项目&#xff0c;激发大家学习机器学习的动力。选择这种手势原因是因为只有24个字母&#xff0c;你的电脑足以带的动&#xff0c;虽然我只训练A、B、…

group by聚合分组后如何获取分组数据

之前用group by分组后一直困惑怎么把分组后的数据拿到&#xff0c;因为分组后同一组的只有一条数据&#xff0c;最后发现了group_concat函数。记录一下&#xff0c;以后能用。语法&#xff1a;group_concat( [distinct] 要连接的字段 [order by 排序字段 asc/desc ] [separator…

【MySQL - InnoDB 存储结构】行格式详解

我们平时对 MySQL 的了解都只是限制在使用层面上&#xff0c;但是难道你就没有一个时刻好奇 MySQL 的内部结构嘛&#xff0c;我们通过 SQL 语句插入的一条条记录在 MySQL 底层到底是以什么格式存储的呢 &#xff1f; 本文就将以 InnoDB 存储引擎为例子&#xff0c;介绍 MySQL 存…

专科top4|临床医生CSC公派博士后美国凯斯西储大学医院赴职

Q医生符合CSC公派博士后申报条件&#xff0c;我们先为其取得Hopkins的邀请函并获CSC批准&#xff0c;后因导师失联&#xff0c;为保险起见&#xff0c;我们又继续申请并获得凯斯西储大学医学院彩虹宝宝和儿童医院的邀请函&#xff0c;该院连续20年被评为全美最好儿童医院&#…

【vulhub漏洞复现】CVE-2015-5254 ActiveMQ反序列化漏洞

一、漏洞详情Apache ActiveMQ是美国阿帕奇&#xff08;Apache&#xff09;软件基金会所研发的一套开源的消息中间件&#xff0c;它支持Java消息服务&#xff0c;集群&#xff0c;Spring Framework等。配置Apache ActiveMQ需要提前有jdk的环境。基于MQTT&#xff0c;消息订阅和分…