Данные, которыми мы оперируем в своих программах, могут обладать важными свойствами — например, у строк есть длина. Как вы увидите далее, это свойство очень важно для реализации алгоритмов, связанных с преобразованием строки (как пример — переворот строки). Как узнать длину строки? Во многих языках длина строки вычисляется с помощью специальной функции и выглядит это примерно так:
import { length } from './hexlet-basics/string.js'
const name = 'Robb'
console.log(length(name)) // => 4
В JavaScript свойства встроены прямо в язык. Они указываются через точку сразу после переменной (или константы):
const name = 'Robb'
const len = name.length
console.log(len) // => 4
Свойства связаны с данными, у которых они берутся. Для стандартных типов все свойства описаны в документации, как например, у строк. При этом у чисел вообще нет свойств.
JavaScript позволяет обращаться к свойствам, которые не существуют (например, при опечатках). В таком случае их значением является undefined:
const name = 'Robb'
console.log(name.whatIsThat) // => undefined
Вопрос для самопроверки. Что распечатает код console.log(name[name.length]) для name, определенного выше? Почему ответ такой?
Методы
В JavaScript у данных есть не только свойства, но и методы. Методы - это функции, которые находятся внутри свойств. Это означает, что метод можно вызвать как функцию, но при этом он работает как свойство и вызывается через точку.
const name = 'Robb'
const upperName = name.toUpperCase()
console.log(upperName) // => ROBB
Встроенные методы всегда оперируют теми данными, с которыми они связаны. Метод .toUpperCase() возвращает такую же строку, но преобразуя все символы в верхний регистр. Методов у данных обычно значительно больше, чем свойств, например, для строк их несколько десятков. В документации, на первый взгляд, они описаны немного странно: String.prototype.toLowerCase(). Это описание раскрывает некоторые внутренние детали реализации, которые сейчас не важны, да и мы не изучили всей необходимой базы для разговора о прототипах.
Методы есть и у чисел:
const temperature = 22.93
// Округление до одного знака после запятой
const roundedTemperature = temperature.toFixed(1)
// Метод возвращает строку, которая содержит преобразованное число
console.log(roundedTemperature); // => 22.9
// Напрямую можно вызывать так
(22.93).toFixed(1) // 22.9
Технически всё несколько сложнее. Методы есть не у самих чисел, а у данных (объектов) типа Number. Числа, записанные в переменные или константы, автоматически преобразуются к данному типу во время обращения к ним, в это время происходит так называемый boxing.
Возникает закономерный вопрос: зачем нужны методы, почему не просто функции? С числами ситуация еще сложнее. Часть операций реализована в виде методов самих чисел, например, .toFixed(), а еще большая часть — в виде методов, доступных через Math.
Есть две причины, почему так сделано:
- Исторически так сложилось. JavaScript разрабатывался слишком быстро и поэтому не все было продумано хорошо.
- Далеко не все функции имеют отношение к конкретному значению. Возьмем для примера
Math.min(). Эта функция находит минимальное число среди всех, которые ему были переданы. Эту функцию нелогично делать методом конкретного числа, например, так —(1).min(). Она не имеет никакой связи с конкретным числом
С другой стороны, функции, работающие с конкретным числом, для единообразия должны быть реализованы как методы. К таким функциям относится получение модуля числа. То есть вместо такого вызова Math.abs(-10), логично иметь такой: (-10).abs().
Что касается методов в целом, то не все так однозначно. Есть языки, в которых методов нет и там все прекрасно, есть языки, где методы — это основной способ работы с функциями. JavaScript — язык, в котором прижились оба подхода, в нем активно используются как обычные функции, так и методы. О плюсах и минусах подобных подходов подробно рассказывается в курсах, посвященных ООП.
Неизменяемость
Что напечатает на экран последний вызов?
const name = 'Tirion'
console.log(name.toUpperCase()) // => TIRION
console.log(name) // => ?
Ответ на этот вопрос зависит от того, как вы поняли урок про неизменяемость примитивных типов данных. Вызов метода .toUpperCase() возвращает новое значение, в котором все буквы преобразованы в верхний регистр, но он не меняет (и не может этого сделать) исходную строку. Поэтому внутри константы (или переменной — это не важно) окажется старое значение: 'Tirion'. Эта логика справедлива для методов всех примитивных типов. Более того, попытка изменить значение свойства этих данных ни к чему не приведет:
const name = 'Tirion'
console.log(name.length) // => 6
name.length = 100
console.log(name.length) // => 6
Вместо изменения значения можно заменить значение. Для этого понадобятся переменные:
let name = 'Tirion'
name = name.toUpperCase()
console.log(name) // => TIRION
Свойства и методы как выражения
Свойства и методы — такие же выражения, как переменные, константы или вызовы функции, а значит, их можно всячески комбинировать.
Использование в операциях:
const name = 'Shaya'
name.length + 5; // 10
`hi, ${name.toUpperCase()}!` // hi, SHAYA!
Использование в параметрах функций:
const name1 = 'Robb'
const name2 = 'Shaya'
console.log(name2.length) // => 5
console.log(name2.toLowerCase()) // => shaya
console.log(Math.min(name1.length, name2.length)) // => 4