Технические моменты: несколько распространенных функций высокого порядка в JavaScript и сценарии их применения.

Ленивые функции JavaScript

нуждаться

Теперь нам нужно написать функцию foo, которая возвращает объект Date при первом вызове.Обратите внимание, что это первый раз.

Решение 1: Обычный метод

let t
function foo() {
    
    
  if (t) return t
  t = new Date()
  return t
}

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

Решение 2: Закрытие

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

let foo = (function () {
    
    
  let t
  return function () {
    
    
    if (t) return t
    t = new Date()
    return t
  }
})()

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

Решение 3. Функциональный объект

Функция также является объектом.Используя эту функцию, мы также можем решить эту проблему.

function foo() {
    
    
  if (foo.t) return foo.t
  foo.t = new Date()
  return foo.t
}

Это по-прежнему не решает проблему необходимости выносить суждение каждый раз, когда его вызывают.

Решение 4. Ленивая функция

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

let foo = function () {
    
    
  let t = new Date()
  foo = function () {
    
    
    return t
  }
  return foo()
}

Сценарии применения

События DOM добавляются. Чтобы обеспечить совместимость с современными браузерами и браузерами IE, нам необходимо принять решение о среде браузера:

// 简化写法
function addEvent(type, el, fn) {
    
    
  if (window.addEventListener) {
    
    
    el.addEventListener(type, fn, false)
  } else if (window.attachEvent) {
    
    
    el.attachEvent('on' + type, fn)
  }
}

Проблема в том, что мы выносим суждение каждый раз, когда используем addEvent.

Используя ленивые функции, мы можем сделать это:

function addEvent(type, el, fn) {
    
    
  if (window.addEventListener) {
    
    
    addEvent = function (type, el, fn) {
    
    
      el.addEventListener(type, fn, false)
    }
  } else if (window.attachEvent) {
    
    
    addEvent = function (type, el, fn) {
    
    
      el.attachEvent('on' + type, fn)
    }
  }
}

Конечно, мы также можем использовать замыкания:

let addEvent = (function () {
    
    
  if (window.addEventListener) {
    
    
    return function (type, el, fn) {
    
    
      el.addEventListener(type, fn, false)
    }
  } else if (window.attachEvent) {
    
    
    return function (type, el, fn) {
    
    
      el.attachEvent('on' + type, fn)
    }
  }
})()

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

Частичные функции JavaScript

определение

Определение частичной функции (частичное применение) в Википедии:

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

перевести на китайский:

В информатике локальное применение означает фиксацию некоторых параметров функции и последующую генерацию другой функции с меньшими элементами.

Что такое юань? Юань относится к количеству параметров функции.Например, функция с двумя параметрами называется двоичной функцией.

Приведите простой пример:

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

// 执行 add 函数,一次传入两个参数即可
add(1, 2) // 3

// 假设有一个 partial 函数可以做到局部应用
let addOne = partial(add, 1)

addOne(2) // 3

Каррирование и локальное применение

Если вы видели функцию каррирования, вы обнаружите, что этот пример очень похож на каррирование, так в чем же разница между ними?

На самом деле, это также очень очевидно:

Каррирование — это преобразование многопараметрической функции в несколько однопараметрических функций, то есть преобразование n-арной функции в n унарных функций.

Локальное применение заключается в фиксации одного или нескольких параметров функции, то есть в преобразовании n-арной функции в n-x-арную функцию.

Сценарии применения

Например, при инкапсуляции axios используется частичная функция. Вы можете передать адрес интерфейса перед вызовом, чтобы подготовиться, а затем передавать параметры только при вызове.

// http.ts
import Axios from 'axios'

const client = Axios.create({
    
    
  baseURL: '/api',
  validateStatus(status) {
    
    
    return status < 500
  },
})

export const http = {
    
    
  get: (url: string) => {
    
    
    const requestFn = (params?: object, config?: object): any =>
      client.get(url, {
    
     params, ...config })
    requestFn.path = url
    return requestFn
  },
  delete: (url: string) => {
    
    
    const requestFn = (params?: object, config?: object): any =>
      client.delete(url, {
    
     params, ...config })
    requestFn.path = url
    return requestFn
  },
  post: (url: string) => {
    
    
    const requestFn = (data: object, config?: object): any => client.post(url, data, config)
    requestFn.path = url
    return requestFn
  },
  put: (url: string) => {
    
    
    const requestFn = (data: object, config?: object): any => client.put(url, data, config)
    requestFn.path = url
    return requestFn
  },
}
// api.ts
import {
    
     http } from './http'
const {
    
     get, post, put,delete } = http

export const personApi = {
    
    
  usertoken: get('/auth/v1/user/token'), // 用户个人详情
  editName: put('/auth/v1/user/username'),
  editTel: put('/auth/v1/user/tel'),
  editPass: put('/auth/v1/user/password'),
  editEmail: put('/auth/v1/user/bind-email'),
  checkTel: post('/auth/v1/user/check-unbind'),
  checkEmail: post('/auth/v1/user/check-mail'),
  sendMail: post('/auth/v1/user/send-mail')
}
import {
    
     http } from './api'

const res = usertoken({
    
     params1: 'xxx', params2: 'xxx' })

Функциональная память JavaScript

определение

Функциональная память относится к кэшированию последнего результата расчета.При следующем вызове, если встречаются те же параметры, данные в кеше будут возвращены напрямую.

например:

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

// 假设 memorize 可以实现函数记忆
let memoizedAdd = memorize(add)

memoizedAdd(1, 2) // 3
memoizedAdd(1, 2) // 相同的参数,第二次调用时,从缓存中取出数据,而非重新计算一次

принцип

Реализовать такую ​​функцию запоминания очень просто.В принципе, вам нужно только сохранить параметры и соответствующие данные результата в объекте.При вызове определить, существуют ли данные, соответствующие параметрам, и вернуть соответствующие данные результата, если это существует.

первое издание

Напишем версию:

// 第一版 (来自《JavaScript权威指南》)
function memoize(f) {
    
    
  let cache = {
    
    }
  return function () {
    
    
    let key = arguments.length + Array.prototype.join.call(arguments, ',')
    if (key in cache) {
    
    
      return cache[key]
    } else return (cache[key] = f.apply(this, arguments))
  }
}

Давайте проверим это:

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

let memoizedAdd = memorize(add)

console.time('use memorize')
for (let i = 0; i < 100000; i++) {
    
    
  memoizedAdd(1, 2, 3)
}
console.timeEnd('use memorize')

console.time('not use memorize')
for (let i = 0; i < 100000; i++) {
    
    
  add(1, 2, 3)
}
console.timeEnd('not use memorize')

В Chrome использование memorize занимает около 60 мс. Если мы не используем функциональную память, это займет около 1,3 мс.

Уведомление

Что, мы использовали, казалось бы, продвинутую функцию памяти, но она оказалась более трудоемкой, почти в 60 раз в этом примере!

Следовательно, функциональная память не всесильна.Если вы посмотрите на этот простой сценарий, то увидите, что она не подходит для функциональной памяти.

Следует отметить, что функциональная память — это всего лишь метод программирования, который по существу жертвует пространственной сложностью алгоритма в обмен на лучшую временную сложность.В клиентском JavaScript сложность времени выполнения кода часто становится узким местом, поэтому в большинстве В этом сценарии такой подход, заключающийся в жертвовании пространством ради времени для повышения эффективности выполнения программы, очень желателен.

второе издание

Поскольку первая версия использовала метод join, мы можем легко предположить, что, когда параметр является объектом, он автоматически вызывает метод toString для его преобразования, [Object object]а затем объединяет строку в качестве значения ключа. Давайте напишем демо, чтобы проверить эту проблему:

let propValue = function (obj) {
    
    
  return obj.value
}

let memoizedAdd = memorize(propValue)

console.log(memoizedAdd({
    
     value: 1 })) // 1
console.log(memoizedAdd({
    
     value: 2 })) // 1

Оба вернули 1, что, очевидно, является проблемой, поэтому давайте посмотрим, как реализована функция memoize в подчеркивании:

// 第二版 (来自 underscore 的实现)
let memorize = function (func, hasher) {
    
    
  let memoize = function (key) {
    
    
    let cache = memoize.cache
    let address = '' + (hasher ? hasher.apply(this, arguments) : key)
    if (!cache[address]) {
    
    
      cache[address] = func.apply(this, arguments)
    }
    return cache[address]
  }
  memoize.cache = {
    
    }
  return memoize
}

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

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

let memoizedAdd = memorize(add)

memoizedAdd(1, 2, 3) // 6
memoizedAdd(1, 2, 4) // 6

Должна быть проблема: если мы хотим поддерживать несколько параметров, нам нужно передать хэш-функцию и настроить сохраненное значение ключа. Поэтому мы рассматриваем возможность использования JSON.stringify:

let memoizedAdd = memorize(add, function () {
    
    
  let args = Array.prototype.slice.call(arguments)
  return JSON.stringify(args)
})

console.log(memoizedAdd(1, 2, 3)) // 6
console.log(memoizedAdd(1, 2, 4)) // 7

Если вы используете JSON.stringify, проблема, связанная с тем, что параметр является объектом, также может быть решена, поскольку сохраняется сериализованная строка объекта.

Применимая сцена

В качестве примера возьмем последовательность Фибоначчи:

let count = 0
let fibonacci = function (n) {
    
    
  count++
  return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2)
}
for (let i = 0; i <= 10; i++) {
    
    
  fibonacci(i)
}

console.log(count) // 453

Мы обнаружим, что окончательный счет равен 453, а это означает, что функция Фибоначчи вызывалась 453 раза! Возможно, вы думаете, я только что зациклился на 10, почему это вызывалось так много раз, поэтому давайте разберем это подробно:

当执行 fib(0) 时,调用 1

当执行 fib(1) 时,调用 1

当执行 fib(2) 时,相当于 fib(1) + fib(0) 加上 fib(2) 本身这一次,共 1 + 1 + 1 = 3

当执行 fib(3) 时,相当于 fib(2) + fib(1) 加上 fib(3) 本身这一次,共 3 + 1 + 1 = 5

当执行 fib(4) 时,相当于 fib(3) + fib(2) 加上 fib(4) 本身这一次,共 5 + 3 + 1 = 9

当执行 fib(5) 时,相当于 fib(4) + fib(3) 加上 fib(5) 本身这一次,共 9 + 5 + 1 = 15

当执行 fib(6) 时,相当于 fib(5) + fib(4) 加上 fib(6) 本身这一次,共 15 + 9 + 1 = 25

当执行 fib(7) 时,相当于 fib(6) + fib(5) 加上 fib(7) 本身这一次,共 25 + 15 + 1 = 41

当执行 fib(8) 时,相当于 fib(7) + fib(6) 加上 fib(8) 本身这一次,共 41 + 25 + 1 = 67

当执行 fib(9) 时,相当于 fib(8) + fib(7) 加上 fib(9) 本身这一次,共 67 + 41 + 1 = 109

当执行 fib(10) 时,相当于 fib(9) + fib(8) 加上 fib(10) 本身这一次,共 109 + 67 + 1 = 177

Значит общее количество казней равно: 177+109+67+41+25+15+9+5+3+1+1=453 раза!

Что, если мы воспользуемся функциональной памятью?

let count = 0
let fibonacci = function (n) {
    
    
  count++
  return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2)
}

fibonacci = memorize(fibonacci)

for (let i = 0; i <= 10; i++) {
    
    
  fibonacci(i)
}

console.log(count) // 12

Обнаружим, что итоговое общее количество раз равно 12. За счет использования памяти функций количество вызовов сокращается с 453 раз до 12 раз!

Пока вы взволнованы, не забудьте подумать: а почему 12 раз?

Результаты от 0 до 10 сохраняются по одному разу.Это должно быть 11 раз? Эй, откуда взялось это дополнительное время?

Поэтому нам нужно внимательно посмотреть на наш метод записи. В нашем методе записи мы фактически перезаписываем исходную функцию Фибоначчи сгенерированной функцией Фибоначчи. Когда мы выполняем fibonacci(0), функция выполняется один раз, а кеш равен {0: 0 }, но когда мы выполняем fibonacci(2), мы выполняем fibonacci(1) + fibonacci(0).Поскольку значение fibonacci(0) равно 0 и результат !cache[address]истинен, функция фибоначчи будет выполнена снова. Оказывается, дополнительное время здесь!

Скажи еще одну вещь

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

Guess you like

Origin blog.csdn.net/Big_YiFeng/article/details/128563457