Статьи

Три странности JavaScript, которые должны знать разработчики Java / C

JavaScript может быть обманчивым языком, и это может быть реальной болью, потому что это не на 100% непротиворечиво. Как хорошо известно, у него есть плохие части , запутанные или избыточные функции, которых следует избегать: печально известные операторы with , неявные глобальные переменные и ошибочное поведение сравнения , вероятно, являются наиболее известными.

JavaScript — один из самых успешных генераторов пламени в истории! Помимо недостатков (которые частично устранены в новых спецификациях ECMAScript), большинство программистов ненавидят JavaScript по двум причинам:

  • DOM, который они ошибочно считают, эквивалентен языку JavaScript, который имеет довольно ужасный API.
  • Они приходят на JavaScript из таких языков, как C и Java. Синтаксис JavaScript обманывает их, заставляя поверить, что он работает так же, как и эти императивные языки. Это неправильное представление приведет к путанице, разочарованию и ошибкам.

Вот почему, как правило, JavaScript имеет худшую репутацию, чем заслуживает.

За свою карьеру я заметил несколько закономерностей: большинство разработчиков языка с фоном Java или C / C ++ предполагают, что языковые функции идентичны в JavaScript, хотя они совершенно разные.

В этой статье собраны самые неприятные из них, сравнивающие путь Java и путь JavaScript, чтобы показать различия и выделить лучшие практики в JavaScript.

Обзорный

Большинство разработчиков начинают работать над JavaScript, потому что они вынуждены, и почти каждый из них начинает писать код, прежде чем начать изучать язык. Каждый такой разработчик был обманут областью JavaScript хотя бы один раз.

Поскольку синтаксис JavaScript очень напоминает (нарочно) языки семейства C, с помощью фигурных скобок, разграничивающих function if , и for , можно разумно ожидать лексическую область действия уровня блока . К сожалению, это не так.

Во-первых, в JavaScript область видимости переменной определяется функциями, а не скобками. Другими словами, if и for тел не создается новая область, и переменная, объявленная внутри их тел, фактически поднимается , то есть создается в начале самой внутренней функции, в которой она объявлена, или глобальной области, в противном случае.

Во-вторых, наличие оператора with делает область видимости JavaScript динамической, которую невозможно определить до времени выполнения. Вы, возможно, не удивитесь, узнав, что использование оператора with устарело: JavaScript, лишенный with , фактически будет языком с лексической областью, то есть область действия может быть полностью определена, если взглянуть на код.

Формально в 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 

Внутри блока if оператор var не объявляет локальную копию переменной i , а перезаписывает объявленную ранее. Обратите внимание, что первый оператор console.log печатает фактическое значение переменной i , которое инициализируется как undefined . Вы можете проверить это с помощью "use strict"; директива как первая строка в функции. В строгом режиме переменные должны быть объявлены перед использованием, но вы можете проверить, что движок JavaScript не будет жаловаться на объявление. Обратите внимание, что вы не получите жалоб на повторное выделение переменной var : если вы хотите отлавливать такие ошибки, вам лучше обработать код с помощью linter, такого как 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; }; } 

См. Раздел ссылок для дальнейшего понимания этих концепций.

С

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

 function foo(y) { var x = 123; with(y) { return x; } } 

Если y есть поле с именем x , то функция foo() вернет yx , в противном случае вернет 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 с помощью let внутри тела if создаст новую переменную, локальную для блока if . В качестве нестандартной альтернативы можно объявить блоки let следующим образом:

 var i = 6; let (i = 0, j = 2) { /* Other code here */ } // prints 6 console.log(i); 

В приведенном выше коде переменные i и j будут существовать только внутри блока. На момент написания, поддержка let была ограничена даже для Chrome.

Сфера в двух словах

Следующая таблица суммирует область действия на разных языках:

Особенность Джава питон JavaScript Предупреждения
Сфера Лексический (блок) Лексический (функция, класс или модуль) да Это работает очень по-другому от Java или C
Блок область да нет ключевое слово `let` (ES6) Опять же, предупреждение: это не Java!
Подъемно Ни за что! нет да Для переменных и функциональных выражений поднимается только объявление. Для объявлений функций определение также поднимается

функции

Другая очень неправильно понятая особенность JavaScript — это функции, особенно потому, что в императивных языках программирования, таких как Java нет такого понятия как функция.

Фактически, JavaScript — это функциональный язык программирования. Что ж, не чисто функциональный язык программирования, как у Haskell — в конце концов он все еще имеет императивный стиль, и изменчивость поощряется, а не просто разрешается, как для Scala . Тем не менее, JavaScript можно использовать как чисто функциональный язык программирования с вызовами функций, лишенными каких-либо побочных эффектов.

Граждане первого класса

Функции в JavaScript могут рассматриваться как любой другой тип, например String и Number : они могут храниться в переменных, передаваться в качестве аргументов функциям, возвращаться функциями и храниться в массивах. Функции также могут иметь свойства и могут изменяться динамически, потому что…

Объекты

Для большинства новичков в JavaScript очень удивительным является то, что функции на самом деле являются объектами. В JavaScript каждая функция на самом деле является объектом Function . Конструктор Function создает новый объект Function :

 var func = new Function(['a', 'b', 'c'], ''); 

Это (почти) эквивалентно:

 function func(a, b, c) { } 

Я сказал, что они почти эквивалентны, потому что использование конструктора Function менее эффективно, создает анонимную функцию и не создает замыкание в контексте ее создания. Объекты 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:

Особенность Джава питон JavaScript Предупреждения
Функции как встроенные типы Лямбда, Ява 8 да да
Обратные вызовы / шаблон команд Объекты (или лямбды для Java 8) да да Функции (обратные вызовы) имеют свойства, которые могут быть изменены «клиентом»
Динамическое создание нет нет `eval` — объект` Function` У `eval` есть проблемы безопасности, и объекты` Function` могут работать неожиданно
свойства нет нет Может иметь свойства Доступ к свойствам функции не может быть ограничен

Затворы

Если бы мне пришлось выбирать свою любимую функцию JavaScript, я бы, конечно, пошел на замыкания. 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() создает и возвращает другую функцию, которая отслеживает среду, в которой она была создана. Хотя выполнение makeCounter() заканчивается, когда назначается переменный counter , локальная переменная i хранится в displayCounter и поэтому может быть доступна внутри ее тела.

Если бы мы снова запустили makeCounter , он бы создал новое замыкание с другой записью для i :

 var counterBis = makeCounter(); // prints 1 counterBis(); // prints 3 counter(); // prints 2 counterBis(); 

Чтобы было немного интереснее, мы можем обновить makeCounter() чтобы она 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(); 

С помощью этого шаблона, используя замыкания, мы можем создать оболочку для имени свойства с помощью нашего собственного метода установки и получения. ES5 сделал это намного проще, поскольку вы можете создавать объекты с геттерами и сеттерами для их свойств, а также контролировать доступ к самим свойствам с максимальной точностью.

Закрытие в двух словах

Следующая таблица описывает закрытие в Java, Python и JavaScript:

Особенность Джава питон JavaScript Предупреждения
закрытие Ослабленный, только для чтения, в анонимных внутренних классах Ослабленный, только для чтения, во вложенном def да Утечки памяти
Шаблон памятки Необходимо использовать общие объекты Возможно использование списков или словарей да Лучше использовать ленивую оценку
Пространство имен / Шаблон модуля Не нужно Не нужно да
Шаблон личных атрибутов Не нужно Невозможно да Может запутаться

Вывод

В этой статье я рассмотрел три функции JavaScript, которые часто неправильно понимают разработчики из разных языков, особенно Java и C. В частности, мы обсуждали такие понятия, как область видимости, хостинг, функции и замыкания. Если вы хотите углубленно изучить эти темы, вот список статей, которые вы можете прочитать: