▍команды, начинающиеся с точки
В режиме REPL можно пользоваться некоторыми специальными командами, которые начинаются с точки. Вот они:
- Команда
.help
выводит справочные сведения по командам, начинающимся с точки. - Команда
.editor
переводит систему в режим редактора, что упрощает ввод многострочного JavaScript-кода. После того, как находясь в этом режиме, вы введёте всё, что хотели, для запуска кода воспользуйтесь командойCtrl D
. - Команда
.break
позволяет прервать ввод многострочного выражения. Её использование аналогично применению сочетания клавишCtrl C
. - Команда
.clear
очищает контекст REPL, а так же прерывает ввод многострочного выражения. - Команда
.load
загружает в текущий сеанс код из JavaScript-файла. - Команда
.save
сохраняет в файл всё, что было введено во время REPL-сеанса. - Команда
.exit
позволяет выйти из сеанса REPL, она действует так же, как два последовательных нажатия сочетания клавишCtrl C
.
Надо отметить, что REPL распознаёт ввод многострочных выражений и без использования команды
.editor
Например, мы начали вводить код итератора:
[1, 2, 3].forEach(num => {
Если, после ввода фигурной скобки, нажать на клавишу
Enter
, REPL перейдёт на новую строку, приглашение в которой будет выглядеть как три точки. Это указывает на то, что мы можем вводить код соответствующего блока. Выглядит это так:
... console.log(num)
... })
Нажатие на
Enter
после ввода последней скобки приведёт к выполнению выражения. Если ввести в этом режиме
.break
, ввод будет прерван и выражение выполнено не будет.
Режим REPL — полезная возможность Node.js, но область её применения ограничена небольшими экспериментами. Нас же интересует нечто большее, чем возможность выполнить пару команд. Поэтому переходим к работе с Node.js в обычном режиме. А именно, поговорим о том, как Node.js-скрипты могут принимать аргументы командной строки.
▍ключевое слово const
Значения переменных, объявленных с использованием ключевых слов
var
или
let
, могут быть перезаписаны. Если же вместо этих ключевых слов используется
const
, то объявленной и инициализированной с его помощью константе новое значение присвоить нельзя.
const a = 'test'
В данном примере константе
a
нельзя присвоить новое значение. Но надо отметить, что если
a
— это не примитивное значение, наподобие числа, а объект, использование ключевого слова
const
не защищает этот объект от изменений.
Когда говорят, что в переменную записан объект, на самом деле имеют в виду то, что в переменной хранится ссылка на объект. Эту вот ссылку изменить не удастся, а сам объект, к которому ведёт ссылка, можно будет изменить.
Ключевое слово const не делает объекты иммутабельными. Оно просто защищает от изменений ссылки на них, записанные в соответствующие константы. Вот как это выглядит:
const obj = {}
console.log(obj.a)
obj.a = 1 //работает
console.log(obj.a)
//obj = 5 //вызывает ошибку
В константу
obj
, при инициализации, записывается новый пустой объект. Попытка обращения к его свойству
a
, несуществующему, ошибки не вызывает. В консоль попадает
undefined
. После этого мы добавляем в объект новое свойство и снова пытаемся обратиться к нему. В этот раз в консоль попадает значение этого свойства —
1
. Если раскомментировать последнюю строку примера, то попытка выполнения этого кода приведёт к ошибке.
Ключевое слово const очень похоже на let, в частности, оно обладает блочной областью видимости.
В современных условиях вполне допустимо использовать для объявления всех сущностей, значения которых менять не планируется, ключевое слово const, прибегая к let только в особых случаях. Почему? Всё дело в том, что лучше всего стремиться к использованию как можно более простых из доступных конструкций для того, чтобы не усложнять программы и избегать ошибок.
▍приём пользовательского ввода из командной строки
Как сделать приложения командной строки, написанные для платформы Node.js, интерактивными? Начиная с 7 версии Node.js содержит модуль
, который позволяет принимать данные из потоков, которые можно читать, например, из
process.stdin
. Этот поток, во время выполнения Node.js-программы, представляет собой то, что вводят в терминале. Данные вводятся по одной строке за раз.
Рассмотрим следующий фрагмент кода:
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
})
readline.question(`What's your name?`, (name) => {
console.log(`Hi ${name}!`)
readline.close()
})
Здесь мы спрашиваем у пользователя его имя, а после ввода текста и нажатия на клавишу
Enter
на клавиатуре, выводим приветствие.
Метод question() выводит то, что передано ему в качестве первого параметра (то есть — вопрос, задаваемый пользователю) и ожидает завершения ввода. После нажатия на Enter он вызывает коллбэк, переданный ему во втором параметре и обрабатывает то, что было введено. В этом же коллбэке мы закрываем интерфейс readline.
Модуль readline поддерживает и другие методы, подробности о них вы можете узнать в документации, ссылка на которую приведена выше.
Если вам, с использованием этого механизма, надо запросить у пользователя пароль, то лучше не выводить его, в ходе ввода, на экран, а показывать вместо введённых символов символ звёздочки — *.
Для того чтобы это сделать, можно воспользоваться пакетом readline-sync, устройство которого похоже на то, как устроен модуль readline, и который поддерживает подобные возможности сразу после установки.
Есть и ещё один пакет, предоставляющий более полное и абстрактное решение подобной проблемы. Это пакет inquirer. Установить его можно так:
npm install inquirer
С его использованием вышеприведённый пример можно переписать следующим образом:
const inquirer = require('inquirer')
var questions = [{
type: 'input',
name: 'name',
message: "What's your name?",
}]
inquirer.prompt(questions).then(answers => {
console.log(`Hi ${answers['name']}!`)
})
Пакет inquirer обладает обширными возможностями. Например, он может помочь задать пользователю вопрос с несколькими вариантами ответа или сформировать в консоли интерфейс с радиокнопками.
Программисту стоит знать о наличии альтернативных возможностей по выполнению неких действий в Node.js. В нашем случае это стандартный модуль readline, пакеты readline-sync и inquirer. Выбор конкретного решения зависит от целей проекта, от наличия времени на реализацию тех или иных возможностей и от сложности пользовательского интерфейса, который планируется сформировать средствами командной строки.
▍прототипное наследование
JavaScript выделяется среди современных языков программирования тем, что поддерживает прототипное наследование. Большинство же объектно-ориентированных языков используют модель наследования, основанную на классах.
У каждого JavaScript-объекта есть особое свойство (__proto__), которое указывает на другой объект, являющийся его прототипом. Объект наследует свойства и методы прототипа.
Предположим, у нас имеется объект, созданный с помощью объектного литерала.
const car = {}
Или мы создали объект, воспользовавшись конструктором
Object
const car = new Object()
В любом из этих случаев прототипом объекта
car
будет
Object.prototype
Если создать массив, который тоже является объектом, его прототипом будет объект Array.prototype.
const list = []
//или так
const list = new Array()
Проверить это можно следующим образом.
car.__proto__ == Object.prototype //true
car.__proto__ == new Object().__proto__ //true
list.__proto__ == Object.prototype //false
list.__proto__ == Array.prototype //true
list.__proto__ == new Array().__proto__ //true
Здесь мы пользовались свойством
__proto__
, оно не обязательно должно быть доступно разработчику, но обычно обращаться к нему можно. Надо отметить, что более надёжным способом получить прототип объекта является использование метода
getPrototypeOf()
глобального объекта
Object
Object.getPrototypeOf(new Object())
Все свойства и методы прототипа доступны объекту, имеющему этот прототип. Вот, например, как выглядит их список для массива.
Подсказка по массиву
Базовым прототипом для всех объектов является Object.prototype.
Array.prototype.__proto__ == Object.prototype
Object.prototype
прототипа нет.
То, что мы видели выше, является примером цепочки прототипов.
При попытке обращения к свойству или методу объекта, если такого свойства или метода у самого объекта нет, их поиск выполняется в его прототипе, потом — в прототипе прототипа, и так — до тех пор, пока искомое будет найдено, или до тех пор, пока цепочка прототипов не кончится.
Помимо создания объектов с использованием оператора new и применения объектных литералов или литералов массивов, создать экземпляр объекта можно с помощью метода Object.create(). Первый аргумент, передаваемый этому методу, представляет собой объект, который станет прототипом создаваемого с его помощью объекта.
const car = Object.create(Object.prototype)
Проверить, входит ли некий объект в цепочку прототипов другого объекта, можно с использованием метода
isPrototypeOf()
const list = []
Array.prototype.isPrototypeOf(list)
Hoisting
Ранее я уже говорил, что при создании переменной в JavaScript, она инициализируются со значением undefined. Это и есть «Hoisting». Интерпретатор JavaScript присваивает переменой значение undefined по умолчанию, во время так называемой фазы «Создания».
Давайте посмотрим, как действует hoisting, на предыдущем примере:
function discountPrices (prices, discount) {
var discounted = undefined
var i = undefined
var discountedPrice = undefined
var finalPrice = undefineddiscounted = []
for (var i = 0; i < prices.length; i ) {
discountedPrice = prices[i] * (1 - discount)
finalPrice = Math.round(discountedPrice * 100) / 100
discounted.push(finalPrice)
}console.log(i) // 3
console.log(discountedPrice) // 150
console.log(finalPrice) // 150return discounted
}
Обратите внимание, что всем объявленным переменным присвоено значение undefined по умолчанию. Вот почему, если вы попытаетесь получить доступ к одной из этих переменных до того, как она была фактически объявлена, вам вернётся undefined.
function discountPrices (prices, discount) {
console.log(discounted) // undefinedvar discounted = []for (var i = 0; i < prices.length; i ) {
var discountedPrice = prices[i] * (1 - discount)
var finalPrice = Math.round(discountedPrice * 100) / 100
discounted.push(finalPrice)
}console.log(i) // 3
console.log(discountedPrice) // 150
console.log(finalPrice) // 150return discounted
}
Теперь, когда вы знаете о var всё что нужно, давайте наконец разберёмся в чём разница между var, let, и const?
Var vs let vs const
Сперва сравним var и let. Главное отличие let в том, что область видимости переменной ограничивается блоком, а не функцией. Другими словами, переменная, созданная с помощью оператора let, доступна внутри блока, в котором она была создана и в любом вложенном блоке.
Давайте в последний раз вернёмся к нашей функции discountPrices.
function discountPrices (prices, discount) {
var discounted = []for (var i = 0; i < prices.length; i ) {
var discountedPrice = prices[i] * (1 - discount)
var finalPrice = Math.round(discountedPrice * 100) / 100
discounted.push(finalPrice)
}console.log(i) // 3
console.log(discountedPrice) // 150
console.log(finalPrice) // 150return discounted
}
Помните, что мы можем логировать i, discountedPrice, и finalPrice вне цикла for, так как они созданы с помощью var, а значит видны в пределах функции.
function discountPrices (prices, discount) {
let discounted = []for (let i = 0; i < prices.length; i ) {
let discountedPrice = prices[i] * (1 - discount)
let finalPrice = Math.round(discountedPrice * 100) / 100
discounted.push(finalPrice)
}console.log(i) // 3
console.log(discountedPrice) // 150
console.log(finalPrice) // 150return discounted
}discountPrices([100, 200, 300], .5) // ❌ ReferenceError: i is not defined
Вот что мы получим: ReferenceError: i is not defined. Это подтверждает, что let переменные ограничены блоком, а не функцией. Поэтому попытка получить к ним доступ приводит к ошибке.
Область видимости
Область видимости определяет, где в коде программы будут доступны переменные и функции. В JavaScript есть два типа области видимости — глобальная и локальная (global scope и function scope). Согласно официальной спецификации:
Если переменная создаётся внутри объявления функции, то её область видимости определяется как локальная и ограничивается этой функцией.
То есть, если вы создаёте переменную внутри функции с оператором var, то она будет доступна только внутри этой функции и вложенных в неё функциях.
function getDate () {
var date = new Date()return date
}getDate()
console.log(date) // ❌ Reference Error
В коде выше, мы пытаемся получить доступ к переменной вне функции, в которой она была объявлена. Так как переменная date ограничена областью видимости функции getDate, она доступна только внутри getDate или для любой вложенной в неё функции (как в примере ниже).
function getDate () {
var date = new Date()function formatDate () {
return date.toDateString().slice(4) // ✅
}return formatDate()
}getDate()
console.log(date) // ❌ Reference Error
Давайте рассмотрим более сложный пример. Допустим у нас есть массивы prices и discount, нам нужно создать функцию, которая возьмёт значения из обоих массивов и вернёт новый массив цен с учётом скидок. В итоге у нас должен получится такой массив:
discountPrices([100, 200, 300], .5)
Реализация:
function discountPrices (prices, discount) {
var discounted = []for (var i = 0; i < prices.length; i ) {
var discountedPrice = prices[i] * (1 - discount)
var finalPrice = Math.round(discountedPrice * 100) / 100
discounted.push(finalPrice)
}return discounted
}
Выглядит довольно просто, но что происходит в области видимости блока? Обратите внимание на цикл for. Доступны ли переменные, которые объявлены внутри цикла, вне его? Оказывается, что да.
function discountPrices (prices, discount) {
var discounted = []for (var i = 0; i < prices.length; i ) {
var discountedPrice = prices[i] * (1 - discount)
var finalPrice = Math.round(discountedPrice * 100) / 100
discounted.push(finalPrice)
}console.log(i) // 3
console.log(discountedPrice) // 150
console.log(finalPrice) // 150return discounted
}
Если JavaScript это единственный язык программирования, которым вы владеете, то вы не заметите ничего странного. Но если вы перешли с другого языка, вы, вероятно, немного запутались в том, что здесь происходит.
Здесь нет ошибки, это просто немного необычно. К тому же, у нас нет необходимости иметь доступ к i, discountedPrice, и finalPrice вне цикла for.
Теперь вы знаете, что такое область видимости, инициализация и объявление переменных; нам осталось разобраться с понятием «поднятие переменной» (hoisting), прежде чем перейти к let и const.
Работа с аргументами командной строки в node.js-скриптах
При запуске Node.js-скриптов им можно передавать аргументы. Вот обычный вызов скрипта:
node app.js
Передаваемые скрипту аргументы могут представлять собой как самостоятельные значения, так и конструкции вида ключ-значение. В первом случае запуск скрипта выглядит так:
node app.js flavio
Во втором — так:
node app.js name=flavio
От того, какой именно способ передачи аргументов используется, зависит то, как с ними можно будет работать в коде скрипта.
Так, для того, чтобы получить доступ к аргументам командной строки, используется стандартный объект Node.js process. У него есть свойство argv, которое представляет собой массив, содержащий, кроме прочего, аргументы, переданные скрипту при запуске.
Первый элемент массива argv содержит полный путь к файлу, который выполняется при вводе команды node в командной строке.
Второй элемент — это путь к выполняемому файлу скрипта.
Все остальные элементы массива, начиная с третьего, содержат то, что было передано скрипту при его запуске.
Перебор аргументов, имеющихся в argv (сюда входят и путь к node, и путь к выполняемому файлу скрипта), можно организовать с использованием цикла forEach:
process.argv.forEach((val, index) => {
console.log(`${index}: ${val}`)
})
Если два первых аргумента вас не интересуют, на основе
argv
можно сформировать новый массив, в который войдёт всё из
argv
кроме первых двух элементов:
const args = process.argv.slice(2)
Предположим, при запуске скрипта, ему передали лишь один аргумент, в виде самостоятельного значения:
node app.js flavio
Обратиться к этому аргументу можно так:
const args = process.argv.slice(2)
args[0]
Теперь попробуем воспользоваться конструкцией вида ключ-значение:
node app.js name=flavio
При таком подходе, после формирования массива
args
, в
args[0]
окажется строка
name=flavio
. Прежде чем пользоваться аргументом, эту строку надо разобрать. Самый удобный способ это сделать заключается в использовании библиотеки
, которая предназначена для облегчения работы с аргументами командной строки:
const args = require('minimist')(process.argv.slice(2))
args['name'] //flavio
Теперь рассмотрим вывод данных в консоль.
Система модулей node.js, использование команды exports
Поговорим о том, как использовать API
module.exports
для того, чтобы открывать доступ к возможностям модулей другим файлам приложения. В Node.js имеется встроенная система модулей, каждый файл при этом считается самостоятельным
. Общедоступный функционал модуля, с помощью команды
require
, могут использовать другие модули:
const library = require('./library')
Здесь показан импорт модуля
library.js
, файл которого расположен в той же папке, в которой находится файл, импортирующий его.
Модуль, прежде чем будет смысл его импортировать, должен что-то экспортировать, сделать общедоступным. Ко всему, что явным образом не экспортируется модулем, нет доступа извне. Собственно говоря, API module.exports позволяет организовать экспорт того, что будет доступно внешним по отношению к модулю механизмам.
Экспорт можно организовать двумя способами.
Первый заключается в записи объекта в module.exports, который является стандартным объектом, предоставляемым системой модулей. Это приводит к экспорту только соответствующего объекта:
const car = {
brand: 'Ford',
model: 'Fiesta'
}
module.exports = car
//..в другом файле
const car = require('./car')
Второй способ заключается в том, что экспортируемый объект записывают в свойство объекта
exports
. Такой подход позволяет экспортировать из модуля несколько объектов, и, в том числе — функций:
const car = {
brand: 'Ford',
model: 'Fiesta'
}
exports.car = car
То же самое можно переписать и короче:
exports.car = {
brand: 'Ford',
model: 'Fiesta'
}
В другом файле воспользоваться тем, что экспортировал модуль, можно так:
const items = require('./items')
items.car
Или так:
const car = require('./items').car
В чём разница между записью объекта в
module.exports
и заданием свойств объекта
exports
В первом экспортируется объект, который записан в module.exports. Во втором случае экспортируются свойства этого объекта.
Тип number
Значения типа
number
в JavaScript представлены в виде 64-битных чисел двойной точности с плавающей запятой.
В коде числовые литералы представлены в виде целых и дробных чисел в десятичной системе счисления. Для записи чисел можно использовать и другие способы. Например, если в начале числового литерала имеется префикс 0x — он воспринимается как число, записанное в шестнадцатеричной системе счисления. Числа можно записывать и в экспоненциальном представлении (в таких числах можно найти букву e).
Вот примеры записи целых чисел:
10
5354576767321
0xCC // шестнадцатеричное число
Вот дробные числа.
3.14
.1234
5.2e4 //5.2 * 10^4
Числовые литералы (такое поведение характерно и для некоторых других примитивных типов), при попытке обращения к ним как к объектам, автоматически, на время выполнения операции, преобразуются в соответствующие объекты, которые называют «объектными обёртками». В данном случае речь идёт об объектной обёртке
Number
Вот, например, как выглядит попытка обратиться к переменной a, в которую записан числовой литерал, как к объекту, в консоли Google Chrome.
Подсказка по объектной обёртке Number
Если, например, воспользоваться методом toString() объекта типа Number, он возвратит строковое представление числа. Выглядит соответствующая команда, которую можно выполнить в консоли браузера (да и в обычном коде) так:
a.toString()
Обратите внимание на двойные скобки после имени метода. Если их не поставить, система не выдаст ошибку, но, вместо ожидаемого вывода, в консоли окажется нечто, совсем не похожее на строковое представление числа 5.
Глобальный объект Number можно использовать в виде конструктора, создавая с его помощью новые числа (правда, в таком виде его практически никогда не используют), им можно пользоваться и как самостоятельной сущностью, не создавая его экземпляры (то есть — некие числа, представляемые с его помощью). Например, его свойство Number.MAX_VALUE содержит максимальное числовое значение, представимое в JavaScript.