阿里前端二面经典手写面试题汇总
实现类的继承
实现类的继承-简版
类的继承在几年前是重点内容,有 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
评论