Иногда JavaScript может вводить разработчика в заблуждение, а иногда — доводить до белого каления из-за своей неполной консистентности. Есть в JavaScript некоторые вещи, которые только запутывают и сбивают с толку. Самые известные из них оператор with, неявные глобальные переменные и странное поведение при операции сравнения.
Пожалуй, вокруг JavaScript разгоралось больше всего споров в истории программирования. Помимо его недостатков (отчасти рассмотренных в новых спецификациях ECMAScript), большинство программистов недовольны следующими моментами:
- DOM, который многие ошибочно считают эквивалентом самого языка JavaScript, обладает очень неудачным API.
- Когда переходишь на JavaScript с языков С и Java, то попадаешь в ловушку синтаксиса, который устроен не так, как в императивных языках. Это очень часто приводит к багам и сильно раздражает.
В результате JavaScript обрёл довольно плохую репутацию, которой он, в общем-то, не заслуживает. И чаще всего это связано с тем, что многие разработчики переносят на JavaScript свой опыт работы на Java или С/С++. Здесь разобраны три наиболее трудных случая, демонстрирующих разницу в подходах между Java и JavaScript.
Область видимости
Большинство разработчиков переходят на JavaScript в связи с необходимостью. И почти все повторяют одну ошибку — начинают писать код, не изучив предварительно особенности языка. Очень многие хотя бы раз испытывают затруднения с областями видимости.
Синтаксис JavaScript очень похож на используемый в семействе С, с его фигурными скобками, разделяющими конструкции функций,
if
и for
. Поэтому многие разработчики предполагают, что и область видимости на уровне блоков устроена по аналогичным принципам. К сожалению, это не так. Во-первых, область видимости переменных определяется функциями, а не скобками. То есть
if
и for
не создают новую область видимости, а объявленная в их конструкциях переменная, на самом деле, «поднимается». То есть создаётся она в начале самой первой функции, в которой она объявлена, иными словами — в глобальной области видимости.Во-вторых, наличие оператора
with
делает область видимости JavaScript динамической, её нельзя определить до начала выполнения программы. Лучше вообще избегать использования with
, без него JavaScript превращается в язык, использующий лексические области видимости. То есть достаточно будет прочитать код, чтобы понять для себя все области видимости. Формально, в JavaScript существует четыре способа включения идентификатора в область видимости:
- Согласно стандарту языка: по умолчанию, все области содержат идентификаторы this и arguments.
- На основе формальных параметров: область видимости любого формального параметра функции ограничена телом функции.
- С помощью объявления функций.
- С помощью объявления переменных.
Но нужно помнить об одном моменте: объявление (неявное) переменных без использования
var
приводит к неявному определению глобальной области видимости. То же самое относится и к указателю this
, когда функция вызывается без явной привязки.Прежде чем перейти к деталям, следует порекомендовать использовать строгий режим (
'use strict';
) и помещать все объявления переменных и функций в начало каждой функции. Избегайте объявления переменных и функций внутри блоков for
и if
. Поднятие
Этот термин применяется для упрощённого описания того, как на самом деле осуществляется объявление. Поднимаемые переменные объявляются в самом начале содержащих их функций, а затем инициализируются как
undefined
. Присваивание осуществляется непосредственно в той строке, где происходит объявление. Рассмотрим пример:
function myFunction() {
console.log(i);
var i = 0;
console.log(i);
if (true) {
var i = 5;
console.log(i);
}
console.log(i);
}
Как вы думаете, какие значения будут выведены на экран?
undefined
0
5
5
Оператор
var
не объявляет локальную копию переменной i
внутри блока if
. Вместо этого он перезаписывает уже объявленную ранее. Обратите внимание, что первый оператор console.log
выводит действительное значение переменной i
, инициализированной как undefined. А если перейти в строгий режим? В строгом режиме переменные должны объявляться до того, как они будут использованы, однако движок JavaScript не потребует это сделать. Кстати, имейте в виду, что от вас не потребуют и переобъявления var
. Если вам нужно выловить подобные баги, то воспользуйтесь инструментами вроде JSHint или JSLint.Давайте рассмотрим пример, демонстрирующий другой способ объявления переменных, который может привести к ошибкам:
var notNull = 1;
function test() {
if (!notNull) {
console.log("Null-ish, so far", notNull);
for(var notNull = 10; notNull <= 0; notNull++){
//..
}
console.log("Now it's not null", notNull);
}
console.log(notNull);
}
В этом примере блок
if
выполняется, потому что локальная копия переменной notNull
объявлена внутри функции test()
и поднята. Свою роль здесь играет и операция приведения типа.Функциональные выражения и объявления функций
Поднятие может применяться не только к переменным, но также и к функциональным выражениям, которые фактически являются переменными, и к объявлениям функций. Здесь лишь вскользь упомянуто об этой особенности. Если кратко, то объявления функций ведут себя в целом как функциональные выражения, за исключением того, что их объявления размещаются в начале их области видимости.
Вот пример объявления функции:
function foo() {
// A function declaration
function bar() {
return 3;
}
return bar();
// This function declaration will be hoisted and overwrite the previous one
function bar() {
return 8;
}
}
А теперь сравните с примером функционального выражения:
function foo() {
// A function expression
var bar = function() {
return 3;
};
return bar();
// The variable bar already exists, and this code will never be reached
var bar = function() {
return 8;
};
}
Для более глубокого понимания вопроса стоит обратиться к публикациям, указанным в конце поста.
With
В этом примере отражена ситуация, когда область видимости можно определить лишь во время выполнения:
function foo(y) {
var x = 123;
with(y) {
return x;
}
}
Если
y
имеет поле x
, тогда функция foo()
вернёт y.x
, в противном случае — 123
. Подобная практика может привести к возникновению ошибок на стадии выполнения, так что рекомендуется избегать использования оператора with
.Взгляд в будущее: ECMAScript 6
Спецификации ECMAScript 6 позволят внедрить пятый способ определения области видимости на уровне блоков: оператор
let
. function myFunction() {
console.log(i);
var i = 0;
console.log(i);
if (false) {
let i = 5;
console.log(i);
}
console.log(i);
}
В ECMAScript 6 объявление
i
внутри if
с помощью let позволит создавать новую локальную переменную в блоке if
. В качестве нестандартной альтернативы можно объявлять блоки let
:var i = 6;
let (i = 0, j = 2) {
/* Other code here */
}
// prints 6
console.log(i);
В этом примере переменные
i
и j
будут существовать только внутри блока. На момент написания поста только в Chrome поддерживается использование let
. В других языках
Ниже представлена сравнительная таблица особенностей реализации областей видимости в разных языках:
Свойство | Java | Python | JavaScript | Примечание |
---|---|---|---|---|
Область видимости | Лексическая (блоки) | Лексическая (функции, классы или модули) | Да | Работает совсем не так, как в Java или С. |
Блочная область видимости | Да | Нет | В связке с let (ES6) |
Работает совсем не так, как в Java. |
Поднимание | Нет | Нет | Да | Для объявления переменных, функций и функциональных выражений. |
Функции
Ещё одним камнем преткновения в JavaScript зачастую становятся функции. Причина в том, что в императивных языках вроде Java используется совсем иная концепция. JavaScript относится к функциональным языкам программирования. Правда, он не чисто функциональный, всё-таки в нём явно прослеживается императивный стиль и поощряется мутабельность. Но как бы то ни было, JavaScript может быть использован исключительно как функциональный язык, без какого-либо внешнего воздействия на вызовы функций.
В JavaScript с функциями можно обращаться как с любыми другими типами данных, например,
String
или Number
. Их можно хранить в переменных и массивах, передавать в качестве аргументов другим функциям и возвращать другими функциями. У них могут быть свойства, их можно динамически изменять, и всё это благодаря объектам.Для многих новичков в JavaScript удивителен тот факт, что функции здесь являются объектами. Конструктор
Function
создаёт объект Function
:var func = new Function(['a', 'b', 'c'], '');
Это почти аналогично:
function func(a, b, c) { }
Почти — потому что использование конструктора менее эффективно. Он генерирует анонимную функцию и не создаёт замыкания для её контекста. Объекты
Function
всегда создаются в глобальной области видимости.Function
, как разновидность функций, основана на базе Object
. Это хорошо видно, если разобрать любую объявляемую нами функцию:function test() {}
// prints "object"
console.log(typeof test.prototype);
// prints function Function() { [native code] }
console.log(test.constructor);
Это значит, что у функции есть свойства. Некоторые из них назначаются при создании. Например
name
или length
, возвращающие, соответственно, наименование и количество аргументов в определении функции.function func(a, b, c) { }
// prints "func"
console.log(func.name);
// prints 3
console.log(func.length);
Любой функции можно задать и другие свойства, по своему усмотрению:
function test() {
console.log(test.custom);
}
test.custom = 123;
// prints 123
test();
В других языках
Сравнительная таблица реализаций функций в разных языках:
Свойство | Java | Python | JavaScript | Примечание |
---|---|---|---|---|
Функции как встроенные типы | Лямбды, Java 8 | Да | Да | |
Шаблон коллбэков/команд | Объекты (или лямбды для Java 8) | Да | Да | Функции (коллбэки) |
Динамическое создание | Нет | Нет | eval (объект Function ) |
eval вызывает вопросы с точки зрения безопасности, объекты Function могут работать непредсказуемо |
Свойства | Нет | Нет | Могут иметь свойства | Доступ к свойствам функций можно ограничить |
Замыкания
JavaScript был первым из основных языков программирования, в котором появились замыкания. Как вы, вероятно, знаете, в Java и Python долгое время были упрощённые версии замыканий, когда можно было только считывать некоторые значения из объемлющих областей видимости. Скажем, в Java анонимный вложенный класс обеспечивает функциональность, аналогичную замыканиям (с некоторыми ограничениями). Например, в их областях видимости могут использоваться только финальные локальные переменные. Точнее, могут быть считаны их значения.
В JavaScript имеется полный доступ к внешним переменным и функциям внешней области видимости. Их можно считывать, записывать и, при необходимости, даже скрывать с помощью локальных определений. Примеры этого были неоднократно представлены в первой главе.
Ещё интереснее то, что созданная в замыкании функция «помнит» окружение, в котором она была создана. Комбинируя замыкания и вложенность функций, можно сделать так, что внешние функции будут возвращать внутренние без их исполнения. Более того, локальные переменные внешних функций могут сохраняться в замыкании внутренней функции ещё долгое время после исполнения той, где они объявлялись последний раз. Это довольно мощный инструмент, но у него есть один недостаток: распространённая проблема утечки памяти в JavaScript-приложениях.
Для лучшего понимания всего вышесказанного, давайте разберём несколько примеров.
function makeCounter () {
var i = 0;
return function displayCounter () {
console.log(++i);
};
}
var counter = makeCounter();
// prints 1
counter();
// prints 2
counter();
Функция
makeCounter()
создаёт и возвращает другую функцию, которая сохраняет связь со своим родительским окружением. Хотя исполнение makeCounter()
закончилось с присвоением переменной counter, локальная переменная i сохраняется в замыкании displayCounter
, внутри тела которого можно получить к ней доступ. Если снова запустить
makeCounter()
, то она создаст новое замыкание с другим начальным значением i
:var counterBis = makeCounter();
// prints 1
counterBis();
// prints 3
counter();
// prints 2
counterBis();
Можно сделать и так, что
makeCounter()
примет аргумент:function makeCounter(i) {
return function displayCounter () {
console.log(++i);
};
}
var counter = makeCounter(10);
// prints 11
counter();
// prints 12
counter();
Аргументы внешних функций также хранятся в замыкании, так что нам не нужно объявлять локальную переменную. При каждом вызове
makeCounter()
будет запоминаться установленное нами начальное значение, от которого и будет вестись отсчёт.Замыкания крайне важны для многих фундаментальных вещей в JavaScript: пространства имён, модулей, закрытых переменных, мемоизации и т.д. Например, вот так можно смоделировать закрытую переменную для объекта:
function Person(name) {
return {
setName: function(newName) {
if (typeof newName === 'string' && newName.length > 0) {
name = newName;
} else {
throw new TypeError("Not a valid name");
}
},
getName: function () {
return name;
}
};
}
var p = Person("Marcello");
// prints "Marcello"
a.getName();
// Uncaught TypeError: Not a valid name
a.setName();
// Uncaught TypeError: Not a valid name
a.setName(2);
a.setName("2");
// prints "2"
a.getName();
Таким образом можно создавать обёртку для имени свойства с нашими собственными сеттером и геттером. В ES 5 это стало делать гораздо проще, поскольку можно создавать объекты с сеттерами/геттерами для их свойств и тонко настраивать доступ к этим свойствам.
В других языках
Сравнительная таблица реализаций замыканий в разных языках:
Свойство | Java | Python | JavaScript | Примечание |
---|---|---|---|---|
Замыкание | С ограниченными возможностями, только чтение, в анонимных вложенных классах | С ограниченными возможностями, только чтение, во вложенных определениях | Да | Утечки памяти |
Шаблон мемоизации | Необходимо использовать совместно используемые объекты | Возможно с использованием списков или словарей | Да | Лучше использовать отложенные вычисления |
Шаблон пространства имён/модуля | Не нужно | Не нужно | Да | |
Шаблон приватных атрибутов | Не нужно | Невозможно | Да | Может ввести в заблуждение |
Заключение
Итак, в этой статье описаны три особенности JavaScript, которые чаще всего сбивают с толку разработчиков, ранее работавших на других языках программирования, особенно на Java и С. Если вы хотите глубже изучить затронутые темы, можно почитать эти ресурсы:
• Scoping in JavaScript
• Function Declarations vs Function Expressions
• Let statement and let blocks