「中高级前端面试」手写代码合集

8,339 阅读8分钟

本合集旨在巩固前端工程师核心基础技能,从各大小厂高频手写题出发,对常见的模拟实现进行总结,坚持学习,会有 彩蛋 呦。

更详尽更 的源代码放在 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 被调用后做了三件事情:

  1. 让实例对象可以访问到私有属性
  2. 让实例对象可以访问构造函数原型 (constructor.prototype) 所在原型链上的属性
  3. 考虑构造函数有返回值的情况
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 的原理

私有属性 一般满足:

  1. 能被 class 内部的不同方法访问,但不能在类外部被访问
  2. 子类不能继承父类的私有属性

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());

上面的乞丐版已经能覆盖大多数的应用场景,但面试的时候请把它忘记!它存在几个问题:

  1. 无法解决 循环引用
  2. 无法拷贝特殊的对象,比如 函数、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-urlquery-string,边界都考虑到了,当然也比我们实现的复杂。

对了,别忘了 nodejs 的 这两个模块方法: url.parsequerystring.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
        }
    }
}

比较两种方法:

  1. 第一种事件会在 n 秒后第一次执行,第二种事件会立刻执行。
  2. 第一种事件停止触发后依然会再执行一次事件,第二种事件停止触发后没有办法再执行事件。

加强版节流

现在按钮已经使用了上面的防抖版本...

  • 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 静态方法梳理:

  1. 传参为一个 Promise,则直接返回它。
  2. 传参为一个 thenable 对象,返回的 Promise 会跟随这个对象,采用它的最终状态作为自己的状态。
  3. 其他情况,直接返回以该值为成功状态的 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 实现梳理:

  1. 传入参数为一个空的可迭代对象,则直接进行 resolve。
  2. 如果参数中有一个promise 失败,那么 Promise.all 返回的 promise 对象失败。
  3. 在任何情况下,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;
      })
    }
  })
}

❤️ 看这里 (* ̄︶ ̄)

如果你觉得这篇内容对你挺有启发,记得点个赞丫,让更多的人也能看到这篇内容,拜托啦,这对我真的很重要。

往期精选:

「中高级前端面试」手写代码合集(二)

字节面试-TLS 握手流程详解

参考文献

JavaScript 手写代码无敌秘籍

JavaScript 标准内置对象

原生 JS 灵魂之问系列

如何写出一个惊艳面试官的深拷贝

When to encode space to plus (+) or %20