Приведение типов — это процесс преобразования значений из одного типа в другой (например — строки в число, объекта — в логическое значение, и так далее). Любой тип в JavaScript, идёт ли речь о примитивном типе, или об объекте, может быть преобразован в другой тип. Напомним, что примитивными типами данных в JS являются Number, String, Boolean, Null, Undefined. К этому списку в ES6 добавился тип Symbol, который ведёт себя совсем не так, как другие типы. Явное приведение типов — процесс простой и понятный, но всё меняется, когда дело доходит до неявного приведения типов. Тут то, что происходит в JavaScript, некоторые считают странным или нелогичным, хотя, конечно, если заглянуть в стандарты, становится понятно, что все эти «странности» являются особенностями языка. Как бы там ни было, любому JS-разработчику периодически приходится сталкиваться с неявным приведением типов, к тому же, каверзные вопросы о приведении типов вполне могут встретиться на собеседовании.

image

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

Проверь себя


Вот список интересных выражений, о которых мы только что говорили:

true + false
12 / "6"
"number" + 15 + 3
15 + 3 + "number"
[1] > null
"foo" + + "bar"
'true' == true
false == 'false'
null == ''
!!"false" == !!"true"
[‘x’] == ‘x’
[] + null + 1
0 || "0" && {}
[1,2,3] == [1,2,3]
{}+[]+{}+[1]
!+[]+[]+![]
new Date(0) - 0
new Date(0) + 0

Тут полно такого, что выглядит более чем странно, но без проблем работает в JS, задействуя неявное приведение типов. В подавляющем большинстве случаев неявного приведения типов в JS лучше всего избегать. Рассматривайте этот список как упражнение для проверки ваших знаний о том, как работает приведение типов в JavaScript. Если же тут для вас ничего нового не нашлось — загляните на wtfjs.com.


JavaScript полон странностей

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

Неявное преобразование типов и явное преобразование типов


Преобразование типов может быть явным и неявным. Когда разработчик выражает намерение сконвертировать значение одного типа в значение другого типа, записывая это соответствующим образом в коде, скажем, в виде Number(value), это называется явным приведением типов (или явным преобразованием типов).

Так как JavaScript — это язык со слабой типизацией, значения могут быть конвертированы между различными типами автоматически. Это называют неявным приведением типов. Обычно такое происходит, когда в выражениях используют значения различных типов, вроде 1 == null, 2/’5', null + new Date(). Неявное преобразование типов может быть вызвано и контекстом выражения, вроде if (value) {…}, где value неявно приводится к логическому типу данных.

Существует оператор, который не вызывает неявного преобразование типов — это оператор строгого равенства, ===. Оператор нестрогого равенства, ==, с другой стороны, выполняет и операцию сравнения, и, если нужно, выполняет неявное преобразование типов.

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

Три вида преобразования типов


Первая особенность работы с типами в JS, о которой нужно знать, заключается в том, что здесь есть только три вида преобразований:

  • В строку (String)
  • В логическое значение (Boolean)
  • В число (Number)

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

Примитивные типы данных


?Преобразование к типу String


Для того чтобы явно преобразовать значение в строку, можно воспользоваться функцией String(). Неявное преобразование вызывает использование обычного оператора сложения, +, с двумя операндами, если один из них является строкой:

String(123) // явное преобразование
123 + ''    // неявное преобразование

Все примитивные типы преобразуются в строки вполне естественным и ожидаемым образом:

String(123)                   // '123'
String(-12.3)                 // '-12.3'
String(null)                  // 'null'
String(undefined)             // 'undefined'
String(true)                  // 'true'
String(false)                 // 'false'

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

String(Symbol('my symbol'))   // 'Symbol(my symbol)'
'' + Symbol('my symbol')      // ошибка TypeError

?Преобразование к типу Boolean


Для того, чтобы явно преобразовать значение к логическому типу, используют функцию Boolean(). Неявное преобразование происходит в логическом контексте, или вызывается логическими операторами (|| && !).

Boolean(2)          // явное преобразование
if (2) { ... }      // неявное преобразование в логическом контексте
!!2                 // неявное преобразование логическим оператором
2 || 'hello'        // неявное преобразование логическим оператором

Обратите внимание на то, что операторы, вроде || и && выполняют преобразование значений к логическому типу для внутренних целей, а возвращают значения исходных операндов, даже если они не являются логическими.

// это выражение возвращает число 123, а не true
// 'hello' и 123 неявно преобразуются к логическому типу при работе оператора && для вычисления значения выражения
let x = 'hello' && 123;   // x === 123

Так как при приведении значения к логическому типу возможны лишь два результата — true или false, легче всего освоить этот вид преобразований, запомнив те выражения, которые выдают false:

Boolean('')           // false
Boolean(0)            // false     
Boolean(-0)           // false
Boolean(NaN)          // false
Boolean(null)         // false
Boolean(undefined)    // false
Boolean(false)        // false

Любое значение, не входящее в этот список, преобразуется в true, включая объекты, функции, массивы, даты, а также типы, определённые пользователем. Значения типа Symbol также преобразуются в true. Пустые объекты и пустые массивы тоже преобразуются в true:

Boolean({})             // true
Boolean([])             // true
Boolean(Symbol())       // true
!!Symbol()              // true
Boolean(function() {})  // true

?Преобразование к типу Number


Явное преобразование к числовому типу выполняется с помощью функции Number() — то есть по тому же принципу, который используется для типов Boolean и String.

Неявное приведение значения к числовому типу — тема более сложная, так как оно применяется, пожалуй, чаще чем преобразование в строку или в логическое значение. А именно, преобразование к типу Number выполняют следующие операторы:

  • Операторы сравнения (>, <, <=, >=).
  • Побитовые операторы (|, &, ^, ~).
  • Арифметические операторы (-, +, *, /, %). Обратите внимание на то, что оператор + с двумя операндами не вызывает неявное преобразование к числовому типу, если хотя бы один оператор является строкой.
  • Унарный оператор +.
  • Оператор нестрогого равенства == (а также !=). Обратите внимание на то, что оператор == не производит неявного преобразования в число, если оба операнда являются строками.

Number('123')   // явное преобразование
+'123'          // неявное преобразование
123 != '456'    // неявное преобразование
4 > '5'         // неявное преобразование
5/null          // неявное преобразование
true | 0        // неявное преобразование

Вот как в числа преобразуются примитивные значения:

Number(null)                   // 0
Number(undefined)              // NaN
Number(true)                   // 1
Number(false)                  // 0
Number(" 12 ")                 // 12
Number("-12.34")               // -12.34
Number("\n")                   // 0
Number(" 12s ")                // NaN
Number(123)                    // 123

При преобразовании строк в числа система сначала обрезает пробелы, а также символы \n и \t, находящиеся в начале или в конце строки, и возвращает NaN, если полученная строка не является действительным числом. Если строка пуста — возвращается 0.

Значения null и undefined обрабатываются иначе: null преобразуется в 0, в то время как undefined превращается в NaN.

Значения типа Symbol не могут быть преобразованы в число ни явно, ни неявно. Более того, при попытке такого преобразования выдаётся ошибка TypeError. Можно было бы ожидать, что подобное вызовет преобразование значения типа Symbol в NaN, как это происходит с undefined, но этого не происходит. Подробности о правилах преобразования значений типа Symbol вы можете найти на MDN.

Number(Symbol('my symbol'))    // Ошибка TypeError
+Symbol('123')                 // Ошибка TypeError

Вот два особых правила, которые стоит запомнить:

При применении оператора == к null или undefined преобразования в число не производится. Значение null равно только null или undefined и не равно ничему больше.

null == 0               // false, null не преобразуется в 0
null == null            // true
undefined == undefined  // true
null == undefined       // true

Значение NaN не равно ничему, включая себя. В следующем примере, если значение не равно самому себе, значит мы имеем дело с NaN

if (value !== value) { console.log("we're dealing with NaN here") }

Преобразование типов для объектов


Итак, мы рассмотрели преобразование типов для примитивных значений. Тут всё довольно просто. Когда же дело доходит до объектов, и система встречает выражения вроде [1] + [2,3], сначала ей нужно преобразовать объект в примитивное значение, которое затем преобразуется в итоговой тип. При работе с объектами, напомним, также существует всего три направления преобразований: в число, в строку, и в логическое значение.

Самое простое — это преобразование в логическое значение: любое значение, не являющееся примитивом, всегда неявно конвертируется в true, это справедливо и для пустых объектов и массивов.

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

Вот псевдо-реализация метода [[ToPrimitive]]:

function ToPrimitive(input, preferredType){
  
  switch (preferredType){
    case Number:
      return toNumber(input);
      break;
    case String:
      return toString(input);
      break
    default:
      return toNumber(input);  
  }
  
  function isPrimitive(value){
    return value !== Object(value);
  }

  function toString(){
    if (isPrimitive(input.toString())) return input.toString();
    if (isPrimitive(input.valueOf())) return input.valueOf();
    throw new TypeError();
  }

  function toNumber(){
    if (isPrimitive(input.valueOf())) return input.valueOf();
    if (isPrimitive(input.toString())) return input.toString();
    throw new TypeError();
  }
}

Методу [[ToPrimitive]] передаётся входное значение и предпочитаемый тип, к которому его надо преобразовать: Number или String. При этом аргумент preferredType необязателен.

И при конверсии в число, и при конверсии в строку используются два метода объекта, передаваемого [[ToPrimitive]]: это valueOf и toString. Оба метода объявлены в Object.prototype, и, таким образом, доступны для любого типа, основанного на Object, например — это Date, Array, и так далее.

В целом, работа алгоритма выглядит следующим образом:

  1. Если входное значение является примитивом — не делать ничего и вернуть его.
  2. Вызвать input.toString(), если результат является значением примитивного типа — вернуть его.
  3. Вызвать input.valueOf(), если результат является значением примитивного типа — вернуть его.
  4. Если ни input.toString(), ни input.valueOf() не дают примитивное значение — выдать ошибку TypeError.

При преобразовании в число сначала вызывается valueOf (3), если результат получить не удаётся — вызывается toString (2). При преобразовании в строку используется обратная последовательность действий — сначала вызывается toString (2), а в случае неудачи вызывается valueOf (3).

Большинство встроенных типов не имеют метода valueOf, или имеют valueOf, который возвращает сам объект, для которого он вызван (this), поэтому такое значение игнорируется, так как примитивом оно не является. Именно поэтому преобразование в число и в строку может работать одинаково — и то и другое сводится к вызову toString().

Различные операторы могут вызывать либо преобразование в число, либо преобразование в строку с помощью параметра preferredType. Но есть два исключения: оператор нестрогого равенства == и оператор + с двумя операндами вызывают конверсию по умолчанию (preferredType не указывается или устанавливается в значение default). В этом случае большинство встроенных типов рассматривают, как стандартный вариант поведения, конверсию в число, за исключением типа Date, который выполняет преобразование объекта в строку.

Вот пример поведения Date при преобразовании типов:

let d = new Date();

// получение строкового представления
let str = d.toString();  // 'Wed Jan 17 2018 16:15:42'

// получение числового представления, то есть - числа миллисекунд с начала эпохи Unix
let num = d.valueOf();   // 1516198542525

// сравнение со строковым представлением
// получаем true так как d конвертируется в ту же строку
console.log(d == str);   // true

// сравнение с числовым представлением
// получаем false, так как d не преобразуется в число с помощью valueOf()
console.log(d == num);   // false

// Результат 'Wed Jan 17 2018 16:15:42Wed Jan 17 2018 16:15:42'
// '+', так же, как и '==', вызывает режим преобразования по умолчанию
console.log(d + d);

// Результат 0, так как оператор '-' явно вызывает преобразование в число, а не преобразование по умолчанию
console.log(d - d);

Стандартные методы toString() и valueOf() можно переопределить для того, чтобы вмешаться в логику преобразования объекта в примитивные значения.

var obj = {
  prop: 101,
  toString(){
    return 'Prop: ' + this.prop;
  },
  valueOf() {
    return this.prop;
  }
};

console.log(String(obj));  // 'Prop: 101'
console.log(obj + '')      // '101'
console.log(+obj);         //  101
console.log(obj > 100);    //  true

Обратите внимание на то, что obj + ‘’ возвращает ‘101’ в виде строки. Оператор + вызывает стандартный режим преобразования. Как уже было сказано, Object рассматривает приведение к числу как преобразование по умолчанию, поэтому использует сначала метод valueOf() а не toString().

Метод Symbol.toPrimitive ES6


В ES5 допустимо менять логику преобразования объекта в примитивное значение путём переопределения методов toString и valueOf.

В ES6 можно пойти ещё дальше и полностью заменить внутренний механизм [[ToPrimitive]], реализовав метод объекта [Symbol.toPrimtive].

class Disk {
  constructor(capacity){
    this.capacity = capacity;
  }

  [Symbol.toPrimitive](hint){
    switch (hint) {
      case 'string':
        return 'Capacity: ' + this.capacity + ' bytes';

      case 'number':
        // преобразование в KiB
        return this.capacity / 1024;

      default:
        // считаем преобразование в число стандартным
        return this.capacity / 1024;
    }
  }
}

// 1MiB диск
let disk = new Disk(1024 * 1024);

console.log(String(disk))  // Capacity: 1048576 bytes
console.log(disk + '')     // '1024'
console.log(+disk);        // 1024
console.log(disk > 1000);  // true

Разбор примеров


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

true + false             // 1
12 / "6"                 // 2
"number" + 15 + 3        // 'number153'
15 + 3 + "number"        // '18number'
[1] > null               // true
"foo" + + "bar"          // 'fooNaN'
'true' == true           // false
false == 'false'         // false
null == ''               // false
!!"false" == !!"true"    // true
['x'] == 'x'             // true 
[] + null + 1            // 'null1'
0 || "0" && {}           // {}
[1,2,3] == [1,2,3]       // false
{}+[]+{}+[1]             // '0[object Object]1'
!+[]+[]+![]              // 'truefalse'
new Date(0) - 0          // 0
new Date(0) + 0          // 'Thu Jan 01 1970 02:00:00(EET)0'

Разберём каждый из этих примеров.

?true + false


Оператор + с двумя операндами вызывает преобразование к числу для true и false:

true + false
==> 1 + 0
==> 1

?12 / '6'


Арифметический оператор деления, /, вызывает преобразование к числу для строки '6':

12 / '6'
==> 12 / 6
==>> 2

?«number» + 15 + 3


Оператор + имеет ассоциативность слева направо, поэтому выражение "number" + 15 выполняется первым. Так как один из операндов является строкой, оператор + вызывает преобразование к строке для числа 15. На втором шаге вычисления выражения "number15" + 3 обрабатывается точно так же:

"number" + 15 + 3 
==> "number15" + 3 
==> "number153"

?15 + 3 + «number»


Выражение 15 + 3 вычисляется первым. Тут совершенно не нужно преобразование типов, так как оба операнда являются числами. На втором шаге вычисляется значение выражения 18 + 'number', и так как один из операндов является строкой — вызывается преобразование в строку.

15 + 3 + "number" 
==> 18 + "number" 
==> "18number"

?[1] > null


Оператор сравнения > выполняет числовое сравнение [1] и null:

[1] > null
==> '1' > 0
==> 1 > 0
==> true

?«foo» + + «bar»


Унарный оператор + имеет более высокий приоритет, чем обычный оператор +. В результате выражение +'bar' вычисляется первым. Унарный + вызывает для строки 'bar' преобразование в число. Так как строка не является допустимым числом, в результате получается NaN. На втором шаге вычисляется значение выражения 'foo' + NaN.

"foo" + + "bar" 
==> "foo" + (+"bar") 
==> "foo" + NaN 
==> "fooNaN"

?'true' == true и false == 'false'


Оператор == вызывает преобразование в число, строка 'true' преобразуется в NaN, логическое значение true преобразуется в 1.

'true' == true
==> NaN == 1
==> false
false == 'false'   
==> 0 == NaN
==> false

?null == ''


Оператор == обычно вызывает преобразование в число, но это не так в случае со значением null. Значение null равно только null или undefined и ничему больше.

null == ''
==> false

?!!«false» == !!«true»


Оператор !! конвертирует строки 'true' и 'false' в логическое true, так как они являются непустыми строками. Затем оператор == просто проверяет равенство двух логических значений true без преобразования типов.

!!"false" == !!"true"  
==> true == true
==> true

?['x'] == 'x'  


Оператор == вызывает для массивов преобразование к числовому типу. Метод объекта Array.valueOf() возвращает сам массив, и это значение игнорируется, так как оно не является примитивом. Метод массива toString() преобразует массив ['x'] в строку 'x'.

['x'] == 'x'  
==> 'x' == 'x'
==>  true

?[] + null + 1  


Оператор + вызывает преобразование в число для пустого массива []. Метод объекта Array valueOf() игнорируется, так как он возвращает сам массив, который примитивом не является. Метод массива toString() возвращает пустую строку.

На втором шаге вычисляется значение выражения '' + null + 1.

[] + null + 1  
==>  '' + null + 1  
==>  'null' + 1  
==> 'null1'

?0 || «0» && {}  


Логические операторы || и && в процессе работы приводят значение операндов к логическому типу, но возвращают исходные операнды (которые имеют тип, отличный от логического). Значение 0 ложно, а значение '0' истинно, так как является непустой строкой. Пустой объект {} так же преобразуется к истинному значению.

0 || "0" && {}  
==>  (0 || "0") && {}
==> (false || true) && true  // внутреннее преобразование
==> "0" && {}
==> true && true             // внутреннее преобразование
==> {}

?[1,2,3] == [1,2,3]


Преобразование типов не требуется, так как оба операнда имеют один и тот же тип. Так как оператор == выполняет проверку на равенство ссылок на объекты (а не на то, содержат ли объекты одинаковые значения) и два массива являются двумя разными объектами, в результате будет выдано false.

[1,2,3] == [1,2,3]
==>  false

?{}+[]+{}+[1]


Все операнды не являются примитивными значениями, поэтому оператор + начинается с самого левого и вызывает его преобразование к числу. Метод valueOf для типов Object и Array возвращают сами эти объекты, поэтому это значение игнорируется. Метод toString() используется как запасной вариант. Хитрость тут в том, что первая пара фигурных скобок {} не рассматривается как объектный литерал, она воспринимается как блок кода, который игнорируется. Вычисление начинается со следующего выражения, +[], которое преобразуется в пустую строку через метод toString(), а затем в 0.

{}+[]+{}+[1]
==> +[]+{}+[1]
==> 0 + {} + [1]
==> 0 + '[object Object]' + [1]
==> '0[object Object]' + [1]
==> '0[object Object]' + '1'
==> '0[object Object]1'

?!+[]+[]+![]


Этот пример лучше объяснить пошагово в соответствии с порядком выполнения операций.

!+[]+[]+![]  
==> (!+[]) + [] + (![])
==> !0 + [] + false
==> true + [] + false
==> true + '' + false
==> 'truefalse'

?new Date(0) — 0


Оператор - вызывает преобразование в число для объекта типа Date. Метод Date.valueOf() возвращает число миллисекунд с начала эпохи Unix.

new Date(0) - 0
==> 0 - 0
==> 0

?new Date(0) + 0


Оператор + вызывает преобразование по умолчанию. Объекты типа Data считают таким преобразованием конверсию в строку, в результате используется метод toString(), а не valueOf().

new Date(0) + 0
==> 'Thu Jan 01 1970 02:00:00 GMT+0200 (EET)' + 0
==> 'Thu Jan 01 1970 02:00:00 GMT+0200 (EET)0'

Итоги


Преобразование типов — это один из базовых механизмом JavaScript, знание которого является основой продуктивной работы. Надеемся, сегодняшний материал помог тем, кто не очень хорошо разбирался в неявном преобразовании типов, расставить всё по своим местам, а тем, кто уверенно, с первого раза, никуда не подсматривая, смог решить «вступительную задачу», позволил вспомнить какой-нибудь интересный случай из их практики.

Уважаемые читатели! А в вашей практике случалось так, чтобы путаница с неявным преобразованием типов в JavaScript приводила к таинственным ошибкам?

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


  1. potan
    30.01.2018 15:15
    +1

    Интересно, а от каких преобразований типов зависит реальный код решающий реальные задачи? Я подозреваю, что если сохранить только преобразования между строками, чистами и логическими значениями, а преобразования массивов, объектов, null и undefined запретить, то все будет работать нормально. Правда со строками и числами веселия уже хватает.


    1. faiwer
      30.01.2018 15:24

      Частично такие проблемы решаются линтерами на JS (eslint) и на TS (tslint). Скажем запрет на == или запрет на складывание чисел и строк в TS.


    1. v1vendi
      31.01.2018 16:39

      Преобразование null и undefined к bool использовать довольно удобно во многих местах.
      Например в React.js null и undefined это допустимые значения, не рендерящие ничего, и зачастую используется выражение:

      var component = someFuncReturningUndefined()
      ...
      render() {
          return (
              <div>{component}</div>
          )
      }
      


  1. faiwer
    30.01.2018 15:22

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

    95% этой статьи состоит из разного мусора, вроде что будет если сложить пару массивов и пустой объект. Это никак не повышает продуктивность вашей работы. Поиск по \b==\b в моём коде выдаёт ровно 0 результатов, хоть я и не пользуюсь TypeScript или Flow. Значит ли это, что я пишу непродуктивный код? А если я явным образом (например Number(someStringNumber)) привожу типы к нужным мне видам, значит ли это, что я просто чайник, и мне следует распечатать таблицы преобразования и заучивать их?


    Честно говоря, всякий раз, когда в статье с тегом JavaScript я вижу что-то вроде !+[]+[]+![], я начинаю относиться к ней более чем предвзято, предполагая, что это очередной набор бессмысленной чепухи от JavaScript-хейтера, или же <irony>очень полезный материал для начинающих</irony>.


    1. xlenz
      31.01.2018 02:28

      Согласен, хотя мне, например, забавно почитать. В реальном коде такое нигде не встречал, разве что, возможно, для обфускации.
      Когда то говорили, пиши код так, как будто его сопровождать будет маньяк, который знает где вы живёте. После подобной строки он бы точно нашёл, о да :)


  1. Romas34
    30.01.2018 15:57

    Немного рассуждений на тему…
    Изначально учил паскаль и С/С++, что-то по мелочи писал, затем Java'у, в частности Java ME, несколько лет на ней, затем ActionScript и MXML… сейчас вот решил изучить JS — возможно придётся пописать на нём… Ко всему подхожу фундаментально, не люблю/не могу хватать вершки, «галопом по европам», так сказать, поэтому начал гуглить и искать какие-то учебники по языку, чтобы проникнуться в основы. И вот первое на что натыкаешься везде и повсюду, так это на приведение типов в JS: все хватаются за голову, пишут какие-то приколы, куча мемов на эту тему и т.п., я даже немного был удивлён и озадачен по началу. Начал читать элементарные темы «приведение типов» и т.п. и всё стало ясно-понятно, если понимать/знать как работает приведение типов в JS. Не совсем понятно, что тут такого особенного и сверхъестественного, с этими задачками, что столько народу так носится с данной темой и бугуртит… :)
    Естественно, все эти задачи носят чисто методический учебный или тестовый характер и совершенно не нужны в реальном применении. Ну такого ведь в любом языке хватает… Я хочу сказать, что ведь это не магия какая-то и не что-то из ряда вон выходящее. По большому счёту, здесь нет никакой хитрости, если знать «правила» и знать основы языка.… Или я чего-то не понимаю… :)
    В любом случае я сомневаюсь, что когда-то придётся писать/использовать нечто подобное :) Ибо, скорее всего везде и всегда будет использоваться явное приведение типа, ибо всегда надо отдавать себе отчёт в том, что ты делаешь и что хочешь получить…


    1. Free_ze
      30.01.2018 17:01
      +1

      Благодаря такой «гибкости» банальные ошибки вместо того, чтобы просто выплюнуть в стектрейс что-то информативное, вызывают интересные спец.эффекты где-то в неожиданном месте приложения. Такое вот искусственное препятствие в языке.


      1. xlenz
        31.01.2018 02:33

        Согласен с вами, но! В любом языке можно допустить опечатки… Не представляю как можно случайно написать! + [] — {} + !![]


        1. Deosis
          31.01.2018 08:23

          Последний пример с датой. Результат зависит от того, прибавляете вы ноль или вычитаете.
          Где ещё можно встретить такое поведение?


          1. xlenz
            01.02.2018 00:54

            Просто поставил плюсик


        1. Free_ze
          31.01.2018 08:23

          В большинстве языков эти «опечатки» не будут фатальны, ибо приведение типов просто вызовет ошибку на месте.

          Не представляю как можно случайно написать
          Представьте, что каждое значение возвращает функция или является полем какого-нибудь объекта — станет не так очевидно.


  1. Romas34
    30.01.2018 15:59

    P.S. За статью спасибо! Добавил в закладки :)


  1. Weks
    30.01.2018 18:12
    +1

    &ltzanuda&gt
    Функционал авто позволяет на полной скорости врезаться в стену. Но мы сознательно так не делаем, потому что это может привести к печальным последствиям.
    &lt/zanuda&gt

    Но как справочник – неплохо.


    1. marenkov
      31.01.2018 15:25

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


  1. klimentRu
    30.01.2018 20:15
    +1

    На JS все что угодно можно с помощью 6 символов написать. www.jsfuck.com


  1. Alexanqp
    31.01.2018 00:02

    Хорошо преподнесли данный материал, спасибо за статью.


  1. olehorg
    31.01.2018 00:02

    Я не програмист уже лет 20, просто интересуюсь.
    Но если бы мне такие вопросы (в количестве больше одного) начали задавать тестово — я бы спросил «Правда? Это обычное качество кода, с которым мне придется работать в вашей компании?» и послал бы рекрутеров.

    Насколько я понимаю — все усилия индустрии програмирования последних 20 лет были направлены на то чтоб ни один из указанных фрагментов ни при каких обстоятельствах не появился в реале.
    Или я ошибаюсь?


    1. Romas34
      31.01.2018 09:30

      Да при чём тут качество кода? Никто не заставляет так писать. Это обычные тестовые задания на выявление понимания основ языка, то что человек знает мат часть, так сказать, а не просто нахватался вершков и что-то там делает, возможно, по примерам, но в некоторых случаях совершенно не понимает как и почему это работает. Например мне совершенно не понятно, как можно программировать на каком-либо языке, не зная как там и что к чему кастится и в каком случае… Что, например, в логическом «И» операнды выполняются слева направо, пока какой-то из них не станет false, а в логическом «ИЛИ», наоборот, пока один из них не окажется true. И это не бесполезные знания, на мой взгляд.


      1. faiwer
        31.01.2018 12:25
        +1

        мне совершенно не понятно, как можно программировать на каком-либо языке, не зная как там и что к чему кастится и в каком случае

        Легко. Я так уже лет 8 работаю. Что в PHP, что в JS я не знаю наизусть таблицы приведения типов (кроме самых базовых вещей, вроде приведения к boolean, и операций со строками и числами).


        В этом списке
        true + false
        12 / "6"
        "number" + 15 + 3
        15 + 3 + "number"
        [1] > null
        "foo" + + "bar"
        'true' == true
        false == 'false'
        null == ''
        !!"false" == !!"true"
        [‘x’] == ‘x’
        [] + null + 1
        0 || "0" && {}
        [1,2,3] == [1,2,3]
        {}+[]+{}+[1]
        !+[]+[]+![]
        new Date(0) - 0
        new Date(0) + 0


        1. Romas34
          31.01.2018 13:12

          я не знаю наизусть таблицы приведения типов

          О каких таблицах речь? Я так понял, существует просто набор правил, если так можно выразиться и часть там да, надо запомнить, а остальное просто понимать.
          Такой код не должен проходить вменяемое review.

          Дак это и не «рабочий» код какой-то. Это тестовые учебные задачи. Вы тестирования что ли никакого никогда не проходили по любому ЯП? Почти везде есть подобное.
          Но статья то совсем не о них. Статья про {}+[]+{}+[1] и !!«false» == !!«true» прочие грабли.

          Да какие тут грабли? Такие же правила обычные, которые, вроде как, не мешало бы и знать, на мой взгляд:
          Для явного преобразования используется двойное логическое отрицание !!value или вызов Boolean(value).

          Строка, если не пустая, то она true при кастинге. Вот и всё, что необходимо знать, чтобы понять что получится в случае !!«false» == !!«true»… Вы не знаете что ли, что + по дефолту операнды кастит к строке, если один операнд строка? Что, чтобы сделать явное приведение к числу, то надо + впереди поставить (+«10»)? Что [] — ссылка на массив, а {} — на объект...? Ну и т.д. и т.п.
          Я согласен что есть какие-то хитрые заморочки с подобным {}+[]+{}+[1], но на то они и тестовые/контрольные задачки :) А остальные вещи по моему довольно просты…


          1. faiwer
            31.01.2018 13:56

            Это тестовые учебные задачи

            Тестовые задачи, имхо, должны проверять что-нибудь полезное, а не складывание массивов с объектами. Правило простое ? не важно чему равно [] + {} ("[object Object]"), если оно равно чепухе. Только воздух сотрясать.


            В реальном коде у вас вообще могут быть .valueOf, get & set, Proxy, with {} (если сильно не повезёт) и прочие хитрые штуки. В лучшем случае вы встретите что-нибудь вроде + new Date() вместо Date.now().


            В серьёзном JS приложении, чем меньше вы хаков примените, тем проще потом будет с этим всем работать. Вот о чём стоило бы написать красным, заглавными буквами, так это о том, что данные нужно приводить к корректному виду как можно раньше (на ранних этапах обработки) при помощи явных преобразований (Boolean(), String(), Number() и пр.), а затем следить за чистотой работы с ними.


            Скажем, получив некий ID из location.query, нужно преобразовать его в Number (если это number) сразу на этапе парсера queryString, в противном случае, возможны трудноуловимые сюрпризы.


            Я считаю подобные статьи, где акцент делается не на том, как избежать проблем, а на зоопарке хитрых преобразований, откровенно вредны. Неопытный разработчик начнёт так писать в своём коде, и вообще думать, что нормально. Разработчики других же языков натыкаясь на такие статьи думают, что у нас в JS мире царит жесточайший дурдом.


            Для явного преобразования используется двойное логическое отрицание !!value или вызов Boolean(value).

            Небольшое количество здравого смысла в статье присутствует, тут соглашусь. Но в основном она про то, как "foo" + + "bar" превращается в "fooNaN".


            но на то они и тестовые/контрольные задачки

            Давайте ещё крышки люков обсуждать. Лично я на собеседованиях задаю те задачи, которые дают мне понять понимает ли человек то, чем занимается, или пишет по принципу попугая (бездумная копипаста и "лишь бы работало"). Если человек не знает и знать не хочет, чем равно 4 - - '2', зато хорошо понимает что такое имутабельность, асимптотика, может написать бинарный поиск, умеет в свою экосистему, умеет правильно решать прикладные задачи, и вообще горит энтузиазмом, то мне дела нет до его 4 - - '2', потому, что он не напишет этого в коде.


            1. Romas34
              31.01.2018 14:16

              По моему мы с Вами говорим несколько о разных вещах :) Я совершенно согласен, вот абсолютно, с тем, что Вы сейчас говорите. Но Вы пытаетесь сравнить, на мой взгляд мягкое с тёплым :) Вот же ранее я писал:

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

              В любом случае я сомневаюсь, что когда-то придётся писать/использовать нечто подобное :) Ибо, скорее всего везде и всегда будет использоваться явное приведение типа, ибо всегда надо отдавать себе отчёт в том, что ты делаешь и что хочешь получить…

              Т.е., как бы естественно, что в реальном приложении/коде такое не надо использовать! :)
              Если человек не знает и знать не хочет, чем равно 4 — - '2', зато хорошо понимает что такое имутабельность, асимптотика, может написать бинарный поиск, умеет в свою экосистему, умеет правильно решать прикладные задачи, и вообще горит энтузиазмом, то мне дела нет до его 4 — - '2', потому, что он не напишет этого в коде.

              Ну я вот просто уверен, что такой человек без проблем ответит и на такой вопрос! :) А тестовые задания (приложения или реализация как-то конкретного функционала), вроде как, итак даются, в конце-концов, при приёме на работу :)
              И вообще, если уж речь зашла о приёме на работу (не понятно с чего, кстати :))), то я тоже считаю, что это всё фигня полная :)
              А для новичка подобные тесты, возможно, помогут лучше понять основы языка, проникнуться, так сказать :) Возможно, благодаря им, он даже заполнит у себя какие-нибудь пробелы в знаниях :)


              1. faiwer
                31.01.2018 14:26

                Ну я вот просто уверен, что такой человек без проблем ответит и на такой вопрос! :)

                А я нет. Выше про список писал. На многие из этих FooNaN-ов я не отвечу, скорее всего. Почему? Ну если человек не имеет привычки отнимать от чисел строки, и вообще заниматься подобной непотребщиной, то он может и не знать, что получится в результате. Он возможно даже когда-то знал это, но за полной бесполезностью эти знания ушли. Я вот не был уверен, что получится 6, т.к. привык относиться к таким вещам так: "получится дичь, обязательно переделать". И на всякий случай проверил в консоли.


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

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


    1. mad_nazgul
      31.01.2018 11:06

      Ну как бы да.
      Просто работают законы Мерфи.
      «Если что-то можно сделать не правильно, то это обязательно будет сделано не правильно»
      Несмотря на кажущуюся «искусственность» примеров, встретится с чем-то подобным в реальных проектах стремится к 100%
      :-)


      1. bopoh13
        31.01.2018 11:24

        Странно другое: не увидел ссылок на автотесты, позволяющие распознать в коде подобные конструкции. Полагаю, как и в других языках, программист просто закончит чтение, когда увидит в коде анекдот «1 + 1 = 2, потому что 1000 / синий». Но потраченное время не вернёшь.


        1. mad_nazgul
          31.01.2018 14:14
          -1

          Какие автотесты в динамически типизированном ЯП?!
          Только если полное множество значений всех типов…
          А так «run and pray» :-)


          1. bopoh13
            31.01.2018 16:47

            По-моему, shitcode не зависит от типизации ЯП.


            1. mad_nazgul
              02.02.2018 10:10

              Скажем так, статичная типизация не позволяет делать некоторых вещей.
              Это не панацея, а просто некоторая строгость.