Области видимости в JavaScript / Хабр

Области видимости в JavaScript / Хабр Хостинг

▍команды, начинающиеся с точки

В режиме 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 = undefined
discounted = []
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) // 150
return discounted
}

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

function discountPrices (prices, discount) {
console.log(discounted) // undefined
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) // 150
return 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) // 150
return 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) // 150
return 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) // 150
return 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.

Оцените статью
Хостинги