четверг, 16 мая 2013 г.

Головоломный JavaScript


Головоломный JavaScript

Время от времени в сети мне попадаются различные WTF примеры на JavaScript. Часто на первый взгляд они выглядят безумно, но на самом деле просты и логичны, поэтому я решил попробовать их собирать и разбирать. Кроме очевидного профита в виде более глубого понимаю языка это еще и отличные вопросы для собеседований)

Чтобы было интереснее я буду прятать ответ и решение под спойлер.

1. Так что помните, дети, всегда подставляем системы счисления!

Для начала простой, но прикольной пример. Что вернут такие вызовы parseInt?

parseInt('fuck');
parseInt('fuck', 16);
Узнать ответ
parseInt('fuck');     // NaN
parseInt('fuck', 16); // 15

Как работает parseInt: он посимвольно проверяет переданную строку на соответствие указанной вторым аргументом системе счисления и если находит некорректный символ, то завершает работу. По умолчанию, система счисления считается десятичной, при условии, что переданная строка не начинается с 0. В десятичной системе счисления символ ‘f’ недопустим и функция заканчивается свою работу не найдя ни одной цифры. А вот в шестнадцатиричной системе символ ‘f’ допустим и соответствует десятичному числу 15. Символ ‘u’ не допустим и функция завершает свою работу.


2. Может кто подскажет?

Чему равно такое выражение?

"Why am I a " + typeof + "";
Узнать ответ

“Who am I a number”

Оператор “+” обладает большим приоритетом чем “typeof”, поэтому указанная запись эквивалентна следующей:

“Why am I a ” + (typeof (+ “”)).

Унарный “+”” выполняет привидение к числу, поэтому “typeof” от результата его выполнения - это number.


3. Кто больше?

Ну и кто больше?

[1, 2, 4]         <  [1, 2, 5]
[1, 2, 'd']       <  [1, 2, 5]
[1, 2, ['d', 5]]  <  [1, 2, [20, 5]]
[1, 2, 5]         == [1, 2, 5]
[1, 2, 5]         <= [1, 2, 5]
[1, 2, 5]         >= [1, 2, 5]
Узнать ответ
[1, 2, 4]         <  [1, 2, 5]        //true
[1, 2, 'd']       <  [1, 2, 5]        //false
[1, 2, ['d', 5]]  <  [1, 2, [20, 5]]  //false
[1, 2, 5]         == [1, 2, 5]        //true
[1, 2, 5]         <= [1, 2, 5]        //true
[1, 2, 5]         >= [1, 2, 5]        //true

Согласно спецификации ECMAScript 5 для выполнения операции сравнения аргументы, которые не являются примитивами, должны быть привидены к примитивам (если кому интересно, то пункт 11.8.5 “Алгоритм сравнения абстрактного отношения”). Сначала JavaScript пробует преобразовать аргумент к числу с помощью метода valueOf и только когда это не удается приводит к строковому типу (изначально я хотел написать здесь про естественность сравнения чисел, и что когда мы говорим, что слово А больше слова Б, то имеем в виду длину. Но из беседы с коллегой выяснилось, что это не правда. Когда мы сравнимаем два слова мы инстанцируем связанные с ними образы и сравнимаем их. И только если образов не нашлось, то сравниваем слова не как метки образов, а именно как набор букв). Для массивов метод valueOf не определен и поэтому они сравниваются как строки:

"1,2,4" < "1,2,5" //true
"1,2,d" < "1,2,5" //false

Но все вышесказаное верно только для операций сравнения. В случае оператора “==”” привидение к примтивам осуществляется только если один из аргументов примитив, а во всех остальных случаях оператор “==” возвращает true только если оба аргумента ссылаются на один объект (пункт 11.9.3 “Алгоритм сравнения абстрактного равенства”), что в нашем случае не так.

Следует заметить, что для объектов сравнение работать не будет, т.к. их дефолтный toString возвращает [object Object].

Ладно, объекты сравнивать нельзя, но почему бы не проверять массивы на равенство с помощью сравнения? Все пишут циклы, а ведь можно обойтись одной строкой:

!(arr1 < arr2)&&!(arr1 > arr2)

Что-то подсказывает, что постоянные игры с привидением типов должны очень сильно отъедать время и ресурсы. Сделаем небольшой наколеночный тест:

var arr1 = [],
    arr2 = [],
    res,
    start,
    end,
    len = 600000;

for(var i=0; i<len; i++){
    arr1[i] = i;
    arr2[i] = i;
   // if(i == len-100) arr2[i] = i+1; //чтобы делать не равные массивы
}

//проверим, что массивы создались
console.log(arr1.length, arr2.length);

//наш метода
start = new Date();

res = !(arr1 < arr2)&&!(arr1 > arr2);

end = new Date();

console.log(end.getTime() - start.getTime(), res);

//классический циклический
start = new Date();

for(var i=0; i<len; i++)
    res &= (arr1[i] === arr2[i]);

end = new Date();

console.log(end.getTime() - start.getTime());

Разница поражает: 123мс на 2015мс в пользу привидения типов!

Несмотря на это я не советую использовать такой метод, т.к. при попадании в массив объектов он начнет врать, кроме того, он использует не самое очевидное поведение языка.




4. Кто больше?-2

По мотивам предыдущей загадки, точнее её решения.

var obj1 = {
  test: 'test',
  toString: function(){ return 10; },
  valueOf: function(){ return 100; }
}

var obj2 = {
  test: 'test',
  toString: function(){ return 100; },
  valueOf: function(){ return 10; }
}

obj1 ? obj2
Узнать ответ
obj1 > obj2

Как было сказано в предыдущем решении, при сравнении JavaScript сначала пытается привести непримитивные аргументы к числам, т.е. вызывает метод valueOf. А 100 > 10.


5. Бесконечности безумия

Что вернет?

parseFloat('Infinity');
Number('Infinity');
parseInt('Infinity');
Узнать ответ
parseFloat('Infinity'); //Infinity
Number('Infinity');     //Infinity
parseInt('Infinity');   //NaN

Число отображается как бесконечность, когда оно превышает максимальное число с плавающей точкой: 1.7976931348623157E+10308. Т.е. по сути Infinity это константа с дробным значением и поэтому она определяется parseFloat и Number, но не определяется parseInt.


6. Игры с плюсами

Для тех, кто внимательно читал предыдущие решения, не составит труда сказать, что вернет вот такое выражение:

"foo" + + "bar"
Узнать ответ
("foo" + + "bar") === "fooNaN" // true

Выделим унарный +: “foo” + (+ “bar”). Как не трудно видеть, он вернет NaN, т.к. строку нельзя привести к числу. Таким образом:

"foo" + NaN === "fooNaN".

7. Самое маленькое число

Кто больше?

Number.MIN_VALUE ? 0

Узнать ответ

Number.MIN_VALUE > 0

Согласно спецификации MIN_VALUE - это число наиболее приближенное к 0, которое позволяет JavaScript. Приблизительно равно 5e-324. Все числа меньшие по модулю конвертируются в 0. Называть его минимальным, пожалуй, логично, т.к. максимальное отрицательное так и называется “максимально отрицательное”, но все-таки вносит определенную путаницу.


8. Налог на роскошь

Что увидим на экране?

alert(111111111111111111111)

Узнать ответ

111111111111111111000

JavaScript интерпретирует большие числа в экспоненциальной форме:

111111111111111111111 = 1.11111111111111111111e20.

Но для хранения 1.11111111111111111111 точности не хватает и поэтому

(1.11111111111111111111e20).toString() == 111111111111111111000.


9. “Булева арифметика”

(true + 1) === 2;​
(true + true) === 2;
true === 2;
true === 1;
Узнать ответ
(true + 1) === 2;​  ​ // true
(true + true) === 2;  // true
true === 2;       // false
true === 1;       // false

Двухместный оператор “+” либо складывает числа, либо конкатенирует строки. При привидении true к числу будет возвращена 1.

Оператор “===” в отличии от “+” не производит привидение типов и поэтому true строго не равно единице.


10. Отрицание пустоты

[] == ![]
Узнать ответ
[] == ![] //true

Небольшая цитата из правила привидение типов в операторе == из спецификации:

1. Если Type(x) – Number и Type(y) – String, вернуть результат сравнения x == ToNumber(y).

2. Если Type(x) – String и Type(y) – Number, вернуть результат сравнения ToNumber(x) == y.

3. Если Type(x) – Boolean, вернуть результат сравнения ToNumber(x) == y.

4. Если Type(y) – Boolean, вернуть результат сравнения x == ToNumber(y).

5. Если Type(x) – либо String, либо Number, и Type(y) – Object, вернуть результат сравнения x == ToPrimitive(y).

6. Если Type(x) – Object и Type(y) – либо String, либо Number, вернуть результат сравнения ToPrimitive(x) == y.

Сначала вычислим правую часть равенства и получим:

[] == false

т.к. объект всегда приводится к true. Теперь

по правилу 4:

[] == 0

по правилу 6 и со знаниями из 3 задачки:

"" == 0

и по правилу 2:

0 == 0

Вуаля!


Заключение

Сколько задачек с первого раза отгадали правильно?) Надеюсь вам понравились небольшие взрывы мозга. По субъективным ощущениям, разбор таких ситуаций очень полезен. Лично для меня это оказалось единственным поводом вдумчиво почитать спецификацию ES, т.к. в обычной жизни мой код достаточно аккуратен, чтобы не создавать таких ситуаций.

И да, главное: не используйте это в боевом коде! Пожалейте тех, кто будет его читать)