[Из песочницы] 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] = "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 объявлен вручную и глобально — защита от «хитрых» библиотек.



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

Комментарии (1)

  • 20 октября 2016 в 16:56

    0

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

© Habrahabr.ru