Три особенности JavaScript

от автора

Иногда 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

ссылка на оригинал статьи http://habrahabr.ru/post/257617/


Комментарии

Добавить комментарий

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