Скачать курсы книги - Нужны ли фигурные скобки в однострочных операциях в JavaScript? Создание понятного кода на JavaScript.

20.05.2019

Допустим, я пишу

If(someVal) alert("True");

Затем приходит следующий разработчик и говорит: «О, мне нужно сделать что-то еще», поэтому они пишут

If(someVal) alert("True"); alert("AlsoTrue");

Теперь, как вы можете видеть, «AndTrue» всегда будет правдой, потому что первый разработчик не использовал фигурные скобки.

2018-12-04T00:00Z

Абсолютно да

Забудьте о «Это личное предпочтение», «код будет работать отлично», «он отлично работает для меня», «это более читаемо» yada yada BS.

Аргумент: «Это личное предпочтение»

Нет. Если вы не один человек, выходящий на Марс, нет. Большую часть времени будут другие люди, читающие / изменяющие ваш код. В любой серьезной команде кодирования это будет рекомендуемым способом, поэтому это не «личное предпочтение».

Аргумент: «код будет работать просто отлично»

Так же и код спагетти! Означает ли это, что это нормально, чтобы создать его?

Аргумент: «Он отлично работает для меня»

В моей карьере я видел так много ошибок, созданных из-за этой проблемы. Вероятно, вы не помните, сколько раз вы прокомментировали "DoSomething()" и сбиты с толку, почему "SomethingElse()" вызывается:

If (condition) DoSomething(); SomethingElse();

If (condition) DoSomething(); SomethingMore();

Вот пример реальной жизни. Кто-то хотел включить все протоколирование, чтобы они запускали find & replace "Console.println" => //"Console.println" :

If (condition) Console.println("something"); SomethingElse();

См. Проблему?

Даже если вы думаете: «Это настолько тривиально, что я никогда этого не сделаю»; помните, что всегда будет член команды с более низкими навыками программирования, чем вы (надеюсь, вы не худшие в команде!)

Аргумент: «это более читаемо»

Если я что-то узнал о программировании, то это очень просто. Очень распространено, что это:

If (condition) DoSomething();

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

If (a != null) if (condition) DoSomething(); else DoSomethingElse(); DoSomethingMore(); else if (b == null) alert("error b"); else alert("error a");

И сравните это с этим:

If (a != null) { if (condition) { DoSomething(); } else { DoSomethingElse(); DoSomethingMore(); } } else if (b == null) { alert("error b"); } else { alert("error a"); }

PS: Бонусные очки идут к тому, кто заметил ошибку в приведенном выше примере

2018-12-11T00:00Z

В дополнение к причине, упомянутой @Josh K (что также относится к Java, C и т. Д.), Одной из специальных проблем в JavaScript является автоматическая вставка точки с запятой . Из примера в Википедии:

Return a + b; // Returns undefined. Treated as: // return; // a + b;

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

If (x) return a + b;

На самом деле не намного лучше писать

If (x) { return a + b; }

но, может быть, здесь ошибка немного легче обнаружить (?)

2018-12-18T00:00Z

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

Например, вопрос спрашивает, нормально ли это:

If (condition) statement;

Он не спрашивает, все ли в порядке:

If (condition) statement;

Я думаю, что оставлять скобки предпочтительнее, потому что это делает код более читаемым с менее излишним синтаксисом.

Мой стиль кодирования заключается в том, чтобы никогда не использовать скобки, если код не является блоком. И никогда не использовать несколько операторов в одной строке (разделенных точкой с запятой). Я считаю, что это легко читать и очищать и никогда не иметь проблем, связанных с высказываниями «если». В результате, используя скобки в одном условии if, требуется 3 строки. Как это:

If (condition) { statement; }

Использование одной строки, если оператор предпочтительнее, потому что он использует меньше вертикального пространства, а код более компактен.

Я бы не принуждал других использовать этот метод, но он работает для меня, и я не мог больше не соглашаться с примерами, приведенными в том, как исключить скобки из-за ошибок кодирования / определения области видимости.

2018-12-25T00:00Z

нет

Это совершенно верно

If (cond) alert("Condition met!") else alert("Condition not met!")

Эта же практика следует во всех языках стиля синтаксиса C с привязкой. C, C ++, Java, даже PHP все поддерживают один оператор строки без брекетов. Вы должны понимать, что вы сохраняете только два персонажа, а с помощью некоторых стилей для некоторых людей вы даже не сохраняете линию. Я предпочитаю полный стиль фигурной скобки (например, следующий), поэтому он имеет тенденцию быть немного дольше. Компромисс встречается очень хорошо с тем, что у вас очень четкая читаемость кода.

If (cond) { alert("Condition met!") } else { alert("Condition not met!") }

2019-01-01T00:00Z

Начальный уровень отступа оператора должен быть равен числу открытых фигурных скобок над ним. (исключая цитируемые или комментируемые фигурные скобки или символы в директивах препроцессора)

В противном случае K & R будет хорошим отступом. Чтобы исправить их стиль, я рекомендую размещать короткие простые операторы if на одной строке.

Третья часть урока, посвященного коду JavaScript. Простые рекомендации для создания легко поддерживаемого проекта.

Избегайте подразумеваемого преобразования типов

JavaScript подразумевает преобразование типов переменных при их сравнении. Поэтому такие сравнения как false == 0 или "" == 0 возвращают true .

Чтобы избежать проблем, вызванных подразумеваемым преобразованием типов, всегда используйте операторы === и!== для проверки и значений и типов сравниваемых выражений:

Существует другая школа программирования, в рамках которой принято считать, что чрезмерно использовать оператор === , когда достаточно оператора == . Например, когда используется typeof , который возвращает строку, нет причин требовать жесткого соответствия. Но JSLint требует жесткого соответствия. И, кроме того, код будет выглядеть более целостным и уменьшит количество размышлений при чтении ("Данный оператор == используется намеренно или по ошибке?").

Избегайте использования eval()

Функция eval() берет произвольную строку и выполняет ее как код JavaScript. Если код известен (а не определяется в ходе выполнения процесса), вообще нет причин использовать eval() . Если код динамически генерируется во время выполнения, то часто возможно достичь цели лучшим методом, чем использование eval() .Например, использование записи с квадратными скобками для динамического доступа к свойствам лучше и проще:

// плохо var property = "name"; alert(eval("obj." + property)); // предпочтительно делать так var property = "name"; alert(obj);

Использование eval() также может повлиять на безопасность, так как вы можете выполнять код (например, полученный из сети), который наносит ущерб. Достаточно распространенная порочная практика работы с ответом JSON на запрос AJAX. В данном случае лучше использовать встроенные методы браузера для разбора ответа JSON, чтобы решить задачу безопасным методом и правильно. Для браузеров, которые не поддерживают JSON.parse(), можно использовать библиотеку с JSON.org.

Также важно помнить, что передача строк функциям setInterval() , setTimeout() , и конструктору Function() в большинстве случаев похоже на использование eval(). Следовательно, таких действий надо избегать. На заднем плане JavaScript оценивает и выполняет строки, которые вы передаете, как программный код:

// плохо setTimeout("myFunc()", 1000); setTimeout("myFunc(1, 2, 3)", 1000); // предпочтиетльно setTimeout(myFunc, 1000); setTimeout(function () { myFunc(1, 2, 3); }, 1000);

Использование нового конструктора new Function() похоже на eval() , поэтому к нему надо относиться с осторожностью. Это мощный инструмент, но часто используемый неправильно. Если вам абсолютно необходимо использовать eval() , рассмотрите вместо него использование new Function() . Есть небольшое потенциальное преимущество, так как код, определяемый в new Function(), будет запускаться в локальном пространстве функции, поэтому переменные, определенные с директивой var в определяемом коде не будут становиться глобальными автоматически. Другой способ избежать автоматического определения глобальных переменных - оборачивать вызов eval() в функцию.

Рассмотрим следующий пример. Здесь только un остается глобальной переменной, загрязняющей пространство имен:

Console.log(typeof un); // "undefined" console.log(typeof deux); // "undefined" console.log(typeof trois); // "undefined" var jsstring = "var un = 1; console.log(un);"; eval(jsstring); // Записывает "1" jsstring = "var deux = 2; console.log(deux);"; new Function(jsstring)(); // Записывает "2" jsstring = "var trois = 3; console.log(trois);"; (function () { eval(jsstring); }()); // Записывает "3" console.log(typeof un); // число console.log(typeof deux); // undefined console.log(typeof trois); // undefined

Другое отличие между eval() и конструктора new Function() заключается в том, что eval() может пересекаться с цепочкой пространства имен, а выполнение Function происходит в песочнице. Не важно, где вы выполняете Function , она использует только глобальное пространство имен. Поэтому она меньше загрязняет локальное пространство имен. В следующем примере eval() может получить доступ и модифицировать переменные в своем внешнем пространстве имен, а Function не может (использование Function и new Function идентично):

(function () { var local = 1; eval("local = 3; console.log(local)"); // Записывает "3" console.log(local); // Записывает "3" }()); (function () { var local = 1; Function("console.log(typeof local);")(); // Записывает "undefined" }());

Преобразование числа с помощью parseInt()

Используя parseInt(), вы можете получить число из строки. Функция принимает второй параметр - основание системы счисления, который часто опускается. А зря. Проблема проявляется, когда надо разобрать строку, начинающуюся с 0: например, часть даты, которую вводят в поле формы. Строка, которая начинается на 0, обрабатывается как восьмеричное число (основание 8), что было определено в ECMAScript 3 (но изменено в ECMAScript 5). Для исключения несовместимости и неожиданных результатов всегда следует использовать параметр основания системы счисления:

Var month = "06", year = "09"; month = parseInt(month, 10); year = parseInt(year, 10);

В данном примере, если вы опустите параметр основания системы счисления (вызовете функцию как parseInt(year)), то получите значение 0 , потому что “ 09 ” подразумевается как восьмеричное число (как будто вы сделали вызов parseInt(year, 8)), а 09 - неправильное число по основанию 8 .

Альтернативные методы преобразования строки в число:

+"08" // результат 8 Number("08") // 8

Данные методы часто выполняются быстрее parseInt() , потому что parseInt() делает разбор строки, а не простое конвертирование. Но если вы предполагаете, что ввод может быть в виде “08 hello”, то parseInt() вернет число, а другие методы - потерпят неудачу с возвратом NaN .

Требования к коду

Важно составить требования к коду и следовать им — такой шаг сделает код целостным, предсказуемым и существенно более легким и понятным. Новый разработчик вашей команды, следующий требования к коду, существенно быстрее войдет в рабочий ритм, воспринимая код, написанный другими участниками проекта.

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

Отступы

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

А где следует делать отступы? Правило простое - везде, где есть фигурные скобки. То есть в теле функций, циклах (do, while, for, for-in), операторах if и switch , и свойствах object . Следующий код показывает примеры использования отступов:

Function outer(a, b) { var c = 1, d = 2, inner; if (a > b) { inner = function () { return { r: c - d }; }; } else { inner = function () { return { r: c + d }; }; } return inner; }

Фигурные скобки

Фигурные скобки всегда следует использовать, даже в случаях, когда они являются опциями. Технически, когда вы имеете только одно выражение в операторе if или for , фигурные скобки не требуются, но их следует использовать все равно. Они делают код более целостным и простым для расширения.

Представим, что у вас есть цикл с одним выражением. Вы можете опустить скобки, что не будет синтаксической ошибкой:

// плохо for (var i = 0; i < 10; i += 1) alert(i);

Но что если позже потребуется добавить еще одну строку в тело цикла?

// плохо for (var i = 0; i < 10; i += 1) alert(i); alert(i + " is " + (i % 2 ? "odd" : "even"));

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

// предпочтительно for (var i = 0; i < 10; i += 1) { alert(i); }

Также и для условий:

// плохо if (true) alert(1); else alert(2); // предпочтительно if (true) { alert(1); } else { alert(2); }

Положение открытой скобки

Часто возникает вопрос, где располагать открытую скобку - на той же строке или на следующей?

If (true) { alert("Сообщение!"); }

If (true) { alert("Сообщение"); }

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

// предупреждение: неожиданный return function func() { return // далее следует блок кода, который никогда не выполнится { name: "Batman" } }

Если вы ожидали, что данная функция вернет объект со свойством name , то будет неприятно удивлены. По причине подразумеваемой точки с запятой функция вернет undefined . Предыдущий код эквивалентен следующему блоку:

// предупреждение: неожиданный return function func() { return undefined; // далее следует блок кода, который никогда не выполнится { name: "Batman" } }

Function func() { return { name: "Batman" }; }

Обратите внимание на точку с запятой. Так же, как и фигурные скобки, всегда следует использовать точку с запятой, даже когда она подразумевается конструкцией кода JavaScript. Это не только дисциплинирует и соответствует более строгому подходу к кодированию, но и помогает избежать двусмысленных ситуаций, как в вышеприведённом примере.

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

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

1. Пропуск фигурных скобок

Одна из ошибок, которую часто допускают новички JavaScript — пропуск фигурных скобок после операторов типа if, else,while и for . Хотя это не запрещается, вы должны быть очень осторожны, потому что это может стать причиной скрытой проблемы и позже привести к ошибке.

Смотрите пример, приведенный ниже:

JS

// Этот код не делает то, что должен! if(name === undefined) console.log("Please enter a username!"); fail(); // До этой строки исполнение никогда не дойдет: success(name); } function success(name){ console.log("Hello, " + name + "!"); } function fail(){ throw new Error("Name is missing. Can"t say hello!"); }

Хотя вызов fail() имеет отступ и, кажется, будто он принадлежит оператору if , это не так. Он вызывается всегда. Так что это полезная практика окружать все блоки кода фигурными скобками, даже если в них присутствует только один оператор.

2. Отсутствие точек с запятой

Во время парсировки JavaScript осуществляется процесс, известный как автоматическая расстановка точек с запятой. Как следует из названия, анализатор расставляет недостающие знаки вместо вас.

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

Вот пример:

JS

// Результатом обработки этого кода станет вывод сообщения об ошибке. Добавление точки с запятой решило бы проблему. console.log("Welcome the fellowship!") ["Frodo", "Gandalf", "Legolas", "Gimli"].forEach(function(name){ hello(name) }) function hello(name){ console.log("Hello, " + name + "!") }

Так как в строке 3 отсутствует точка с запятой, анализатор предполагает, что открывающаяся скобка в строке 5 является попыткой доступа к свойству, используя синтаксис массива аксессора (смотри ошибку № 8), а не отдельным массивом, который является не тем, что предполагалось.

Это приводит к ошибке. Исправить это просто — всегда вставляйте точку с запятой.

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

3. Непонимание приведений типа

JavaScript поддерживает динамические типы. Это означает, что вам не нужно указывать тип при объявлении новой переменной, вы можете свободно переназначить или конвертировать его значение.

Это делает JavaScript гораздо более простым, чем, скажем, C# или Java . Но это таит в себе потенциальную опасность ошибок, которые в других языках выявляются на этапе компиляции.

Вот пример:

JS

// Ожидание события ввода из текстового поля var textBox = document.querySelector("input"); textBox.addEventListener("input", function(){ // textBox.value содержит строку. Добавление 10 содержит // строку "10", но не выполняет ее добавления.. console.log(textBox.value + " + 10 = " + (textBox.value + 10)); });

HTML

Проблема может быть легко исправлена с применением parseInt(textBox.value, 10) , чтобы перевести строку в число перед добавлением к ней 10.

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

Чтобы не допустить преобразования типа при сравнении переменных в операторе if , вы можете использовать проверку строгого равенства (=== ).

4. Забытые var

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

В первый раз, когда он увидит, что вы использовали переменную без оператора var , он автоматически объявит его глобально. Это может привести к некоторым ошибкам.

Вот пример, который кроме того иллюстрирует и другую ошибку — недостающую запятую при объявлении сразу нескольких переменных:

JS

var a = 1, b = 2, c = 3; function alphabet(str){ var a = "A", b = "B" // Упс, здесь пропущена ","! c = "C", d = "D"; return str + " " + a + b + c + "…"; } console.log(alphabet("Let"s say the alphabet!")); // О, нет! Что-то не так! У c новое значение! console.log(a, b, c);

Когда анализатор достигает строки 4, он автоматически добавит точку с запятой, а затем интерпретирует объявления c и d в строке 5, как глобальные.

Это приведет к изменению значения другой переменной c. Больше о подводных камнях JavaScript здесь .

5. Арифметические операции с плавающей точкой

Эта ошибка характерна практически для любого языка программирования, в том числе и JavaScript . Из-за способа, которым числа с плавающей точкой представляются в памяти , арифметические операции выполняются не совсем так, как вы думаете.

Например:

JS

var a = 0.1, b = 0.2; // Сюрприз! Это неправильно: console.log(a + b == 0.3); // Потому что 0.1 + 0.2 не дает в сумме то число, что вы ожидали: console.log("0.1 + 0.2 = ", a + b);

Чтобы обойти эту проблему, вы не должны использовать десятичные числа, если вам нужна абсолютная точность — используйте целые числа, или если вам нужно работать с денежными единицами, используйте библиотеку типа bignumber.js .

6. Использование конструкторов вместо оригинальных обозначений

Когда программисты Java и C # начинают писать на JavaScript, они часто предпочитают создавать объекты с использованием конструкторов: new Array(), new Object(), new String() .

JS

var elem4 = new Array(1,2,3,4); console.log("Four element array: " + elem4.length); // Создание массива из одного элемента. Это не работает так, как вы думаете: var elem1 = new Array(23); console.log("One element array? " + elem1.length); /* Объекты строки также имеют свои особенности */ var str1 = new String("JavaScript"), str2 = "JavaScript"; // Строгое равенство не соблюдается: console.log("Is str1 the same as str2?", str1 === str2);

Решение этой проблемы просто: попробуйте всегда использовать буквальные оригинальные
обозначения. Кроме того, в JS не обязательно указывать размер массивов заранее.

7. Непонимание того, как разделяются диапазоны

Одна из трудных для понимания новичками вещей в JS , это правила разграничения и закрытия диапазонов. И это действительно не просто:

JS

for(var i = 0; i < 10; i++){ setTimeout(function(){ console.log(i+1); }, 100*i); } /* Чтобы исправить проблему, заключите код в выражение самовыполняющейся функции: for(var i = 0; i < 10; i++){ (function(i){ setTimeout(function(){ console.log(i+1); }, 100*i); })(i); } */

Функции сохраняют связь с переменными в пределах родительских диапазонов. Но поскольку мы откладываем выполнение через setTimeout , когда наступит время для запуска функций, то цикл будет уже фактически завершен и переменная i увеличивается до 11.

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

8. Использование Eval

Eval есть зло . Это считается плохой практикой, и в большинстве случаев, когда вы думаете его применить, можно найти лучший и более быстрый способ.

JS

// Это плохая практика. Пожалуйста, не делайте так: console.log(eval("obj.name + " is a " + obj." + access)); // Вместо этого для доступа к свойствам динамически используйте массив примечаний: console.log(obj.name + " is a " + obj); /* Использование eval в setTimout */ // Это также неудачная практика. Она медленна и сложна для проверки и отладки: setTimeout(" if(obj.age == 30) console.log("This is eval-ed code, " + obj + "!");", 100); // Так будет лучше: setTimeout(function(){ if(obj.age == 30){ console.log("This code is not eval-ed, " + obj + "!"); } }, 100);

Код внутри eval — это строка. Отладочные сообщения, связанные с Eval-блоками непонятны, и вам придется поломать голову, чтобы правильно расставить одинарные и двойные кавычки.

Не говоря уже о том, что это будет работать медленнее, чем обычный JavaScript . Не используйте Eval если вы не знаете точно, что вы делаете.

9. Непонимание асинхронного кода

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

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

Вот пример, в котором я использую сервис FreeGeoIP для определения вашего местоположения по IP-адресу:

JS

// Определение данных местоположения текущего пользователя. load(); // Вывод местоположения пользователя. Упс, это не работает! Почему? console.log("Hello! Your IP address is " + userData.ip + " and your country is " + userData.country_name); // Загружаемая функция будет определять ip текущего пользователя и его местоположение // через ajax, используя сервис freegeoip. Когда это сделано она поместит возвращаемые // данные в переменную userData. function load(){ $.getJSON("http://freegeoip.net/json/?callback=?", function(response){ userData = response; // Выведите из комментариев следующую строку, чтобы увидеть возвращаемый // результат: // console.log(response); }); }

Несмотря на то, что console.log располагается после вызова функции load() , на самом деле он выполняется перед определением данных.

10. Злоупотребление отслеживанием событий

Давайте предположим, что вы хотите отслеживать клик кнопки, но только при условии установленного чеккера.

Ошибки в программе достаточно сложно найти. И чтобы упростить себе задачу, нужно писать код правильно и красиво (читаемо). Хороший код должен быть: удобочитаемым, непротиворечивым, предсказуемым, правильно оформленным, а так же он должен иметь документацию.

Важные моменты по оформлению кода

Отступы

Если вы не делаете отступы, то код невозможно читать. Обычно отступы оформляют символом табуляции, но есть и те, кто отступ делает при помощи пробелов, в основном 3-4 пробела. Далее пример правильной и не правильной расстановки отступов:

/* Правильные отступы */

< max; i++) {
if (arr[i] % 2 === 0) {
answer = {
sum: sum += arr[i]
};
}
}
/* А тут все плохо */
var answer, arr = , sum = 0;
for (var i = 0, max = arr.length; i < max; i++) {
if (arr[i] % 2 === 0) {
answer = {
sum: sum += arr[i]
};
}
}

Фигурные скобки

Всегда используйте фигурные скобки, даже если они не нужны. С технической точки зрения, если тело конструкции if, while или for состоит из одной инструкции, то фигурные скобки можно не писать. Всегда написанные фигурные скобки упрощают последующие внесение изменений. Далее пример, как правильно, а как нет:

/* Правильная запись */
for (var i = 0; i < 5; i++) {
alert(i);
}
/* А такая нет */
for (var i = 0; i < 5; i++)
alert(i);

Местоположение открывающей скобки

Есть два типа программистов:

If (a > b) {
...
}
if (a > b)
{
...
}

Как писать открывающую фигурную скобку решать вам. Мы уже знаем, JavaScript сам подставляет точки с запятой и здесь возникает проблема во втором способе. Допустим, вы напишите вот такой кусок кода:

Return
{
id: 1
};

И что сделает интерпретатор? Он подставить точку с запятой после return и получится, что функция вернет undefined . То есть интепретатор видит этот кусок кода так:

Return undefined;
{
id: 1
};

Пробелы

Пробелы нужно использовать правильно. Вы когда пишите на бумаге, оставляете после запятой пустое пространство? Наверно да. И при перечислении чего-либо в JavaScript нужно делать пробел после запятой. Где нужно ставить пробел, чтобы код был понятным?

1. После точек с запятой в инструкции for

For (var i = 0; i < 100; i++) {...}

2. При объявлении нескольких переменных в инструкции for

For (var i = 0, max = arr.length; i < max; i++) {...}

3. После запятых, между элементами массива

Меня всегда удивлял JavaScript прежде всего тем, что он наверно как ни один другой широко распространенный язык поддерживает одновременно обе парадигмы: нормальные и ненормальное программирование. И если про адекватные best-практики и шаблоны прочитано почти все, то удивительный мир того, как не надо писать код но можно, остается лишь слегка приоткрытым.


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


Предыдущая задача:

Формулировка

Реализуйте функцию-декоратор, считающую количество вызовов переданной функции и предоставляющую возможность получить это число по требованию. В решении запрещено использовать фигурные скобки и глобальные переменные.

Счетчик вызовов - это лишь повод, ведь есть console.count() . Суть в том, что наша функция аккумулирует некоторые данные при вызове обернутой функции и предоставляет некий интерфейс для доступа к ним. Это может быть и сохранение всех результатов вызова, и сбор логов, и некая мемоизация. Просто счетчик - примитивен и понятен всем.


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

Привычное решение

Сначала надо выбрать отправную точку. Обычно, если язык или его расширение не предоставляют необходимой функции декорации, мы реализуем некоторый контейнер самостоятельно: оборачиваемая функция, аккумулируемые данные и интерфейс доступа к ним. Часто это класс:


class CountFunction { constructor(f) { this.calls = 0; this.f = f; } invoke() { this.calls += 1; return this.f(...arguments); } } const csum = new CountFunction((x, y) => x + y); csum.invoke(3, 7); // 10 csum.invoke(9, 6); // 15 csum.calls; // 2

Это нам сразу не годится, так как:

  1. В JavaScript таким образом нельзя реализовать приватное свойство: мы можем как читать calls экземпляра (что нам и нужно), так и записывать в него значение извне (что нам НЕ нужно). Конечно, мы можем использовать замыкание в конструкторе , но тогда в чем смысл класса? А свежие приватные поля я бы пока опасался использовать без babel 7 .
  2. Язык поддерживает функциональную парадигму, и создание экземпляра через new кажется тут не лучшим решением. Приятнее написать функцию, возвращающую другую функцию. Да!
  3. Наконец, синтаксис ClassDeclaration и MethodDefinition не позволит нам при всем желании избавиться от всех фигурных скобок.

Но у нас есть замечательный , который реализует приватность с помощью замыкания:


function count(f) { let calls = 0; return { invoke: function() { calls += 1; return f(...arguments); }, getCalls: function() { return calls; } }; } const csum = count((x, y) => x + y); csum.invoke(3, 7); // 10 csum.invoke(9, 6); // 15 csum.getCalls(); // 2

С этим уже можно работать.

Занимательное решение

Для чего вообще здесь используются фигурные скобки? Это 4 разных случая:

  1. Определение тела функции count (FunctionDeclaration )
  2. Инициализация возвращаемого объекта
  3. Определение тела функции invoke (FunctionExpression ) с двумя выражениями
  4. Определение тела функции getCalls (FunctionExpression ) с одним выражением

Начнем со второго пункта. На самом деле нам незачем возвращать новый объект, при этом усложняя вызов конечной функции через invoke . Мы можем воспользоваться тем фактом, что функция в JavaScript является объектом, а значит может содержать свои собственные поля и методы. Создадим нашу возвращаемую функцию df и добавим ей метод getCalls , который через замыкание будет иметь доступ к calls как и раньше:


function count(f) { let calls = 0; function df() { calls += 1; return f(...arguments); } df.getCalls = function() { return calls; } return df; }

С этим и работать приятнее:


const csum = count((x, y) => x + y); csum(3, 7); // 10 csum(9, 6); // 15 csum.getCalls(); // 2

C четвертым пунктом все ясно: мы просто заменим FunctionExpression на ArrowFunction . Отсутствие фигурных скобок нам обеспечит короткая запись стрелочной функции в случае единственного выражения в ее теле:


function count(f) { let calls = 0; function df() { calls += 1; return f(...arguments); } df.getCalls = () => calls; return df; }

С третьим - все посложнее. Помним, что первым делом мы заменили FunctionExpression функции invoke на FunctionDeclaration df . Чтобы переписать это на ArrowFunction придется решить две проблемы: не потерять доступ к аргументам (сейчас это псевдо-массив arguments ) и избежать тела функции из двух выражений.


С первой проблемой нам поможет справиться явно указанный для функции параметр args со spread operator . А чтобы объединить два выражения в одно, можно воспользоваться logical AND . В отличии от классического логического оператора конъюнкции, возвращающего булево, он вычисляет операнды слева направо до первого "ложного" и возвращает его, а если все "истинные" – то последнее значение. Первое же приращение счетчика даст нам 1, а значит это под-выражение всегда будет приводится к true. Приводимость к "истине" результата вызова функции во втором под-выражении нас не интересует: вычислитель в любом случае остановится на нем. Теперь мы можем использовать ArrowFunction :


function count(f) { let calls = 0; let df = (...args) => (calls += 1) && f(...args); df.getCalls = () => calls; return df; }

Можно немного украсить запись, используя префиксный инкремент:


function count(f) { let calls = 0; let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; }

Решение первого и самого сложного пункта начнем с замены FunctionDeclaration на ArrowFunction . Но у нас пока останется тело в фигурных скобках:


const count = f => { let calls = 0; let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; };

Если мы хотим избавиться от обрамляющих тело функции фигурных скобок, нам придется избежать объявления и инициализации переменных через let . А переменных у нас целых две: calls и df .


Сначала разберемся со счетчиком. Мы можем создать локальную переменную, определив ее в списке параметров функции, а начальное значение передать вызовом с помощью IIFE (Immediately Invoked Function Expression):


const count = f => (calls => { let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; })(0);

Осталось конкатенировать три выражения в одно. Так как у нас все три выражения представляют собой функции, приводимые всегда к true, то мы можем также использовать logical AND :


const count = f => (calls => (df = (...args) => ++calls && f(...args)) && (df.getCalls = () => calls) && df)(0);

Но есть еще один вариант конкатенации выражений: с помощью comma operator . Он предпочтительнее, так как не занимается лишними логическими преобразованиями и требует меньшего количества скобок. Операнды вычисляются слева-направо, а результатом является значение последнего:


const count = f => (calls => (df = (...args) => ++calls && f(...args), df.getCalls = () => calls, df))(0);

Наверно мне удалось вас обмануть? Мы смело избавились от объявления переменной df и оставили только присвоение нашей стрелочной функции. В этом случае эта переменная будет объявлена глобально, что недопустимо! Повторим для df инициализацию локальной переменной в параметрах нашей IIFE функции, только не будем передавать никакого начального значения:


const count = f => ((calls, df) => (df = (...args) => ++calls && f(...args), df.getCalls = () => calls, df))(0);

Таким образом цель достигнута.

Вариации на тему

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


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


const bind = (f, ctx, ...a) => (...args) => f.apply(ctx, a.concat(args));

Однако, если аргумент f не является функцией, по-хорошему мы должны выбросить исключение. А исключение throw не может быть выброшено в контексте выражения. Можно подождать throw expressions (stage 2) и попробовать еще раз. Или у кого-то уже сейчас есть мысли?


Или рассмотрим класс, описывающий координаты некоторой точки:


class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return `(${this.x}, ${this.y})`; } }

Который может быть представлен функцией:


const point = (x, y) => (p => (p.x = x, p.y = y, p.toString = () => ["(", x, ", ", y, ")"].join(""), p))(new Object);

Только мы здесь потеряли прототипное наследование: toString является свойством объекта-прототипа Point , а не отдельно созданного объекта. Можно ли этого избежать, если изрядно постараться?


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

Заключение

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

Теги: Добавить метки

Похожие статьи