Конспект JS-course

JavaScript Garden. Типы

Источник: http://bonsaiden.github.io/JavaScript-Garden/ru/#types

Равенство и сравнение

JavaScript имеет 2 различных способа сравнения значений объектов на равенство.

Оператор сравнения

Оператор сравнения состоит из двух символов равенства: ==

Слабая типизированность языка JavaScript подразумевает приведение обеих переменных к одному типу для того, чтобы произвести сравнение.

""           ==   "0"           // false
0            ==   ""            // true
0            ==   "0"           // true
false        ==   "false"       // false
false        ==   "0"           // true
false        ==   undefined     // false
false        ==   null          // false
null         ==   undefined     // true
" \t\r\n"    ==   0             // true

В таблице выше показаны результаты приведения типов и это главная причина, почему использование == повсеместно считается плохой практикой: оно приводит к трудностям в отслеживании ошибок из-за сложных правил преобразования типов.

Кроме того, приведение типов во время сравнения также влияет на производительность; например, строка должна быть преобразована в число перед сравнением с другим числом.

Оператор строгого равенства

Оператор строгого равенства состоит из трёх символов равенства: ===

В отличие от обычного оператора равенства, оператор строгого равенства не выполняет приведение типов между операндами.

""           ===   "0"           // false
0            ===   ""            // false
0            ===   "0"           // false
false        ===   "false"       // false
false        ===   "0"           // false
false        ===   undefined     // false
false        ===   null          // false
null         ===   undefined     // false
" \t\r\n"    ===   0             // false

Результаты выше более понятны и позволяют быстрее выявлять ошибки в коде. Это в определённой степени улучшает код, а также дает прирост производительности в случае, если операнды имеют различные типы.

Сравнение объектов

Хотя оба оператора == и === заявлены как операторы равенства, они ведут себя по-разному, когда хотя бы один из операндов является Object.

{} === {};                   // false
new String('foo') === 'foo'; // false
new Number(10) === 10;       // false
var foo = {};
foo === foo;                 // true

Здесь оба операнда сравниваются на идентичность, а не на равенство; то есть будет проверяться, являются ли операнды одним экземпляром объекта, так же как делает is в Python и сравниваются указатели в С.

Заключение

Крайне рекомендуется использовать только операторы строгого равенства. В случае, когда намечается преобразование типов, нужно сделать явное приведение и не оставлять их на совести языковых хитростей с преобразованиями.

Оператор typeof

Оператор typeof (вместе с instanceof) — это, вероятно, самая большая недоделка в JavaScript, поскольку, похоже, он поломан более, чем полностью.

Хотя instanceof еще имеет ограниченное применение, typeof на самом деле имеет только один практический случай применения, который при всём при этом не является проверкой типа объекта.

Таблица типов JavaScript

Значение            Класс      Тип
-------------------------------------
"foo"               String     string
new String("foo")   String     object
1.2                 Number     number
new Number(1.2)     Number     object
true                Boolean    boolean
new Boolean(true)   Boolean    object
new Date()          Date       object
new Error()         Error      object
[1,2,3]             Array      object
new Array(1, 2, 3)  Array      object
new Function("")    Function   function
/abc/g              RegExp     object (function в Nitro/V8)
new RegExp("meow")  RegExp     object (function в Nitro/V8)
{}                  Object     object
new Object()        Object     object

В таблице выше Тип представляет собой значение, возвращаемое оператором typeof. Как хорошо видно, это значение может быть абсолютно любым, но не логичным результатом.

Класс представляет собой значение внутреннего свойства [[Class]] объекта.

Из спецификации: Значением [[Class]] может быть одна из следующих строк: Arguments, Array, Boolean, Date, Error, Function, JSON, Math, Number, Object, RegExp, String. Для того, чтобы получить значение [[Class]], необходимо вызвать метод toString у Object.prototype.

Класс объекта

Спецификация предоставляет только один способ доступа к значению [[Class]] — используя Object.prototype.toString.

function is(type, obj) {
    var clas = Object.prototype.toString.call(obj).slice(8, -1);
    return obj !== undefined && obj !== null && clas === type;
}

is('String', 'test'); // true
is('String', new String('test')); // true

В примере выше Object.prototype.toString вызывается со значением this, являющимся объектом, значение [[Class]] которого нужно получить.

Проверка переменных на определённость

typeof foo !== 'undefined'

Выше проверяется, было ли foo действительно объявлено или нет; просто обращение к переменной приведёт к ReferenceError. Это единственное, чем на самом деле полезен typeof.

Заключение

Для проверки типа объекта настоятельно рекомендуется использовать Object.prototype.toString — это единственный надежный способ. Как показано выше в таблице типов, некоторые возвращаемые typeof значения не определены в спецификации: таким образом, они могут отличаться в различных реализациях.

Кроме случая проверки, была ли определена переменная, typeof следует избегать во что бы то ни стало.

Оператор instanceof

Оператор instanceof сравнивает конструкторы двух операндов. Это полезно только когда сравниваются пользовательские объекты. Использование на встроенных типах почти так же бесполезно, как и оператор typeof.

Сравнение пользовательских объектов

function Foo() {}
function Bar() {}
Bar.prototype = new Foo();

new Bar() instanceof Bar; // true
new Bar() instanceof Foo; // true

// Всего лишь присваиваем Bar.prototype объект функции Foo,
// но не экземпляра Foo
Bar.prototype = Foo;
new Bar() instanceof Foo; // false

Использование instanceof со встроенными типами

new String('foo') instanceof String; // true
new String('foo') instanceof Object; // true

'foo' instanceof String; // false
'foo' instanceof Object; // false

Здесь надо отметить одну важную вещь: instanceof не работает на объектах, которые происходят из разных контекстов JavaScript (например, из различных документов в web-браузере), так как их конструкторы и правда не будут конструкторами тех самых объектов.

Заключение

Оператор instanceof должен использоваться только при обращении к пользовательским объектам, происходящим из одного контекста JavaScript. Так же, как и в случае оператора typeof, любого другого использования необходимо избегать.

Приведение типов

JavaScript — слабо типизированный язык, поэтому преобразование типов будет применяться везде, где возможно.

// Эти равенства — истинны
new Number(10) == 10; // объект типа Number преобразуется
                      // в числовой примитив в результате неявного вызова
                      // метода Number.prototype.valueOf

10 == '10';           // Strings преобразуется в Number
10 == '+10 ';         // Ещё чуток строко-безумия
10 == '010';          // и ещё
isNaN(null) == false; // null преобразуется в 0,
                      // который конечно же не NaN

// Эти равенства — ложь
10 == 010;
10 == '-10';

Для того, чтобы избежать этого, настоятельно рекомендуется использовать оператор строгого равенства. Впрочем, хотя это и позволяет избежать многих распространенных ошибок, существует ещё много дополнительных вопросов, которые возникают из-за слабости типизации JavaScript.

Конструкторы встроенных типов

Конструкторы встроенных типов, например, Number и String ведут себя различным образом, в зависимости от того, вызываются они с ключевым словом new или без.

new Number(10) === 10;     // False, Object и Number
Number(10) === 10;         // True, Number и Number
new Number(10) + 0 === 10; // True, из-за неявного преобразования

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

Кроме того, присутствие литералов или переменных, которые не являются объектами, приведет к еще большему насилию над типами.

Лучший вариант — это явное приведение к одному из трех возможных типов.

Приведение к строке

'' + 10 === '10'; // true

Путём добавления в начале пустой строки, значение легко приводится к строке.

Приведение к числовому типу

+'10' === 10; // true

Используя унарный оператор плюс, можно преобразовать значение в число.

Приведение к булеву типу

Используя оператор not (!) дважды, значение может быть приведено к логическому (булеву) типу.

!!'foo';   // true
!!'';      // false
!!'0';     // true
!!'1';     // true
!!'-1'     // true
!!{};      // true
!!true;    // true