Read this article in English, Chinese.
- Значения по умолчанию из ES5
- ES6 значения по умолчанию: базовый пример
- Детали реализации
- Заключение
В этой небольшой заметке мы затронем еще одну тему ES6 — параметры функций со значениями по умолчанию. Как мы увидим, там есть некоторые тонкости.
Значения по умолчанию из ES5
Раньше значения параметров по умолчанию обрабатывались вручную несколькими альтернативными способами:
function log(message, level) { level = level || 'warning'; console.log(level, ': ', message); } log('low memory'); // warning: low memory log('out of memory', 'error'); // error: out of memory
Для того, чтобы избежать обработку возможных “ложных значений” (“falsey values”), часто можно было встретить использование typeof
для этих целей:
if (typeof level == 'undefined') { level = 'warning'; }
Иногда также можно встретить проверку arguments.length
:
if (arguments.length == 1) { level = 'warning'; }
Данные подходы работали достаточно хорошо, однако все они являются слишком “ручными” и менее абстрактным. ES6 стандартизировал синтаксическую конструкцию для определения значений по умолчанию параметров, прямо в заголовке функции.
ES6 значения по умолчанию: базовый пример
Значения параметров по умолчанию присутствуют во многих языках, поэтому базовая форма, вероятно, должна быть знакома большинству программистов:
function log(message, level = 'warning') { console.log(level, ': ', message); } log('low memory'); // warning: low memory log('out of memory', 'error'); // error: out of memory
Вполне обычная обработка значений по умолчанию, и вместе с тем — удобная. Стоит рассмотреть детали реализации для уточнения и разъяснения некоторых неочевидных особенностей, возникающих при работе со значениями по умолчанию.
Детали реализации
Ниже представлены несколько специфичных для ES6 деталей реализации, связанных с обработкой значений параметров по умолчанию.
Вычисление значения во время активации функции
В отличие от некоторых других языков (например, Python), которые могут вычислять значения по умолчанию единожды, во время создания функции, ECMAScript вычисляет эти значения при каждом очередном вызове функции. Данный выбор в дизайне языка был сделан во избежание путаницы, которая может возникнуть при обработке объектов в качестве значений по умолчанию. Рассмотрим следующий пример:
def foo(x = []): x.append(1) return x # Видим, что значения по умолчанию вычислены единожды, # когда функция создается, и хранятся в качестве # обычного свойства функции. print(foo.__defaults__) # ([],) foo() # [1] foo() # [1, 1] foo() # [1, 1, 1] # Причина этому, как мы уже отметили: print(foo.__defaults__) # ([1, 1, 1],)
Чтобы избежать подобной ситуации, Python-программисты часто прибегают к трюку хранения значения по умолчанию как None
, и использованию затем явной ручной проверки:
def foo(x = None): if x is None: x = [] x.append(1) print(x) print(foo.__defaults__) # (None,) foo() # [1] foo() # [1] foo() # [1] print(foo.__defaults__) # ([None],)
Однако данный подход вновь возвращает нас к неудобной ручной обработке фактического значения по умолчанию, а первоначальная версия — и попросту вызывает путаницу. Как уже было отмечено, во избежание подобных случаев, ECMAScript вычисляет значения по умолчанию каждый раз при вызове функции:
function foo(x = []) { x.push(1); console.log(x); } foo(); // [1] foo(); // [1] foo(); // [1]
Все хорошо и интуитивно. Теперь, давайте, посмотрим, когда семантика ES также может вызывать путаницу, если не знать, как данный механизм работает.
Затенение внешней области видимости
Рассмотрим следующий пример:
var x = 1; function foo(x, y = x) { console.log(y); } foo(2); // 2, не 1!
Здесь мы получаем значение 2
для y
, а не 1
, как визуально могло бы показаться. Причина этому в том, что x
из параметров — не тот же, что и из глобальный области видимости. И поскольку вычисление значений происходит при вызове функции, то при присвоении = x
, значение x
уже вычисляется во внутренней области видимости, и относится к самому параметру x
. Т.е., параметр x
затенил (shadowed) глобальную переменную с таким же именем, и каждое обращение к имени x
, уже относится к параметру.
Временная “мертвая” зона для параметров
ES6 отмечает, так называемую, временную “мертвую” зону (Temporal Dead Zone, сокр. TDZ) — это участок кода, где переменная или параметр не могут быть использованы до момента их инициализации (т.е. до того, как они получат значение).
Относительно параметров — параметр не может иметь значение по умолчанию самого себя:
var x = 1; function foo(x = x) { // ошибка! ... }
Присваивание = x
, как было отмечено, вычисляет x
в области видимости параметров, которая затеняет глобальный x
. Однако параметр x
, будучи в “мертвой зоне”, не может быть вычислен до того, как ему будет присвоено начальное значение. Очевидно, параметр не может инициализировать сам себя.
Обратим внимание, что предыдущий пример выше с y
корректен, поскольку x
уже инициализирован (ему присвоено неявное значение по умолчанию undefined
, при входе в контекст). Покажем еще раз:
function foo(x, y = x) { // OK ... }
Данное поведение доступно, поскольку ECMAScript инициализирует параметры в порядке слева направо, и мы имеем x
уже доступным для использования.
Мы отмечали, что параметры связаны уже с некой “внутренней областью видимости”. Из ES5 мы могли бы предположить, что это область видимости тела функции. Однако, данный момент несколько усложнен: это может быть область видимости тела функции или же, это может быть промежуточная область видимости, созданная специально для хранения параметров. Давайте рассмотрим эти области видимости.
Условная промежуточная область видимости для параметров
В сущности, если какой-либо (хотя бы один) из параметров содержит значение по умолчанию, ES6 создает промежуточную область видимости (intermediate scope) для хранения параметров, и эта область не разделяется с областью видимости тела функции. Это главное отличие от ES5 в этом отношении. Продемонстрируем на примере:
var x = 1; function foo(x, y = function() { x = 2; }) { var x = 3; y(); // Параметр `x` разделяется с телом функции? console.log(x); // Нет, по-прежнему 3, не 2 } foo(); // И внешний `x` также остался нетронутым. console.log(x); // 1
В данном случае у нас имеется три области видимости: глобальная область, область параметров, и область функции:
: {x: 3} // внутренняя -> {x: undefined, y: function() { x = 2; }} // параметры -> {x: 1} // глобальная
Нетрудно заметить, что когда запускается функция y
, она находит x
в ближайшей области видимости (т.е. в той же области), и даже не видит область видимости тела функции.
Компиляция в ES5
Если нам необходимо скомпилировать ES6 код в ES5, то данная промежуточная область видимости будет выглядеть следующим образом:
// ES6 function foo(x, y = function() { x = 2; }) { var x = 3; y(); // Параметр `x` разделяется с телом функции? console.log(x); // Нет, по-прежнему 3, не 2 } // Скомпилированный to ES5 function foo(x, y) { // Установка значений по умолчанию. if (typeof y == 'undefined') { y = function() { x = 2; }; // теперь видим, что это `x` из параметров } return function() { var x = 3; // теперь видим, что этот `x` из тела функции y(); console.log(x); }.apply(this, arguments); }
Причина для области видимости параметров
Однако, в чем реальное назначение данной промежуточной области видимости, выделяемой для параметров? Почему мы не могли так же, как и в ES5, дальше создавать параметры в той же области видимости, что и тело функции? Причина этому: переменные в теле функции с теми же именами не должны воздействовать на замкнутые переменные с таким же именами из внешний областей видимости.
Покажем на примере:
var x = 1; function foo(y = function() { return x; }) { // замыкаем `x` var x = 2; return y(); } foo(); // правильно 1, не 2
Если бы мы создали функцию-параметр y
в теле функции, она бы замкнула внутренний x
, т.е. 2
. Однако очевидно, что функция y
должна захватить внешний x
, т.е. 1
(но только в том случае, если он не затенен параметром с тем же именем, как мы отмечали).
В то же время, мы не можем создать функцию y
во внешней области видимости, поскольку это означало бы невозможность обращения к параметрам из такой функции, а мы должны иметь эту возможность:
var x = 1; function foo(y, z = function() { return x + y; }) { // можем видеть `x` и `y` var x = 3; return z(); } foo(1); // 2, не 4
Когда область параметров не создается
Описанная выше семантика в корне отличается от тех случаев, когда мы обрабатываем значения по умолчанию вручную:
var x = 1; function foo(x, y) { if (typeof y == 'undefined') { y = function() { x = 2; }; } var x = 3; y(); // Параметр `x` разделяется с телом функции? console.log(x); // да! 2 } foo(); // И внешний `x` вновь остался нетронутым. console.log(x); // 1
И это интересная особенность: в случае, если функция не содержит параметров со значениями по умолчанию, то промежуточная область видимости не создается. В подобном случае параметры находятся в области видимости тела функции, т.е. работают в режиме ES5.
Здесь может возникнуть ряд справедливых вопросов. Зачем нужны подобные усложнения? Почему бы всегда не создавать эту промежуточную область видимости для хранения параметров? Сделано ли это лишь с целью оптимизации? Не совсем. Причина здесь именно в обратной совместимость с ES5: пример выше с ручной обработкой значений по умолчанию должен изменять x
из тела функции (который является параметром и находится в самой области видимости тела функции).
Обратите внимание также, что подобные повторные объявления переменных доступны лишь при использовании ключевого слова var
и функций. При использовании let
или const
с таким же именем перезаписать параметр невозможно:
function foo(x = 5) { let x = 1; // ошибка const x = 2; // ошибка }
Проверка на undefined
Другим интересным фактом является то, что принятие решения, использовать ли значение по умолчанию, осуществляется именно проверкой значения на undefined
. Продемонстрируем на примере:
function foo(x, y = 2) { console.log(x, y); } foo(); // undefined, 2 foo(1); // 1, 2 foo(undefined, undefined); // undefined, 2 foo(1, undefined); // 1, 2
Обычно в языках программирования параметры со значениями по умолчанию идут после обязательных параметров. Однако факт, описанный выше, позволяет нам иметь следующие конструкции в JavaScript:
function foo(x = 2, y) { console.log(x, y); } foo(1); // 1, undefined foo(undefined, 1); // 2, 1
Значения по умолчанию деструктурированных компонентов
Значения по умолчанию также участвуют в операциях деструктивного присваивания (destructuring assignment). Тема деструктивного присваивания подробно не рассматривается в данной статье, однако, давайте покажем несколько небольших примеров. Обработка значений по умоланию в случае деструктуризации параметра функции осуществляется по тому же правилу: “две области видимости, если необходимо”.
function foo({x, y = 5}) { console.log(x, y); } foo({}); // undefined, 5 foo({x: 1}); // 1, 5 foo({x: 1, y: 2}); // 1, 2
Также, значения по умолчанию компонентов при деструктуризации более общие и могут быть использованы не только в функциях:
var {x, y = 5} = {x: 1}; console.log(x, y); // 1, 5
Заключение
Надеемся, что данная небольшая заметка помогла объяснить детали реализации значений по умолчани в ES6. Обратите внимание, что на момент написания статьи (21 августа 2014 г.), ни одна из реализаций не обрабатывает значения по умолчанию корректно (все они создают лишь одну область видимости, которая разделяется с телом функции), поскольку данная “промежуточная область видимости” была добавлена в черновик спецификации лишь недавно. Значения по умолчанию, несомненно, являются полезным дополнением в ES, которое сделает наш код более кратким и элегантным.
Автор перевода: Дмитрий Сошников
Дата перевода: 25 сентября 2014 г.
Автор оригинала: Дмитрий Сошников
Дата публикации: 21 августа 2014 г.
(моему давнему Интернет-коллеге по JS, Zeroglif)
Спасибо большое за статью! Ну и за все предыдущие заодно, давно вас читаю 😉
А за переводы отдельная благодарность, очень приятно видеть действительно качественный материал на родном языке. Даже непривычно как-то 🙂
@Семён, спасибо, рад, что оказалось полезным!
Да, согласен с Семёном, большое вам спасибо за статьи! Вы отлично пишете и всегда по делу! Многому у вас научился!
@Иван, благодарю, рад, что полезно!