Render-функции | Vue.js

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

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

<h2>
  <a name="hello-world" href="#hello-world">
    Hello world!
  </a>
</h2>

1
2
3
4
5

Такие заголовки будут использоваться часто, поэтому сразу стоит создать компонент:

<anchored-heading :level="1">Hello world!</anchored-heading>

1

Компонент должен генерировать заголовок, в зависимости от входного параметра level, что скорее всего приведёт к такому решению:

const { createApp } = Vue
const app = createApp({})
app.component('anchored-heading', {
  template: `
    <h2 v-if="level === 1">
      <slot></slot>
    </h2>
    <h3 v-else-if="level === 2">
      <slot></slot>
    </h3>
    <h4 v-else-if="level === 3">
      <slot></slot>
    </h4>
    <h5 v-else-if="level === 4">
      <slot></slot>
    </h5>
    <h5 v-else-if="level === 5">
      <slot></slot>
    </h5>
    <h6 v-else-if="level === 6">
      <slot></slot>
    </h6>
  `,
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

21
22
23
24
25
26
27
28
29
30
31
32

Шаблон такого компонента выглядит не очень. Он не только многословен, но и дублирует <slot></slot> для каждого уровня заголовка. А при добавлении нового элемента якоря, потребуется снова дублировать его в каждой ветке v-if/v-else-if.

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

const { createApp, h } = Vue
const app = createApp({})
app.component('anchored-heading', {
  render() {
    return h(
      'h' + this.level, // имя тега
      {}, // входные параметры/атрибуты
      this.$slots.default() // массив дочерних элементов
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

Реализация с функцией render() получилась гораздо проще, но требует больше знаний о свойствах экземпляра компонента. Для этого примера потребуется знать, что при передаче дочерних элементов в компонент без директивы v-slot, например Hello world!

внутрь anchored-heading, они будут доступны в экземпляре компонента через $slots.default(). Если это ещё непонятно, рекомендуем сначала прочитать раздел API свойств экземпляра перед углублением в render-функции.

DOM-дерево

Прежде чем погрузиться в изучение render-функций, важно немного подробнее разобраться в том, как работают браузеры. Возьмём, к примеру, этот HTML:

<div>
  <h2>My title</h2>
  Some text content
  <!-- TODO: Add tagline -->
</div>

1
2
3
4
5

Когда браузер читает этот код, он строит дерево «DOM узлов» (opens new window), чтобы помочь себе отслеживать всё.

Для HTML из примера выше дерево DOM-узлов получится таким:

Каждый элемент является узлом. Каждый текст является узлом. Каждый комментарий является узлом! Каждый узел может иметь дочерние элементы (т.е. каждый узел может содержать другие узлы).

Эффективно обновлять все эти узлы — непростая задача, но, к счастью, это не потребуется делать вручную. Требуется лишь сообщать Vue какой HTML нужен на странице в шаблоне:

<h2>{{ blogTitle }}</h2>

1

Или в render-функции:

render() {
  return h('h2', {}, this.blogTitle)
}

1
2
3

В обоих случаях Vue будет автоматически поддерживать страницу в обновлённом состоянии, даже при изменениях значения

blogTitle.

Виртуальное DOM-дерево

Vue поддерживает страницу в обновлённом состоянии с помощью виртуального DOM. Он помогает определить изменения, которые необходимо внести в реальный DOM. Взглянем внимательнее на эту строку:

return h('h2', {}, this.blogTitle)

1

Что вернёт функция h()? Это не совсем настоящий DOM-элемент. Возвращается обычный объект с информацией для Vue, какой узел должен отобразиться на странице, в том числе описание любых дочерних элементов. Это описание называют «виртуальным узлом» или «виртуальной нодой», обычно сокращая до

VNode. «Виртуальным DOM» можно назвать всё дерево из VNode, созданных по дереву компонентов Vue.

Аргументы

h()

Функция h() — утилита для создания VNode. Возможно её стоило назвать createVNode() для точности, но она называется h() из-за частого использования и для краткости. Функция принимает три аргумента:

// @returns {VNode}
h(
  // {String | Object | Function} тег
  // Имя HTML-тега, компонента, асинхронного или функционального компонента.
  // Использование функции, возвращающей null, будет отрисовывать комментарий.
  //
  // Обязательный параметр
  'div',
  // {Object} входные параметры
  // Объект, соответствующий атрибутам, входным параметрам
  // и событиям, которые использовались бы в шаблоне.
// // Опциональный {}, // {String | Array | Object} дочерние элементы // Дочерние VNode, созданные с помощью `h()`, // или строки для получения 'текстовых VNode' или // объект со слотами. // // Опциональный [ 'Какой-то текст в начале.', h('h2', 'Заголовок'), h(MyComponent, { someProp: 'foobar' }) ] )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

19
20
21
22
23
24
25
26
27
28
29
30

Если входных параметров нет, то дочерние элементы можно передать вторым аргументом. В случаях, если это может добавить путаницы, можно указывать null вторым аргументом, чтобы третьим явно передавать дочерние элементы.

Полный пример

С полученными знаниями можно теперь завершить начатый компонент:

const { createApp, h } = Vue
const app = createApp({})
/** Рекурсивно получаем текст из дочерних узлов */
function getChildrenTextContent(children) {
  return children
    .
-|-$)/g, '') // удаляем в начале и конце висящие тире return h('h' + this.level, [ h( 'a', { name: headingId, href: '#' + headingId }, this.$slots.default() ) ]) }, props: { level: { type: Number, required: true } } })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

Ограничения

VNode должны быть уникальными

В дереве компонентов все VNode должны быть уникальными. Это означает, что следующая render-функция некорректна:

render() {
  const myParagraphVNode = h('p', 'hi')
  return h('div', [
    // НЕПРАВИЛЬНО - одинаковые VNode!
    myParagraphVNode, myParagraphVNode
  ])
}

1
2
3
4
5
6
7

Если требуется многократно дублировать один и тот же элемент/компонент, то реализовать это можно с помощью функции-фабрики. Например, следующая render-функция будет абсолютно корректным способом для отрисовки 20 одинаковых параграфов:

render() {
  return h('div',
    Array.from({ length: 20 }).map(() => {
      return h('p', 'hi')
    })
  )
}

1
2
3

4
5
6
7

Создание VNode компонентов

При создании VNode для компонента первым аргументом h должен быть сам компонент:

render() {
  return h(ButtonCounter)
}

1
2
3

Если необходимо разрешить компонент по имени, можно использовать resolveComponent:

const { h, resolveComponent } = Vue
// ...
render() {
  const ButtonCounter = resolveComponent('ButtonCounter')
  return h(ButtonCounter)
}

1
2
3
4
5
6
7
8

Функцию resolveComponent используют шаблоны под капотом для разрешения компонентов по имени.

В функции render обычно приходится использовать resolveComponent

для компонентов зарегистрированных глобально. При локальной регистрации компонентов обычно можно обойтись без неё. Рассмотрим следующий пример:

// Такой код можно упростить
components: {
  ButtonCounter
},
render() {
  return h(resolveComponent('ButtonCounter'))
}

1
2
3
4
5
6
7

Вместо регистрации компонента по имени, а затем поиска, можно использовать его сразу:

render() {
  return h(ButtonCounter)
}

1
2
3

Замена возможностей шаблона обычным JavaScript

v-if и v-for

Всё что используется можно легко реализовать на простом JavaScript, для render-функции Vue не создаёт никакой проприетарной альтернативы. Например, шаблон с v-if и v-for:

<ul v-if="items.length">
  <li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>Элементов не найдено.</p>

1
2
3
4

Можно переписать с использованием JavaScript if/else и map() в render-функции:

props: ['items'],
render() {
  if (this. items.length) {
    return h('ul', this.items.map((item) => {
      return h('li', item.name)
    }))
  } else {
    return h('p', 'Элементов не найдено.')
  }
}

1
2
3
4
5
6
7
8
9
10

В шаблоне иногда удобно использовать тег <template>, чтобы указать v-if или v-for. При миграции на использование render-функции тег <template> можно просто опустить.

v-model

На этапе компиляции шаблона директива v-model раскладывается на входные параметры modelValue и onUpdate:modelValue — их потребуется указать самостоятельно:

props: ['modelValue'],
emits: ['update:modelValue'],
render() {
  return h(SomeComponent, {
    modelValue: this.modelValue,
    'onUpdate:modelValue': value => this.$emit('update:modelValue', value)
  })
}

1
2
3
4
5
6
7
8

v-on

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

render() {
  return h('div', {
    onClick: $event => console.log('кликнули!', $event.target)
  })
}

1
2
3
4
5

Модификаторы событий

Модификаторы событий .passive, .capture и .once необходимо указывать после имени события в camelCase.

Например:

render() {
  return h('input', {
    onClickCapture: this.doThisInCapturingMode,
    onKeyupOnce: this.doThisOnce,
    onMouseoverOnceCapture: this.doThisOnceInCapturingMode
  })
}

1
2
3
4
5
6
7

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

Модификатор(ы)Эквивалент в обработчике
.stopevent.stopPropagation()
.preventevent.preventDefault()
. selfif (event.target !== event.currentTarget) return
Клавиши:
например, .enter
if (event.key !== 'Enter') return

Замените Enter на соответствующий key (opens new window)

Модификаторы клавиш:
.ctrl, .alt, .shift, .meta
if (!event.ctrlKey) return

Замените ctrlKey на altKey, shiftKey или metaKey

Пример обработчика со всеми этими модификаторами, используемыми вместе:

render() {
  return h('input', {
    onKeyUp: event => {
      // Отменяем обработку, если элемент вызвавший событие
      // не является элементом, к которому событие было привязано
      if (event.target !== event.currentTarget) return
      // Отменяем обработку, если код клавиши не соответствовал
      // enter и клавиша shift не была нажата в то же время
      if (!event. shiftKey || event.key !== 'Enter') return
      // Останавливаем всплытие события
      event.stopPropagation()
      // Останавливаем поведение по умолчанию для этого элемента
      event.preventDefault()
      // ...
    }
  })
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

Слоты

Доступ к содержимому слотов в виде массива VNode можно получить через this.$slots:

render() {
  // `<div><slot></slot></div>`
  return h('div', this.$slots.default())
}

1
2
3
4

props: ['message'],
render() {
  // `<div><slot :text="message"></slot></div>`
  return h('div', this.$slots.default({
    text: this.message
  }))
}

1
2
3
4
5
6
7

Для VNode компонента необходимо передать дочерние элементы в h в виде объекта, а не массива. Каждое свойство будет использовано для заполнения одноимённого слота:

render() {
  // `<div><child v-slot="props"><span>{{ props. text }}</span></child></div>`
  return h('div', [
    h(
      resolveComponent('child'),
      null,
      // передаём `slots` как дочерний объект в формате
      // { slotName: props => VNode | Array<VNode> }
      {
        default: (props) => h('span', props.text)
      }
    )
  ])
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14

Слоты передаются как функции, что позволяет дочернему компоненту управлять созданием содержимого каждого слота. Любые реактивные данные должны быть доступны внутри функции слота, чтобы гарантировать, что они зарегистрированы как зависимость дочернего компонента, а не родительского. И наоборот, обращения к resolveComponent нужно делать вне функции слота, иначе они будут разрешаться относительно неправильного компонента:

// `<MyButton><MyIcon :name="icon" />{{ text }}</MyButton>`
render() {
  // Вызовы resolveComponent должны находиться вне функции слота
  const Button = resolveComponent('MyButton')
  const Icon = resolveComponent('MyIcon')
  return h(
    Button,
    null,
    {
      // Используем стрелочную функцию для сохранения значения `this`
      default: (props) => {
        // Реактивные свойства должны считываться внутри функции слота,
        // чтобы они стали зависимостями для отрисовки дочернего компонента
        return [
          h(Icon, { name: this. icon }),
          this.text
        ]
      }
    }
  )
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

Если компонент получает слоты из родителя — их можно передать напрямую дочернему:

render() {
  return h(Panel, null, this.$slots)
}

1
2
3

Но можно также передавать их по-отдельности или оборачивать по необходимости:

render() {
  return h(
    Panel,
    null,
    {
      // Если хотим просто передать функцию слота
      header: this.$slots.header,
      // Если требуется как-то управлять слотом,
      // тогда нужно обернуть его в новую функцию
      default: (props) => {
        const children = this.$slots.default ? this.$slots.default(props) : []
        return children.concat(h('div', 'Extra child'))
      }
    }
  )
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

<component> и is

Шаблоны для реализации атрибута is используют под капотом resolveDynamicComponent. Можно воспользоваться этой же функцией, если в создаваемой render-функции требуется вся гибкость, предоставляемая is:

const { h, resolveDynamicComponent } = Vue
// ...
// `<component :is="name"></component>`
render() {
  const Component = resolveDynamicComponent(this.name)
  return h(Component)
}

1
2
3
4
5
6
7
8
9

Аналогично is, resolveDynamicComponent поддерживает передачу имени компонента, имени HTML-элемента или объекта с опциями компонента.

Но обычно такой уровень гибкости не требуется. Поэтому resolveDynamicComponent часто можно заменить на более конкретную альтернативу.

К примеру, если нужно поддерживать только имена компонентов — можно использовать resolveComponent.

Если VNode всегда будет HTML-элементом — можно передавать имя напрямую в h:

// `<component :is="bold ? 'strong' : 'em'"></component>`
render() {
  return h(this. bold ? 'strong' : 'em')
}

1
2
3
4

Аналогично, если передаваемое в is значение будет объектом опций компонента, то не нужно ничего разрешать и можно сразу передать его первым аргументом в h.

Подобно тегу <template>, тег <component> нужен в шаблонах только в качестве синтаксического сахара и его следует опустить при миграции на render-функции.

Пользовательские директивы

Пользовательские директивы можно применить к VNode с помощью withDirectives:

const { h, resolveDirective, withDirectives } = Vue
// ...
// <div v-pin:top.animate="200"></div>
render () {
  const pin = resolveDirective('pin')
  return withDirectives(h('div'), [
    [pin, 200, 'top', { animate: true }]
  ])
}

1
2
3
4
5
6
7
8
9

Функция resolveDirective используется в шаблонах под капотом, чтобы разрешить директиву по имени. Это нужно лишь в случаях, когда нет прямого доступа к объекту с объявлением директивы.

Встроенные компоненты

Встроенные компоненты, такие как <keep-alive>, <transition>, <transition-group> и <teleport> по умолчанию не регистрируются глобально. Это позволяет системе сборки выполнять tree-shaking, чтобы добавлять эти компоненты в сборку только в случае, если они используются. Но это также означает, что не выйдет получить к ним доступ с помощью resolveComponent или resolveDynamicComponent.

Шаблоны имеют специальную обработку для этих компонентов, автоматически импортируя их при использовании. Но при создании собственных render функций импортировать потребуется их самостоятельно:

const { h, KeepAlive, Teleport, Transition, TransitionGroup } = Vue
// ...
render () {
  return h(Transition, { mode: 'out-in' }, /* ... */)
}

1
2
3
4
5
6
7

Возвращаемые значения render-функций

Во всех примерах, рассматривавшихся ранее, функция render возвращала один корневой узел VNode. Но могут быть и другие варианты.

Если вернуть строку, то будет создана текстовая VNode без какого-либо элемента-обёртки:

render() {
  return 'Привет мир!'
}

1
2
3

Также можно вернуть массив дочерних узлов, не оборачивая их в корневой узел. В таком случае будет создан фрагмент:

// Аналогично шаблону `Пример<br>мир!`
render() {
  return [
    'Привет',
    h('br'),
    'мир!'
  ]
}

1
2
3
4
5
6
7
8

Если компоненту не нужно ничего отображать (например, потому что ещё загружаются данные), то можно просто вернуть null. Тогда в DOM будет создан узел комментария.

JSX

При создании множества render-функций может быть мучительно писать подобное:

h(
  resolveComponent('anchored-heading'),
  {
    level: 1
  },
  {
    default: () => [h('span', 'Привет'), ' мир!']
  }
)

1
2
3
4
5
6
7
8
9

Особенно, когда эквивалент в шаблоне выглядит очень лаконично:

<anchored-heading :level="1"> <span>Привет</span> мир! </anchored-heading>

1

Поэтому есть плагин для Babel (opens new window), чтобы использовать JSX во Vue и получить синтаксис, близкий к шаблонам:

import AnchoredHeading from '. /AnchoredHeading.vue'
const app = createApp({
  render() {
    return (
      <AnchoredHeading level={1}>
        <span>Привет</span> мир!
      </AnchoredHeading>
    )
  }
})
app.mount('#demo')

1
2
3
4
5
6
7
8
9
10
11
12
13

Подробнее о том, как JSX преобразуется в JavaScript смотрите в документации плагина (opens new window).

Функциональные компоненты

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

Для создания функционального компонента нужно использовать простую функцию, а не объект с опциями компонента. Она фактически является render-функцией компонента. И так как у функционального компонента нет this, то props будут передаваться первым аргументом:

const FunctionalComponent = (props, context) => {
  // . ..
}

1
2
3

Вторым аргументом передаётся context, содержащий три свойства: attrs, emit и slots. Они эквивалентны свойствам экземпляра $attrs, $emit и $slots.

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

FunctionalComponent.props = ['value']
FunctionalComponent.emits = ['click']

1
2

Если не указана опция props, то передаваемый в функцию объект props будет содержать все атрибуты, как и attrs. Также имена входных параметров не будут нормализоваться в camelCase.

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

Компиляция шаблона

Интересный факт, все шаблоны во Vue компилируются в render-функции. Обычно нет нужды знать такие детали реализации, но может быть любопытно увидеть как же компилируются те или иные возможности шаблона. Небольшая демонстрация ниже показывает работу метода Vue.compile при компиляции строковых шаблонов на лету, попробуйте сами:

Функция возвращает Функцию — efim360.ru

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

Мы поэтапно дойдём до понимания сути вопроса «ЗАЧЕМ ЭТО НУЖНО?»

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

  1. Функция может быть ОБЪЯВЛЕНА
  2. Функция может быть ВЫЗВАНА

ОБЪЯВЛЕННАЯ функция по сути хранит инструкцию о действиях, которые нужно будет совершить в момент её вызова. ВЫЗВАННАЯ функция находит инструкцию с нужными действиями и выполняет их строго по списку.

Заметьте, что у Строк или Чисел очень сложно представить какое-либо иное состояние. Как они выглядят, тем они и являются. Число 10 — это 10, а строка «Привет мир!» — это строка «Привет мир!». Строка или Число это и есть итоговые данные, которые можно получить при прямом обращении к ним. С такими примитивами не нужно ничего вычислять.

Т. к. функции имеют разные состояния, то они также могут быть возвращены в разных состояниях:

  1. Функция может быть возвращена в качестве ОБЪЯВЛЕННОЙ (нам вернётся инструкция)
  2. Функция может быть возвращена в качестве ВЫЗВАННОЙ (нам вернётся результат выполненной инструкции)

 

Видеоролик

 

1. Самый простой пример возврата функций в двух состояниях

1.

1 Функция возвращена в качестве ОБЪЯВЛЕННОЙ

Как это выглядит в простом примере?

function R(){
   return function G(){}
}

В этом примере функция R при вызове возвращает вложенную в неё ОБЪЯВЛЕННУЮ функцию G. Функция G НЕ ВЫЗВАНА, она отдана в качестве инструкции. Давайте посмотрим на скриншот из браузера:

Функция R вернула после вызова функцию G

В таком виде ВОЗВРАЩЁННАЯ ОБЪЯВЛЕННАЯ функция G не имеет абсолютно никакого смысла т. к. она ничего не делает. Но что мы поняли из этого примера?

Мы поняли то, что функция может возвращать не только результаты ВЫЗОВА других функций, но и САМИ ФУНКЦИИ БЕЗ ВЫЗОВА.

 

1.2 Функция возвращена в качестве ВЫЗВАННОЙ

function R(){
   return function G(){}()
}

Обратите внимание! В этих двух примерах отличается только пара круглых скобок () после объявления функции G. То есть во втором примере мы объявили функцию и тут же вызвали её. Что мы получим, когда вызовем функцию R ?

На этот раз мы получим undefined

Почему?

Потому что оператор return вернёт нам НЕ инструкцию функции G, а результат её вызова.

Функция R вернула после вызова результат вызова функции G — не инструкцию

 

2. Полезное применение — Самый простой счётчик — Код шаблона

Чтобы до конца понять смысл возврата функций в качестве инструкций, предлагаю написать свой собственный счётчик. Его стартовое значение будет равно нулю — 0. Каждый вызов функции-счётчика будет увеличивать его значение на единицу. Мы сделаем такой универсальный счётчик, чтобы иметь возможность вешать его на любые процессы в программе.

Предлагаю назвать нашу функцию counter. Выглядеть она будет так:

function counter() {
   let x = 0;
   return function generator() {
      x = x + 1;
      return x;
   };
}

Давайте внимательно посмотрим что в ней происходит.

Первое выражение:

let x = 0;

Это выражение поможет нам зафиксировать в функции counter стартовое значение счётчика равным нулю. Переменная x объявлена внутри тела функции counter — это значит, что переменная x не видна вне тела функции counter. То есть к переменной x нельзя обратиться из другой части программы напрямую. К переменной x можно обратиться только изнутри функции counter и всех её вложенных функциях (если таковые будут, а они будут).

Второе выражение:

return function generator() {
   x = x + 1;
   return x;
}

Оно говорит нам о том, что результатом вызова функции counter будет являться возврат ОБЪЯВЛЕННОЙ функции generator. (Не ВЫЗОВ, А ОБЪЯВЛЕНИЕ). То есть инструкция.

Внутри тела функции generator мы ОБРАЩАЕМСЯ к какому-то существующему x (икс) и у этого x мы меняем значение на единицу в положительную сторону. И в конце возвращаем сам x.

 

3. Как пользоваться нашим шаблоном счётчика?

Давайте создадим новую переменную, которая будет новым счётчиком.

let s1 = counter()

Переменной s1 мы присваиваем результат вызова функции counter. Что мы имеем в s1 ?

Создали новый счётчик s1

Если сейчас посмотреть на набор инструкций, которые хранит s1, то мы почему то не увидим объявление переменной x (икс) с присваиванием нулевого значения. Куда оно делось? Исчезло? Нет.

В тот момент, когда мы присвоили переменной s1 вызов функции counter, мы ЗАМКНУЛИ переменную x (икс) со значением НОЛЬ внутри тела функции counter. С этого момента в переменной s1 начала жить НОВАЯ функция counter (её клон), которая навсегда заняла для переменной x (икс) значение в оперативной памяти устройства, на котором производится вызов этой НОВОЙ функции.

Достучаться до этого x (икс) извне невозможно т. к. нет прямого доступа к переменной x (икс). ЗАМЫКАНИЕ не позволяет этого сделать. Теперь на переменную x (икс) может влиять только функция generator, которая в настоящий момент является s1.

При прямом обращении к инструкции s1 мы будем видеть только ОБЪЯВЛЕННУЮ функцию generator. Вызов s1 будет вызывать generator.

Давайте же сделаем наконец наш первый вызов функции s1

s1()

Скриншот:

Первый вызов счётчика s1

Сделаем ещё 3 вызова:

s1()
s1()
s1()

Скриншот:

Четыре вызова счётчика s1

Четвёртый вызов функции s1 вернул нам значение 4. Четвёртый вызов функции s1 навсегда изменил значение ЗАМКНУТОЙ переменной x (икс) в клоне функции counter.

 

4. Зачем мы это замудрили?

Все эти манипуляции были созданы для того, чтобы иметь возможность создавать любое количество НЕЗАВИСИМЫХ друг от друга счётчиков, под любые задачи.

У нас уже есть счётчик s1. Последним значением он вернул 4. Давайте теперь создадим новый счётчик и назовём его ddd.

let ddd = counter()

Запустим счётчик ddd два раза.

ddd()
ddd()

А теперь запустим s1.

s1()

Смотрим историю вызовов:

Создали и запустили новый счётчик ddd

Второй вызов ddd вернул нам значение 2. Следующий за ним вызов s1 вернул нам значение 5. Это значит, что в оперативной памяти появилась новая замкнутая переменная x (икс), которая живёт во втором клоне оригинальной функции counter.(в ddd)

 

5. Итог

Хочу подвести общий итог из всего перечисленного. В основном функция возвращает функцию в тех случаях, когда мы хотим написать независимую логику работы для вызовов внутренней функции, которая знает о существовании каких-то замкнутых переменных за её пределами, но не выше самой функции-шаблона. Замыкание срабатывает в момент объявления

Такой подход в официальной семантике программирования называется «Шаблоны проектирования» (Design patterns). В общей практике проектирования систем их существует огромное количество. Но это уже другая история.

Передача функций в качестве аргументов в JavaScript — советы и подводные камни | by Ciaran Morinan

Функции в JavaScript являются «первоклассными», что означает, что они обрабатываются как любая другая переменная, включая передачу или возврат из других функций.

Когда они передаются в качестве аргумента другой функции, они называются «обратными вызовами» — их вызывают, когда другая функция готова для них.

Общие примеры обратных вызовов включают функции, предоставляемые:

  • forEach или map для вызова каждого элемента в коллекции это поселились

Это может не всегда идти по плану — этот блог затрагивает несколько причин, почему.

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

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

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

Здесь setTimeout ищет первый аргумент для предоставления0034 что нужно сделать, когда будете готовы ’. Если мы вызываем sayBoo() , а не ссылаемся на него, мы фактически предоставляем setTimeout то, что возвращает sayBoo() — в данном случае ничего, что он может выполнить. Если бы sayBoo() вернул функцию, то эта функция была бы доступна для setTimeout .

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

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

Например, если мы хотим сделать обратный отсчет, который вызывает сам себя:

… мы не можем предоставить обратный отсчет(--n) таким образом, потому что он вызывается, как только оценивается строка 4, а не звонит setTimeout , когда все будет готово. Добавление скобок () (с аргументами внутри них или без них) означает, что мы выполняем функцию на месте, а не передаем ее в качестве аргумента.

setTimeout и setInterval на самом деле имеют свой собственный способ справиться с этим — вы можете указать третий аргумент (и столько, сколько хотите) любому из них, который будет передан функции обратного вызова [не поддерживается IE9 ].

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

Для таких функций, как forEach , map и then , у которых есть готовые аргументы для передачи в их функции обратного вызова (например, текущее значение в повторяемой коллекции или значение разрешенного промиса ), вы можете воспользоваться возможностью JavaScript использовать неявное или «бесточечное» программирование.

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

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

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

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

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

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

Если вы используете конструктор объекта, вы также можете связать его методы с ним, как только вы его создадите, чтобы вам больше не пришлось об этом беспокоиться:

Все чаще, с более новым синтаксисом JavaScript , поможет объявление функций со стрелочным синтаксисом: они автоматически привяжут this к области, в которой объявлена ​​функция.

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

Введение в JavaScript: Функции | Махендра Чоудхари

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

В конце этой статьи вы сможете:

  • Понимать функции JavaScript, почему мы их используем, и уметь писать правильный синтаксис функций.
  • Запись и вызов функций с использованием аргументов и параметров.
  • Объясните область действия функции и оператор возврата.
  • Введение в JavaScript: основы
  • Введение в JavaScript: поток управления

Научитесь понимать функции JavaScript, почему мы их используем, и научиться писать правильный синтаксис функций.

Теперь, когда у нас есть основы JavaScript, типы данных, условные операторы и переменные, нам нужны функции для их вычисления, изменения и выполнения каких-либо действий с ними. Мы можем думать о функциях как о небольших компьютерных программах. Функции необходимы для того, чтобы наш код «запустился» в JavaScript. Хотя вы можете этого не осознавать, вы все это время использовали функции. Функции позволяют нам писать код, который будет использоваться снова и снова, сохраняя наш код DRY . Таким образом, функции часто принимают входные данные и выдают выходные данные, хотя это не всегда требуется. У функций также есть своя «область видимости», в которой мы можем присвоить переменную внутри функции, которая больше нигде недоступна.

Есть три способа построить функцию. В этой статье мы сосредоточимся только на первом. Для наших целей прямо сейчас все три способа будут действовать одинаково.

В этой статье мы будем использовать первый способ, а о других поговорим в следующих статьях.

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

В этом примере мы объявляем функцию logsHello и устанавливаем для нее значение console.log 'hello' . Затем мы видим, что для запуска этой функции нам нужно написать или вызвать ее имя со скобками после него. Это синтаксис для запуска функции. Функция всегда нуждается в круглых скобках для запуска.

Научитесь писать и вызывать функции с использованием аргументов и параметров.

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

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

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

После того, как мы настроили параметры в нашей функции, теперь мы можем передавать данные в функцию. Для этого мы будем использовать круглые скобки, которые мы пишем при вызове функции. Мы называем эти фрагменты данных аргументов . аргументы могут быть ЛЮБОГО типа данных (строка, число, логическое значение, объект, массив и даже другие функции!). В отличие от других языков, JavaScript не требует от нас установки типа данных при написании функции, хотя вы должны приложить усилия, чтобы понять, какой тип данных будет поступать в функцию (и если вы используете предварительно созданные функции, вам должен знать, какой тип данных ожидает эта функция).

Чтобы использовать аргумент, просто поместите данные в круглые скобки вызова функции следующим образом:

Если у вас более одного параметра, вы будете использовать более одного аргумента:

Аргументы всегда будут соответствовать параметрам по порядку, поэтому первый аргумент будет первым параметром и т. д.

Если аргумент не заданный для параметра, параметр будет равен undefined .

Научитесь объяснять область действия функции и оператор return.

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

Область действия определяется тем, к каким переменным у нас в настоящее время есть доступ и где. До сих пор в этом курсе мы работали в области Global , в которой мы можем получить доступ к любой созданной нами переменной в любом месте нашего кода. Существует несколько разных уровней области видимости: возможно, вы слышали об области block (используемой в if операторах и for loops), в которой переменная, использующая либо let , либо const , доступна только внутри оператор или цикл.

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