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

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


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

Автор: EugeneGantz

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js