В JavaScript определены семь встроенных типов данных. Шесть примитивных типов и один тип, который представляет из себя структуру данных:

  1. boolean - логический тип данных, который принимает значения true иди false

    let val = false;
    val = true;
    
  2. number - числовой тип данных, который используется как для целых, так и для дробных чисел

    let val = 50;
    val = 3.14159;
    val = -200;
    

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

    Ноль в JavaScript имеет два представления: -0 и +0. («0» это синоним +0). На практике это имеет малозаметный эффект. Например, выражение +0 === -0 является истинным.

    А также есть специальные значения, которые по сути не являются числами, но принадлежат к числовому типу данных: Infinity (бесконечность) и NaN (Not a Number - “Не Число”)

    let inf = Infinity; // прямое присвоение бесконечности
    inf = 57 / 0; // Infinity получится при делении на ноль
    inf = -Infinity; // есть отрицательная бесконечность
    inf = 57 / -0; // -Infinity
    
    let notNumber = NaN;
    notNumber = "строка" * 5; // При ошибке вычисления вернёт NaN
    

    Для получения самого большого или самого меньшего доступного значения в пределах +/-Infinity, можно использовать константы Number.MAX_VALUE или Number.MIN_VALUE. А начиная с ECMAScript 2015, вы также можете проверить, находится ли число в безопасном для целых чисел диапазоне, используя метод Number.isSafeInteger(), либо константы Number.MAX_SAFE_INTEGER и Number.MIN_SAFE_INTEGER. За пределами этого диапазона операции с целыми числами будут небезопасными, и возвращать приближённые значения.

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

    let value = ?
    
    console.log(i * i) // 0
    console.log(i + 1) // 1
    console.log(i - 1) // -1
    console.log(i / i) // 1
    
    Посмотреть ответ ```javascript let value = Number.MIN_VALUE; // имеет минимальное значение, практически равное нулю: 5e-324. Поэтому в первых трёх выражениях будет вести себя как ноль, а в последнем даст единицу. ``` Стоит заметить, что результатом деления на ноль `0/0` является `NaN`, поэтому использование нуля в этой задаче не дало бы необходимого результата.
  3. string - тип данных строка. Строка представляет собой цепочку «элементов» 16-битных беззнаковых целочисленных значений. Каждый такой элемент занимает свою позицию в строке. Первый элемент имеет индекс 0, следующий — 1, и так далее. Длина строки — это количество элементов в ней. Строки в JavaScript могут быть как в двойных, так и в одинарных кавычках.

    let str = "Это строка";
    console.log(str[0]); // Э - первый символ строки
    console.log(str.length); // 10
    console.log(str[str.length - 1]); // а - последний символ строки
    
    str = 'А это "строка в двойных кавычках" внутри строки';
    

    Также соединения строк (конкатенации) можно использовать оператор + или метод String.concat(). Также для составления строк с использованием значений переменных удобно применять шаблонизацию через обратные кавычки - `строка с переменной ${value}`

    let name = "Василий";
    let helloMsg = "Привет, " + name + "!"; // Привет, Василий!
    let newHelloMsg = name.concat(", здравствуйте. ", "Удачного дня!"); // Василий, здравствуйте. Удачного дня!
    
    //Для помещения значений переменных в шаблон используется конструкция ${...}
    helloMsg = `Привет, ${name}`; // Привет, Василий!
    
  4. symbol - это уникальный и неизменяемый тип данных, который может быть использован как идентификатор для свойств объектов. Чтобы создать новый символьный примитив, достаточно написать Symbol(), указав по желанию строку в качестве описания этого символа:

    let sym1 = Symbol();
    let sym2 = Symbol("foo");
    
  5. null - этот тип данных имеет всего одно значение: null. Которое выражает нулевое или «пустое» значение. Можно сказать, что оно передает смысл «нет значения»

    let data = null;
    
  6. undefined - тип данных, который так же имеет только одно соответствующее значение: undefined. И отражает то, что данное значение «не определено». Например, если переменная была объявлена без присвоения ей какого-либо значение, то её значение будет undefined.

    let data;
    console.log(data); // undefined
    
  7. object - тип данных Объект относится к структуре данных. Он содержит в себе данные и инструкции по работе с этими данными. Объект — это ссылочный тип данных, то есть это значение в памяти, на которое возможно сослаться с помощью идентификатора. Все другие структуры данных, такие как Функции (Functions), Массивы (Arrays), Коллекции (Maps, Sets, WeakMaps, WeakSets) и т.д. тоже являются объектами. Подробнее объекты и структуры данных будут рассматриваться в других частях курса.

Определение типов оператором typeof

Оператор typeof возвращает строку, указывающую тип операнда. Синтаксис вызова этого оператора: typeof operand. Например:

// Числа
console.log(typeof 42); // "number"
console.log(typeof Infinity); // "number"
console.log(typeof NaN); // "number", несмотря на то, что смысл этого значения "Not-A-Number" (не число)

// Строки
console.log(typeof "строка"); // "string"

let name = "Василий";
console.log(typeof `Привет, ${name}`); // "string"

// Булевы значения
console.log(typeof true); // "boolean"
console.log(typeof false); // "boolean"

// Символы
console.log(typeof Symbol()); // "symbol"

// undefined
let declaredButUndefinedVariable;
console.log(typeof declaredButUndefinedVariable); // "undefined";

// Объекты
console.log(typeof { a: 1 }); // "object"
console.log(typeof [1, 2, 3]); // такая структура данных, как массив, тоже "object"

Но есть два значения, для которых оператор typeof не совсем корректно отражает их тип:

  • Значение null, для него typeof возвращает тип “object”, что является официально признанной ошибкой в языке, которая сохраняется для совместимости. На самом деле это не объект, а отдельный тип данных null.

    // null
    console.log(typeof null); // "object"
    
  • Для функций оператор typeof возвращает тип “function”, хотя в действительности функция не является отдельным встроенным типом языка, и является подвидом объекта, как и другие структуры данных.

    // function
    console.log(typeof function() {}); // "function"
    

Примитивные и ссылочные типы данных

Особенность примитивных типов данных заключается в том, что они неизменяемы (иммутабельны) и передаются по значению. В отличие от объектов, которые передаются по ссылке. При этом важно понимать, что объект или примитив, это не сама переменная, а соответствующий указатель на объект или само значение примитивного типа, которое этой переменной присвоено.

Например рассмотрим следующий код:

// Примитивный тип number
let num = 5;

// Функция для прибавления двойки к примитиву
function addTwo(num) {
 num = num + 2;
}

// Вызов функции
addTwo(num);
console.log(num); // 5

Что происходит после вызова этой функций?

В первую функцию addTwo в качестве параметра передаётся значение переменной num из глобальной области видимости, то есть 5. Таким образом Запись Окружения этой функции после вызова будет выглядеть так:

let num = 5;

function addTwo(num) {
 // на этапе создания контекста - {num: 5}, где num, это локальная переменная функции addTwo
 num = num + 2; // на этапе выполнения контекста, эта строка изменит запись на {num: 7}
}

addTwo(num);
console.log(num); // 5

Но что же и этом произойдет со значением переменной num из глобальной области видимости? В действительности её значение останется таким же, как и было до вызова функции addTwo, так как в неё просто было скопировано значение этой переменной. И все манипуляции уже производились над другой переменной из области видимости функции addTwo. В текущей ей реализации она просто производит операцию над локальной переменной и не возвращает никакого определенного значения (а точнее она вернёт undefined).

Если переписать эту функцию, чтобы она возвращала новое значение локальной переменной num и потом присвоить это значение обратно уже в глобальную переменную num, то только в таком случае её значение поменяется:

let num = 5;

function addTwo(num) {
 // на этапе создания контекста - {num: 5}
 num = num + 2; // на этапе выполнения контекста, эта строка изменит запись на {num: 7}
 return num; // вернуть результатом функции новое значение
}

num = addTwo(num); // присвоить результат функции как новое значение переменной
console.log(num); // 7

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

Теперь рассмотрим как передаются значения объектов:

// Ссылочный тип object
let obj = { key: 5 };

// Функция для прибавления двойки к свойству объекта
function addTwoObj(obj) {
 obj.key = obj.key + 2;
}

// Вызов функции
addTwoObj(obj);
console.log(obj.key); // 7

Почему в данном случае свойство объекта изменилось, хотя никаких дополнительных действий по возврату нового значения и его присваивания не происходило?

В этом случае важно понимать, что переменная содержит не сам объект, а грубо говоря указатель (ссылку) на то место в памяти, где этот объект хранится. Поэтому параметром в функцию будет передаваться именно это ссылка объект и Запись Окружения этой функции после вызова будет выглядеть так:

let obj = { key: 5 };

function addTwoObj(obj) {
 // на этапе создания контекста - {obj: ссылка на объект { key: 5 } }, здесь переменная obj опять же является уже локальной переменной функции addTwoObj

 obj.key = obj.key + 2; // на этапе выполнения контекста, эта строка сначала по переданной ссылке в obj найдет сам объект { key: 5 } изменит свойство key самого объекта, а не его копии. Запись Окружения станет {obj: ссылка на всё тот же объект, только уже с новым значением его свойства - { key: 7 } }
}

addTwoObj(obj); // переменная obj хранит в себе ссылку а объект { key: 5 }, поэтому параметром передаётся именно эта ссылка
console.log(obj.key); // Был изменен сам объект, который был передан по ссылке, поэтому значение его свойства будет 7

А что выведется в консоль, если изменить локальную переменную obj?

let obj = { key: 5 };

function addTwoObj(obj) {
 obj.key = obj.key + 2;
 obj = { num: 6 }; // присвоить другой объект
 obj = null; // или же вообще присвоить нулевое значение
}

addTwoObj(obj);
console.log(obj.key);

Здесь в консоли снова выведется 7. Так как в функции addTwoObj переменная obj является локальной и затеняет одноименную глобальную переменную, то присваивания ей в функции новых значений никак не отразится на глобальной переменной obj. Это лишь приведет к перезаписи переданной ссылки на объект { key: 5 } новыми значениями.

let obj = { key: 5 };

function addTwoObj(obj) {
 // на этапе создания контекста - {obj: ссылка на объект { key: 5 } }, здесь переменная obj опять же является уже локальной переменной функции addTwoObj

 obj.key = obj.key + 2; // на этапе выполнения контекста, эта строка сначала по переданной ссылке в obj найдет сам объект { key: 5 } изменит свойство key самого объекта, а не его копии. Запись Окружения станет {obj: ссылка на всё тот же объект, только уже с новым значением его свойства - { key: 7 } }

 obj = { num: 6 }; // после выполнения этой строки измениться лишь значение локальной переменной в Записи Окружения: {obj: ссылка на новый объект - {num: 6} }

 obj = null; // а здесь вообще в локальную переменную запишется значение null примитивного типа {obj: null}
}

addTwoObj(obj); // переменная obj хранит в себе ссылку а объект { key: 5 }, поэтому параметром передаётся именно эта ссылка
console.log(obj.key); // Был изменен сам объект, который был передан по ссылке, поэтому значение его свойства будет 7

Обертки примитивных типов в JavaScript

В отличие от объектов, у примитивов нет своих методов, но у всех них, за исключением null и undefined, есть объектные аналоги, который оборачивает значение примитивного типа и позволяют производить над ними различные преобразования:

  • String для string примитива.
  • Number для number примитива.
  • Boolean для boolean примитива.
  • Symbol для symbol примитива.

Что происходит когда вызывается какой-либо метод у примитивного типа данных, например:

let char = "текст".charAt(1); // е

Так как у примитивного типа строки нет своих методов, то сначала создаётся его копия и неявно оборачивается в его объектный аналог с помощью конструктора new String(something). И уже в рамках этого объекта существует набор различных встроенных методов, дин из них - charAt(), который возвращает символ строки по указанной позиции. После вызова метода возвращается его результат и эта объектная обёртка уничтожается. Поэтому сам вызов метода никак не влияет на изначальное значение примитива, а только возвращает вычисленное значение.

Явно этот код можно записать так:

let char = new String("текст").charAt(1); // "е"

И именно поэтому вызов следующих методов будет лишь возвращать новые значения и никак не повлияет на исходную переменную str:

let str = "текст";
let upper = str.toUpperCase();
let substr = str.substring(0, 3);

console.log(upper); // "ТЕКСТ"
console.log(substr); // "тек"
console.log(str); // "текст"

У каждой такой объектной обертки есть метод valueOf(), который возвращает соответствующее значение примитивного типа. Например:

var numObj = new Number(10);
console.log(typeof numObj); // object

var num = numObj.valueOf();
console.log(num); // 10
console.log(typeof num); // number

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

Обнаружили ошибку или хотите добавить что-то своё в документацию? Отредактируйте эту страницу на GitHub!

Оставить комментарий