Доброго времени суток, друзья!

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

Итак, поехали.

1. Определение специального типа объекта


Как все мы знаем, в JS существует 5 примитивных типов данных и объекты. Но знаете ли вы, что объекты также делятся на своего рода типы. Объект может быть массивом, функцией, картой (map) и т.д. Как же нам получить «специальный тип» объекта?

Код:

function toRawType(value) {
    let _toString = Object.prototype.toString

    let str = _toString.call(value)

    let res = str.slice(8, -1)
    
    console.log(res)
    
    return res
}

Объяснение:

ECMAScript содержит следующие правила:

19.1.3.6 Object.prototype.toString

Когда вызывается метод toString, выполняются следующие шаги:

  1. Если значением this является undefined, вернуть "[object Undefined]"
  2. Если значением this является null, вернуть "[object Null]"
  3. Пусть O будет ! ToObject(значение this)
  4. Пусть isArray будет ? isArray(O)
  5. Если isArray есть true, пусть builtinTag будет "Array"
  6. Иначе если O есть необычный объект String, пусть builtinTag будет "String"
  7. Иначе если O имеет внутренний слот [[ParameterMap]], пусть builtinTag будет "Arguments"
  8. Иначе если O имеет внутренний метод [[Call]], пусть builtinTag будет "Function"
  9. Иначе если O имеет внутренний слот [[ErrorData]], пусть builtinTag будет "Error"
  10. Иначе если O имеет внутренний слот [[BooleanData]], пусть builtinTag будет "Boolean"
  11. Иначе если O имеет внутренний слот [[NumberData]], пусть builtinTag будет "Number"
  12. Иначе если О имеет внутренний слот [[DateValue]], пусть builtinTag будет "Date"
  13. Иначе если О имеет внутренний слот [[RegExpMatcher]], пусть builtinTag будет "RegExp"
  14. Иначе пусть builtinTag будет "Object"
  15. Пусть tag будет ? Get(O, @@toStringTag)
  16. Если Type(tag) не является String, пусть tag будет builtinTag.
  17. Вернуть конкатенацию строк "[object ", tag и "]"

О том, как читать спецификацию, см. здесь.



Для разных объектов вызов Object.prototype.toString() возвращает разные результаты.



Значение, возвращаемое Object.prototype.toString(), всегда имеет формат '[object ' + 'tag' + ']'. Для того, чтобы получить tag, можно использовать регулярное выражение или String.prototype.slice().

Примеры:

toRawType(null) // Null

toRawType(/sdfsd/) // RegExp

toRawType(() => {}) // Function

toRawType([1, a, {}]) // Array

2. Кэширование результатов выполнения функции


Допустим, у нас есть такая функция:

function computed(str) {
    // предположим, что расчеты, производимые функцией, занимают очень много времени
    console.log('прошло 2000 секунд')

    return 'result'
}

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

Код:

function cached(fn) {
    // создаем объект для хранения результатов, возвращаемых функцией при каждом выполнении
    const cache = Object.create(null)

    // возвращаем обернутую функцию
    return function cachedFn(str) {
        // если кэш не содержит результата, функция выполняется
        if (!cache[str]) {
            let result = fn(str)

            // сохраняем результат выполнения функции в кэше
            cache[str] = result
        }
        return cache[str]
    }
}

Пример:



3. Array.prototype.map


Это полезный встроенный метод, но сможете ли вы написать его самостоятельно?

Код:

const selfMap = function(fn, context) {
    let arr = Array.prototype.slice.call(this)
    let mappedArr = Array()
    for (let i = 0; i < arr.length; i++) {
        if (!arr.hasOwnProperty(i)) continue
        mappedArr[i] = fn.call(context, arr[i], i, this)
    }
    return mappedArr
}

Array.prototype.selfMap = selfMap

Пример:



4. Array.prototype.filter


Код:

const selfFilter = function(fn, context) {
    let arr = Array.prototype.slice.call(this)
    let filteredArr = []
    for (let i = 0; i < arr.length; i++) {
        if (!arr.hasOwnProperty(i)) continue
        fn.call(context, arr[i], i, this) && filteredArr.push(arr[i])
    }
    return filteredArr
}

Array.prototype.selfFilter = selfFilter

Пример:



5. Array.prototype.some


Код:

const selfSome = function(fn, context) {
    let arr = Array.prototype.slice.call(this)
    if (!arr.length) return false
    for (let i = 0; i < arr.length; i++) {
        if (!arr.hasOwnProperty(i)) continue
        let res = fn.call(context, arr[i], i, this)
        if (res) return true
    }
    return false
}

Array.prototype.selfSome = selfSome

Пример:



6. Array.prototype.reduce


Код:

const selfReduce = function(fn, initialValue) {
    let arr = Array.prototype.slice.call(this)
    let res
    let startIndex
    if (initialValue === undefined) {
        for (let i = 0; i < arr.length; i++) {
            if (!arr.hasOwnProperty(i)) continue
            startIndex = i
            res = arr[i]
            break
        }
    } else {
        res = initialValue
    }
    for (let i = ++startIndex || 0; i < arr.length; i++){
        if(!arr.hasOwnProperty(i)) continue
        res = fn.call(null, res, arr[i], i, this)
    }
    return res
}

Array.prototype.selfReduce = selfReduce

Пример:



Мне не очень нравится пример, приводимый автором, поэтому я приведу свой:

let arr = [1, 2, 3, 4, 5]

arr.reduce((a, b) => a + b) // 15

arr.selfReduce((a, b) => a + b) // 15

7. Array.prototype.flat


Код:

const selfFlat = function(depth = 1){
    let arr = Array.prototype.slice.call(this)
    if(depth === 0) return arr
    return arr.reduce((pre, cur) => {
        if(Array.isArray(cur)){
            return [...pre, ...selfFlat.call(cur, depth - 1)]
        } else {
            return [...pre, cur]
        }
    }, [])
}

Array.prototype.selfFlat = selfFlat

Пример:



8. Каррирование (Curry)


Каррирование — это преобразование функции с несколькими аргументами в последовательность функций с одним аргументом: f(x, y, z) --> f(x)(y)(z).

Другими словами, после каррирования функция, которая принимала несколько параметров, принимает первый параметр и возвращает новую функцию, принимающую второй параметр и возвращающую новую функцию, принимающую третий параметр и т.д.

Какая от этого польза?

  • Позволяет избежать повторной передачи переменной.
  • Помогает создавать функции высшего порядка (декораторы). Это очень полезно в обработчиках событий.
  • Можно настраивать и повторно использовать отдельные части функций (частичное применение).

Давайте взглянем на простую функцию «add». Она принимает три операнда и возвращает их сумму:

function add(a,b,c){
    return a + b + c
}

Вы не можете вызвать функцию «add» с меньшим или большим количеством аргументов:

add(1,2,3) --> 6
add(1,2) --> NaN
add(1,2,3,4) --> 6 // лишние параметры игнорируются

Как из обычной функции сделать «каррированную»?

Код:

function carry(fn){
    if(fn.length <= 1) return fn
    const generator = (...args) => {
        if(fn.length === args.length){
            return fn(...args)
        } else{
            return(...args2) => {
                return generator(...args, ...args2)
            }
        }
    }
    return generator
}

Пример:



Подробнее о каррировании можно почитать здесь.

Debouncing


Debouncing — это функция-декоратор, позволяющая ограничить количество вызовов функции в определенный промежуток времени. Эту функцию можно рассматривать как своего рода задержку выполнения функции. При этом будет выполнена только та функция, которая в установленный временной интервал была вызвана последней. Существует несколько ситуаций, когда debouncing используется для повышения производительности браузера. Например, рассмотрим поиск на сайте.

Предположим, пользователь хочет получить «учебный набор Tutorix». Он вводит каждую букву в поисковой строке. После ввода каждой буквы API посылает запрос к серверу о наличии товара (автозаполнитель). Таким образом, пока мы вводим «учебный комплект Tutorix», браузер обратится к серверу 24 раза (если учитывать пробелы). Теперь представьте, что поиском одновременно пользуется несколько человек. Это приведет к росту количества запросов на сервер, что, в свою очередь, приведет к снижению производительности браузера. Для решения этой проблемы и предназначена debouncing.

В данном случае debouncing устанавливает определенный временной интервал, скажем, 2 секунды, между двумя вызовами API. В течение этих 2 секунд пользователь может печатать символы. По истечении 2 секунд будет вызван API. Обратите внимание, что API будет вызван только для последней строчки, состоящей из введенных пользователем символов.

Код:

const debounce = (func, time = 17, options = {
    leading: true,
    context: null
}) => {
    let timer
    const _debounce = function(...args){
        if(timer){
            clearTimeout(timer)
        }
        if(options.leading && !timer){
            timer = setTimeout(null, time)
            func.apply(options.context, args)
        } else{
            timer = setTimeout(() => {
                func.apply(options.context, args)
                timer = null
            }, time)
        }
    }

    _debounce.cancel = function(){
        clearTimeout(timer)
        timer = null
    }
    return _debounce
}

10. Throttling


Throttling — это функция-декоратор, позволяющая вызывать функцию только один раз в определенный промежуток времени. Например, мы можем сделать так, чтобы функция вызывалась один раз в секунду, независимо от количество нажатий кнопки пользователем.



Код:

const throttle = (func, time = 17, options = {
    leading: true,
    trailing: false,
    context: null
}) => {
    let previous = new Date(0).getTime()
    let timer
    const _throttle = function(...args){
        let now = new Date().getTime()

        if(!options.leading){
            if(timer) return
            timer = setTimeout(() => {
                timer = null
                func.apply(options.context, args)
            }, time)
        } else if(now - previous > time){
            func.apply(options.context, args)
            previous = now
        } else if(options.trailing){
            clearTimeout(timer)
            timer = setTimeout(() => {
                func.apply(options.context, args)
            }, time)
        }
    }
    _throttle.cancel = () => {
        previous = 0
        clearTimeout(timer)
        timer = null
    }
    return _throttle
}

Подробнее о debouncing и throttling можно почитать здесь.

11. Ленивая загрузка изображений


Ленивая загрузка означает асинхронную загрузку изображений — после полной загрузки предшествующего содержимого или после «попадания» изображения в область просмотра. Это означает, что изображение, находящееся за пределами области просмотра, загружаться не будет.

Код:

// getBoundingClientRect
let imgList1 = [...document.querySelectorAll('.get_bounding_rect')]
let num = imgList1.length

let lazyLoad1 = (function(){
    let count = 0
    return function(){
        let deleteIndexList = []
        imgList1.forEach((img, index) => {
            let rect = img.getBoundingClientRect()
            if(rect.top < window.innerHeight){
                img.src = img.dataset.src
                // добавляем изображение в список на удаление после успешной загрузки
                deleteIndexList.push(index)
                count++
                if(count === num){
                    // когда все изображения загружены, "отвязываем" событие scroll
                    document.removeEventListener('scroll', lazyLoad1)
                }
            }
        })
        // удаляем загруженные изображения
        imgList1 = imgList1.filter((_, index) => !deleteIndexList.includes(index))
    }
})()

12. Перемешивание элементов массива


Иногда возникает необходимость перетасовать элементы массива.

Код:

// случайным образом выбираем один из элементов, следующих после текущего элемента, для того, чтобы поменять его местами с текущим элементом
function shuffle(arr){
    for(let i=0; i<arr.length; i++){
        let randomIndex = i + Math.floor(Math.random() * (arr.length - i));
        [arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]]
    }
    return arr
}

// генерируем новый массив, случайным образом выбираем элемент из оригинального массива и помещаем его в новый массив
function shuffle2(arr){
    let _arr = []
    while(arr.length){
        let randomIndex = Math.floor(Math.random() * (arr.length))
        _arr.push(arr.slice(randomIndex, 1)[0])
    }
    return arr
}

Пример:



13. Синглтон (Singleton)


Шаблон проектирования «Синглтон» ограничивает конкретный объект одним экземпляром (т.е. можно создать лишь один экземпляр определенного объекта). Этот единственный экземпляр и называется синглтоном.

Синглтоны полезны, когда необходима координация широкого спектра действий из единого центра. Примером такого центра является пул соединений с базой данных. Пул управляет созданием, уничтожением и временем существования соединений с базой данных всех приложений с гарантией того, что ни одно соединение не будет «потеряно».

Синглтоны уменьшают количество глобальных переменных, что очень важно в JS, поскольку большое количество таких переменных засоряет глобальное пространство имен и может привести к коллизиям имен переменных.

Код:

function proxy(func){
    let instance
    let handler = {
        construct(target, args){
            if(!instance){
                // создаем экземпляр, если его не существует
                instance = Reflect.construct(func, args)
            }
            return instance
        }
    }
    return new Proxy(func, handler)
}

// пример
function Person(name, age){
    this.name = name
    this.age = age
}

const SingletonPerson = proxy(Person)

let person1 = new SingletonPerson('Jekyll', 34)

let person2 = new SingletonPerson('Hyde', 43)

console.log(person1 === person2) // true

Пример:



Подробнее о синглтоне можно почитать здесь.

14. JSON.stringify


Это полезный встроенный метод, но сможете ли вы написать его самостоятельно?

Код:

const isString = value => typeof value === 'string'
const isSymbol = value => typeof value === 'symbol'
const isUndefined = value => typeof value === 'undefined'
const isDate = obj => Object.prototype.toString.call(obj) === '[object Date]'
const isFunction = obj => Object.prototype.toString.call(obj) === '[object Function]'
const isComplexDataType = value => (typeof value === 'object' || typeof value === 'function') && value !== null
const isValidBasicDataType = value => value !== undefined && !isSymbol(value)
const isValidObj = obj => Array.isArray(obj) || Object.prototype.toString.call(obj) === '[object Object]'
const isInfinity = value => value === Infinity || value === -Infinity

// symbol, undefined и function в массиве превратятся в null
// Infinity и NaN также станут null
const processSpecialValueInArray = value => isSymbol(value) || isFunction(value) || isUndefined(value) || isInfinity(value) || isNaN(value) ? null : value

// обрабатываем значения свойств в соответствии со спецификацией JSON
const processValue = value => {
    if(isInfinity(value) || isNaN(value)){
        return null
    }
    if(isString(value)) return `"${value}"`
    return value
}

// obj.loop = obj
const jsonStringify = (function(){
    // замыкание + WeakMap предотвращают замкнутые ссылки
    let wm = new WeakMap()

    // это функция в замыкании, которая рекурсивно вызывает jsonstringify, а не функциональное выражение, объявленное с помощью const
    return function jsonStringify(obj){
        if(wm.get(obj)) throw new TypeError('Попытка преобразования замкнутой структуры в JSON')
        let res = ''

        if(isComplexDataType(obj)){
            if(obj.toJSON) return obj.toJSON
            if(!isValidObj(obj)) return
            wm.set(obj, obj)

            if(Array.isArray(obj)){
                res += '['
                let temp = []
                obj.forEach(value => {
                    temp.push(
                        isComplexDataType(value) && !isFunction(value) ?
                        jsonStringify(value) :
                        `${processSpecialValueInArray(value, true)}`
                    )
                })
                res += `${temp.join(',')}]`
            } else{
                res += '{'
                let temp = []
                Object.keys(obj).forEach(key => {
                    if(isComplexDataType(obj[key])){
                        if(isValidObj(obj[key])){
                            temp.push(`"${key}":${jsonStringify(obj[key])}`)
                        } else if(isDate(obj[key])){
                            temp.push(`"${key}":"${obj[key].toISOString()}"`)
                        } else if(!isFunction(obj[key])){
                            temp.push(`"${key}":{}`)
                        }
                    } else if(isValidBasicDataType(obj[key])){
                        temp.push(`"${key}":${processValue(obj[key])}`)
                    }
                })
                res += `${temp.join(',')}}`
            }
        } else if(isSymbol(obj)){
            return
        } else{
            return obj
        }
        return res
    }
})()

// пример
let s = Symbol('s')
let obj = {
    str: '123',
    arr: [1, {b: 1}, s, () => {}, undefined, Infinity, NaN],
    obj: {a: 1},
    isInfinity: -Infinity,
    nan: NaN,
    undef: undefined,
    symbol: s,
    date: new Date(),
    reg: /123/g,
    func: () => {},
    dom: document.querySelector('body'),
}
console.log(jsonStringify(obj))
console.log(JSON.stringify(obj))

Пример:



Благодарю за внимание. Счастливого кодинга!