За время, что мне довелось писать на Javascript, у меня сложился образ, что js и его спецификация это шкатулка с потайным дном. Иногда кажется, что ничего секретного в ней нет, как вдруг магия стучит в ваш дом: шкатулка раскрывается, оттуда выскакивают черти, по-домашнему исполняют блюз и резво скрываются обратно в шкатулке. Позднее вы узнаете причину: стол повело и шкатулку наклонило на 5 градусов, что вызвало чертей. С тех пор вы не знаете, это фича шкатулки, или лучше все-таки покрепче замотать её изолентой. И так до следующего раза, пока шкатулка не подарит новую историю.
И если записывать каждую такую историю, может получиться небольшая статья, которой я и хочу поделиться.
«Сумма пустот»
При сливании массива в строку используя метод .join(), некоторые пустые типы: null, undefined, массив с нулевой длиной — конвертируются в пустую строку. И справедливо это только для случая когда они расположены в массиве.
[void 0, null, []].join("") == false // => true [void 0, null, []].join("") === "" // => true // Не работает при сложении со строкой. void 0 + "" // => "undefined" null + "" // => "null" [] + "" // => ""
На практике такое поведение можно использовать для отсева действительно пустых данных
var isEmpty = (a, b, c) => { return ![a, b, c].join(""); } var isEmpty = (...rest) => { return !rest.join(""); } isEmpty(void 0, [], null) // => true isEmpty(void 0, [], null, 0) // => false isEmpty(void 0, [], null, {}) // => false. С пустым объектом такой трюк не проходит // Или так, в случае если аргумент один var isEmpty = (arg) => { return !([arg] + ""); } isEmpty(null) // => true isEmpty(void 0) // => true isEmpty(0) // => false
«Странные числа»
Попытка определить типы для NaN и Infinity при помощи оператора typeof как результат вернет "number"
typeof NaN // => "number" typeof Infinity// => "number" !isNaN(Infinity) // => true
Юмор в том, что NaN — это сокращение от "Not-A-Number", а бесконечность (Infinity) сложно назвать числом.
Как вообще тогда определять числа? Проверить их конечность!
function isNumber(n) { return isFinite(n); } isNumber(parseFloat("mr. Number")) // => false isNumber(0) // => true isNumber("1.2") // => true isNumber("abc") // => false isNumber(1/0) // => false
«Для отстрела ноги возьмите объект»
Для javascript Object — одна из самых первых структур данных и в тот же момент, на мой взгляд, — король хитросплетений.
К примеру, обходя в цикле объект, используемый в качестве хэш-таблицы, желательно проверять, чтобы итерируемые свойства были собственными.
В противном случае, в итерацию могут попасть свойства из расширения прототипа.
Object.prototype.theThief = "Альберт Спика"; Object.prototype.herLover = "Майкл"; var obj = { theCook: "Ричард Борст", hisWife: "Джорджина" }; for (var prop in obj) { obj[prop]; // Цикл обойдет: "Ричард Борст", "Джорджина", "Альберт Спика", "Майкл" if (!obj.hasOwnProperty(prop)) continue; obj[prop]; // Цикл обойдет: "Ричард Борст", "Джорджина" }
Между тем, Object можно создать и без наследования протот��па.
// Несложная инструкция по прострелу ноги var obj = Object.create(null); obj.key_a = "value_a"; obj.hasOwnProperty("key_a") // => Выбросит ошибку.
"Эй, кэп, а зачем это нужно?"
В таком хэше отсутствуют наследуемые ключи — только собственные (гипотетическая экономия памяти). Так, проектируя API к библиотекам, где пользователю позволено передавать собственные коллекции данных, про это легко забыть — тем самым выстрелить себе в ногу.
И так как в таком случае вы не можете контролировать вводимые данные, необходим универсальный способ проверять собственные ключи в объекте.
Способ первый. Можно получить все ключи. Неоптимальный, если выполнять indexOf внутри цикла: лишний обход массива.
Object.keys(obj); // => ["key_a"]
Способ второй. Вызывать метод hasOwnProperty с измененным контекстом
Object.prototype.hasOwnProperty.call(obj, "key_a") // => true
Казалось бы, вот он идеальный способ. Но, Internet Explorer.
// Выполнять в IE // Создать объект без прототипа var obj = Object.create(null); obj[0] = "a"; obj[1] = "b"; obj[2] = "c"; Object.prototype.hasOwnProperty.call(obj, 1); // => false Object.prototype.hasOwnProperty.call(obj, "1"); // => false Object.keys(obj); // => ["0", "1", "2"] obj.a = 1; Object.prototype.hasOwnProperty.call(obj, 1); // => true Object.prototype.hasOwnProperty.call(obj, "1"); // => true // Случай когда объект создается с прототипом от Object obj = Object.create(Object.prototype); obj["2"] = 2; obj.hasOwnProperty("2"); // => false obj.a = "a"; obj.hasOwnProperty("2"); // => true delete obj.a; obj.hasOwnProperty("2"); // => false
Вам не показалось, IE действительно отказывается проверять цифровые ключи в объектах созданный через Object.create(), до тех пор, пока в нем не появится хотя бы один строчный.
И этот факт портит весь праздник.
UPD:
Решение предложенное Дмитрием Коробкиным
UPD:
bingo347 справедливо заметил, что если не писать скрипты для "динозавров", то перебор собственных свойств целесообразней выполнять при помощи Object.keys(obj) и Object.getOwnPropertyNames(obj)
Но, следует иметь ввиду ньюанс, что getOwnPropertyNames возвращает все собственные ключи, даже те, что неитерабельны.
Object.keys([1, 2, 3]); // => ["0", "1", "2"] Object.getOwnPropertyNames([1, 2, 3]); // => ["0", "1", "2", "length"]
«лже-undefined»
Часто разработчики проверяют переменные на undefined прямым сравнением
((arg) => { return arg === undefined; // => true })();
Аналогично поступают и с присваиванием
(() => { return { "undefined": undefined } })();
"Засада" кроется в том, что undefined можно переопределить
((arg) => { var undefined = "Happy debugging m[\D]+s!"; return { "undefined": undefined, "arg": arg, "arg === undefined": arg === undefined, // => false }; })();
Эти знания лишают сна: получается, что можно сломать весь проект, просто переопределив undefined внутри замыкания.
Но есть пара надежных способов сравнить или назначить undefined — это использовать оператор void или объявить пустую переменную
((arg) => { var undefined = "Happy debugging!"; return { "void 0": void 0, "arg": arg, "arg === void 0": arg === void 0 // => true }; })(); ((arg) => { var undef, undefined = "Happy!"; return { "undef": undef, "arg": arg, "arg === undef": arg === undef // => true }; })();
«Сравнение Шрёдингера»
Однажды коллеги поделились со мной интересной аномалией.
0 < null; // false 0 > null; // false 0 == null; // false 0 <= null; // true 0 >= null // true
Происходит это потому, что сравнение больше-меньше — это числовое сравнение, где обе части выражения приводятся к числу.
В то время как равенство чисел с null всегда возвращает false.
Если принять во внимание, что null после приведения в число становится +0, внутри компилятора сравнение приблизительно выглядит так:
0 < 0; // false 0 > 0; // false 0 == null; // false 0 <= 0; // true 0 >= 0 // true
Сравнение чисел с Boolean
-1 == false; // => false -1 == true; // => false
В javascript при сравнении Number с Boolean, последний приводится к числу, после производится сравнение Number == Number.
И, так как, false приводится к +0, а true приводится к +1, внутри компилятора сравнение обретает вид:
-1 == 0 // => false -1 == 1 // => false
Однако.
if (-1) "true"; // => "true" if (0) "false"; // => undefined if (1) "true"; // => "true" if (NaN) "false"; // => undefined if (Infinity) "true" // => "true"
Потому что 0 и NaN всегда приводятся к false, все остальное true.
Проверка на массив
В JS Array наследуются от Object и, по сути, являются объектами с числовыми ключами
typeof {a: 1}; // => "object" typeof [1, 2, 3]; // => "object" Array.isArray([1, 2, 3]); // => true
Штука в том, что Array.isArray() работает только начиная с IE9+
Но есть и другой способ
Object.prototype.toString.call([1, 2, 3]); // => "[object Array]" // Соответственно function isArray(arr) { return Object.prototype.toString.call(arr) == "[object Array]"; } isArray([1, 2, 3]) // => true
Вообще используя Object.prototype.toString.call(something) можно получить много других типов.
UPD:
boldyrev_gene задал, на мой взгляд, хороший вопрос: почему не использовать instanceof?
Экземпляры массива созданные внутри фреймов и других окон будут иметь разные экземпляры конструкторов.
var iframe = document.querySelector("iframe"), IframeArray = iframe.contentWindow.Array; new IframeArray() instanceof Array; // => false Array.isArray(new IframeArray()); // => true Object.prototype.toString.call(new IframeArray()); // => "[object Array]"
arguments — не массив
Настолько часто забываю об этом, что решил даже выписать.
(function fn() { return [ typeof arguments, // => "object" Array.isArray(arguments), // => false Object.prototype.toString.call(arguments) // => "[object Arguments]"; ]; })(1, 2, 3);
А так как arguments — не массив, то в нем недоступны привычные методы .push(), .concat() и др. И в случае если нам необходимо работать с arguments как с коллекцией, существует решение:
(function fn() { arguments = Array.prototype.slice.call(arguments, 0); // Превращение в массив return [ typeof arguments, // => "object" Array.isArray(arguments), // => true Object.prototype.toString.call(arguments) // => "[object Array]"; ]; })(1, 2, 3);
а вот ...rest — массив
(function fn(...rest) { return Array.isArray(rest) // => true. Oh, wait... })(1, 2, 3);
Поймать global. Или определяем среду выполнения скрипта
При построении изоморфных библиотек, например, из ряда тех, что собираются через Webpack, рано или поздно, возникает необходимость определить в какой среде запущен скрипт.
И так как в JS не предусмотрен механизм определения среды выполнения на уровне стандартной библиотеки, можно сделать финт используя особенность поведения указателя внутри анонимных функций в нестрогом режиме.
В анонимных функциях указатель this ссылается на глобальный объ��кт.
function getEnv() { return (function() { var type = Object.prototype.toString.call(this); if (type == "[object Window]") return "browser"; if (type == "[object global]") return "nodejs"; })(); };
Однако в строгом режиме this является undefined, что ломает способ. Этот способ актуален в случае если global или window объявлен вручную и глобально — защита от "хитрых" библиотек.
Спасибо за внимание! Надеюсь, кому-нибудь эти заметки пригодятся и послужат пользой.
