1 августа 2019 г.

Функции

Материал на этой странице устарел, поэтому скрыт из оглавления сайта.

В функциях основные изменения касаются передачи параметров, плюс введена дополнительная короткая запись через стрелочку =>.

Параметры по умолчанию

Можно указывать параметры по умолчанию через равенство =, например:

function showMenu(title = "Без заголовка", width = 100, height = 200) {
  alert(title + ' ' + width + ' ' + height);
}

showMenu("Меню"); // Меню 100 200

Параметр по умолчанию используется при отсутствующем аргументе или равном undefined, например:

function showMenu(title = "Заголовок", width = 100, height = 200) {
  alert('title=' + title + ' width=' + width + ' height=' + height);
}

// По умолчанию будут взяты 1 и 3 параметры
// title=Заголовок width=null height=200
showMenu(undefined, null);

При передаче любого значения, кроме undefined, включая пустую строку, ноль или null, параметр считается переданным, и значение по умолчанию не используется.

Параметры по умолчанию могут быть не только значениями, но и выражениями.

Например:

function sayHi(who = getCurrentUser().toUpperCase()) {
  alert('Привет, ' + who);
}

function getCurrentUser() {
  return 'Вася';
}

sayHi(); // Привет, ВАСЯ

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

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

Оператор spread вместо arguments

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

function showName(firstName, lastName, ...rest) {
  alert(firstName + ' ' + lastName + ' - ' + rest);
}

// выведет: Юлий Цезарь - Император,Рима
showName("Юлий", "Цезарь", "Император", "Рима");

В rest попадёт массив всех аргументов, начиная с третьего.

Заметим, что rest – настоящий массив, с методами map, forEach и другими, в отличие от arguments.

Оператор … должен быть в конце

Оператор собирает «все оставшиеся» аргументы, поэтому такое объявление не имеет смысла:

function f(arg1, ...rest, arg2) { // arg2 после ...rest ?!
  // будет ошибка
}

Параметр ...rest должен быть в конце функции.

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

'use strict';

let numbers = [2, 3, 15];

// Оператор ... в вызове передаст массив как список аргументов
// Этот вызов аналогичен Math.max(2, 3, 15)
let max = Math.max(...numbers);

alert( max ); // 15

Формально говоря, эти два вызова делают одно и то же:

Math.max(...numbers);
Math.max.apply(Math, numbers);

Похоже, что первый – короче и красивее.

Деструктуризация в параметрах

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

'use strict';

let options = {
  title: "Меню",
  width: 100,
  height: 200
};

function showMenu({title, width, height}) {
  alert(title + ' ' + width + ' ' + height); // Меню 100 200
}

showMenu(options);

Можно использовать и более сложную деструктуризацию, с соответствиями и значениями по умолчанию:

'use strict';

let options = {
  title: "Меню"
};

function showMenu({title="Заголовок", width:w=100, height:h=200}) {
  alert(title + ' ' + w + ' ' + h);
}

// объект options будет разбит на переменные
showMenu(options); // Меню 100 200

Заметим, что в примере выше какой-то аргумент у showMenu() обязательно должен быть, чтобы разбить его на переменные.

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

'use strict';

function showMenu({title="Заголовок", width:w=100, height:h=200} = {}) {
  alert(title + ' ' + w + ' ' + h);
}

showMenu(); // Заголовок 100 200

В коде выше весь объект аргументов по умолчанию равен пустому объекту {}, поэтому всегда есть что деструктурировать.

Имя «name»

В свойстве name у функции находится её имя.

Например:

'use strict';

function f() {} // f.name == "f"

let g = function g() {}; // g.name == "g"

alert(f.name + ' ' + g.name) // f g

В примере выше показаны Function Declaration и Named Function Expression. В синтаксисе выше довольно очевидно, что у этих функций есть имя name. В конце концов, оно указано в объявлении.

Но современный JavaScript идёт дальше, он старается даже анонимным функциям дать разумные имена.

Например, при создании анонимной функции с одновременной записью в переменную или свойство – её имя равно названию переменной (или свойства).

Например:

'use strict';

// свойство g.name = "g"
let g = function() {};

let user = {
  // свойство user.sayHi.name == "sayHi"
  sayHi: function() {}
};

Функции в блоке

Объявление функции Function Declaration, сделанное в блоке, видно только в этом блоке.

Например:

'use strict';

if (true) {

  sayHi(); // работает

  function sayHi() {
    alert("Привет!");
  }

}
sayHi(); // ошибка, функции не существует

То есть, иными словами, такое объявление – ведёт себя в точности как если бы let sayHi = function() {…} было сделано в начале блока.

Функции через =>

Появился новый синтаксис для задания функций через «стрелку» =>.

Его простейший вариант выглядит так:

'use strict';

let inc = x => x+1;

alert( inc(1) ); // 2

Эти две записи – примерно аналогичны:

let inc = x => x+1;

let inc = function(x) { return x + 1; };

Как видно, "x => x+1" – это уже готовая функция. Слева от => находится аргумент, а справа – выражение, которое нужно вернуть.

Если аргументов несколько, то нужно обернуть их в скобки, вот так:

'use strict';

let sum = (a,b) => a + b;

// аналог с function
// let sum = function(a, b) { return a + b; };

alert( sum(1, 2) ); // 3

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

'use strict';

// вызов getTime() будет возвращать текущее время
let getTime = () => new Date().getHours() + ':' + new Date().getMinutes();

alert( getTime() ); // текущее время

Когда тело функции достаточно большое, то можно его обернуть в фигурные скобки {…}:

'use strict';

let getTime = () => {
  let date = new Date();
  let hours = date.getHours();
  let minutes = date.getMinutes();
  return hours + ':' + minutes;
};

alert( getTime() ); // текущее время

Заметим, что как только тело функции оборачивается в {…}, то её результат уже не возвращается автоматически. Такая функция должна делать явный return, как в примере выше, если конечно хочет что-либо возвратить.

Функции-стрелки очень удобны в качестве коллбеков, например:

`use strict`;

let arr = [5, 8, 3];

let sorted = arr.sort( (a,b) => a - b );

alert(sorted); // 3, 5, 8

Такая запись – коротка и понятна. Далее мы познакомимся с дополнительными преимуществами использования функций-стрелок для этой цели.

Функции-стрелки не имеют своего this

Внутри функций-стрелок – тот же this, что и снаружи.

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

'use strict';

let group = {
  title: "Наш курс",
  students: ["Вася", "Петя", "Даша"],

  showList: function() {
    this.students.forEach(
      student => alert(this.title + ': ' + student)
    )
  }
}

group.showList();
// Наш курс: Вася
// Наш курс: Петя
// Наш курс: Даша

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

Если бы в forEach вместо функции-стрелки была обычная функция, то была бы ошибка:

'use strict';

let group = {
  title: "Наш курс",
  students: ["Вася", "Петя", "Даша"],

  showList: function() {
    this.students.forEach(function(student) {
      alert(this.title + ': ' + student); // будет ошибка
    })
  }
}

group.showList();

При запуске будет "попытка прочитать свойство title у undefined", так как .forEach(f) при запуске f не ставит this. То есть, this внутри forEach будет undefined.

Функции стрелки нельзя запускать с new

Отсутствие у функции-стрелки "своего this" влечёт за собой естественное ограничение: такие функции нельзя использовать в качестве конструктора, то есть нельзя вызывать через new.

=> это не то же самое, что .bind(this)

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

  • Вызовом .bind(this) мы передаём текущий this, привязывая его к функции.
  • При => привязки не происходит, так как функция стрелка вообще не имеет контекста this. Поиск this в ней осуществляется так же, как и поиск обычной переменной, то есть, выше в замыкании. До появления стандарта ES-2015 такое было невозможно.

Функции-стрелки не имеют своего arguments

В качестве arguments используются аргументы внешней «обычной» функции.

Например:

'use strict';

function f() {
  let showArg = () => alert(arguments[0]);
  showArg();
}

f(1); // 1

Вызов showArg() выведет 1, получив его из аргументов функции f. Функция-стрелка здесь вызвана без параметров, но это не важно: arguments всегда берутся из внешней «обычной» функции.

Сохранение внешнего this и arguments удобно использовать для форвардинга вызовов и создания декораторов.

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

'use strict';

function defer(f, ms) {
  return function() {
    setTimeout(() => f.apply(this, arguments), ms)
  }
}

function sayHi(who) {
  alert('Привет, ' + who);
}

let sayHiDeferred = defer(sayHi, 2000);
sayHiDeferred("Вася"); // Привет, Вася через 2 секунды

Аналогичная реализация без функции-стрелки выглядела бы так:

function defer(f, ms) {
  return function() {
    let args = arguments;
    let ctx = this;
    setTimeout(function() {
      return f.apply(ctx, args);
    }, ms);
  }
}

В этом коде пришлось создавать дополнительные переменные args и ctx для передачи внешних аргументов и контекста через замыкание.

Итого

Основные улучшения в функциях:

  • Можно задавать параметры по умолчанию, а также использовать деструктуризацию для чтения приходящего объекта.
  • Оператор spread (троеточие) в объявлении позволяет функции получать оставшиеся аргументы в массив: function f(arg1, arg2, ...rest).
  • Тот же оператор spread в вызове функции позволяет передать в неё массив как список аргументов (вместо apply).
  • У функции есть свойство name, оно содержит имя, указанное при объявлении функции, либо, если его нет, то имя свойства или переменную, в которую она записана. Есть и некоторые другие ситуации, в которых интерпретатор подставляет «самое подходящее» имя.
  • Объявление Function Declaration в блоке {...} видно только в этом блоке.
  • Появились функции-стрелки:
    • Без фигурных скобок возвращают выражение expr: (args) => expr.
    • С фигурными скобками требуют явного return.
    • Не имеют своих this и arguments, при обращении получают их из окружающего контекста.
    • Не могут быть использованы как конструкторы, с new.
Карта учебника

Комментарии

перед тем как писать…
  • Если вам кажется, что в статье что-то не так - вместо комментария напишите на GitHub.
  • Для одной строки кода используйте тег <code>, для нескольких строк кода — тег <pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
  • Если что-то непонятно в статье — пишите, что именно и с какого места.