Javascript-паноптикум

За час, що мені довелося писати на 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] = "с";

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
оголошений вручну і глобально — захист від "хитрих" бібліотек.


Дякую за увагу! Сподіваюся, кому-небудь ці нотатки знадобляться і послужать користю.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.