Обычные и стрелочные функции в JavaScript

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

Виды функций

В современном 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:


Перевод статьи «4 Important Differences Between Regular And Arrow Functions».

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх