阿里前端二面经典手写面试题汇总
实现类的继承
实现类的继承-简版
类的继承在几年前是重点内容,有 n 种继承方式各有优劣,es6 普及后越来越不重要,那么多种写法有点『回字有四样写法』的意思,如果还想深入理解的去看红宝书即可,我们目前只实现一种最理想的继承方式。
ES5 实现继承-详细
第一种方式是借助 call 实现继承
这样写的时候子类虽然能够拿到父类的属性值,但是问题是父类中一旦存在方法那么子类无法继承。那么引出下面的方法
第二种方式借助原型链实现继承:
看似没有问题,父类的方法和属性都能够访问,但实际上有一个潜在的不足。举个例子:
明明我只改变了 s1 的 play 属性,为什么 s2 也跟着变了呢?很简单,因为两个实例使用的是同一个原型对象
第三种方式:将前两种组合:
之前的问题都得以解决。但是这里又徒增了一个新问题,那就是 Parent3 的构造函数会多执行了一次(
Child3.prototype = new Parent3();)。这是我们不愿看到的。那么如何解决这个问题?
第四种方式: 组合继承的优化 1
这里让将父类原型对象直接给到子类,父类构造函数只执行一次,而且父类属性和方法均能访问,但是我们来测试一下
子类实例的构造函数是 Parent4,显然这是不对的,应该是 Child4。
第五种方式(最推荐使用):优化 2
这是最推荐的一种方式,接近完美的继承。
实现 instanceOf
思路:
步骤 1:先取得当前类的原型,当前实例对象的原型链
步骤 2:一直循环(执行原型链的查找机制)
取得当前实例对象原型链的原型链(
proto = proto.__proto__,沿着原型链一直向上查找)如果 当前实例的原型链
__proto__上找到了当前类的原型prototype,则返回true如果 一直找到
Object.prototype.__proto__ == null,Object的基类(null)上面都没找到,则返回false
实现一个队列
基于链表结构实现队列
实现 every 方法
实现 LRU 淘汰算法
LRU 缓存算法是一个非常经典的算法,在很多面试中经常问道,不仅仅包括前端面试
LRU英文全称是Least Recently Used,英译过来就是” 最近最少使用 “的意思。LRU是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间t,当须淘汰一个页面时,选择现有页面中其t值最大的,即最近最少使用的页面予以淘汰
通俗的解释:
假如我们有一块内存,专门用来缓存我们最近发访问的网页,访问一个新网页,我们就会往内存中添加一个网页地址,随着网页的不断增加,内存存满了,这个时候我们就需要考虑删除一些网页了。这个时候我们找到内存中最早访问的那个网页地址,然后把它删掉。这一整个过程就可以称之为
LRU算法
上图就很好的解释了 LRU 算法在干嘛了,其实非常简单,无非就是我们往内存里面添加或者删除元素的时候,遵循最近最少使用原则
使用场景
LRU 算法使用的场景非常多,这里简单举几个例子即可:
我们操作系统底层的内存管理,其中就包括有
LRU算法我们常见的缓存服务,比如
redis等等比如浏览器的最近浏览记录存储
vue中的keep-alive组件使用了LRU算法
梳理实现 LRU 思路
特点分析:
我们需要一块有限的存储空间,因为无限的化就没必要使用
LRU算发删除数据了。我们这块存储空间里面存储的数据需要是有序的,因为我们必须要顺序来删除数据,所以可以考虑使用
Array、Map数据结构来存储,不能使用Object,因为它是无序的。我们能够删除或者添加以及获取到这块存储空间中的指定数据。
存储空间存满之后,在添加数据时,会自动删除时间最久远的那条数据。
实现需求:
实现一个
LRUCache类型,用来充当存储空间采用
Map数据结构存储数据,因为它的存取时间复杂度为O(1),数组为O(n)实现
get和set方法,用来获取和添加数据我们的存储空间有长度限制,所以无需提供删除方法,存储满之后,自动删除最久远的那条数据
当使用
get获取数据后,该条数据需要更新到最前面
具体实现
set 方法:往map里面添加新数据,如果添加的数据存在了,则先删除该条数据,然后再添加。如果添加数据后超长了,则需要删除最久远的一条数据。data.keys().next().value便是获取最后一条数据的意思。get 方法:首先从map对象中拿出该条数据,然后删除该条数据,最后再重新插入该条数据,确保将该条数据移动到最前面
继续插入数据,此时会超长,代码如下:
此时我们发现存储时间最久的 name 已经被移除了,新插入的数据变为了最前面的一个。
我们使用 get 获取数据,代码如下:
我们发现此时 sex 字段已经跑到最前面去了
总结
LRU算法其实逻辑非常的简单,明白了原理之后实现起来非常的简单。最主要的是我们需要使用什么数据结构来存储数据,因为map的存取非常快,所以我们采用了它,当然数组其实也可以实现的。还有一些小伙伴使用链表来实现LRU,这当然也是可以的。
实现 Promise 相关方法
实现 Promise 的 resolve
实现 resolve 静态方法有三个要点:
传参为一个
Promise, 则直接返回它。传参为一个
thenable对象,返回的Promise会跟随这个对象,采用它的最终状态作为自己的状态。其他情况,直接返回以该值为成功状态的
promise对象。
实现 Promise.reject
Promise.reject 中传入的参数会作为一个 reason 原封不动地往下传, 实现如下:
实现 Promise.prototype.finally
前面的
promise不管成功还是失败,都会走到finally中,并且finally之后,还可以继续then(说明它还是一个 then 方法是关键),并且会将初始的promise值原封不动的传递给后面的then.
Promise.prototype.finally 最大的作用
finally里的函数,无论如何都会执行,并会把前面的值原封不动传递给下一个then方法中如果
finally函数中有promise等异步任务,会等它们全部执行完毕,再结合之前的成功与否状态,返回值
Promise.prototype.finally 六大情况用法
源码实现
实现 Promise.all
对于 all 方法而言,需要完成下面的核心功能:
传入参数为一个空的可迭代对象,则直接进行
resolve。如果参数中有一个
promise失败,那么Promise.all返回的promise对象失败。在任何情况下,
Promise.all返回的promise的完成状态的结果都是一个数组
实现 promise.allsettle
MDN:
Promise.allSettled()方法返回一个在所有给定的promise都已经fulfilled或rejected后的promise,并带有一个对象数组,每个对象表示对应的promise`结果
当您有多个彼此不依赖的异步任务成功完成时,或者您总是想知道每个promise的结果时,通常使用它。
【译】
Promise.allSettled跟Promise.all类似, 其参数接受一个Promise的数组, 返回一个新的Promise, 唯一的不同在于, 其不会进行短路, 也就是说当 Promise 全部处理完成后我们可以拿到每个Promise的状态, 而不管其是否处理成功。
用法 | 测试用例
实现
实现 Promise.race
race 的实现相比之下就简单一些,只要有一个 promise 执行完,直接 resolve 并停止执行
实现一个简版 Promise
使用 class 实现
Promise 实现-详细
可以把
Promise看成一个状态机。初始是pending状态,可以通过函数resolve和reject,将状态转变为resolved或者rejected状态,状态一旦改变就不能再次变化。then函数会返回一个Promise实例,并且该返回值是一个新的实例而不是之前的实例。因为Promise规范规定除了pending状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个then调用就失去意义了。对于
then来说,本质上可以把它看成是flatMap
实现 Promisify
完整实现 Promises/A+规范
参考 前端进阶面试题详细解答
实现一个简易的 MVVM
实现一个简易的
MVVM我会分为这么几步来:
首先我会定义一个类
Vue,这个类接收的是一个options,那么其中可能有需要挂载的根元素的id,也就是el属性;然后应该还有一个data属性,表示需要双向绑定的数据其次我会定义一个
Dep类,这个类产生的实例对象中会定义一个subs数组用来存放所依赖这个属性的依赖,已经添加依赖的方法addSub,删除方法removeSub,还有一个notify方法用来遍历更新它subs中的所有依赖,同时 Dep 类有一个静态属性target它用来表示当前的观察者,当后续进行依赖收集的时候可以将它添加到dep.subs中。然后设计一个
observe方法,这个方法接收的是传进来的data,也就是options.data,里面会遍历data中的每一个属性,并使用Object.defineProperty()来重写它的get和set,那么这里面呢可以使用new Dep()实例化一个dep对象,在get的时候调用其addSub方法添加当前的观察者Dep.target完成依赖收集,并且在set的时候调用dep.notify方法来通知每一个依赖它的观察者进行更新完成这些之后,我们还需要一个
compile方法来将 HTML 模版和数据结合起来。在这个方法中首先传入的是一个node节点,然后遍历它的所有子级,判断是否有firstElmentChild,有的话则进行递归调用 compile 方法,没有firstElementChild的话且该child.innderHTML用正则匹配满足有/\{\{(.*)\}\}/项的话则表示有需要双向绑定的数据,那么就将用正则new Reg('\\{\\{\\s*' + key + '\\s*\\}\\}', 'gm')替换掉是其为msg变量。完成变量替换的同时,还需要将
Dep.target指向当前的这个child,且调用一下this.opt.data[key],也就是为了触发这个数据的get来对当前的child进行依赖收集,这样下次数据变化的时候就能通知child进行视图更新了,不过在最后要记得将Dep.target指为null哦(其实在Vue中是有一个targetStack栈用来存放target的指向的)那么最后我们只需要监听
document的DOMContentLoaded然后在回调函数中实例化这个Vue对象就可以了
coding :
需要注意的点:
childNodes会获取到所有的子节点以及文本节点(包括元素标签中的空白节点)firstElementChild表示获取元素的第一个字元素节点,以此来区分是不是元素节点,如果是的话则调用compile进行递归调用,否则用正则匹配这里面的正则真的不难,大家可以看一下
完整代码如下:
简化版 2
实现 JSONP 方法
利用
<script>标签不受跨域限制的特点,缺点是只能支持get请求
创建
script标签设置
script标签的src属性,以问号传递参数,设置好回调函数callback名称插入到
html文本中调用回调函数,
res参数就是获取的数据
设置
CORS: Access-Control-Allow-Origin:*postMessage
实现 Ajax
步骤
创建
XMLHttpRequest实例发出 HTTP 请求
服务器返回 XML 格式的字符串
JS 解析 XML,并更新局部页面
不过随着历史进程的推进,XML 已经被淘汰,取而代之的是 JSON。
了解了属性和方法之后,根据 AJAX 的步骤,手写最简单的 GET 请求。
对象数组列表转成树形结构(处理菜单)
实现代码如下:
异步串行 | 异步并行
手写深度比较 isEqual
思路:深度比较两个对象,就是要深度比较对象的每一个元素。=> 递归
递归退出条件:
被比较的是两个值类型变量,直接用“===”判断
被比较的两个变量之一为
null,直接判断另一个元素是否也为null提前结束递推:
两个变量
keys数量不同传入的两个参数是同一个变量
递推工作:深度比较每一个
key
数组中的数据根据 key 去重
给定一个任意数组,实现一个通用函数,让数组中的数据根据 key 排重:
实现
使用
实现节流函数(throttle)
节流函数原理:指频繁触发事件时,只会在指定的时间段内执行事件回调,即触发事件间隔大于等于指定的时间才会执行回调函数。总结起来就是: 事件,按照一段时间的间隔来进行触发 。
像 dom 的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多
手写简版
使用时间戳的节流函数会在第一次触发事件时立即执行,以后每过 wait 秒之后才执行一次,并且最后一次触发事件不会被执行
时间戳方式:
定时器方式:
使用定时器的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,当最后一次停止触发后,还会再执行一次函数
适用场景:
DOM元素的拖拽功能实现(mousemove)搜索联想(
keyup)计算鼠标移动的距离(
mousemove)Canvas模拟画板功能(mousemove)监听滚动事件判断是否到页面底部自动加载更多
拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
缩放场景:监控浏览器
resize动画场景:避免短时间内多次触发动画引起性能问题
总结
函数防抖 :将几次操作合并为一次操作进行。原理是维护一个计时器,规定在 delay 时间后触发函数,但是在 delay 时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
函数节流 :使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。
实现一个 sleep 函数,比如 sleep(1000) 意味着等待 1000 毫秒
树形结构转成列表(处理菜单)
实现代码如下:
实现 lodash 的 chunk 方法--数组按指定长度拆分
题目
实现
设计一个方法提取对象中所有 value 大于 2 的键值对并返回最新的对象
实现:
方法有很多种,这里提供一种比较简洁的写法,用到了ES10的Object.fromEntries():
实现一个链表结构
链表结构
看图理解 next 层级
数组去重方法汇总
首先:我知道多少种去重方式
1. 双层 for 循环
思想: 双重
for循环是比较笨拙的方法,它实现的原理很简单:先定义一个包含原始数组第一个元素的数组,然后遍历原始数组,将原始数组中的每个元素与新数组中的每个元素进行比对,如果不重复则添加到新数组中,最后返回新数组;因为它的时间复杂度是O(n^2),如果数组长度很大,效率会很低
2. Array.filter() 加 indexOf/includes
思想: 利用
indexOf检测元素在数组中第一次出现的位置是否和元素现在的位置相等,如果不等则说明该元素是重复元素
3. ES6 中的 Set 去重
思想: ES6 提供了新的数据结构 Set,Set 结构的一个特性就是成员值都是唯一的,没有重复的值。
4. reduce 实现对象数组去重复
这种方法是利用高阶函数
reduce进行去重, 这里只需要注意initialValue得放一个空数组[],不然没法push










评论