Стрелочные функции (arrow functions) - это функции вида «arrow function expression», которые имеют укороченный (по сравнению с функциональным выражением «function expression») синтаксис следующего вида:
1 2 3 |
(param1, param2, …, paramN) => { statements } // или (param1, param2, …, paramN) => { return expression; } |
Если единственным оператором в выражении стрелочной функции является return, можно удалить return и окружающие фигурные скобки:
1 |
(param1, param2, …, paramN) => expression |
Особенности стрелочных функций JS:
- более лаконичный (краткий) синтаксис;
- отсутствие псевдомассива аргументов arguments;
- лексическое определение this (this стрелочной функции постоянно замкнут на this внешней функции, в которой она создана);
- не могут использоваться:
- в качестве конструкторов (с оператором new);
- для создания генераторов;
- всегда анонимны, в результате чего:
- вы не сможете отследить имя функции или точный номер строки, где произошла ошибка;
- нет самопривязки, т.е. ваша функция не может ссылаться на саму себя (например, рекурсия, обработчик событий, который необходимо отменить, не сработают).
Проблема отсутствия arguments в стрелочных функциях решается путём сочетания стрелочной функции с rest оператором:
1 2 3 |
const printParams = (...props) => console.log(props); printParams("test", 123); // ["test", 123] |
Ещё пример:
1 2 3 4 5 6 7 8 9 10 11 |
const sumNew = (...props) => { const params = props.slice(); // (4) [1, 2, 3, 4] // или аналогично: // const params = props.slice.call(props); // const params = Array.prototype.slice.call(props); if (!params.length) return 0; return params.reduce((prev, next) => prev + next); }; console.log(sumNew(1, 2, 3, 4)); // 10 console.log(sumNew()); // 0 |
Так как rest оператор всегда возвращает нативный массив (а не псевдомассив), то переменная props (или params) будет истинным массивом, следовательно, можно записать короче:
1 2 3 4 5 6 7 |
const sumNew = (...params) => { if (!params.length) return 0; return params.reduce((prev, next) => prev + next); }; console.log(sumNew(1, 2, 3, 4)); // 10 console.log(sumNew()); // 0 |
Синтаксис (объявление) стрелочных функций в JavaScript
1. Без параметров (пустые круглые скобки перед) => :
1 |
() => 42 |
2. Одиночный параметр (круглые скобки необязательны):
1 2 3 |
x => 42; // или (x) => 42; |
3. Несколько параметров (требуются скобки):
1 |
(x, y) => 42; |
4. Тело функции (блок кода) должно иметь фигурные скобки и return:
1 2 3 4 5 6 7 8 9 |
const resObj = (str = "") => { return { value: str, length: str.length }; }; console.log(resObj("Hello, world!")); // Object { value: "Hello, world!", length: 13 } // ИЛИ короткая запись const resObj1 = (str = "") => ({ value: str, length: str.length }); // круглые скобки вместо return console.log(resObj1("Hello!")); // Object { value: "Hello, world!", length: 13 } |
5. Если возвращается объектный литерал, его нужно заключить в круглые скобки:
1 2 3 |
x => ({ y: x }); // или (x) => ({ y: x }); |
В некоторых случаях стрелочные функции позволяют использовать более короткую запись функции:
1 2 3 4 5 6 7 8 9 |
function resSum(x, y) { return x + y; } console.log(resSum(2, 5)); // 7 // ИЛИ с использованием стрелочной функции (в одну строку) const resSumm = (x, y) => x + y; console.log(resSumm(2, 5)); // 7 |
Ещё пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// Обычная функция function minus(firstNum) { if (!firstNum) firstNum = 0; return function (lastNum) { if (!lastNum) lastNum = 0; return (firstNum - lastNum); }; } console.log(minus(10)(6)); // 4 console.log(minus(10)()); // 10 console.log(minus()(6)); // -6 console.log(minus()()); // 0 // Аналогично через стрелочные функции const minusArr = (a = 0) => (b = 0) => a - b; console.log(minusArr(10)(6)); // 4 console.log(minusArr(10)()); // 10 console.log(minusArr()(6)); // -6 console.log(minusArr()()); // 0 |
Ещё пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// Обычная функция высшего порядка с замыканием function multiplyMaker(multi) { return function (num) { return (multi *= num); }; } const multiply = multiplyMaker(2); console.log(multiply(2)); // 4 (2 * 2) console.log(multiply(2)); // 8 (4 * 2) console.log(multiply(3)); // 24 (8 * 3) console.log(multiply(10)); // 240 (12 * 10) // Аналогично через стрелочные функции const multiplyMaker1 = (multi) => { return (arg) => (multi *= arg); }; const multiply1 = multiplyMaker1(2); console.log(multiply1(2)); // 4 (2 * 2) console.log(multiply1(2)); // 8 (4 * 2) console.log(multiply1(3)); // 24 (8 * 3) console.log(multiply1(10)); // 240 (12 * 10) |
Когда следует использовать стрелочные функции:
- В качестве callback функций.
- Когда от this требуется привязка к контексту, а не к самой функции.
- Удобно использовать с такими методами, как map и reduce (в некоторых случаях делает код более удобочитаемым).
Когда не следует использовать стрелочные функции:
- Если нужно, чтобы контекст был динамическим.
- В методах объектов.
- В сложном коде (затрудняют чтение кода).
Особенности this в стрелочных функциях
Утверждение о том, что при обращении к this в методе объекта этому ключевому слову соответствует объект, которому принадлежит метод, не относится к стрелочным функциям. Стрелочные функции не содержат собственный контекст this, а используют значение this окружающего контекста.
Стрелочные функции (arrow functions) являются исключением из правила в случае определения значения this: их this всегда лексический (статический), а не динамический. Т.е. их функциональная запись окружения не предоставляет значение this, и оно наследуется из родительского окружения (this стрелочной функции постоянно замкнут на this внешней функции, в которой она определена).
Таким образом, в отношении стрелочных функций действует правило:
- this будет таким, каким он был на момент создания стрелочной функции, т.е. захватывается из текущего контекста (и вовсе не обязательно это будет window).
1 2 3 4 5 6 7 8 9 |
let user = { firstName: "Alex", sayHi() { let arrow = () => console.log(this.firstName); arrow(); // arrow стрелочная, берет this внешней функции sayHi() }, }; user.sayHi(); // Alex |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
const objReg = { hello: function () { return this; // текущий контекст objReg }, objReg_1: { by: function () { return this; // текущий контекст objReg_1 }, hello: () => this, }, }; let objArrow = { hello: () => this, }; console.log(objReg.hello()); // возвращает объект objReg {hello: ƒ, objReg_1: {…}}, this корректно передан через вызов метода вида obj.method() console.log(objArrow.hello()); // возвращает объект Window (т.к. стрелочная и контекст формируется внешней console.log()) console.log(objReg.objReg_1.by()); // возвращает объект objReg_1 {by: ƒ, hello: ƒ}, this корректно передан через вызов метода вида obj.method() console.log(objReg.objReg_1.hello()); // возвращает объект Window (т.к. стрелочная и контекст формируется внешней console.log()) |
Так как значение this определяется лексикой:
- правила строгого режима (strict mode) относительно this игнорируются;
- вызов стрелочных функций с помощью методов call() или apply(), даже если передать аргументы в эти методы, не влияет на значение this.
В выражении obj.method() можно заметить две операции:
- оператор точка '.' возвращает свойство объекта – его метод obj.method (если вывести obj.method в консоль - это будет код метода, т.е. код функции);
- скобки () вызывают этот метод (исполняется код метода).
Каким же образом информация о this передаётся из первой части во вторую?
Если мы поместим указанные выше операции ( '.' и () ) в отдельные строки, то значение this будет потеряно:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
let user = { name: "Alex", hi() { console.log("Привет, " + this.name); }, }; user.hi(); // Привет, Alex (в этом случае this = user{}, работает корректно) // разделим получение метода объекта и его вызов в разных строках let hi = user.hi; hi(); // Ошибка, т.к. значением this является undefined |
Здесь hi = user.hi сохраняет функцию метода объекта user в переменной как отдельный объект (отдельную функцию, не привязанную к объекту user). Поэтому, если её далее вызывать hi(), то она вызывается в глобальном контексте, при этом в строгом режиме значением this внутри функции становится undefined (в обычном режиме значением this станет глобальный объект Window).
Для сохранения значения this при вызове метода объекта используется синтаксис obj.method(), который корректно передает this.
При вызове типа obj.method() точка '.' возвращает не саму функцию, а специальное значение «ссылочного типа», называемого Reference Type. Этот ссылочный тип (Reference Type) является внутренним типом, который используется внутри движка.
Значение ссылочного типа – это «триплет»: комбинация из трёх значений (base, name, strict), где:
- base – это объект;
- name – это имя свойства объекта;
- strict – это режим исполнения (true, если действует строгий режим use strict).
Таким образом, результатом доступа к свойству user.hi при использовании синтаксиса obj.method() является не функция, а значение ссылочного типа, которое в строгом режиме будет таким:
1 2 3 |
// значение ссылочного типа (Reference Type) (user, "hi", true) |
Когда скобки () применяются к значению ссылочного типа (происходит вызов), то они получают полную информацию об объекте и его методе, и могут поставить правильный this по значению base (в данном случае base = user).
Ссылочный тип – исключительно внутренний, промежуточный, используемый чтобы передать информацию от точки . до вызывающих скобок (). При любой другой операции, например, присваивании hi = user.hi, ссылочный тип заменяется на собственно значение user.hi (т.е. функцию без привязки к объекту), и дальше работа уже идёт только с ней.
Таким образом, значение this передаётся правильно, только если функция вызывается напрямую с использованием следующего синтаксиса:
- точечной нотации obj.method() или
- скобочной нотации obj['method']().
Для сохранения значения this можно применить, например, методы CALL(), APPLY() и BIND(), которые рассмотрены ниже. Например:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
user = { name: "Alex", hi() { console.log("Привет, " + this.name); }, }; // разделим получение метода объекта и его вызов в разных строках let hi = user.hi; hi.bind(user)(); // Привет, Alex (в этом случае this = user{}) hi.call(user); hi.apply(user); |
Лексическое окружение — это область спецификации, описывающая связывание идентификаторов с переменными и функциями, основанная на лексической вложенности кода, написанного на ECMAScript (JavaScript). Лексическое окружение состоит из таблицы символов и ссылки на внешнее лексическое окружение (нулевой для глобального окружения, т.к. глобальное окружение не имеет внешнего окружения).
Каждый контекст выполнения имеет лексическое окружение, которое хранит переменные и их значения, а также имеет ссылку на внешнее окружение. Лексическим окружением может быть:
- глобальное окружение, или
- окружение модуля (содержащее привязки имён к символам, объявленным в этом модуле), или
- окружение функции (созданное в процессе её вызова).
Обычно лексическое окружение связано с синтаксической конструкцией в коде на ECMAScript, такой как объявление функции, блок кода (окружённый фигурными скобками), или блок catch в try-инструкции.
Новое лексическое окружение создаётся каждый раз при исполнении такого кода (например, при вызове функции).
Создаем объект obj, содержащий метод bar, который возвращает функцию, которая, в свою очередь, возвращает свой this. Возвращаемая функция создана как стрелочная функция, таким образом её this постоянно замкнут на this функции, в которой она создана. Значение bar может быть установлено в вызове, который, в свою очередь, устанавливает значение возвращаемой функции.
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 |
'use strict'; let obj = { bar: function () { let x = () => this; return x; }, }; // Вызываем bar как метод объекта obj, устанавливая его this на obj и присваиваем ссылку возвращаемой функции переменной fn let fn = obj.bar(); // при вызове bar() формируется контекст функции, привязанный к объекту obj console.log(fn); // () => this; (функция fn не вызывается, т.е. вернется только её код, переданный в переменную x) // Вызываем fn без установки this, что в обычных функциях указывало бы на глобальный объект Window или undefined в строгом режиме console.log(fn()); // вернет this = {bar: ƒ} (this корректно передан через точечную нотацию obj.bar(), см. спойлер выше) // Пример потери контекста (потери this при неверном вызове) let fn2 = obj.bar; // в переменную fn2 передан код метода bar (без вызова, т.е. без формирования контекста и без привязки к объекту obj) // this стрелочной функции замкнут на this функции, в которой она создана (он следует за this из fn2) console.log(fn2); // {let x = () => this; return x;} console.log(fn2()); // () => this (вызвана функция fn2, т.е. вернется только код стрелочной функции, переданный в переменную x) console.log(fn2()()); // undefined (this obj потерян, в строгом режиме вернет undefined, в обычном - Window, т.к. функция fn2() принадлежит глобальному контексту) |