笔者开源的前端进阶之道已有三年之久,至今也有 17k star,承蒙各位读者垂爱。在当下部份内容已经略微过期,所以决定提笔翻新内容。html
翻新后的内容会所有集合在「干爆前端」中,有兴趣的读者能够前往查看。前端
阅读前重要提示:c++
本文非百科全书,只专为面试复习准备、查漏补缺、深刻某知识点的引子、了解相关面试题等准备。git
笔者一直都是崇尚学会面试题底下涉及到的知识点,而不是刷一大堆面试题,结果变了个题型就不会的那种。因此本文和别的面经不同,旨在提炼面试题底下的经常使用知识点,而不是甩一大堆面试题给各位看官。github
你们也能够在笔者的网站上阅读,体验更佳!面试
JS 数据类型分为两大类,九个数据类型:算法
其中原始类型又分为七种类型,分别为:windows
boolean
number
string
undefined
null
symbol
bigint
对象类型分为两种,分别为:数组
Object
Function
其中 Object
中又包含了不少子类型,好比 Array
、RegExp
、Math
、Map
、Set
等等,也就不一一列出了。promise
原始类型存储在栈上,对象类型存储在堆上,可是它的引用地址仍是存在栈上。
注意:以上结论前半句是不许确的,更准确的内容我会在闭包章节里说明。
bigint
,固然再加上字符串的处理会更好。NaN
如何判断另外还有一类常见的题目是对于对象的修改,好比说往函数里传一个对象进去,函数内部修改参数。
function test(person) {
person.age = 26
person = {}
return person
}
const p1 = {
age: 25
}
复制代码
这类题目咱们只须要牢记如下几点:
类型判断有好几种方式。
原始类型中除了 null
,其它类型均可以经过 typeof
来判断。
typeof null
的值为 object
,这是由于一个久远的 Bug,没有细究的必要,了解便可。若是想具体判断 null
类型的话直接 xxx === null
便可。
对于对象类型来讲,typeof
只能具体判断函数的类型为 function
,其它均为 object
。
instanceof
内部经过原型链的方式来判断是否为构建函数的实例,经常使用于判断具体的对象类型。
[] instanceof Array
复制代码
都说 instanceof
只能判断对象类型,其实这个说法是不许确的,咱们是能够经过 hake 的方式得以实现,虽然不会有人这样去玩吧。
class CheckIsNumber {
static [Symbol.hasInstance](number) {
return typeof number === 'number'
}
}
// true
1 instanceof CheckIsNumber
复制代码
另外其实咱们还能够直接经过构建函数来判断类型:
// true
[].constructor === Array
复制代码
前几种方式或多或少都存在一些缺陷,Object.prototype.toString
综合来看是最佳选择,能判断的类型最完整。
上图是一部分类型判断,更多的就不列举了,[object XXX]
中的 XXX
就是判断出来的类型。
同时还存在一些判断特定类型的 API,选了两个常见的:
instanceof
原理instanceof
类型转换分为两种状况,分别为强制转换及隐式转换。
强制转换就是转成特定的类型:
Number(false) // -> 0
Number('1') // -> 1
Number('zb') // -> NaN
(1).toString() // '1'
复制代码
这部分是平常经常使用的内容,就不具体展开说了,主要记住强制转数字和布尔值的规则就行。
转布尔值规则:
undefined、null、false、NaN、''、0、-0
都转为 false
。true
,包括全部对象。转数字规则:
true
为 1,false
为 0null
为 0,undefined
为 NaN
,symbol
报错NaN
隐式转换规则是最烦的,其实笔者也记不住那么多内容。何况根据笔者目前收集到的最新面试题来讲,这部分考题基本绝迹了,固然讲仍是讲一下吧。
对象转基本类型:
Symbol.toPrimitive
,转成功就结束valueOf
,转成功就结束toString
,转成功就结束四则运算符:
==
操做符
若是这部分规则记不住也不碍事,确实有点繁琐,并且考的也愈来愈少了,拿一道之前常考的题目看看吧:
[] == ![] // -> ?
复制代码
this
是不少人会混淆的概念,可是其实他一点都不难,不要被那些长篇大论的文章吓住了(我其实也不知道为何他们能写那么多字),你只须要记住几个规则就能够了。
function foo() {
console.log(this.a)
}
var a = 1
foo()
var obj = {
a: 2,
foo: foo
}
obj.foo()
// 以上状况就是看函数是被谁调用,那么 `this` 就是谁,没有被对象调用,`this` 就是 `window`
// 如下状况是优先级最高的,`this` 只会绑定在 `c` 上,不会被任何方式修改 `this` 指向
var c = new foo()
c.a = 3
console.log(c.a)
// 还有种就是利用 call,apply,bind 改变 this,这个优先级仅次于 new
复制代码
由于箭头函数没有 this
,因此一切妄图改变箭头函数 this
指向都是无效的。
箭头函数的 this
只取决于定义时的环境。好比以下代码中的 fn
箭头函数是在 windows
环境下定义的,不管如何调用,this
都指向 window
。
var a = 1
const fn = () => {
console.log(this.a)
}
const obj = {
fn,
a: 2
}
obj.fn()
复制代码
这里通常都是考 this
的指向问题,牢记上述的几个规则就够用了,好比下面这道题:
const a = {
b: 2,
foo: function () { console.log(this.b) }
}
function b(foo) {
// 输出什么?
foo()
}
b(a.foo)
复制代码
首先闭包正确的定义是:假如一个函数能访问外部的变量,那么这个函数它就是一个闭包,而不是必定要返回一个函数。这个定义很重要,下面的内容须要用到。
let a = 1
// fn 是闭包
function fn() {
console.log(a);
}
function fn1() {
let a = 1
// 这里也是闭包
return () => {
console.log(a);
}
}
const fn2 = fn1()
fn2()
复制代码
你们都知道闭包其中一个做用是访问私有变量,就好比上述代码中的 fn2
访问到了 fn1
函数中的变量 a
。可是此时 fn1
早已销毁,咱们是如何访问到变量 a
的呢?不是都说原始类型是存放在栈上的么,为何此时却没有被销毁掉?
接下来笔者会根据浏览器的表现来从新理解关于原始类型存放位置的说法。
先来讲下数据存放的正确规则是:局部、占用空间肯定的数据,通常会存放在栈中,不然就在堆中(也有例外)。 那么接下来咱们能够经过 Chrome 来帮助咱们验证这个说法说法。
上图中画红框的位置咱们能看到一个内部的对象 [[Scopes]]
,其中存放着变量 a
,该对象是被存放在堆上的,其中包含了闭包、全局对象等等内容,所以咱们能经过闭包访问到本该销毁的变量。
另外最开始咱们对于闭包的定位是:假如一个函数能访问外部的变量,那么这个函数它就是一个闭包,所以接下来咱们看看在全局下的表现是怎么样的。
let a = 1
var b = 2
// fn 是闭包
function fn() {
console.log(a, b);
}
复制代码
从上图咱们能发现全局下声明的变量,若是是 var 的话就直接被挂到 globe
上,若是是其余关键字声明的话就被挂到 Script
上。虽然这些内容一样仍是存在 [[Scopes]]
,可是全局变量应该是存放在静态区域的,由于全局变量无需进行垃圾回收,等须要回收的时候整个应用都没了。
只有在下图的场景中,原始类型才多是被存储在栈上。
这里为何要说可能,是由于 JS 是门动态类型语言,一个变量声明时能够是原始类型,立刻又能够赋值为对象类型,而后又回到原始类型。这样频繁的在堆栈上切换存储位置,内部引擎是否是也会有什么优化手段,或者干脆所有都丢堆上?只有
const
声明的原始类型才必定存在栈上?固然这只是笔者的一个推测,暂时没有深究,读者能够忽略这段瞎想。
所以笔者对于原始类型存储位置的理解为:局部变量才是被存储在栈上,全局变量存在静态区域上,其它都存储在堆上。
固然这个理解是创建的 Chrome 的表现之上的,在不一样的浏览器上由于引擎的不一样,可能存储的方式仍是有所变化的。
闭包能考的不少,概念和笔试题都会考。
概念题就是考考闭包是什么了。
笔试题的话基本都会结合上异步,好比最多见的:
for (var i = 0; i < 6; i++) {
setTimeout(() => {
console.log(i)
})
}
复制代码
这道题会问输出什么,有哪几种方式能够获得想要的答案?
new
操做符能够帮助咱们构建出一个实例,而且绑定上 this
,内部执行步骤可大概分为如下几步:
在第四步返回新对象这边有一个状况会例外:
function Test(name) {
this.name = name
console.log(this) // Test { name: 'yck' }
return { age: 26 }
}
const t = new Test('yck')
console.log(t) // { age: 26 }
console.log(t.name) // 'undefined'
复制代码
当在构造函数中返回一个对象时,内部建立出来的新对象就被咱们返回的对象所覆盖,因此通常来讲构建函数就别返回对象了(返回原始类型不影响)。
new
作了那些事?new
返回不一样的类型时会有什么表现?new
的实现过程做用域能够理解为变量的可访问性,总共分为三种类型,分别为:
let
、const
就能够产生该做用域其实看完前面的闭包、this
这部份内部的话,应该基本能了解做用域的一些应用。
一旦咱们将这些做用域嵌套起来,就变成了另一个重要的知识点「做用域链」,也就是 JS 究竟是如何访问须要的变量或者函数的。
首先做用域链是在定义时就被肯定下来的,和箭头函数里的 this
同样,后续不会改变,JS 会一层层往上寻找须要的内容。
其实做用域链这个东西咱们在闭包小结中已经看到过它的实体了:[[Scopes]]
图中的 [[Scopes]]
是个数组,做用域的一层层往上寻找就等同于遍历 [[Scopes]]
。
原型在面试里只须要几句话、一张图的概念就够用了,没人会让你长篇大论讲上一堆内容的,问原型更多的是为了引出继承这个话题。
根据上图,原型总结下来的概念为:
__proto__
指向一个对象,也就是原型constructor
找到构造函数,构造函数也能够经过 prototype
找到原型__proto__
找到 Function
对象__proto__
找到 Object
对象__proto__
链接起来,这样称之为原型链。当前对象上不存在的属性能够经过原型链一层层往上查找,直到顶层 Object
对象,再往上就是 null
了即便是 ES6 中的 class
也不是其余语言里的类,本质就是一个函数。
class Person {}
Person instanceof Function // true
复制代码
其实在当下都用 ES6 的状况下,ES5 的继承写法已经没啥学习的必要了,可是由于面试还会被问到,因此复习一下仍是须要的。
首先来讲下 ES5 和 6 继承的区别:
super()
才能拿到子类,ES5 的话是经过 apply
这种绑定的方式let
这些一致接下来就是回字的几种写法的名场面了,ES5 实现继承的方式有不少种,面试了解一种已经够用:
function Super() {}
Super.prototype.getNumber = function() {
return 1
}
function Sub() {}
Sub.prototype = Object.create(Super.prototype, {
constructor: {
value: Sub,
enumerable: false,
writable: true,
configurable: true
}
})
let s = new Sub()
s.getNumber()
复制代码
class
有何区别两个对象第一层的引用不相同就是浅拷贝的含义。
咱们能够经过 assign
、扩展运算符等方式来实现浅拷贝:
let a = {
age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1
b = {...a}
a.age = 3
console.log(b.age) // 2
复制代码
两个对象内部全部的引用都不相同就是深拷贝的含义。
最简单的深拷贝方式就是使用 JSON.parse(JSON.stringify(object))
,可是该方法存在很多缺陷。
好比说只支持 JSON 支持的类型,JSON 是门通用的语言,并不支持 JS 中的全部类型。
同时还存在不能处理循环引用的问题:
若是想解决以上问题,咱们能够经过递归的方式来实现代码:
// 利用 WeakMap 解决循环引用
let map = new WeakMap()
function deepClone(obj) {
if (obj instanceof Object) {
if (map.has(obj)) {
return map.get(obj)
}
let newObj
if (obj instanceof Array) {
newObj = []
} else if (obj instanceof Function) {
newObj = function() {
return obj.apply(this, arguments)
}
} else if (obj instanceof RegExp) {
// 拼接正则
newobj = new RegExp(obj.source, obj.flags)
} else if (obj instanceof Date) {
newobj = new Date(obj)
} else {
newObj = {}
}
// 克隆一份对象出来
let desc = Object.getOwnPropertyDescriptors(obj)
let clone = Object.create(Object.getPrototypeOf(obj), desc)
map.set(obj, clone)
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = deepClone(obj[key])
}
}
return newObj
}
return obj
}
复制代码
上述代码解决了常见的类型以及循环引用的问题,固然仍是一部分缺陷的,可是面试时候能写出上面的代码已经足够了,剩下的能口述思路基本这道题就能拿到高分了。
好比说递归确定会存在爆栈的问题,由于执行栈的大小是有限制的,到必定数量栈就会爆掉。
所以遇到这种问题,咱们能够经过遍历的方式来改写递归。这个就是如何写层序遍历(BFS)的问题了,经过数组来模拟执行栈就能解决爆栈问题,有兴趣的读者能够咨询查阅。
Promise
是一个高频考点了,可是更多的是在笔试题中出现,概念题反倒基本没有,可能是来问 Event loop 的。
对于这块内容的复习咱们须要熟悉涉及到的全部 API,由于考题里可能会问到 all
、race
等等用法或者须要你用这些 API 实现一些功能。
对于 Promise
进阶点的知识能够具体阅读笔者的这篇文章,这里就不复制过来占用篇幅了:Promise 你真的用明白了么?
all
实现并行需求all
的实现另外还有一道很常见的串行题目:
页面上有三个按钮,分别为 A、B、C,点击各个按钮都会发送异步请求且互不影响,每次请求回来的数据都为按钮的名字。 请实现当用户依次点击 A、B、C、A、C、B 的时候,最终获取的数据为 ABCACB。
这道题目主要两个考点:
其实咱们无需本身去构建一个队列,直接利用 promise.then
方法就能实现队列的效果了。
class Queue {
promise = Promise.resolve();
excute(promise) {
this.promise = this.promise.then(() => promise);
return this.promise;
}
}
const queue = new Queue();
const delay = (params) => {
const time = Math.floor(Math.random() * 5);
return new Promise((resolve) => {
setTimeout(() => {
resolve(params);
}, time * 500);
});
};
const handleClick = async (name) => {
const res = await queue.excute(delay(name));
console.log(res);
};
handleClick('A');
handleClick('B');
handleClick('C');
handleClick('A');
handleClick('C');
handleClick('B');
复制代码
await
和 promise
同样,更多的是考笔试题,固然偶尔也会问到和 promise
的一些区别。
await
相比直接使用 Promise
来讲,优点在于处理 then
的调用链,可以更清晰准确的写出代码。缺点在于滥用 await
可能会致使性能问题,由于 await
会阻塞代码,也许以后的异步代码并不依赖于前者,但仍然须要等待前者完成,致使代码失去了并发性,此时更应该使用 Promise.all
。
下面来看一道很容易作错的笔试题。
var a = 0
var b = async () => {
a = a + await 10
console.log('2', a) // -> ?
}
b()
a++
console.log('1', a) // -> ?
复制代码
这道题目大部分读者确定会想到 await
左边是异步代码,所以会先把同步代码执行完,此时 a
已经变成 1,因此答案应该是 11。
其实 a
为 0 是由于加法运算法,先算左边再算右边,因此会把 0 固定下来。若是咱们把题目改为 await 10 + a
的话,答案就是 11 了。
在开始讲事件循环以前,咱们必定要牢记一点:JS 是一门单线程语言,在执行过程当中永远只能同时执行一个任务,任何异步的调用都只是在模拟这个过程,或者说能够直接认为在 JS 中的异步就是延迟执行的同步代码。另外别的什么 Web worker、浏览器提供的各类线程都不会影响这个点。
你们应该都知道执行 JS 代码就是往执行栈里 push
函数(不知道的本身搜索吧),那么当遇到异步代码的时候会发生什么状况?
其实当遇到异步的代码时,只有当遇到 Task、Microtask 的时候才会被挂起并在须要执行的时候加入到 Task(有多种 Task) 队列中。
从图上咱们得出两个疑问:
首先咱们来解决问题一。
Task(宏任务):同步代码、setTimeout
回调、setInteval
回调、IO、UI 交互事件、postMessage
、MessageChannel
。
MicroTask(微任务):Promise
状态改变之后的回调函数(then
函数执行,若是此时状态没变,回调只会被缓存,只有当状态改变,缓存的回调函数才会被丢到任务队列)、Mutation observer
回调函数、queueMicrotask
回调函数(新增的 API)。
宏任务会被丢到下一次事件循环,而且宏任务队列每次只会执行一个任务。
微任务会被丢到本次事件循环,而且微任务队列每次都会执行任务直到队列为空。
假如每一个微任务都会产生一个微任务,那么宏任务永远都不会被执行了。
接下来咱们来解决问题二。
Event Loop 执行顺序以下所示:
若是你以为上面的表述不大理解的话,接下来咱们经过代码示例来巩固理解上面的知识:
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
queueMicrotask(() => console.log('queueMicrotask'))
console.log('promise');
});
console.log('script end');
复制代码
console.log
执行并打印setTimeout
,将回调加入宏任务队列Promise.resolve()
,此时状态已经改变,所以将 then
回调加入微任务队列console.log
执行并打印此时同步任务所有执行完毕,分别打印了 'script start' 以及 'script end',开始判断是否有微任务须要执行。
then
回调函数queueMicrotask
,将回到加入微任务队列console.log
执行并打印queueMicrotask
回调console.log
执行并打印此时发现微任务队列已经清空,判断是否须要进行 UI 渲染。
setTimeout
回调console.log
执行并打印执行一个宏任务即结束,寻找是否存在微任务,开始循环判断...
其实事件循环没啥难懂的,理解 JS 是个单线程语言,明白哪些是微宏任务、循环的顺序就行了。
最后须要注意的一点:正是由于 JS 是门单线程语言,只能同时执行一个任务。所以全部的任务均可能由于以前任务的执行时间过长而被延迟执行,尤为对于一些定时器而言。
当下模块化主要就是 CommonJS 和 ES6 的 ESM 了,其它什么的 AMD、UMD 了解下就好了。
ESM 我想应该没啥好说的了,主要咱们来聊聊 CommonJS 以及 ESM 和 CommonJS 的区别。
CommonJs 是 Node 独有的规范,固然 Webpack 也本身实现了这套东西,让咱们能在浏览器里跑起来这个规范。
// a.js
module.exports = {
a: 1
}
// or
exports.a = 1
// b.js
var module = require('./a.js')
module.a // -> log 1
复制代码
在上述代码中,module.exports
和 exports
很容易混淆,让咱们来看看大体内部实现
// 基本实现
var module = {
exports: {} // exports 就是个空对象
}
// 这个是为何 exports 和 module.exports 用法类似的缘由
var exports = module.exports
var load = function (module) {
// 导出的东西
var a = 1
module.exports = a
return module.exports
};
复制代码
根据上面的大体实现,咱们也能看出为何对 exports
直接赋值不会有任何效果。
对于 CommonJS 和 ESM 的二者区别是:
require(${path}/xx.js)
,后者使用 import()
本小结内容创建在 V8 引擎之上。
首先聊垃圾回收以前咱们须要知道堆栈究竟是存储什么数据的,固然这块内容上文已经讲过,这里就再也不赘述了。
接下来咱们先来聊聊栈是如何垃圾回收的。其实栈的回收很简单,简单来讲就是一个函数 push 进栈,执行完毕之后 pop 出来就当能够回收了。固然咱们往深层了讲深层了讲就是汇编里的东西了,操做 esp 和 ebp 指针,了解下便可。
而后就是堆如何回收垃圾了,这部分的话会分为两个空间及多个算法。
两个空间分别为新生代和老生代,咱们分开来说每一个空间中涉及到的算法。
新生代中的对象通常存活时间较短,空间也较小,使用 Scavenge GC 算法。
在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,一定有一个空间是使用的,另外一个空间是空闲的。新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,若是有失活的对象就会销毁。当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。
老生代中的对象通常存活时间较长且数量也多,使用了两个算法,分别是标记清除和标记压缩算法。
在讲算法前,先来讲下什么状况下对象会出如今老生代空间中:
老生代中的空间很复杂,有以下几个空间
enum AllocationSpace {
// TODO(v8:7464): Actually map this space's memory as read-only.
RO_SPACE, // 不变的对象空间
NEW_SPACE, // 新生代用于 GC 复制算法的空间
OLD_SPACE, // 老生代常驻对象空间
CODE_SPACE, // 老生代代码对象空间
MAP_SPACE, // 老生代 map 对象
LO_SPACE, // 老生代大空间对象
NEW_LO_SPACE, // 新生代大空间对象
FIRST_SPACE = RO_SPACE,
LAST_SPACE = NEW_LO_SPACE,
FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,
LAST_GROWABLE_PAGED_SPACE = MAP_SPACE
};
复制代码
在老生代中,如下状况会先启动标记清除算法:
在这个阶段中,会遍历堆中全部的对象,而后标记活的对象,在标记完成后,销毁全部没有被标记的对象。在标记大型对内存时,可能须要几百毫秒才能完成一次标记。这就会致使一些性能上的问题。为了解决这个问题,2011 年,V8 从 stop-the-world 标记切换到增量标志。在增量标记期间,GC 将标记工做分解为更小的模块,可让 JS 应用逻辑在模块间隙执行一会,从而不至于让应用出现停顿状况。但在 2018 年,GC 技术又有了一个重大突破,这项技术名为并发标记。该技术可让 GC 扫描和标记对象时,同时容许 JS 运行,你能够点击 该博客 详细阅读。
清除对象后会形成堆内存出现碎片的状况,当碎片超过必定限制后会启动压缩算法。在压缩过程当中,将活的对象像一端移动,直到全部对象都移动完成而后清理掉不须要的内存。
由于 JS 采用 IEEE 754 双精度版本(64位),而且只要采用 IEEE 754 的语言都有该问题。
不止 0.1 + 0.2 存在问题,0.7 + 0.一、0.2 + 0.4 一样也存在问题。
存在问题的缘由是浮点数用二进制表示的时候是无穷的,由于精度的问题,两个浮点数相加会形成截断丢失精度,所以再转换为十进制就出了问题。
解决的办法能够经过如下代码:
export const addNum = (num1: number, num2: number) => {
let sq1;
let sq2;
let m;
try {
sq1 = num1.toString().split('.')[1].length;
} catch (e) {
sq1 = 0;
}
try {
sq2 = num2.toString().split('.')[1].length;
} catch (e) {
sq2 = 0;
}
m = Math.pow(10, Math.max(sq1, sq2));
return (Math.round(num1 * m) + Math.round(num2 * m)) / m;
};
复制代码
核心就是计算出两个浮点数最大的小数长度,好比说 0.1 + 0.22 的小数最大长度为 2,而后两数乘上 10 的 2次幂再相加得出数字 32,而后除以 10 的 2次幂便可得出正确答案 0.32。
你是否在平常开发中遇到一个问题,在滚动事件中须要作个复杂计算或者实现一个按钮的防二次点击操做。
这些需求均可以经过函数防抖动来实现。尤为是第一个需求,若是在频繁的事件回调中作复杂计算,颇有可能致使页面卡顿,不如将屡次计算合并为一次计算,只在一个精确点作操做。
PS:防抖和节流的做用都是防止函数屡次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于阈值,防抖的状况下只会调用一次,而节流会每隔必定时间调用函数。
咱们先来看一个袖珍版的防抖理解一下防抖的实现:
// func是用户传入须要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
// 缓存一个定时器id
let timer = 0
// 这里返回的函数是每次用户实际调用的防抖函数
// 若是已经设定过定时器了就清空上一次的定时器
// 开始一个新的定时器,延迟执行用户传入的方法
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
// 不难看出若是用户调用该函数的间隔小于 wait 的状况下,上一次的时间还未到就被清除了,并不会执行函数
复制代码
这是一个简单版的防抖,可是有缺陷,这个防抖只能在最后调用。通常的防抖会有immediate选项,表示是否当即调用。这二者的区别,举个栗子来讲:
当即执行
的防抖函数,它老是在第一次调用,而且下一次调用必须与前一次调用的时间间隔大于wait才会触发。下面咱们来实现一个带有当即执行选项的防抖函数
// 这个是用来获取当前时间戳的
function now() {
return +new Date()
}
/** * 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行 * * @param {function} func 回调函数 * @param {number} wait 表示时间窗口的间隔 * @param {boolean} immediate 设置为ture时,是否当即调用函数 * @return {function} 返回客户调用函数 */
function debounce (func, wait = 50, immediate = true) {
let timer, context, args
// 延迟执行函数
const later = () => setTimeout(() => {
// 延迟函数执行完毕,清空缓存的定时器序号
timer = null
// 延迟执行的状况下,函数会在延迟函数中执行
// 使用到以前缓存的参数和上下文
if (!immediate) {
func.apply(context, args)
context = args = null
}
}, wait)
// 这里返回的函数是每次实际调用的函数
return function(...params) {
// 若是没有建立延迟执行函数(later),就建立一个
if (!timer) {
timer = later()
// 若是是当即执行,调用函数
// 不然缓存参数和调用上下文
if (immediate) {
func.apply(this, params)
} else {
context = this
args = params
}
// 若是已有延迟执行函数(later),调用的时候清除原来的并从新设定一个
// 这样作延迟函数会从新计时
} else {
clearTimeout(timer)
timer = later()
}
}
}
复制代码
总体函数实现的不难,总结一下。
null
,就能够再次点击了。防抖动和节流本质是不同的。防抖动是将屡次执行变为最后一次执行,节流是将屡次执行变成每隔一段时间执行。
/** * underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait * * @param {function} func 回调函数 * @param {number} wait 表示时间窗口的间隔 * @param {object} options 若是想忽略开始函数的的调用,传入{leading: false}。 * 若是想忽略结尾函数的调用,传入{trailing: false} * 二者不能共存,不然函数不能执行 * @return {function} 返回客户调用函数 */
_.throttle = function(func, wait, options) {
var context, args, result;
var timeout = null;
// 以前的时间戳
var previous = 0;
// 若是 options 没传则设为空对象
if (!options) options = {};
// 定时器回调函数
var later = function() {
// 若是设置了 leading,就将 previous 设为 0
// 用于下面函数的第一个 if 判断
previous = options.leading === false ? 0 : _.now();
// 置空一是为了防止内存泄漏,二是为了下面的定时器判断
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function() {
// 得到当前时间戳
var now = _.now();
// 首次进入前者确定为 true
// 若是须要第一次不执行函数
// 就将上次时间戳设为当前的
// 这样在接下来计算 remaining 的值时会大于0
if (!previous && options.leading === false) previous = now;
// 计算剩余时间
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 若是当前调用已经大于上次调用时间 + wait
// 或者用户手动调了时间
// 若是设置了 trailing,只会进入这个条件
// 若是没有设置 leading,那么第一次会进入这个条件
// 还有一点,你可能会以为开启了定时器那么应该不会进入这个 if 条件了
// 其实仍是会进入的,由于定时器的延时
// 并非准确的时间,极可能你设置了2秒
// 可是他须要2.2秒才触发,这时候就会进入这个条件
if (remaining <= 0 || remaining > wait) {
// 若是存在定时器就清理掉不然会调用二次回调
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// 判断是否设置了定时器和 trailing
// 没有的话就开启一个定时器
// 而且不能不能同时设置 leading 和 trailing
timeout = setTimeout(later, remaining);
}
return result;
};
};
复制代码
class Events {
constructor() {
this.events = new Map();
}
addEvent(key, fn, isOnce, ...args) {
const value = this.events.get(key) ? this.events.get(key) : this.events.set(key, new Map()).get(key)
value.set(fn, (...args1) => {
fn(...args, ...args1)
isOnce && this.off(key, fn)
})
}
on(key, fn, ...args) {
if (!fn) {
console.error(`没有传入回调函数`);
return
}
this.addEvent(key, fn, false, ...args)
}
fire(key, ...args) {
if (!this.events.get(key)) {
console.warn(`没有 ${key} 事件`);
return;
}
for (let [, cb] of this.events.get(key).entries()) {
cb(...args);
}
}
off(key, fn) {
if (this.events.get(key)) {
this.events.get(key).delete(fn);
}
}
once(key, fn, ...args) {
this.addEvent(key, fn, true, ...args)
}
}
复制代码
instanceof
能够正确的判断对象的类型,由于内部机制是经过判断对象的原型链中是否是能找到类型的 prototype
。
function instanceof(left, right) {
// 得到类型的原型
let prototype = right.prototype
// 得到对象的原型
left = left.__proto__
// 判断对象的类型是否等于类型的原型
while (true) {
if (left === null)
return false
if (prototype === left)
return true
left = left.__proto__
}
}
复制代码
Function.prototype.myCall = function(context, ...args) {
context = context || window
let fn = Symbol()
context[fn] = this
let result = context[fn](...args)
delete context[fn]
return result
}
复制代码
Function.prototype.myApply = function(context) {
context = context || window
let fn = Symbol()
context[fn] = this
let result
if (arguments[1]) {
result = context[fn](...arguments[1])
} else {
result = context[fn]()
}
delete context[fn]
return result
}
复制代码
Function.prototype.myBind = function (context) {
var _this = this
var args = [...arguments].slice(1)
// 返回一个函数
return function F() {
// 由于返回了一个函数,咱们能够 new F(),因此须要判断
if (this instanceof F) {
return new _this(...args, ...arguments)
}
return _this.apply(context, args.concat(...arguments))
}
}
复制代码
其余手写题上文已经有说起,好比模拟 new
、ES5 实现继承、深拷贝。
另外你们可能常常能看到手写 Promise 的文章,其实根据笔者目前收集到的数百道面试题以及读者的反馈来看,压根就没人遇到这个考点,因此咱们大可没必要在这上面花时间。
以上就是本篇基础的所有内容了,若是有各位读者认为重要的知识点笔者却遗漏的话,欢迎你们指出。
你们也能够在笔者的网站上阅读,体验更佳!