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

实现类的继承

实现类的继承-简版

类的继承在几年前是重点内容,有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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值