(Нe) всё в JavaScript — объект

Перевод заметки Daniel Li: (Not) Everything in JavaScript is an Object.

Те, кто просто хочет получить ответы, не стесняйтесь сразу переходить к «Итого» в конце заметки

Существует много путаницы в том, является ли JavaScript объектно-ориентированным языком программирования (ООП) или функциональным языком. Действительно, JavaScript может работать и так, и так.

Но это заставило людей задуматься: «Все ли в JavaScript — объекты?», «А что насчет функций?».

Эта заметка расставит все на свои места.

Начнем с самого начала

В JavaScript существует шесть примитивных типов данных:

  • Булевые значения — true или false
  • null
  • undefined
  • number - 64-битный флоат (в JavaScript нет целых чисел)
  • string
  • symbol (появился в ES6)

В дополнение к этим шести примитивным типам, стандарт ECMAScript также определяет тип объекта, представляющий собой хранилище ключей.

const object = {
key: "value"
}

Итак, короче говоря, все, что не является примитивным типом, является объектом, включая функции и массивы.

Все функции — объекты
// Примитивные типы
true instanceof Object; // false
null instanceof Object; // false
undefined instanceof Object; // false
0 instanceof Object; // false
'bar' instanceof Object; // false
// Непримитивные типы
const foo = function () {}
foo instanceof Object; // true

Примитивные типы

Примитивные типы не имеют методов; поэтому вы никогда не встретите undefined.toString(). Кроме того, из-за этого примитивные типы неизменяемы, потому что у них нет методов, которые могли бы их изменить.

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

const answer = 42
answer.foo = "bar";
answer.foo; // undefined
Примитивные типы неизменяемы

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

"dog" === "dog"; // true
14 === 14; // true
{} === {}; // false
[] === []; // false
(function () {}) === (function () {}); // false
Примитивные типы хранятся по значению, объекты хранятся по ссылке

Функции

Функция — это особый тип объекта со специальными свойствами, например, constructor и call.

const foo = function (baz) {};
foo.name; // "foo"
foo.length; // 1

И как к обычным объектам, вы можете добавлять новые свойства:

foo.bar = "baz";
foo.bar; // "baz"

Это делает функции объектами первого класса, так как их можно передать в качестве аргумента в другие функции, как и любой другой объект.

Методы

Метод — это свойство объекта, являющееся функцией.

const foo = {};
foo.bar = function () { console.log("baz"); };
foo.bar(); // "baz"

Функции-конструкторы

Если у вас есть несколько объектов, использующих одну и ту же реализацию, вы можете поместить эту логику внутри функции-конструктора, а затем вызвать конструктор для создания этих объектов.

Функция-конструктор ничем не отличается от любой другой функции. Функция используется в качестве конструктора, когда она используется после ключевого слова new.

Любая функция может быть функцией-конструктором
const Foo = function () {};
const bar = new Foo();
bar; // {}
bar instanceof Foo; // true
bar instanceof Object; // true

Функция-конструктор вернет объект. Вы можете использовать this внутри тела функции, чтобы назначать новые свойства объекту. Поэтому, если мы хотим сделать много объектов с свойством bar и его значением baz, мы можем создать новую функцию-конструктор Foo, инкапсулирующую эту логику.

const Foo = function () {
this.bar = "baz";
};
const qux = new Foo();
qux; // { bar: "baz" }
qux instanceof Foo; // true
qux instanceof Object; // true
Вы можете использовать функцию-конструктор для создания нового объекта

Запуск функции-конструктора, такой как Foo(), без new запустит Foo как обычную функцию. this внутри функции будет соответствовать контексту выполнения. Поэтому, если мы вызовем Foo() вне всех функций, она фактически изменит объект window.

Foo(); // undefined
window.bar; // "baz"

И наоборот, запуск обычной функции в качестве функции-конструктора обычно возвращает новый пустой объект, как вы уже видели.

const pet = new String("dog");

Объекты-оболочки

Путаница возникает из-за таких функций, как String, Number, Boolean, Function и так далее, которые при вызове с new создают объекты-оболочки для соответствующих типов.

String - глобальная функция, создающая примитивную строку при передаче аргумента; она попытается преобразовать аргумент в строку.

String(1337); // "1337"
String(true); // "true"
String(null); // "null"
String(undefined); // "undefined"
String(); // ""
String("dog") === "dog" // true
typeof String("dog"); // "string"

Но вы также можете использовать функцию String в качестве функции-конструктора.

const pet = new String("dog")
typeof pet; // "object"
pet === "dog"; // false

Будет создан новый объект (часто называемый объектом-оболочкой), представляющий строку dog со следующими свойствами:

{
0: "d",
1: "o",
2: "g",
length: 3
}

Автоупаковка (autoboxing)

Интересно, что конструктором как примитивных строк, так и объекта является функция String. Еще интереснее, что вы можете вызвать .constructor примитивной строки (и это при том, что мы уже рассмотрели, что примитивные типы не могут иметь методы!).

const pet = new String("dog")
pet.constructor === String; // true
String("dog").constructor === String; // true

Этот процесс называется автоупаковкой (autoboxing). Когда вы пытаетесь вызвать свойство или метод для определенных примитивных типов, JavaScript преобразует его во временный объект-оболочку и получает доступ к его свойству или методу, не затрагивая сам оригинал.

const foo = "bar";
foo.length; // 3
foo === "bar"; // true

В приведенном выше примере, чтобы получить доступ к свойству length, JavaScript упаковывает foo в объект-оболочку, получает доступ к свойству length объекта-оболочки, а после уничтожает его. Это делается без изменения foo (foo по-прежнему является примитивной строкой).

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

const foo = 42;
foo.bar = "baz"; // Присвоение, выполняемое на объекте-оболочке
foo.bar; // undefined

Он будет возмущаться, если вы попробуете проделать это с примитивным типом, не имеющим объект-оболочку, таким как undefined или null.

const foo = null;
foo.bar = "baz"; // Uncaught TypeError: Cannot set property 'bar' of null

Итого

  1. Не все в JavaScript — объект
  2. В JavaScript 6 примитивных типов
  3. Все, что не является примитивным типом, является объектом
  4. Функции — особый тип объекта
  5. Функции могут использоваться для создания новых объектов
  6. Строки, булевые значения и числа могут быть представлены в качестве примитивных типов и в качестве объектов
  7. Определенные примитивные типы (строки, булевые значения, числа) ведут себя как объекты благодаря наличию в JavaScript автоупаковки.

Слушайте наш подкаст в iTunes и SoundCloud, читайте нас на Medium, контрибьютьте на GitHub, общайтесь в группе Telegram, следите в Twitter и канале Telegram, рекомендуйте в VK и Facebook.

Заметка на GitHub