Сборник рукописного кода "Intermediate and Advanced Front-end Interview"

опрос

Этот сборник призван закрепить основные базовые навыки проектировщиков.Начиная с часто задаваемых от руки вопросов различных крупных и малых предприятий, он обобщает распространенные реализации моделирования.彩蛋Эй.

Более подробныймногоИсходный код размещен вgithubВ проекте один вопрос содержит несколько вариантов решения, и кому интересно, можете скачать и изучить.Долгосрочные обновления и обслуживание.

PS:文章如有误,请不吝指正。Твой「赞」Это движущая сила авторского творчества.

Следующий:Сборник рукописного кода "Intermediate and Advanced Front-end Interview" (2)

Чтобы начать с темы, сначала поставьте интеллект-карту:

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

私有属性Обычно встречаются:

  1. Можно получить доступ различными методами внутри класса, но не вне класса
  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
}

быстрая сортировка

Идея реализации: выберите среднее опорное значение, зациклите исходный массив, поместите массив слева, если он меньше опорного значения, поместите массив справа, если он больше опорного значения, затем объедините комбинацию и наконец, завершить сортировку рекурсией.

function quickSort(arr) {
    if (arr.length <= 1) return arr
    let left = [], right = [], mid = arr.splice(0, 1)
    for (let i=0; i<arr.length; i++) {
    	if (arr[i] < mid) {
      	    left.push(arr[i])
      	} else {
      	    right.push(arr[i])
    	}
    }
    return quickSort(left).concat(mid, quickSort(right)) // 别忘了 mid
}

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

в соответствии сспецификация картыдля имитации реализации:

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

в соответствии суменьшить спецификациюдля имитации реализации:

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

в соответствии сспецификация фильтрадля имитации реализации:

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. Последовательность Фибоначчи

Вы голодны после столь долгого обучения? Поздравляю, вы нашли...

посмотриКухня из основных объектов фибоначчи, молодец, слезы текли из уголков рта невольно...

// 递归,时间复杂度为 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Ждать.

На работе, если мы сталкиваемся со сложными объектами, мы можем использовать библиотеки инструментов, такие как lodashcloneDeepметод, не злоупотребляйте!

Начните простое расчесывание:

Теперь давайте реализуем глубокую копию, которая охватывает большинство сцен (вы можете пропустить это и посмотреть следующую версию для лучшего понимания):

// 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: Почему я нажимаю эту кнопку и отправляю два запроса подряд?
  • ФЭ: Ты нажал ее дважды подряд, потому что был слишком быстр.
  • QA: Тогда вы делаете предел.

Итак防抖, используется, чтобы избежать ненужных операций, таких как эта кнопка, QA хочет выполнить только операцию последнего щелчка за короткое время.

Есть еще один способ, который я обычно запоминаю: маг читает статью, а второй будет планировать кастинг первого...

const debounce = (fn, delay) => {
    let timer = null
    return function(...args) {
        if (timer) clearTimeout(timer)
        timer = setTimeout(() => { // 这里是箭头函数,this 指向外层的非箭头函数的 this
            fn.apply(this, args)
        }, delay)
    }
}

дросселирование

节流То есть используется триггер с фиксированной частотой.scrollСобытие как нельзя более актуально.

function throttle(fn, interval) {
    let flag = true
    return function(...args) {
    	if (!flag) return;
    	flag = false
    	setTimeout(() => {
      	    fn.apply(this, args)
      	    flag = true
    	}, interval)
    }
}

// 或者可以这样,挑你喜欢的。
function throttle(fn, interval) {
    let last = 0 // 首次直接执行
    return function (...args) {
    	let now = +new Date()
    	if(now - last < interval) return;
    	last = now // 时间一到就更新 last
    	fn.apply(this, args)
    }
}

Улучшенное регулирование

Теперь кнопка использовала версию стабилизации изображения выше...

  • QA нажал два раза подряд, только последний запрос был сделан, тест пройден, и сравнивался жест ОК.
  • ФЕ вздохнул с облегчением.
  • QA вернулся с большей энергией и начал все время нажимать кнопку, не слишком уставший.
  • QA: Почему он не срабатывает все время?
  • ФЭ:...

加强版节流Неограниченный антивибрационный триггер с фиксированной частотой.

const throttle = (fn, delay) => {
    let timer = null, last = 0
    return function(...args) {
    	let now = +new Date()
    	if (now - last < delay && timer) {
      	    clearTimeout(timer)
      	    timer = setTimeout(() => {
            	fn.apply(this, args)
      	    }, delay)
        } else { // 到了 delay 时间直接触发,不管了...
      	    last = now
      	    fn.apply(this, 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

элемента дом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Встроенный API браузера реализует мониторинг окнаscrollмероприятие,判断是否在视口中так же как节流Три функции. Для этого API требуется полифилл.

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Для достижения расчесывания:

  • цепной вызов
  • Перехват ошибок (бублинг)
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. Если параметр является обещанием, он будет возвращен напрямую.
  2. Параметр передается как объект, который можно использовать, и возвращаемый Promise будет следовать за этим объектом и использовать его конечное состояние как собственное состояние.
  3. В других случаях объект обещания со статусом успеха возвращается напрямую.
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. Его аргумент является пустым, что затем напрямую разрешается.
  2. Если один из аргументов не работает, объект обещания, возвращаемый Promise.all, не работает.
  3. В любом случае результат выполнения обещания, возвращаемый Promise.all, представляет собой массив.
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.race

Promise.raceКак только одно обещание выполнено, оно разрешается напрямую и прекращает выполнение.

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

❤️ смотри здесь (* ̄︶ ̄)

Если вы считаете, что этот контент вас очень вдохновляет, не забудьте поставить лайк, чтобы этот контент увидело больше людей, пожалуйста, это действительно важно для меня.

Прошлые выборы:

Сборник рукописного кода "Intermediate and Advanced Front-end Interview" (2)

Подробное описание процесса рукопожатия Byte Interview-TLS

использованная литература

JavaScript рукописный код непобедимые читы

Стандартные встроенные объекты JavaScript

Серия вопросов о родной JS Soul

Как написать глубокий текст, который удивит интервьюера

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