Обычные и стрелочные функции часто воспринимаются как взаимозаменяемые. Но они не одинаковы. Между ними есть некоторые важные различия. В этом руководстве мы эти различия рассмотрим. Это поможет вам лучше ориентироваться в том, когда какие функции использовать.
Виды функций
В современном JavaScript функции можно писать двумя разными способами. Можно писать обычные, а можно стрелочные.
Если вы решите использовать обычные функции, можно будет выбрать из двух видов синтаксиса. Первый — объявление функции (function declaration), а второй — функциональное выражение (function expression).
// Function declaration example: function calculateCircleArea(radius) { return MathMath.PI * (radius ** 2) } // Function expression example: const calculateCircleArea = function(radius) { return MathMath.PI * (radius ** 2) }
Если вы решите использовать стрелочные функции, все сразу упрощается. Для них есть только один вид синтаксиса — функциональное выражение.
// Arrow function example: const calculateCircleArea = (radius) => { return MathMath.PI * (radius ** 2) }
Если вы сравните синтаксис обычной функции (функционального выражения) и стрелочной, вы увидите, что в первой есть ключевое слово function
, а во второй — стрелка =>
.
Но различия, не относящиеся к синтаксису, куда более интересны.
1. this
Первое важное различие между обычными и стрелочными функциями — в ключевом слове this
. В обычных функциях this
очень динамично. Оно может вести себя четырьмя разными способами, в зависимости от ситуации.
Глобальная область видимости
Когда вы вызываете обычную функцию в глобальной области видимости, значением this
будет глобальный объект window
. При вызове этой функции в строгом режиме значение this
будет undefined
.
// Create regular function in a global scope: function logThis() { console.log(this) } // Call logThis(): logThis() // Output: // { // window: Window, // self: Window, // ... // } // With strict mode: // Turn on strict mode: 'use strict' // Create regular function in a global scope: function logThis() { console.log(this) } // Call logThis(): logThis() // Output: // undefined
Методы объекта (с обычными функциями)
Если для определения и вызова метода объекта вы используете обычную функцию, this
будет родительским объектом. Это будет объект, внутри которого вы определили метод.
// Create a simple object: const user = { name: 'user', active: true, // Create object method: getParentObj () { // Return this: return this } } // Call the "getParentObj()" method on "user" object: user.getParentObj() // Output: // { // name: 'user', // active: true, // getParentObj: ƒ getParentObj() // }
Конструкторы (с обычными функциями)
Когда вы используете обычную функцию для создания функции-конструктора, this
будет отдельным экземпляром, который вы создаете при помощи этого конструктора.
// Create a function construct or that accepts one parameter: function MyFunctionConstructor(name) { // Use parameter to create prop: this.name = name // Log this: console.log(this) } // Create the first instance of "MyFunctionConstructor": const myFunctionInstanceOne = new MyFunctionConstructor('Charlie') // Output: // MyFunctionConstructor { // name: 'Charlie', // __proto__: { constructor: ƒ MyFunctionConstructor() } // } // Create the first instance of "MyFunctionConstructor": const myFunctionInstanceTwo = new MyFunctionConstructor('Jenny') // Output: // MyFunctionConstructor { // name: 'Jenny', // __proto__: { constructor: ƒ MyFunctionConstructor() } // }
call() и apply() (с обычными функциями)
Наконец, используя методы apply()
и call()
, функцию можно вызвать не напрямую. Эти два метода позволяют менять значение this
функции и вызывать ее, используя этот новый this
. Это означает, что this
может быть чем угодно.
// Create object for new "this": const newThis = { planet: 'Earth' } // Create a regular function: function logThis() { console.log(this) } // Invoke "logThis()" with default this: logThis() // Output: // { // window: Window, // self: Window // ... // } // Invoke "logThis()" with "call()" method // and "newThis" object: logThis.call(newThis) // Output: // { planet: 'Earth' } // Invoke "logThis()" with "apply()" method // and "newThis" object: logThis.apply(newThis) // Output: // { planet: 'Earth' }
this и стрелочные функции
В том, что касается this
, стрелочные функции намного проще: они всегда ведут себя одинаково. Значение this
всегда является значением родительской, внешней функции.
Дело в том, что стрелочные функции не имеют собственного this
. Они «получают» его лексически, из своей лексической области видимости, из внешнего скоупа.
Если вы попытаетесь изменить this
стрелочной функции при помощи call()
или apply()
, стрелочная функция все это проигнорирует. Она все равно будет получать this
из своей лексической области видимости.
// Global scope example: // Create arrow function in a global scope: const logThis = () => console.log(this) // Invoke "logThis()": logThis() // Output: // { // window: Window, // self: Window // ... // } // Object method example: // Create a simple object: const shape = { name: 'square', width: 15, height: 15, // Create object method: getParentObj: () => { // Return this: return this } } // Invoke "getParentObj()" on "shape" object: shape.getParentObj() // Output: // { // window: Window, // self: Window // ... // } // "call()" and "apply()" methods example: const newThis = { name: 'Alexander Joseph Luthor', alias: 'Lex Luthor', type: 'Egotistical Mastermind' } const logThis = () => console.log(this) // Invoke "logThis()" with "call()" method: logThis.call(newThis) // Output: // { // window: Window, // self: Window // ... // } // Invoke "logThis()" with "apply()" method: logThis.apply(newThis) // Output: // { // window: Window, // self: Window // ... // }
Получение this
лексическим образом также означает, что при использовании стрелочных функций не нужно связывать методы объекта и класса. А вот с обычными функциями при изменении this
это делать приходится.
// Regular function example: // Create "Person" class: class Person { // Add some properties: constructor(name, age) { this.name = name this.age = age } // Add class method: getName() { console.log(this.name) } } // Create instance of "Person": const jack = new Person('Jack', 44) // Log the name: jack.getName() // Output: // 'Jack' // Log the name with different this: setTimeout(jack.getName, 1000) // Output: // '' // Bind this manually: setTimeout(jack.getName.bind(jack), 1000) // Output: // 'Jack' // Arrow function example: class Person { constructor(name, age) { this.name = name this.age = age } getName = () => { console.log(this.name) } } // Create instance of "Person": const jack = new Person('Jack', 44) // Log the name: jack.getName() // Output: // 'Jack' // Log the name with timeout: setTimeout(jack.getName, 1000) // Output: // 'Jack'
2. Неявный (имплицитный) return
При создании обычной функции она имплицитно возвращает undefined
. Это можно изменить, явно добавив оператор return
с каким-нибудь выражением.
Если вы добавите выражение, но без оператора return
, обычная функция вернет undefined
.
// Create an empty regular function: function FnReturningNothing() {} // Invoke "FnReturningNothing()": FnReturningNothing() // Output: // undefined // Create a regular function without return statement: function fnWithoutStatement() { const randomNumber = Math.floor(Math.random() * 100) } // Invoke "fnWithoutStatement()": fnWithoutStatement() // Output: // undefined // Create a regular function with return statement: function fnWithStatement() { const randomNumber = Math.floor(Math.random() * 100) return randomNumber } // Invoke "fnWithStatement()": fnWithStatement() // Output: // 7
Использовать оператор return
для возврата какого-нибудь выражения можно и в стрелочных функциях. Но у них есть специальный функционал, позволяющий сократить этот шаг.
Если тело функции взято в фигурные скобки, а выражение в ней только одно, имплицитно стрелочная функция возвращает выражение.
// Create arrow function with implicit return: const returnRandomNumber = () => Math.floor(Math.random() * 10) // Note: it implicitly returns expression // that follows after the "=>" (fat arrow). // Invoke the "returnRandomNumber()": returnRandomNumber() // Output: // 0 // The same as: const returnRandomNumber = () => { // Return random number explicitly: return Math.floor(Math.random() * 10) } // Invoke the "returnRandomNumber()": returnRandomNumber() // Output: // 7
3. Объект arguments
При создании обычной функции JavaScript также создает специальный объект с именем arguments
.
Этот подобный массиву объект доступен только внутри этой функции. Он содержит список аргументов, с которыми вы вызвали функцию.
Так происходит, даже если данная конкретная функция не принимает никаких параметров.
// Create a regular function without parameters: function logArguments() { // Log "argument" object: console.log(arguments) } // Invoke the "logArguments()": logArguments() // Output: // { // length: 0, // callee: ƒ logArguments(), // __proto__: { ... } // } // Create a regular function with one parameter: function logArguments(hobby) { // Log "argument" object: console.log(arguments) } // Invoke the "logArguments()": logArguments('reading') // Output: // { // '0': 'reading', // length: 1, // callee: ƒ logArguments(), // __proto__: { ... } // } // Create a regular function with two parameters: function logArguments(fistName, lastName) { // Log "argument" object: console.log(arguments) } // Invoke the "logArguments()": logArguments('Jack', 'Jones') // Output: // { // '0': 'Jack', // '1': 'Jones', // length: 2, // callee: ƒ logArguments(), // __proto__: { ... } // } // Create a regular function with two parameters: function logArguments(fistName, lastName) { // Log "argument" object: console.log(arguments) } // Invoke the "logArguments()" and pass more arguments: logArguments('Jack', 'Tobias', 'Jones', 'Junior') // Output: // { // '0': 'Jack', // '1': 'Tobias', // '2': 'Jones', // '3': 'Junior', // length: 4, // callee: ƒ logArguments(), // __proto__: { ... } // }
В стрелочных функциях нет собственного объекта arguments
.
Если определить стрелочную функцию внутри обычной, она унаследует объект arguments
из родительской.
А если вы определите стрелочную функцию в глобальной области видимости и попытаетесь обратиться к объекту arguments
, JavaScript выбросит ReferenceError
.
// Create arrow function in a global scope: const logArguments = () => { // Try to log "argument" object: console.log(arguments) } // Invoke the "logArguments()": logArguments() // Output: // ReferenceError: arguments is not defined // Try adding parameters: const logArguments = (a, b) => { // Try to log "argument" object: console.log(arguments) } // Invoke the "logArguments()" with some arguments: logArguments('One', 'Two') // Output: // ReferenceError: arguments is not defined // Create arrow function inside a regular function: function parentFunction() { const logArguments = () => { // Try to log "argument" object: console.log(arguments) } // Invoke "logArguments()": logArguments() } // Invoke the "logArguments()": parentFunction('One', 'Two') // Output: // { // '0': 'One', // '1': 'Two', // length: 2, // callee: ƒ parentFunction(), // __proto__: { ... } // }
4. Функции-конструкторы
Один из способов использования обычных функций — создание функций-конструкторов.
Функцию-конструктор можно считать шаблоном для создания объектов. По сути она — обычная функция, хотя и имеет некоторые отличия.
Во-первых, ее имя начинается с заглавной буквы.
Когда вы хотите использовать функцию-конструктор, вы вызываете ее при помощи ключевого слова new
. Это слово указывается перед именем конструктора и скобками.
Внутри конструктора можно использовать this
для создания и присваивания свойств. Эти свойства будут создаваться для каждого экземпляра, созданного при помощи этой функции-конструктора.
// Create function constructor "Human": function Human(name, age) { // Create and assign new properties: this.name = name this.age = age // Add constructor method: this.sayHello = () => `Hi, my name is ${this.name}.` } // Create new instance of "Human": const joe = new Human('Joel', 33) // Check if "joe" is instance of "Human": console.log(joe instanceof Human) // Output: // true // Call the "sayHello()" method on "joe" instance: joe.sayHello() // Output: // 'Hi, my name is Joel.'
Со стрелочными функциями конструкторы не работают.
У стрелочных функций нет собственного this
. А this
в функциях-конструкторах встречается часто. По этой причине (возможно, и по другим тоже) при помощи стрелочных функций функции-конструкторы не создаются. Если вы попытаетесь это сделать, JavaScript выбросит TypeError
.
// Try to create function constructor with arrow function: const Human = (name, age) => { this.name = name this.age = age } // Try to create new instance of "Human": const jess = new Human('Jessica', 28) // Output: // TypeError: Human is not a constructor
Заключение
Различия между обычными и стрелочными функциями выходят далеко за пределы синтаксиса. Надеюсь, эта статья помогла вам понять, чем отличаются эти два вида функций и когда лучше использовать каждый из них.
От редакции Techrocks. Возможно, вас также заинтересуют другие статьи о JavaScript:
- Одинарные, двойные и обратные кавычки в JavaScript
- Инструменты JavaScript, о которых незаслуженно мало говорят
- Тернарный оператор в JavaScript
Перевод статьи «4 Important Differences Between Regular And Arrow Functions».