Как стать автором
Обновить

Комментарии 53

НЛО прилетело и опубликовало эту надпись здесь

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

НЛО прилетело и опубликовало эту надпись здесь

Автор не мог поставить два минуса. За одного человека можно голосовать только один раз.

Ещё одна особенность JavaScript? ;)

НЛО прилетело и опубликовало эту надпись здесь

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

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

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

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

Докинул третий за карманытьё на ровном месте.

Шикарная статья! Js мой "второй" язык по работе - но вот подставу с for я никогда не замечал - в шоке от того, что оно так работает)

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

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

Спору нет, но это предельно неинтуитивно, и ошибки вызванные таким поведением вполне могут жить в коде годами.

Можно было как нибудь это обыграть - например сделав вместо let - отдельное ключевое слово - чтобы было видно - что тут уникальное поведение)

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

Печально, что в самом популярном языке на планете, такие ужастное проблемы. Но всё же в разработке не часто приходится сталкиваться с такими проблемами. Ожидаю, когда уже будет типизация и когда устранят большенство проблем. А eval, это зло и никогда не рекомендуется использовать.

И какие проблемы устранятся с типизацией? Типизации в самом js не будет никогда. Это все только для ide.

V8 научился запускать typescript без компиляции в js. По сути это оно и есть: js со статической типизацией

Можно ссылочку?

Я полагаю нода у себя внутри также и будет транспилировать ts в js

Нет. Никакой транспиляции, и никаких проверок. Проверять придется отдельно. Только стирание информации о типах (Type stripping). Именно поэтому ts поддерживается не в полном объеме. Не поддерживается то, что требует изменения кода js - типа перечислений или декораторов.

https://nodejs.org/api/typescript.html#type-stripping

Только вот это не фича V8 (как заявлено выше), а конкретно Node.js, и поддерживает она только strip (который просто заменяет все ваши типы пробелами и не поддерживает все фичи TS) и transform (под отдельным флагом) режимы, без проверки самих типов (это всё ещё делает tsc). В стандарте ещё ничего нет, но есть разные предложения (например https://github.com/tc39/proposal-type-annotations), соответственно, никакой поддержки в движках нет.

Сам js во время работы и компиляции (в байткод) никаких проверок этой типизации не делает и не будет делать. Только ide (типа VSCode) этим будет заниматься. А движок js (пока только в Node) просто удаляет информацию о типах (заменяет ее пробелами).

Запускать-то научился, но типизация всё ещё только для IDE и CI.

Суть в том, что ни одну из 6 описанных проблем статическая типизация TypeScript не решит.

Типизация ?

Самое печальное, что этот мем уже неоднократно становился реальностью.

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

На удивление, интересная статья.

странности, которым не хватило место: - и +0, NaN не равен самому себе, == и !=. так работает абсолютно любой язык. в других статься ещё было, что typeof NaN это number. это не спецификация js, а физическое устройство процессора

В списке любых языков есть Си?

Статья интересная, со многими "граблями" уже встречался. Но по своему опыту могу сказать, вряд-ли кто-то будет держать это всё в голове постоянно. Механизм такой: наткнулся => узнал => исправил => забыл

"JavaScript отстой, потому что '0' == 0!" — буквально каждый когда-либо

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

Ура! Согласен! Наконец-то будет статья которая не будет обсасывать сравнение чисел и строк!

  1. eval хуже, чем вы думаете

А, нет, расходимся. Автор решил пробить дно и писать об eval в 2025 году

Скриптовый язык состоит из костылей, чего тут странного.

Вставка элемента в массив. Если индекс выходит за пределы массива, массив наполняется пустыми элементами:

const msgArray = [];
msgArray[0] = "Привет";
msgArray[99] = "мир";

if (msgArray.length === 100) {
  console.log("Длина равна 100.");
}

Нет, не наполняется. Обновляется только длина, а сам массив становится "дырявым":

let x = [1];
x[9] = 2;
console.log(x.length); // 10
console.log(x.hasOwnProperty(0)); // true
console.log(x.hasOwnProperty(1)); // false

Очень подлая история с точкой с запятой. Когда-то писал что-то вроде

functionCalled(someArgs)
[1, 2, 3].includes(1)

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

А можно еще вот так

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1);
}

не сильно отличается от

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1);
}

но результат абсолютно разный

В какой-то степени это логично и скорее нюанс блочной видимости let. Так-то блок для каждой итерации цикла "свой", и значение замыкается "своё". Для var эта область функциональная, и поэтому цикл входит в нее "полностью". Можно представить как

for (let _ = 0; _ < 3; _++) {
  const i = _;
  setTimeout(() => console.log(i), 1);
}

---------- 
let i;
for (let _ = 0; _ < 3; _++) {
  i = _;
  setTimeout(() => console.log(i), 1);
}


да уж, JS был хорош лет 20 назад, когда надо было прикрутить простенькую логику к сайту на PHP. но сейчас он уже полностью морально устарел. когда там к WASM прикрутят нормальную поддержку взаимодействия с браузером, чтобы можно было полностью отказаться от JS и использовать нормальные современные языки программирования?

Вся статья - следствие незнания концептов работы и устройства JS и виртуальной машины. И ее бы не было, если бы люди читали документацию ;) А то как в цитате: «в мире так много магии, если не учить физику в школе».

И писали бы на нём единицы, был бы у нас web как в 2000-м году, но у каких-нибудь MS сайт умел бы делать AJAX.

Тем не менее, большинство высокоуровневых языков (JS, Java, C# и др.) захватывают переменные по ссылке:

Это не так, в C# замыкание работает иначе и захватывает саму переменную.
https://csharpindepth.com/Articles/Closures где-то тут про это было написано.

Но в циклах это особенно нежелательно: там обычно нужно что-то сделать с итератором внутри колбэка:
// Код на C#:
// выводит "3 3 3" — вероятно, не то, что ождали

Вообще-то, это именно ожидаемый результат.
setTimeout() подразумевает выполнение фрагмента кода через определённый промежуток времени. Не выполнение сейчас и получение результата через определённую задержку, а именно начало выполнения по прошествии указанного периода времени.
В данном случае очевидно, что цикл завершится быстрее, чем отработает таймаут ожидания (за исключением разве что прохода i == 0 и то вряд ли).
Таким образом, к моменту первого выполнения WriteLine цикл уже закончит свою работу и в i совершенно ожидаемо будет 3.

При этом, если переписать код на лямбду с параметром, то всё будет работать как и всегда работает в C# - передачей по значению.
В данном случае, если я ещё не всё окончательно забыл, передать значение по ссылке вообще невозможно, т.к. язык явно запрещает это.

В качестве "решения" стандарт ECMAScript сделал так, чтобы переменные цикла в заголовке for имели особое поведение

Именно поэтому JS - это ужасный язык, особое поведение на особом поведении едет и особым поведением погоняет.

Это не так, в C# замыкание работает иначе и захватывает саму переменную.

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

Вообще-то, это именно ожидаемый результат.

Хорошо если вы именно такой результат ожидаете, однако для новичков это всё ещё грабли.

Именно поэтому JS - это ужасный язык, особое поведение на особом поведении едет и особым поведением погоняет.

Но оператор foreach в C# имеет точно такое же особое поведение. Почему ему можно, а оператору for нельзя?

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

Ровно до того момента, когда "деталь реализации" станет важна. И тогда тот самый новичок, запомнивший, что передаётся по ссылке, будет пытаться понять, а что же не так.

однако для новичков это всё ещё грабли

Какие? Необходимость разобраться с моментом времени, в который исполняется код?

Но оператор foreach в C# имеет точно такое же особое поведение.

Так не имеет же. Если Вы про переменную заголовка, то она read only по определению, т.е. случай полностью аналогичен лямбде с параметром (0 1 2).
Если про вынесенную перед циклом переменную, то она точно также будет иметь значение, зависящее от момента времени выполнения кода (3 3 3).

Так не имеет же. Если Вы про переменную заголовка, то она read only по определению

По какому именно определению? В ранних версиях C# никакое определение не мешало циклу foreach работать с замыканиями так же как и цикл for.

По какому именно определению?

If the foreach_statement contains both or neither ref and readonly, the iteration variable denotes a variable that is treated as read-only.
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#1395-the-foreach-statement

В ранних версиях C# никакое определение не мешало циклу foreach работать с замыканиями так же как и цикл for.

Интересно, не знал.

Только вот read-only означает лишь, что программист не может менять переменную, ничто не мешает переменной измениться через другой механизм (собственно, в древнем C# именно так оно и работало).

Меня, конечно, изрядно удивило, что 👨‍👩‍👧‍👦‍👨‍👩‍👧‍👦 — это 23 символа. Скопировал в консоль — действительно 23.
Но потом я попробовал вручную ввести эти символы — и, блин, получилось 22. Что за…?

Пошёл разбираться дальше — и понял: в статье речь шла не о двух эмодзи “семья”, а о двух слитых эмодзи, между которыми стоит специальный символ \u200D. Именно он и добавляет один лишний символ, потому что склеивает семьи.

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

Так что, по сути:
const family = "👨‍👩‍👧‍👦‍👨‍👩‍👧‍👦";
Это верно, что результат — 1, ведь это считается одним символом с точки зрения пользователя.

Насчет ';' - для меня полезная информация, пару раз попадал. Причем, когда меняешь постоянно язык (C,С++, py, С#, js) - это убивает - вдруг не так работает в общем то предсказуемая конструкция...

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

Ниже я разберу тезисы материала, покажу что почти каждый из них несостоятелен и говорит скорее о том, о чем сказал Дуглас Крокфорд еще в далеком 2000 году:

JavaScript:The World's Most Misunderstood Programming Language

Судите сами:

"JavaScript отстой, потому что '0' == 0

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

 eval хуже, чем вы думаете:

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

Для того, чтобы понять поведение eval нужно понимать следующие его(JS) нормы:

  1. Это динамический интерпретируемый язык, что за исключением правил Static Semantics диктует правила интерпретации любого его statement именно в момент его интерпретации. То есть Агент исполняющий код - понятия не имеет до начала интерпретации конкретного statement что с ним делать. То есть для интерпретатора все что перед вами это набор идентефикаторов поведение которых связывается с определенным алгоритмом именно в момент интерпретации.

  2. Для любого Агента - не существует никакой функции eval math console и так далее. Это все идентификаторы которые ссылаются на что-то. И что делать с этим что-то, можно понять только в конкретный момент интерпретации.

  3. eval это всего лишь property от Global Object. Который может быть связан с чем угодно.

  4. конструкция вида identifier() интерпретируется агентом как Callable Expression. Агент ничего не знает о идентификаторе. Как следствие код вида: eval(';'); ничего для еганта не означает кроме того, что перед ним выражение которое нужно разбить на части, получить результаты выполнения каждой из них и подставить их в связанный алгоритм.

  5. До появления "strict mode", eval был связан с поведением, которое описывалось как стандартное выполнение кода в глобальном окружении. С появлением "strict mode", super statement в спецификацию добавили поведение в описания алгоритма работы функционального обьекта, где в случае использования eval для инициализации окружения заданного кода - требуется брать не глобальное окружение, но окружение где напрямую eval был задекларирован.

А теперь собираем все в кучу:

eval - это обычный идентификатор который для агента, сам по себе, ничего не означает. Просто набор символов который интерпретируется как Reference Expression. Даже без какой либо проверки на то существует ли вообще такой Reference.

С появлением strict mode и super поведение Regular Function было изменено, что потребовало изменения ИМЕННО АЛГОРИТМА самого Callable Expression и как следствие внесение правок в поведение eval.

Сравнить просто ссылку на функциональный обьект описывающий eval с ссылкой внутри любого другого идентификатора (то что автор назвал переименованием) просто так нельзя. По причине существование super() который НЕ является вызовом функции, но является особым специфическим поведением, в отличии от eval который является типичным expression.

Добавте к этому тот факт, что способов вызвать функциональный обьект в JS больше 14 разных штук. И каждый из них должен быть учтен в связи с новым поведением strict super etc, что в случае с eval смерти подобно, так как это фундаментальная часть языка.

Я постарался очень сжато описать функционал того чем является eval. Деталей и ньюансов его реализации масса. Как следствие это не eval странный - это автор не понимает как он работает.

Циклы в JS делают вид, что их переменные захватываются по значению

В JavaScript ничего не делается по значению если в спецификации не указано явным образом обратное.

Algorithm steps may declare named aliases for any value using the form “Let x be someValue”. These aliases are reference-like in that both x and someValue refer to the same underlying data and modifications to either are visible to both. Algorithm steps that want to avoid this reference-like behaviour should explicitly make a copy of the right-hand side: “Let x be a copy of someValue” creates a shallow copy of someValue.

Как только это становится очевидным, то и eval уже не такой странный и for statement оказывается работает более чем логично.

Тот самый "ложный" объект

Общепринято считать, что в JavaScript есть 8 ложных (falsy) значений: false+0-0NaN""nullundefined и 0n

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

То что Вы привели как falsy - это поведение абстрактной операции toBolean, которая не обязательна для вызова при любых типах сравнения.

Далее, ваша претензия [[IsHTMLDDA]] не верна по сути, потому, что она описана в спецификации HTML5 и второе - использовать этот механизм может любой External Object а не только document.all.

Ну и наконец вот Вам пара примеров для размышления

(
	()=>{
		var theObj = {
		}

		console.log(true == theObj);  //false

		theObj.valueOf = ()=>1;
		console.log(true == theObj);  // true

		theObj.valueOf = ()=>"0";
		console.log(true == theObj);  // false

	}
)();

Это не falsy значения, или странные обьекты. Это не знание фундаментального принципа заложенного в JS с момента еге первой спецификации. Очень рекомендую к ознакомлению главы о том что такое Object в JS и что такое Exotic Object

Графемы и перебор строк

Тут вообще сплошная ваша. Разложим по полочкам:

  1. Человек оценивая длину строки, ожидает увидеть количество ОТОБРАЖАЕМЫХ символов

  2. Эволюция произвела десятки разных способов формирования такого символа ( латиница, иероглифиы, арабская вязь и т.д.) где способы формирования такого символа не укаладываются в схему: таблица где каждому коду соотвествует один знак

  3. unicode это стандарт, который попытался унифицировать эту работу. Обьем документации по этому стандарту больше чем весь JS вместе взятый

  4. Есть стандарт unicode (описывающий способы формирования символа) а есть способ кодирования этого способа. UTF8, UTF16, UTF32 - это способы кодирования где 8,16,32 обозначатает количество используемых бит на реализацию нашей задачи.

  5. В зависимости от того, какое кодирование мы используем, один и тот же символ может кодироваться разной последовательностью байт. То есть один и тот же символ может быть закодирован как одним байтом, двумя, тремя и так далее. И все это может происходит в рамках одной и той же системы кодирования. Например: один и тот же символ представлен разной последовательностью кодирования

    `Á`.length; // 1
    `A\u0301`; // 2

    и оба эти символа с точки зрения Unicode идентичны. Это А с ударением.

  6. Стандарт Unicode пытается описать все возможные комбинации подобных поведений. Кодирование - пытается закодировать все это в рамках выданных бит. В результате чего представление символа может занимать последовательность байт.

  7. JS реализует систему кодирования UTF16.

  8. Возможности языка JS позволяют как адресовать конкретный байт в строке, так и разбирать его по два байта на один управляющий символ.
    Потому

    `\uD83d\ude00`; // '😀'
    '😀'.length; // 2
    [...'😀'].length; // 1

    Так как Exotic Object String - для метода length реализует алгоритм, где подсчитывается простое колличества байт на кодирование UTF16 то для '😀' мы видим ответ 2;
    В то же создавая Exotic Object Array мы используем spread, который вызывает Iterator у Object String который в свою очередь использует не побайтовую адресацию, но преобразование CodePointAt который итерируетсся уже по UTF16 кодированным символам (парам или нет - не важно).
    При этом разбирая строку именно по CodePoint кодированию стандарта. Но не по представлению символа. То есть если у нас будет представление которое потребует больше 16 бит, итератор разобьет это на два проперти.

Теперь мы должны понимать, что привычные операции существующие в JS считают либо сырые байты, либо CodePoint. Чего совершенно недостаточно для того чтобы получить ожидаемый результат для человека - сколько вижу столько и считаю.

Выход из этой ситуации - это либо самостоятельная реалиация станадрта Unicode со всеми его 900 страниц спецификации, либо использование регулярных выражений с ключем /u для максимальной поддержки старых браузеров или ключем /v для того чтобы получить максимальный комфорт с работой со строками с поддержкой ВСЕХ символьных классов стандарта Unicode.

Разрежённые массивы

В JS то что вы создается при помощи литерала [] или конструктора Array называется Exotic Object Array. Это не Array в том смысле как это может быть в других языках. Это обьект с особым поведением.

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

Что такое Exotic Object я давал ссылку раньше.

Вместо ИГОГО

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

Всё, что связано с == и !=

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

Всё, что связано с this

Реализуется простой блок схемой. Если максимально упростить - то все что нужно знать так это, что все в JS это ссылочныее типы (Reference Like термин спецификации) и что this это просто скрытый параметр для вызова Regular Function. Установка которого зависит от формы callable expression

NaN не равен ничему, даже самому себе

+0 против -0

Ошибки из-за плавающей точности (IEEE 754)

Это все функционирует в рамках стандарта IEE754 и абсолютно идентично другим языкам программирования которые реализуют этот стандарт. Например C Pascal Fort и так далее.
То есть утверждение автора ложно. Никаких особенностей там нет. Есть не знание языка или стандарта.

typeof null - это "object"

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

Особенности работы без строгого режима и использование var

Возврат примитивных значений из конструкторов

и т.д. и т.п.

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

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

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

Вы ответили так, будто автор имел в виду, что эти особенности необъяснимы, а он всего лишь имел в виду, что они контринтуитивны.

Если я скажу, что странно, кинув вещь с орбиты вниз, поймать её позади себя при пролёте после оборота, это не будет значить, что это против физики.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации