Стрелочные функции javascript контекст

Стрелочные функции и что о них стоит помнить

Идея написать статью про стрелочные функции в 2023 году выглядит не самой очевидной, но я постараюсь объяснить свою мотивацию. Я разработчик, который пришел в профессию после того, как в JavaScript появились такие инструменты как классы, async/await, стрелочные функции и так далее. В результате я воспринимаю их как данность и не всегда понимаю, какой важный вклад они внесли в современный JS. И из‑за этого непонимания в коде появляются ошибки, которых можно избежать, если оглянуться назад и изучить, какие проблемы эта технология была призвана решить в момент выхода. В этой статье я хочу разобраться: зачем появились стрелочные функции, чем они отличаются от обычных и какие особенности содержат.

Что было до стрелочных функций

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

До того, как появились стрелочные функции, в JS существовали только функции, которые можно было объявить через ключевое слово function :

Главная проблема классической функции — это то, что контекст this в ней связан не с местом объявления функции, а с местом вызова (т.н. runtime binding). Звучит немного запутанно, давайте разберемся. Напишем класс Dog , у которого есть свойство name и метод eat , который принимает на вход массив из вкусняшек и по одной ест их:

class Dog < constructor(name)< this.name = name; >eat(food) < food.forEach(function(item) < console.log(`$is eating $`) >); > > const bim = new Dog('Bim'); bim.eat(['bone', 'cookie'])

В таком виде вы получите ошибку.

Контекст this в классических функциях вычисляется в момент вызова. Мы ожидаем, что, если функция была объявлена внутри класса, то и this будет указывать на класс, но this обычной функции определяется в момент вызова. В данном случае анонимная функция передана как callback внутрь forEach , следовательно, она вызывается в методе массива. Контекст this внутри метода массива связать не с чем, поэтому this определяется как undefined , и код падает с ошибкой «Не могу прочитать свойство undefined».

В данном случае this равен undefined , потому что это происходит внутри класса, в котором строгий режим включен автоматически. Без строгого режима this был бы равен глобальному this, что привело бы к появлению undefined на месте this.name. Но эти подробности в данной статье не рассматриваются.

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

Теперь все работает так, как мы ожидали.

На вооружении у разработчиков до появления стрелочной функции было также связывание функции с необходимым this явно. Для связывания функции с необходимым this можно использовать метод bind (а также call и apply), который есть у функций (bind буквально переводится как «связывать»):

. eat(food) < food.forEach(function(item) < console.log(`$is eating $`) >.bind(this)); > . 

Надеюсь, теперь фраза «контекст this классической функции связан с местом вызова» стала понятна, а еще стало понятно, почему в ES6 было принято решение внедрить инструмент, который мог бы избавить разработчиков от необходимости использовать bind или присваивание нужного this в переменную. Один из минусов метода bind заключается в том, что он создает копию функции, где подменяет this , а стрелочная функция должна была оптимизировать расход памяти на копирование.

Читайте также:  When threads are used in java

this в стрелочных функциях

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

Перепишем наш пример с использованием стрелочной функции, а также залогируем this в методе и внутри стрелочной функции:

class Dog < constructor(name)< this.name = name; >eat(food) < console.log('method', this) food.forEach((item) => < console.log('arrow func', this) console.log(`$is eating $`) >); > > const bim = new Dog('Bim'); bim.eat(['bone', 'cookie'])

Вот что мы получим в консоли:

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

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

ВАЖНО: Приведенные выше сравнения не отражают реального положения дел, this это НЕ СВОЙСТВО и НЕ ССЫЛКА. Я использую это сравнение только для подчеркивания разницы в this стрелочной и обычной функций.

Для более глубокого погружения в то, как устроен this , советую прочитать соответствующую статью на MDN.

Особенности, о которых стоит знать

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

Стрелочную функцию лучше не использовать как метод в объектах и классах

Начнем с объекта. Если мы напишем следующий код, то он не будет работать:

const person = < name: 'John', sayName: () =>< console.log(`Hi! My name is $`) > > person.sayName();

В консоли мы получим ошибку:

А если мы напишем метод, объявленный стандартным способом, то все будет работать исправно.

const person = < name: 'John', sayName () < console.log(`Hi! My name is $`) > > person.sayName();

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

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

Но если мы обратимся к классам, то такой ошибки мы не встретим:

class Person < constructor(name)< this.name = name >sayName = () => < console.log(`Hi! My name is $`) > > const john = new Person('John') john.sayName();

Дело в том, что у класса, в отличие от объекта в JS, есть собственный контекст, к которому в момент создания стрелочная функция может привязаться. Но использовать стрелочную функцию как метод все же не стоит по другой причине. Когда вы создаете метод обычным способом, то он записывается в прототип класса, и когда вы создаете новый экземпляр, то он содержит ссылку на метод родителя, что экономит ресурсы. А если вы решили использовать стрелочную функцию, то она не будет записана в прототип, и будет копироваться каждый раз заново. Давайте убедимся в этом:

class Person < constructor(name, age)< this.name = name this.age = age >sayName = () => < console.log(`Hi! My name is $`) > getAge () < console.log(this.age) >> const john = new Person('John', 32) console.log(john)

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

Читайте также:  Http ответ html код

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

В стрелочной функции невозможно изменить this

Как мы узнали выше, для переопределения this в обычной функции можно использовать методы bind , apply или call . У стрелочной функции эти методы также доступны, но они не меняют this , потому что this в стрелочной функции не изменяется на всем протяжении жизненного цикла.

const sayNameGlobalArr = () => < console.log(`Hi! My name is $`) > class Person < constructor(name, age)< this.name = name this.age = age >sayName () < sayNameGlobalArr.bind(this)() >getAge () < console.log(this.age) >> const john = new Person('John') john.sayName();

В стрелочной функции недоступен объект arguments

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

function howManyArguments () < console.log(arguments.length) >howManyArguments("Hello", "World", "!");

В стрелочной функции доступа к переменной arguments нет, но проблема решается использованием spread оператора:

const howManyArguments = (. props) => < console.log(props) console.log(props.length) >howManyArguments("Hello", "World", "!");

Конечно, использование spread оператора это не 100% повторение переменной arguments , но для части задач это решение годится.

Что еще можно держать в голове (хотя, скорее всего, это вам не понадобится)

  • Стрелочные функции нельзя использовать как функцию-конструктор. Попытка использовать ключевое слово new со стрелочной функцией приведет к ошибке.
  • Нельзя использовать ключевое слово yield внутри стрелочной функции, что значит, что стрелочная функция не может быть функцией генератором. Но при этом внутри стрелочной функции можно объявить функцию генератор, внутри которой можно использовать yield .

Заключение

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

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

Читайте также:  Setting memory limit in php ini

В завершение хочу порекомендовать бесплатный урок от моих друзей из OTUS по теме: «Управление сложным состоянием на основе XState».

Источник

Повторяем стрелочные функции

Стрелочные функции – это не просто «сокращение», чтобы меньше писать. У них есть ряд других полезных особенностей.

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

  • arr.forEach(func) – func выполняется forEach для каждого элемента массива.
  • setTimeout(func) – func выполняется встроенным планировщиком.
  • …и так далее.

Это очень в духе JavaScript – создать функцию и передать её куда-нибудь.

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

У стрелочных функций нет «this»

Как мы помним из главы Методы объекта, «this», у стрелочных функций нет this . Если происходит обращение к this , его значение берётся снаружи.

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

let group = < title: "Our Group", students: ["John", "Pete", "Alice"], showList() < this.students.forEach( student =>alert(this.title + ': ' + student) ); > >; group.showList();

Здесь внутри forEach использована стрелочная функция, таким образом this.title в ней будет иметь точно такое же значение, как в методе showList : group.title .

Если бы мы использовали «обычную» функцию, была бы ошибка:

let group = < title: "Our Group", students: ["John", "Pete", "Alice"], showList() < this.students.forEach(function(student) < // Error: Cannot read property 'title' of undefined alert(this.title + ': ' + student) >); > >; group.showList();

Ошибка возникает потому, что forEach по умолчанию выполняет функции с this , равным undefined , и в итоге мы пытаемся обратиться к undefined.title .

Это не влияет на стрелочные функции, потому что у них просто нет this .

Отсутствие this естественным образом ведёт к другому ограничению: стрелочные функции не могут быть использованы как конструкторы. Они не могут быть вызваны с new .

Существует тонкая разница между стрелочной функцией => и обычной функцией, вызванной с .bind(this) :

  • .bind(this) создаёт «связанную версию» функции.
  • Стрелка => ничего не привязывает. У функции просто нет this . При получении значения this – оно, как обычная переменная, берётся из внешнего лексического окружения.

Стрелочные функции не имеют «arguments»

У стрелочных функций также нет переменной arguments .

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

Например, defer(f, ms) принимает функцию и возвращает обёртку над ней, которая откладывает вызов на ms миллисекунд:

function defer(f, ms) < return function() < setTimeout(() =>f.apply(this, arguments), ms) >; > function sayHi(who) < alert('Hello, ' + who); >let sayHiDeferred = defer(sayHi, 2000); sayHiDeferred("John"); // выводит "Hello, John" через 2 секунды

То же самое без стрелочной функции выглядело бы так:

Здесь мы были вынуждены создать дополнительные переменные args и ctx , чтобы функция внутри setTimeout могла получить их.

Итого

  • Не имеют this .
  • Не имеют arguments .
  • Не могут быть вызваны с new .
  • (У них также нет super , но мы про это не говорили. Про это будет в главе Наследование классов).

Всё это потому, что они предназначены для небольшого кода, который не имеет своего «контекста», выполняясь в текущем. И они отлично справляются с этой задачей!

Источник

Оцените статью