Pull to refresh

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

Reading time 7 min
Views 21K

За время, что мне довелось писать на 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 объявлен вручную и глобально — защита от "хитрых" библиотек.




Спасибо за внимание! Надеюсь, кому-нибудь эти заметки пригодятся и послужат пользой.

Tags:
Hubs:
+33
Comments 79
Comments Comments 79

Articles