本合集旨在巩固前端工程师核心基础技能,从各大小厂高频手写题出发,对常见的模拟实现进行总结,坚持学习,会有 彩蛋
呦。
更详尽更 多 的源代码放在 github 项目上,一种题含多种解法,有兴趣的可以下载学习,长期更新和维护。
PS:文章如有误,请不吝指正。
您的 「赞」
是笔者创作的动力。
正题开始,先放思维导图:
1.instanceof
instanceof
判断左边的原型是否存在于右边的原型链中。
实现思路:逐层往上查找原型,如果最终的原型为 null
,证明不存在原型链中,否则存在。
function myInstanceof(left, right) {
if (typeof left !== 'object' || left === null) return false // 基础类型一律为 false
let proto = Object.getPrototypeOf(left) // 获取对象的原型
while(true) {
if (proto === null) return false
if (proto === right.prototype) return true
proto = Object.getPrototypeOf(proto)
}
}
2.Object.create
const myCreate = function(obj) {
function F() {}
F.prototype = obj
return new F() // 创建一个继承 obj 原型属性的纯净对象
}
3.new
new
被调用后做了三件事情:
- 让实例对象可以访问到私有属性
- 让实例对象可以访问构造函数原型 (constructor.prototype) 所在原型链上的属性
- 考虑构造函数有返回值的情况
function myNew(ctor, ...args) {
let fn = Array.prototype.shift.call(arguments) // 取出第一个参数 ctor
if (typeof fn !== 'function') throw `${fn} is not a constructor`
let obj = Object.create(fn.prototype)
let res = fn.apply(obj, args) // 考虑构造函数有返回值的情况,直接执行
let isObject = typeof res === 'object' && res !== null
let isFunction = typeof res === 'function'
return isObject || isFunction ? res : obj
}
4.call & apply
实现思路:利用谁调用函数,函数的 this
就指向谁这一特点来实现。
Function.prototype.myCall = function() { // myApply 通用
if (typeof this !== 'function') throw 'caller must be a function'
let self = arguments[0] || window
self._fn = this
let args = [...arguments].flat().slice(1) // 展开后取参数列表
let res = self._fn(...args) // 谁调用函数,函数的 this 就指向谁
Reflect.deleteProperty(self, '_fn') // 删除 _fn 属性
return res
}
5.bind
bind
用于改变函数 this
指向,并返回一个函数。
Function.prototype.myBind = function() {
if (typeof this !== 'function') throw 'caller must be a function'
let self = this
let context = arguments[0]
let args = Array.prototype.slice.call(arguments, 1)
let fn = function() {
let fnArgs = Array.prototype.slice.call(arguments)
// bind 函数的参数 + 延迟函数的参数
self.apply(this instanceof self ? this : context, args.concat(fnArgs)
)
}
fn.prototype = Object.create(self.prototype) // 维护原型
return fn
}
6.柯里化
柯里化
:
- 定义:将函数与其参数的一个子集绑定起来后返回个新函数。
- 好处:减少代码冗余,增加可读性,是一种简洁的实现函数委托的方式。
举个简单的 🌰:
function multiFn(x, y, z) {
return x * y * z
}
function curry() { ... } // 假设有一个 curry 函数可以做到柯里化
let multi = curry(multiFn)
multi(2, 3, 4)
multi(2)(3)(4)
multi(2, 3)(4)
multi(2)(3, 4) // 以上结果都是 3,柯里化将参数拆开自由绑定,结果不变。
let seniorMulti = multi(2) // seniorMulti 可以多次使用
seniorMulti(3)(4) // 当我们觉得重复传递参数 2 总是冗余时,可以这样。
代码实现:
function curry(fn, args=[]) {
return function() {
let newArgs = args.concat(Array.prototype.slice.call(arguments))
if (newArgs.length < fn.length) { // 假如:实参个数 < 形参个数
return curry.call(this, fn, newArgs)
} else {
return fn.apply(this, newArgs)
}
}
}
// ES6 高颜值写法
const curry = fn =>
judge = (...args) =>
args.length === fn.length
? fn(...args)
: (arg2) => judge(...args, arg2)
7.寄生组合继承
function Parent() {
this.favorite = 'money'
}
function Child() {
Parent.call(this) // 继承父类的属性
this.age = 18
}
Child.prototype = Object.create(Parent.prototype) // 继承父类的原型属性
Object.setPrototypeOf(Child, Parent) // 继承父类的静态方法
Child.prototype.constructor = Child // constructor 重新指向 Child
8.TypeScript 中 Class 私有属性 private 的原理
私有属性
一般满足:
- 能被 class 内部的不同方法访问,但不能在类外部被访问
- 子类不能继承父类的私有属性
ES6 中已经提供了一个 #
给我们使用,但需要 babel
转译,所以让我们自己实现一个吧。
const MyClass = (function() { // 利用闭包和 WeakMap
const _x = new WeakMap()
class InnerClass {
constructor(x) {
_x.set(this, x)
}
getX() {
return x.get(this)
}
}
return InnerClass
})()
let myClass = new MyClass(5)
console.log(myClass.getX()) // 5
console.log(myClass.x) // undefined
9.数组排序
冒泡
function bubbleSort(arr) {
let len = arr.length
for (let i=len; i>=2; i--) { // 排完第 2 个,第一个自动为最小
for (let j=0; j<i-1; j++) { // 逐渐缩小范围
if (arr[j] > arr[j+1])
[arr[j], arr[j+1]] = [arr[j+1], arr[j]]
}
}
return arr
}
选择排序
实现思路:遍历自身以后的元素,最小的元素跟自己调换位置。
function selectSort(arr) {
let len = arr.length
for (let i=0; i<len-1; i++) {
for (let j=i; j<len; j++) {
if (arr[j] < arr[i])
[arr[i], arr[j]] = [arr[j], arr[i]]
}
}
return arr
}
插入排序
实现思路:将元素插入到已排序好的数组中。
function insertSort(arr) {
for (let i=1; i<arr.length; i++) { // arr[0] 默认为已排序的数组
for (let j=i; j>0; j--) {
if (arr[j] < arr[j-1]) {
[arr[j],arr[j-1]] = [arr[j-1],arr[j]]
} else { break }
}
}
return arr
}
快速排序
实现思路:选择基准值 mid,循环原数组,小于基准值放左边数组,大于放右边数组,然后 concat 组合,最后依靠递归完成排序。
function quickSort(arr) {
if (arr.length <= 1) return arr
let left = [], right = []
// 偷懒写法,对于数组长度不大的可以使用,实际使用时慎用!
// let midVal = arr.splice(0, 1)
// 若 length 超过 1000,下面的 mid 还需要先预处理:每隔几百取一个值拼装成数组,再在里面取 mid
const mid = Math.floor(arr.length / 2)
const midVal = arr.splice(mid, 1)[0]
for (let i = 0; i < arr.length; i++) {
if (arr[i] < midVal) {
left.push(arr[i])
} else {
right.push(arr[i])
}
}
return quickSort(left).concat(midVal, quickSort(right))
}
sort
这里仅仅理一下 sort
的实现思路,sort 方法在 V8 引擎内部相对与其他方法而言是一个比较高深的算法。
先甩个表格:
排序算法 | 平均时间复杂度 | 空间复杂度 | 最好情况! | 稳定性 |
---|---|---|---|---|
冒泡排序 | O(n^2) | O(1) | O(n) | 稳定 |
选择排序 | O(n^2) | O(1) | O(n^2) | 不稳定 |
插入排序 | O(n^2) | O(1) | O(n) | 稳定 |
快速排序 | O(nlogn) | O(logn) | O(nlogn) | 不稳定 |
稳定性定义:排序前后两个相等的数相对位置不变,则算法稳定。好处是第一个键排序的结果可以为第二个键排序所用。
设要排序的元素个数是 n:
- 当
n <= 10
时,采用插入排序 (在 n 足够小时,插入排序比快排更快,参考表格)。 - 当
n > 10
时,采用三路快速排序。
其中 10 < n <= 1000
,采用中位数作为哨兵元素;n > 1000
,每隔 200 ~ 215 个元素挑出一个元素,放到一个新数组,然后对它排序,找到中间位置的数,以此作为中位数。
10.数组去重
双层循环
function unique(arr) {
for (let i=0; i<arr.length; i++) { // 注意这里的 arr 长度是变化的
for (let j=i+1; j<arr.length; j++) {
if (arr[i] === arr[j]) {
arr.splice(j, 1)
j--
}
}
}
return arr
}
indexOf
function unique(arr) {
let res = []
for (let i=0; i<arr.length; i++) {
let current = arr[i]
if (res.indexOf(current) === -1) res.push(current)
}
return res
}
// 或者可以这样,利用 filter + indexOf
function unique(arr) {
let res = arr.filter(function(item, index, array){
return arr.indexOf(item) === index;
})
return res;
}
排序后去重
function unique(arr) {
let res = []
let sortedArray = arr.concat().sort()
let lastVal
for (let i=0; i<sortedArray.length; i++) {
// 如果是第一个元素或者相邻的元素不相同
if (!i || lastVal !== sortedArray[i])
res.push(sortedArray[i])
lastVal = sortedArray[i]
}
return res
}
// 或者可以这样,利用排序 + filter
function unique(arr) {
return arr.concat().sort().filter(function(item, index, array){
return !index || item !== arr[index - 1]
})
}
ES6 Set or Map
function unique(arr) {
return [...new Set(arr)];
}
// 或者可以这样,利用 Map
function unique (arr) {
const last = new Map()
return arr.filter((item) => !last.has(item) && last.set(item, 1))
}
11.map
依据 map 规范 来模拟实现:
Array.prototype.map = function(callbackFn, thisArg) {
if (this === null || this === undefined)
throw new TypeError(`Cannot read property 'map' of ${this}`)
// 处理回调类型异常
if (Object.prototype.toString.call(callbackFn) !== '[object Function]')
throw new TypeError(`${callbackFn} is not a function`)
let O = Object(this), // 规定 this 要先转化为对象
T = thisArg,
len = O.length >>> 0, // 保证 len 为数字且为整数
A = new Array(len)
for (let k=0; k<len; k++) {
if (k in O) { // 原型链查找属性
let mappedValue = callbackFn.call(T, O[k], k, O)
A[k] = mappedValue
}
}
return A
}
12.reduce
依据 reduce 规范 来模拟实现:
Array.prototype.reduce = function(callbackFn, initialValue) {
if (this === null || this === undefined)
throw new TypeError(`Cannot read property 'reduce' of ${this}`)
// 处理回调类型异常
if (Object.prototype.toString.call(callbackFn) !== '[object Function]')
throw new TypeError(`${callbackFn} is not a function`)
let O = Object(this), // 规定 this 要先转化为对象
k = 0,
len = O.length >>> 0, // 保证 len 为数字且为整数
accumulator = initialValue
if (accumulator === undefined) {
for (; k<len; k++) {
if (k in O) {
accumulator = O[k]
k++
break
}
}
}
if (k === len && accumulator === undefined) // 表示数组全为空
throw new Error('Each element of the array is empty')
for(; k<len; k++) {
if (k in O) {
accumulator = callbackfn.call(undefined, accumulator, O[k], k, O);
}
}
return accumulator
}
13.filter
依据 filter 规范 来模拟实现:
Array.prototype.filter = function(callbackFn, thisArg) {
if (this === null || this === undefined)
throw new TypeError(`Cannot read property 'filter' of ${this}`)
// 处理回调类型异常
if (Object.prototype.toString.call(callbackFn) !== '[object Function]')
throw new TypeError(`${callbackFn} is not a function`)
let O = Object(this), // 规定 this 要先转化为对象
resLen = 0,
len = O.length >>> 0, // 保证 len 为数字且为整数
res = []
for (let i=0; i<len; i++) {
if (i in O) { // 原型链查找属性
let element = O[i];
if (callbackfn.call(thisArg, O[i], i, O)) res[resLen++] = element
}
}
return res
}
14.随机字符串
function generateRandomString(len) {
let randomStr = ''
for (; randomStr.length<len; randomStr+=Math.random().toString(36).substr(2)) {}
return randomStr.substr(0, len)
}
15.斐波那契数列
学了这么久该饿了吧?恭喜你,发现了...
来看下 某美食 up 主做出来的实体斐波那契数列 ,好家伙,眼泪不知不觉从嘴角流了出来...
// 递归,时间复杂度为 O(2^n)
function fibSequence(n) {
if (n === 1 || n === 2) return n - 1;
return fib(n - 1) + fib(n - 2)
}
// 或者使用迭代,时间复杂度为 O(n),推荐!
function fibSequence(n) {
let a = 0, b = 1, c = a + b
for (let i=3; i<n; i++) {
a = b
b = c
c = a + b
}
return c
}
16.浅拷贝
浅拷贝
:
- 只能拷贝一层对象,如果有对象的嵌套,浅拷贝无能为力。
- 潜在问题:假若拷贝的属性是引用类型,拷贝的就是内存地址,修改内容会互相影响。
const shallowClone = (target) => {
if (typeof target === 'object' && target !== null) {
const cloneTarget = Array.isArray(target) ? []: {}
for (let prop in target) {
if (target.hasOwnProperty(prop)) { // 是否是自身(非继承)属性
cloneTarget[prop] = target[prop] // 只考虑一层对象
}
}
return cloneTarget
} else {
return target // 基础类型直接返回
}
}
// 或者你可以
console.log(Object.assign(array, ...sources))
console.log(array.concat())
console.log(array.slice())
console.log([...array])
17.深拷贝
深拷贝
为对象创建一个副本,两者的引用地址不同。当你希望使用一个对象,但又不想修改原对象时,深拷贝是一个很好的选择。
JSON.parse(JSON.stringify());
上面的乞丐版已经能覆盖大多数的应用场景,但面试的时候请把它忘记!它存在几个问题:
- 无法解决
循环引用
。 - 无法拷贝特殊的对象,比如
函数、RegExp、Date、Set、Map
等。
工作中,如果遇到复杂对象,我们可以使用工具库,比如 lodash 的 cloneDeep 方法,切忌滥用!
开始简单梳理:
现在,我们来实现一个能覆盖大多数场景的深拷贝(可以跳过这个看下面一版,更易理解):
// Map 强引用,需要手动清除属性才能释放内存。
// WeakMap 弱引用,随时可能被垃圾回收,使内存及时释放,是解决循环引用的不二之选。
function cloneDeep(obj, map = new WeakMap()) {
if (obj === null || obj === undefined) return obj // 不进行拷贝
if (obj instanceof Date) return new Date(obj)
if (obj instanceof RegExp) return new RegExp(obj)
// 基础类型不需要深拷贝
if (typeof obj !== 'object' && typeof obj !== 'function') return obj
// 处理普通函数和箭头函数
if (typeof obj === 'function') return handleFunc(obj)
// 是对象的话就要进行深拷贝
if (map.get(obj)) return map.get(obj)
let cloneObj = new obj.constructor()
// 找到的是所属类原型上的 constructor,而原型上的 constructor 指向的是当前类本身。
map.set(obj, cloneObj)
if (getType(obj) === '[object Map]') {
obj.forEach((item, key) => {
cloneObj.set(cloneDeep(key, map), cloneDeep(item, map));
})
}
if (getType(obj) === '[object Set]') {
obj.forEach(item => {
cloneObj.add(cloneDeep(item, map));
})
}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key], map)
}
}
return cloneObj
}
// 获取更详细的数据类型
function getType(obj) {
return Object.prototype.toString.call(obj)
}
// 处理普通函数和箭头函数
function handleFunc(func) {
if(!func.prototype) return func // 箭头函数直接返回自身
const bodyReg = /(?<={)(.|\n)+(?=})/m
const paramReg = /(?<=\().+(?=\)\s+{)/
const funcString = func.toString()
// 分别匹配 函数参数 和 函数体
const param = paramReg.exec(funcString)
const body = bodyReg.exec(funcString)
if(!body) return null
if (param) {
const paramArr = param[0].split(',')
return new Function(...paramArr, body[0])
} else {
return new Function(body[0])
}
}
下面这版我改过的:
function cloneDeep(obj, map = new WeakMap()) {
if (!(obj instanceof Object)) return obj; // 基本数据
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
if (map.get(obj)) return map.get(obj); // 解决循环引用
if (obj instanceof Function) { // 解决函数
return function () {
return obj.apply(this, [...arguments]);
};
}
const res = new obj.constructor(); // 下面是数组/普通对象/Set/Map 的处理
obj instanceof Object && map.set(obj, res);
if (obj instanceof Map) {
obj.forEach((item, index) => {
res.set(cloneDeep(index, map), cloneDeep(item, map));
});
}
if (obj instanceof Set) {
obj.forEach((item) => {
res.add(cloneDeep(item, map));
});
}
Object.keys(obj).forEach((key) => {
if (obj[key] instanceof Object) {
res[key] = cloneDeep(obj[key], map);
} else {
res[key] = obj[key];
}
});
return res;
}
const map = new Map();
map.set({ a: 1 }, "1");
const source = {
name: "Jack",
meta: {
age: 12,
birth: new Date("1997-10-10"),
ary: [1, 2, { a: 1 }],
say() {
console.log("Hello");
},
map
},
};
source.source = source;
const newObj = cloneDeep(source);
console.log(newObj.meta.ary[2] === source.meta.ary[2]); // false
console.log(newObj.meta.birth === source.meta.birth); // false
console.log(newObj);
18.解析 URL
先看看完整的 URL 长什么样子?
举个 🌰:https://keith:miao@www.foo.com:80/file?test=3&miao=4#heading-0
利用正则简单实现:
function parseUrl(url) {
// scheme://user:passwd@ 部分
let schemeStr = '(?:([^/?#]+))?//(?:([^:]*)(?::?(.*))@)?',
// host:port path?query 部分
urlStr = '(?:([^/?#:]*):?([0-9]+)?)?([^?#]*)(\\?(?:[^#]*))?',
// #fragment 部分
fragmentStr = '(#(?:.*))'
let pattern = RegExp(`^${schemeStr}${urlStr}${fragmentStr}?`)
let matched = url.match(pattern) || []
return {
protocol: matched[1], // 协议
username: matched[2], // 用户名
password: matched[3], // 密码
hostname: matched[4], // 主机
port: matched[5], // 端口
pathname: matched[6], // 路径
search: matched[7], // 查询字符串 queryString
hash: matched[8], // 锚点
}
}
// 或者你可以这样
function parseUrl(url) {
const urlObj = new URL(url)
return {
protocol: urlObj.protocol,
username: urlObj.username,
password: urlObj.password,
hostname: urlObj.hostname,
port: urlObj.port,
pathname: urlObj.pathname,
search: urlObj.search,
hash: urlObj.hash
}
}
单独解析查询字符串 queryString
:
function parseQueryString(query) {
if (!query) return {}
query = query.replace(/^\?/, '')
const queryArr = query.split('&')
const result = {}
queryArr.forEach(query => {
let [key, value] = query.split('=')
try {
key = decodeURLComponent(key || '').replace(/\+/g, ' ')
value = decodeURLComponent(value || '').replace(/\+/g, ' ')
} catch(e) {
return console.log(e) // 非法字符不处理
}
const type = getQueryType(key)
switch(type) {
case 'ARRAY':
key = key.replace(/\[\]$/, '') // 对于形如 `list[]` 的解析成数组
if (!result[key]) {
result[key] = [value]
} else {
result[key].push(value)
}
break;
case 'JSON':
key = key.replace(/\{\}$/, '') // 对于形如 obj{} 的解析为对象
value = JSON.parse(value)
result.json = value
break;
default:
result[key] = value
}
})
return result
}
function getQueryType (key) {
if (key.endsWith('[]')) return 'ARRAY'
if (key.endsWith('{}')) return 'JSON'
return 'DEFAULT'
}
// 或者你可以这样,如果你做好了被面试官打si的准备...
// 简易版
function getUrlQuery(search) {
let searchObj = {};
for (let [key, value] of new URLSearchParams(search)) {
searchObj[key] = value
}
return searchObj
}
当然,这里还并不严谨,没有考虑到如下问题:
- 相同字段如何处理
- 没有替换
+
为%20
- 只有
key
/ 只有value
- ...
工作中,推荐两个开源库:js-url 和 query-string,边界都考虑到了,当然也比我们实现的复杂。
对了,别忘了 nodejs 的 这两个模块方法: url.parse,querystring.parse 已经很好用了。
19.JSONP
JSONP
:常见的跨域手段,利用 <script>
标签没有跨域限制的漏洞,来达到与第三方通讯的目的。
const jsonp = ({url, params, callbackName}) => {
const generateURL = () => { // 根据 URL 格式生成地址
let dataStr = ''
for (let key in params) {
dataStr += `${key}=${params[key]}&`
}
dataStr += `callback=${callbackName}`
return `${url}?${dataStr}`
}
return new Promise((resolve, reject) => {
callbackName = callbackName || Math.random().toString()
let scriptEle = document.createElement('script')
scriptEle.src = generateURL()
document.body.appendChild(scriptEle)
// 服务器返回字符串 `${callbackName}(${服务器的数据})`,浏览器解析即可执行。
window[callbackName] = (data) => {
resolve(data)
document.body.removeChild(scriptEle) // 别忘了清除 dom
}
})
}
20.防抖/节流
防抖
- QA:为什么我按了这个 Button,连续发送了两个请求出去?
- FE:因为你太快了,连续按了两次。
- QA:那你做下限制呗。
好的,所以 防抖
,就是用来避免不必要的操作,比如这个按钮,QA 想短时间内只执行最后一次的点击操作。
我平常的记忆方式还有一种:法师读条,后一个会打算前一个的施法...
const debounce = (fn, delay) => {
let timer = null
return function(...args) {
let context = this // 修复 this 指向
if (timer) clearTimeout(timer)
timer = setTimeout(() => { // 这里是箭头函数,this 指向外层的非箭头函数的 this
fn.apply(context, args)
}, delay)
}
}
如果我想立即执行...
const debounce = (fn, delay, immediate) => {
let timer = null
return function(...args) {
let context = this // 修复 this 指向
if (timer) clearTimeout(timer)
if (immediate) {
let callNow = !timer
timer = setTimeout(() => {
timer = null
}, delay)
if (callNow) fn.apply(context, args)
}
timer = setTimeout(() => { // 这里是箭头函数,this 指向外层的非箭头函数的 this
fn.apply(context, args)
}, delay)
}
}
节流
节流
即固定频率触发,使用在 scroll
事件上再贴切不过了。
function throttle(fn, interval) {
let timeout = null
return function(...args) {
const context = this
if (!timeout) {
timeout = setTimeout(() => {
timeout = null
fn.apply(context, args)
}, interval)
}
}
}
// 或者可以这样,挑你喜欢的。
function throttle(fn, interval) {
let last = 0 // 首次直接执行
return function (...args) {
let now = +new Date()
const context = this
if(now - last > interval) {
fn.apply(context, args)
last = now // 时间一到就更新 last
}
}
}
比较两种方法:
- 第一种事件会在 n 秒后第一次执行,第二种事件会立刻执行。
- 第一种事件停止触发后依然会再执行一次事件,第二种事件停止触发后没有办法再执行事件。
加强版节流
现在按钮已经使用了上面的防抖版本...
- QA 连续按了两次,只有最后一次发出请求了,测试通过,比了个 OK 的手势。
- FE 松了口气。
- QA 又来了劲,开始对着 Button 一直点点点点点,不嫌累。
- QA:怎么始终不触发?
- FE:...
加强版节流
无限防抖,固定频率触发。
const throttle = (fn, delay) => {
let timer = null, last = 0
return function(...args) {
let now = +new Date()
const context = this
if (now - last < delay && timer) {
clearTimeout(timer)
timer = setTimeout(() => {
timer = null
fn.apply(context, args)
}, delay)
} else { // 到了 delay 时间直接触发,不管了...
if (timer) {
clearTimeout(timer)
timer = null
}
last = now
fn.apply(context, args)
}
}
}
21.图片懒加载
有时候你费心费力地进行了好几样性能优化,结果还没有你将图片压缩,懒加载显示来的更佳有效划算。所以性能优化,不是无脑优化。
图片懒加载
实现的方式一般有三种:
- clientHeight、scrollTop 和 offsetTop
- getBoundingClientRect
- IntersectionObserver
首先,给所有的图片一个占位资源:
<img src="default.jpg" data-src="https://www.xxx.com/target-1.jpg" />
<img src="default.jpg" data-src="https://www.xxx.com/target-2.jpg" />
......
<img src="default.jpg" data-src="https://www.xxx.com/target-39.jpg" />
① clientHeight、scrollTop 和 offsetTop
先上图:
可以看到图片的 offsetTop
小于 紫色 的线(scrollHeight
+ clientHeight
)就会显示在窗口中。
let imgs = document.getElementsByTagName("img"), count = 0
// 首次加载
lazyLoad()
// 通过监听 scroll 事件来判断图片是否到达视口,别忘了防抖节流
window.addEventListener('scroll', throttle(lazyLoad, 160))
function lazyLoad() {
let viewHeight = document.documentElement.clientHeight //视口高度
//滚动条卷去的高度
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop
for(let i=count; i<imgs.length; i++) {
// 元素现在已经出现在视口中
if(imgs[i].offsetTop < scrollTop + viewHeight) {
if(imgs[i].getAttribute("src") !== "default.jpg") continue;
imgs[i].src = imgs[i].getAttribute("data-src")
count ++
}
}
}
② getBoundingClientRect
dom 元素的 getBoundingClientRect().top
属性可以直接判断图片是否出现在了当前视口。
// 只修改一下 lazyLoad 函数
function lazyLoad() {
for(let i=count; i<imgs.length; i++) {
if(imgs[i].getBoundingClientRect().top < document.documentElement.clientHeight) {
if(imgs[i].getAttribute("src") !== "default.jpg") continue;
imgs[i].src = imgs[i].getAttribute("data-src")
count ++
}
}
}
③ IntersectionObserver
IntersectionObserver
浏览器内置的 API,实现了监听 window 的 scroll
事件、判断是否在视口中
以及 节流
三大功能。该 API 需要 polyfill。
let imgs = document.getElementsByTagName("img")
const observer = new IntersectionObserver(changes => {
for(let i=0, len=imgs.length; i<len; i++) {
let img = imgs[i]
// 通过这个属性判断是否在视口中,返回 boolean 值
if(img.isIntersecting) {
const imgElement = img.target
imgElement.src = imgElement.getAttribute("data-src")
observer.unobserve(imgElement) // 解除观察
}
}
})
Array.from(imgs).forEach(item => observer.observe(item)) // 调用
22.异步回调 Promise 系列
Promise
Promise
实现梳理:
- 链式调用
- 错误捕获(冒泡)
const PENDING = 'PENDING'; // 进行中
const FULFILLED = 'FULFILLED'; // 已成功
const REJECTED = 'REJECTED'; // 已失败
class Promise {
constructor(exector) {
// 初始化状态
this.status = PENDING;
// 将成功、失败结果放在this上,便于then、catch访问
this.value = undefined;
this.reason = undefined;
// 成功态回调函数队列
this.onFulfilledCallbacks = [];
// 失败态回调函数队列
this.onRejectedCallbacks = [];
const resolve = value => {
// 只有进行中状态才能更改状态
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
// 成功态函数依次执行
this.onFulfilledCallbacks.forEach(fn => fn(this.value));
}
}
const reject = reason => {
// 只有进行中状态才能更改状态
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
// 失败态函数依次执行
this.onRejectedCallbacks.forEach(fn => fn(this.reason))
}
}
try {
// 立即执行executor
// 把内部的resolve和reject传入executor,用户可调用resolve和reject
exector(resolve, reject);
} catch(e) {
// executor执行出错,将错误内容reject抛出去
reject(e);
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function'? onRejected:
reason => { throw new Error(reason instanceof Error ? reason.message:reason) }
// 保存this
const self = this;
return new Promise((resolve, reject) => {
if (self.status === PENDING) {
self.onFulfilledCallbacks.push(() => {
// try捕获错误
try {
// 模拟微任务
setTimeout(() => {
const result = onFulfilled(self.value);
// 分两种情况:
// 1. 回调函数返回值是Promise,执行then操作
// 2. 如果不是Promise,调用新Promise的resolve函数
result instanceof Promise ? result.then(resolve, reject) :
resolve(result)
})
} catch(e) {
reject(e);
}
});
self.onRejectedCallbacks.push(() => {
// 以下同理
try {
setTimeout(() => {
const result = onRejected(self.reason);
// 不同点:此时是reject
result instanceof Promise ? result.then(resolve, reject) :
reject(result)
})
} catch(e) {
reject(e);
}
})
} else if (self.status === FULFILLED) {
try {
setTimeout(() => {
const result = onFulfilled(self.value)
result instanceof Promise ? result.then(resolve, reject) : resolve(result)
});
} catch(e) {
reject(e);
}
} else if (self.status === REJECTED){
try {
setTimeout(() => {
const result = onRejected(self.reason);
result instanceof Promise ? result.then(resolve, reject) : reject(result)
})
} catch(e) {
reject(e)
}
}
});
}
catch(onRejected) {
return this.then(null, onRejected);
}
static resolve(value) {
if (value instanceof Promise) {
// 如果是Promise实例,直接返回
return value;
} else {
// 如果不是Promise实例,返回一个新的Promise对象,状态为FULFILLED
return new Promise((resolve, reject) => resolve(value));
}
}
static reject(reason) {
return new Promise((resolve, reject) => {
reject(reason);
})
}
}
Promise.prototype.finally = function(callback) {
this.then(value => {
return Promise.resolve(callback()).then(() => {
return value
})
}, error => {
return Promise.resolve(callback()).then(() => {
throw error
})
})
}
Promise.resolve
Promise.resolve
静态方法梳理:
- 传参为一个 Promise,则直接返回它。
- 传参为一个 thenable 对象,返回的 Promise 会跟随这个对象,采用它的最终状态作为自己的状态。
- 其他情况,直接返回以该值为成功状态的 promise 对象。
Promise.resolve = (param) => {
if(param instanceof Promise) return param // 符合 1
return new Promise((resolve, reject) => {
if (param && param.then && typeof param.then === 'function') { // 符合 2
// param 状态变为成功会调用resolve,将新 Promise 的状态变为成功,反之亦然
param.then(resolve, reject)
} else { // 符合 3
resolve(param)
}
})
}
Promise.reject
// 冒泡捕获
Promise.reject = function (reason) {
return new Promise((resolve, reject) => {
reject(reason)
})
}
Promise.all
Promise.all
实现梳理:
- 传入参数为一个空的可迭代对象,则直接进行 resolve。
- 如果参数中有一个promise 失败,那么 Promise.all 返回的 promise 对象失败。
- 在任何情况下,Promise.all 返回的 promise 的完成状态的结果都是一个数组。
Promise.all = function(promises) {
return new Promise((resolve, reject) => {
let result = [],
index = 0,
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.allSettled
Promise.all 的优化
if (Promise && !Promise.allSettled) {
Promise.allSettled = function(promises) {
return Promise.all(promises.map(function(promise) {
return Promise.resolve(promise).then(function(value) {
return { status: 'fulfilled', value }
}).catch(function(reason) {
return { status: 'rejected', reason }
})
}))
}
}
Promise.race
Promise.race
只要有一个 promise 执行完,直接 resolve 并停止执行,注意传空数组即 len === 0 时并没有 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++) {
// promises[i] 可能不是一个 promise 对象
Promise.resolve(promises[i]).then(data => {
resolve(data)
return;
}).catch(err => {
reject(err)
return;
})
}
})
}
❤️ 看这里 (* ̄︶ ̄)
如果你觉得这篇内容对你挺有启发,记得点个赞丫,让更多的人也能看到这篇内容,拜托啦,这对我真的很重要。
往期精选: