За время, что мне довелось писать на 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 Infinite // => "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
Вам не показалось, IE действительно отказывается проверять цифровые ключи в объектах без прототипов, до тех пор, пока в нем не появится хотя бы один строчный.
И этот факт портит весь праздник.
Приходиться делать "костыль" вроде такого
if (Object.prototype.isPrototypeOf(obj)) {
return obj.hasOwnProperty(prop);
}
return prop in obj;
«лже-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. Сравнение с 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)
можно получить много других типов.
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
объявлен вручную и глобально — защита от "хитрых" библиотек.
Спасибо за внимание! Надеюсь, кому-нибудь эти заметки пригодятся и послужат пользой.
Автор: EugeneGantz