Руководство по JavaScript, часть 7: строгий режим, ключевое слово this, события, модули, математические вычисления

в 11:00, , рубрики: javascript, Блог компании RUVDS.com, обучение, разработка, Разработка веб-сайтов

Сегодня, в седьмой части перевода руководства по JavaScript, мы поговорим о выполнении кода в строгом режиме, об особенностях ключевого слова this, о событиях, о модулях, о математических вычислениях. Здесь же мы затронем темы работы с таймерами и асинхронного программирования.

Часть 1: первая программа, особенности языка, стандарты
Часть 2: стиль кода и структура программ
Часть 3: переменные, типы данных, выражения, объекты
Часть 4: функции
Часть 5: массивы и циклы
Часть 6: исключения, точка с запятой, шаблонные литералы
Часть 7: строгий режим, ключевое слово this, события, модули, математические вычисления

Руководство по JavaScript, часть 7: строгий режим, ключевое слово this, события, модули, математические вычисления - 1

Строгий режим

Строгий режим (strict mode) появился в стандарте ES5. В этом режиме меняется семантика языка, он нацелен на то, чтобы улучшить поведение JavaScript, что приводит к тому, что код в этом режиме ведёт себя не так, как обычный. Фактически, речь идёт о том, что в этом режиме устраняются недостатки, неоднозначности языка, устаревшие возможности, которые сохраняются в нём из соображений совместимости.

▍Включение строгого режима

Для того чтобы использовать в некоем коде строгий режим, его нужно явным образом включить. То есть, речь не идёт о том, что этот режим применяется по умолчанию. Такой подход нарушил бы работу бесчисленного количества существующих программ, опирающихся на механизмы языка, присутствовавшие в нём с самого начала, с 1996 года. На самом деле, значительные усилия тех, кто разрабатывает стандарты JavaScript, направлены именно на обеспечение совместимости, на то, чтобы код, написанный в расчёте на старые версии стандартов, можно было бы выполнять на сегодняшних JS-движках. Такой подход можно считать одним из залогов успеха JavaScript как языка для веб-разработки.

Для того чтобы включить строгий режим, используется особая директива, которая выглядит так.

'use strict'

К тому же эффекту приведёт и директива, записанная в виде "use strict", и та же директива, после которой поставлена точка с запятой ('use strict'; и "use strict";). Эту директиву (именно так — вместе с кавычками), для того, чтобы весь код в некоем файле выполнялся бы в строгом режиме, помещают в начале этого файла.

'use strict'
const name = 'Flavio'
const hello = () => 'hey'
//...

Строгий режим может быть включён и на уровне отдельной функции. Для этого соответствующую директиву надо поместить в начале кода тела функции.

function hello() {
  'use strict'
  return 'hey'
}

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

Надо отметить, что, если строгий режим включён, выключить его во время выполнения программы нельзя.

Рассмотрим некоторые особенности строгого режима.

▍Борьба со случайной инициализацией глобальных переменных

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

Например, следующий код приводит к созданию именно такой переменной.

;(function() {
  variable = 'hey'
})()

Переменная variable будет доступна в глобальной области видимости после выполнения IIFE.

Если включить на уровне этой функции строгий режим, тот же самый код вызовет ошибку.

;(function() {
  'use strict'
  variable = 'hey'
})()

▍Ошибки, возникающие при выполнении операций присваивания значений

JavaScript, в обычном режиме, никак не сообщает о некоторых ошибках, возникающих в ходе выполнения операций присваивания значений.

Например, в JS имеется значение undefined, которое представляет собой одно из примитивных значений языка и представлено свойством глобального объекта undefined. В обычном JS вполне возможна такая команда.

undefined = 1

Выглядит это как запись единицы в некую переменную с именем undefined, а на самом деле это — попытка записи нового значения в свойство глобального объекта, которое, кстати, в соответствии со стандартом, нельзя перезаписывать. В обычном режиме, хотя такая команда и возможна, она ни к чему не приведёт — то есть, и значение undefined изменено не будет, и сообщение об ошибке не появится. В строгом режиме подобное вызовет ошибку. Для того чтобы увидеть это сообщение об ошибке, а заодно убедиться в том, что значение undefined не переопределяется в обычном режиме, попробуйте выполнить следующий код в браузере или в Node.js.

undefined = 1
console.log('This is '+undefined)
;(() => {
  'use strict'
  undefined = 1
})()

Такое же поведение системы характерно и при работе с такими сущностями, как значения Infinity и NaN, а также с другими подобными. Строгий режим позволяет всего этого избежать.

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

const car = {}
Object.defineProperty(car, 'color', {
   value: 'blue', 
   writable: false 
})
console.log(car.color)
car.color = 'yellow'
console.log(car.color)

Обратите внимание на атрибут writable: false, использованный при настройке свойства color.

Вышеприведённый код, выполненный в обычном режиме, не приведёт ни к изменению свойства объекта color, ни к выводу ошибки. Попытка поменять это свойство в строгом режиме окончится выдачей ошибки.

;(() => {
  'use strict'
  car.color = 'red'
})()

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

const car = {
  get color() {
    return 'blue'
  }
}
console.log(car.color)
car.color = 'red'
console.log(car.color)

А попытка выполнить то же самое в строгом режиме вызовет ошибку, сообщающая о попытке установки свойства объекта, у которого есть лишь геттер.

;(() => {
  'use strict'
  car.color = 'yellow' 
}
)()

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

const car = { color: 'blue' }
Object.preventExtensions(car)
console.log(car.model)
car.model = 'Fiesta'
console.log(car.model)

Здесь обе попытки вывести свойство объекта model приведут к появлению в консоли значения undefined. Такого свойства в объекте не было, попытка создать его после того, как объект был сделан нерасширяемым, ни к чему не привела. То же действие в строгом режиме приводит к выдаче сообщения об ошибке.

;(() => {
  'use strict'
  car.owner = 'Flavio'
}
)()

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

let one = 1
one.prop = 2
console.log(one.prop)

То же самое в строгом режиме приведёт к появлению сообщения об ошибке, указывающем на то, что у числа 1 нельзя создать свойство prop. Похожим образом система ведёт себя и при работе с другими примитивными типами данных.

▍Ошибки, связанные с удалением сущностей

В обычном режиме, если попытаться удалить, помощью оператора delete, свойство объекта, которое удалить нельзя, delete просто возвратит false и всё тихо закончится неудачей.

delete Object.prototype

В строгом режиме здесь будет выдана ошибка.

▍Аргументы функций с одинаковыми именами

Функции могут иметь параметры с одинаковыми именами, ошибок это не вызывает (хотя подобное выглядит как ошибка того, кто такую функцию создал).

;(function(a, a, b) {
  console.log(a, b)
})(1, 2, 3) //2 3

Этот код в обычном режиме выводит в консоль 2 3. В строгом режиме подобное вызовет ошибку.

Кстати, если при объявлении стрелочной функции её параметры будут иметь одинаковые имена, это, и в обычном режиме, приведёт к выводу сообщения об ошибке.

▍Восьмеричные значения

В обычном JavaScript можно пользоваться восьмеричными значениями, добавляя в начало числа 0.

;(() => {
  console.log(010)
})() //8

Здесь в консоль попадёт десятичное представление восьмеричного числа 10, то есть 8. Этот 0 перед числом может быть поставлен случайно. В строгом режиме работать с восьмеричными числами, заданными в таком формате, нельзя. Но если нужно и пользоваться строгим режимом и работать с восьмеричными числами, записывать их можно в формате 0oXX. Следующий код тоже выведет 8.

;(() => {
  'use strict'
  console.log(0o10)
})() //8

▍Оператор with

Оператор with, использование которого может привести к путанице, в строгом режиме запрещён.
Изменение поведения кода в строгом режиме не ограничиваются теми, которые мы обсудили выше. В частности, в этом режиме иначе ведёт себя ключевое слово this, с которым мы уже сталкивались, и о котором сейчас мы поговорим подробнее.

Особенности ключевого слова this

Ключевое слово this, или контекст выполнения, позволяет описать окружение, в котором производится выполнение JS-кода. Его значение зависит от места его использования и от того, включён или нет строгий режим.

▍Ключевое слово this в строгом режиме

В строгом режиме значение this, передаваемое в функции, не приводится к объекту. Это преобразование не только требует ресурсов, но и даёт функциям доступ к глобальному объекту в том случае, если они вызываются с this, установленным в undefined или null. Такое поведение означает, что функция может получить несанкционированный доступ к глобальному объекту. В строгом режиме преобразования this, передаваемого функции, не производится. Для того чтобы увидеть разницу между поведением this в функциях в разных режимах — попробуйте этот код с использованием директивы 'use strict' и без неё.

;(function() {
  console.log(this)
})()

▍Ключевое слово this в методах объектов

Метод — это функция, ссылка на которую записана в свойство объекта. Ключевое слово this в такой функции ссылается на этот объект. Это утверждение можно проиллюстрировать следующим примером.

const car = {
  maker: 'Ford',
  model: 'Fiesta',
  drive() {
    console.log(`Driving a ${this.maker} ${this.model} car!`)
  }
}
car.drive()
//Driving a Ford Fiesta car!

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

Обратите внимание на то, что вышеприведённый способ объявления метода объекта аналогичен такому:

const car = {
  maker: 'Ford',
  model: 'Fiesta',
  drive: function() {
    console.log(`Driving a ${this.maker} ${this.model} car!`)
  }
}

То же самое поведение ключевого слова this в методе объекта можно наблюдать и при использовании следующей конструкции.

const car = {
  maker: 'Ford',
  model: 'Fiesta'
}
car.drive = function() {
  console.log(`Driving a ${this.maker} ${this.model} car!`)
}
car.drive()
//Driving a Ford Fiesta car!

▍Ключевое слово this и стрелочные функции

Попробуем переписать вышеприведённый пример с использованием, в качестве метода объекта, стрелочной функции.

const car = {
  maker: 'Ford',
  model: 'Fiesta',
  drive: () => {
    console.log(`Driving a ${this.maker} ${this.model} car!`)
  }
}
car.drive()
//Driving a undefined undefined car!

Как видно, тут, вместо названий производителя автомобиля и его модели выводятся значения undefined. Дело в том, что, как мы уже говорили, this в стрелочной функции содержит ссылку на контекст, включающий в себя функцию.

К стрелочной функции нельзя привязать this, а к обычной функции можно

▍Привязка this

В JavaScript существует такое понятие, как привязка this. Сделать это можно разными способами. Например, при объявлении функции привязать её ключевое слово this можно к некоему объекту с использованием метода bind().

const car = {
  maker: 'Ford',
  model: 'Fiesta'
}
const drive = function() {
  console.log(`Driving a ${this.maker} ${this.model} car!`)
}.bind(car)
drive()
//Driving a Ford Fiesta car!

С помощью этого же метода к методу одного объекта, в качестве this, можно привязать другой объект.

const car = {
  maker: 'Ford',
  model: 'Fiesta',
  drive() {
    console.log(`Driving a ${this.maker} ${this.model} car!`)
  }
}
const anotherCar = {
  maker: 'Audi',
  model: 'A4'
}
car.drive.bind(anotherCar)()
//Driving a Audi A4 car!

Привязку this можно организовать и на этапе вызова функции, используя методы call() и apply().

const car = {
  maker: 'Ford',
  model: 'Fiesta'
}
const drive = function(kmh) {
  console.log(`Driving a ${this.maker} ${this.model} car at ${kmh} km/h!`)
}
drive.call(car, 100)
//Driving a Ford Fiesta car at 100 km/h!
drive.apply(car, [100])
//Driving a Ford Fiesta car at 100 km/h!

К this привязывается то, что передаётся этим методам в качестве первого аргумента. Разница между этими методами заключается в том, что apply(), в качестве второго аргумента, принимает массив с аргументами, передаваемыми функции, а call() принимает список аргументов.

▍О привязке this в обработчиках событий браузера

В коллбэках обработчиков событий this указывает на HTML-элемент, с которым произошло то или иное событие. Для того чтобы привязать к коллбэку, в качестве this, что-то другое, можно воспользоваться методом bind(). Вот пример, иллюстрирующий это.

<!DOCTYPE html>
<html>
  <body>
  
    <button id="el">Element (this)</button>
    <button id="win">Window (this</button>

    <script>
      const el = document.getElementById("el")
      el.addEventListener('click', function () {
          alert(this) //object HTMLButtonElement
      })

      const win = document.getElementById("win")
      win.addEventListener('click', function () {
          alert(this) //object Window
      }.bind(this))
    </script>
  </body>
</html>

События

JavaScript в браузере использует событийную модель программирования. Те или иные действия выполняются кодом в ответ на происходящие события. В этом разделе мы поговорим о событиях и о том, как их обрабатывать.

В качестве события может выступать, например, завершение загрузки DOM, получение данных, выполненное в результате асинхронного запроса, щелчок мышью по элементу страницы, прокрутка страницы, ввод некоего символа с клавиатуры. На самом деле, существует множество событий, обрабатывая которые, JS-код страницы позволяет решать широкий спектр задач по взаимодействию приложения с пользователями, с элементами страницы, с окружением, в котором работает код.

▍Обработчики событий

Реагировать на события можно с помощью обработчиков событий (event handler), которые представляют собой функции, вызываемые в тот момент, когда происходят события.
При необходимости для обработки одного и того же события можно зарегистрировать несколько обработчиков, которые будут вызываться в том случае, если это событие произойдёт. Регистрировать обработчики событий можно различными способами. Рассмотрим три таких способа.

▍Встроенные обработчики событий

В наши дни встроенные обработчики событий используются редко из-за их ограниченности. Раньше они применялись гораздо чаще. Для задания такого обработчика события его код добавляется в HTML-разметку элемента в виде особого атрибута. В следующем примере такой вот простейший обработчик события onclick, возникающего при щелчке по кнопке, назначен кнопке с надписью Button 1.

<!DOCTYPE html>
<html>
  <body>
  
    <button onclick="alert('Button 1!')">Button 1</button>
    <button onclick="doSomething()">Button 2</button>

    <script>

        function doSomething(){
            const str = 'Button 2!'
            console.log(str)
            alert(str)
        }
      
    </script>
  </body>
</html>

В HTML-коде кнопки Button 2 применяется похожий подход, но здесь указывается функция, код которой выполняется в ответ на нажатие кнопки. Этот код выполняет вывод заданной строки в консоль и выводит окно с тем же текстом.

▍Назначение обработчика свойству HTML-элемента

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

Например, у объекта window есть событие onload, которое вызывается после загрузки HTML-кода страницы и всех дополнительных ресурсов, необходимых ей, например — стилей и изображений. Если назначить этому событию обработчик, то при его вызове можно быть уверенным в том, что браузер загрузил всё содержимое страницы, с которым теперь можно работать программно, не опасаясь того, что какие-то элементы страницы ещё не загружены.

window.onload = () => {
    alert('Hi!') //страница полностью загружена
}

Такой подход часто используют при обработке XHR-запросов. Так, в ходе настройки запроса можно задать обработчик его события onreadystatechange, который будет вызван при изменении состояния его свойства readyState. Вот пример использования этого подхода для загрузки JSON-данных из общедоступного API.

<!DOCTYPE html>
<html>
  <body>
  
    <button onclick="loadData()">Start</button>

    <script>

        function loadData (){
            const xhr = new XMLHttpRequest()
            const method = 'GET'
            const url = 'https://jsonplaceholder.typicode.com/todos/1'
            xhr.open(method, url, true)
            xhr.onreadystatechange = function () {
                if(xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
                    console.log(xhr.responseText)
                }
            }
            xhr.send()
       }
      
    </script>
  </body>
</html>

Проверить, назначен ли обработчик некоему событию, можно так.

if (window.onload){}

▍Использование метода addEventListener()

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

window.addEventListener('load', () => {
  //загрузка завершена
})

Обратите внимание на то, что браузер IE8 (и его более старые версии) не поддерживает метод addEventListener(). Тут используется похожий метод attachEvent(). Это нужно учитывать в том случае, если ваша программа должна поддерживать устаревшие браузеры.

▍О назначении обработчиков событий различным элементам

Подключать обработчики событий к объекту window можно для обработки «глобальных» событий, таких, как нажатия кнопок на клавиатуре. В то же время, отдельным HTML-элементам назначают обработчики событий, которые реагируют на то, что происходит с этими элементами, например — на щелчки по ним мышью. Поэтому метод addEventListener() используют и с объектом window, и с обычными элементами.

▍Объект Event

В качестве первого параметра обработчик события может принимать объект события — Event. Набор свойств этого объекта зависит от события, которое он описывает. Вот, например, код, который демонстрирует обработку событий нажатия клавиш клавиатуры с использованием события keydown объекта window.

<!DOCTYPE html>
<html>
  <body>
    <script>
        window.addEventListener('keydown', event => {
            //нажата клавиша на клавиатуре
            console.log(event.type, event.key)
        })
        window.addEventListener('mousedown', event => {
            //нажата кнопка мыши
            //0 - левая кнопка, 2 - правая
            console.log(event.type, event.button, event.clientX, event.clientY)
        })
    </script>
  </body>
</html>

Как видно, здесь, для вывода в консоль сведений о нажатой клавише, используется свойство объекта key. Здесь же используется и свойство type, указывающее на тип события. В этом примере, на самом деле, мы работаем с объектом KeyboardEvent, который используется для описаний событий, связанных с клавиатурой. Этот объект является наследником объекта Event. Объекты, предназначенные для обработки разнообразных событий, расширяют возможности стандартного объекта события.

В этом же примере, для обработки событий, связанных с мышью, используется объект MouseEvent. В обработчике события mousedown мы выводим в консоль тип события, номер кнопки (свойство button) и координаты указателя в момент щелчка (свойства clientX и clientY).

Объект DragEvent применяется при обработке событий, возникающих при перетаскивании элементов страницы.

Среди свойств объекта Event, доступных и в других объектах событий, можно отметить уже упомянутое свойство type и свойство target, указывающее на DOM-элемент, на котором произошло событие. У объекта Event есть и методы. Например — метод createEvent() позволяет создавать новые события.

▍Всплытие событий

Рассмотрим следующий пример.

<!DOCTYPE html>
<html>
    <head>
        <style>
            #container { 
                height: 100px;
                width: 200px;
                background-color: blue;
            }

            #child { 
                height: 50px;
                width: 100px;
                background-color: green;
            }
        </style>
    </head>
    <body>
    <div id="container">
        <div id="child">
        </div>
    </div>
    <script>
        const contDiv = document.getElementById('container')
        contDiv.addEventListener('click', event => {
            console.log('container')
        })

        const chDiv = document.getElementById('child')
        chDiv.addEventListener('click', event => {
            console.log('child')
        })

        window.addEventListener('click', event => {
            console.log('window')
        })
      
    </script>
  </body>
</html>

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

window
container
window
child
container
window

Руководство по JavaScript, часть 7: строгий режим, ключевое слово this, события, модули, математические вычисления - 2

Всплытие событий

То, что здесь можно наблюдать, называется всплытием события (event bubbling). А именно, событие, возникающее у дочернего элемента, распространяется на родительский элемент. Этот процесс продолжается до тех пор, пока событие не достигнет самого «верхнего» элемента. Если у элементов, по которым проходит всплывающее событие, определены соответствующие обработчики, они будут вызваны в соответствии с порядком распространения события.

Всплытие событий можно останавливать, пользуясь методом stopPropagation() объекта события. Например, если нужно, чтобы, после щелчка мышью по элементу child, соответствующее событие не распространялось бы дальше, нам нужно переписать код, в котором мы назначаем ему обработчик события click, следующим образом.

chDiv.addEventListener('click', event => {
    console.log('child')
    event.stopPropagation()
})

Если теперь выполнить ту же последовательность действий, которую мы выполняли выше, то есть — щёлкнуть в свободной области окна, потом — по элементу container, а потом — по child, то в консоль будет выведено следующее.

window
container
window
child

▍Часто используемые события

Рассмотрим некоторые события, обработка которых нужна чаще всего.

Событие load

Событие load вызывается у объекта window при завершении загрузки страницы. Подобные события есть и у других элементов, например, у HTML-элемента body.

События мыши

Событие click вызывается по щелчку кнопкой мыши. Событие dblclick — по двойному щелчку. При этом если заданы обработчики событий click и dblclick, сначала вызывается обработчик события click, а потом — dblclick. События mousedown, mousemove, mouseup, можно использовать для обработки перетаскивания объектов по странице. При этом надо отметить, что если элементу назначен обработчик события mousemove, этот обработчик будет вызываться, в ходе перемещения указателя мыши над элементом, очень много раз. В подобных ситуациях, если в соответствующем обработчике выполняются какие-то достаточно тяжёлые вычисления, есть смысл ограничить частоту выполнения этих вычислений. Мы поговорим об этом ниже.

События клавиатуры

Событие keydown вызывается при нажатии на клавишу клавиатуры. Если клавишу удерживают нажатой, оно продолжает вызываться. Когда клавишу отпускают — вызывается событие keyup.

Событие scroll

Событие scroll вызывается для объекта window при прокрутке страницы. В обработчике события можно, для того, чтобы узнать позицию прокрутки, обратиться к свойству window.scrollY.

Это событие, так же, как и событие mousemove, вызывается в ходе выполнения соответствующей операции много раз.

Ограничение частоты выполнения вычислений в обработчиках событий

События mousemove и scroll дают сведения о координатах мыши и о позиции прокрутки. Выполнение в обработчиках таких событий каких-то серьёзных вычислений может привести к замедлению работы программы. В подобной ситуации есть смысл задуматься об ограничении частоты выполнения таких вычислений. Этот приём называют «троттлингом» (throttling), его реализации можно найти в специализированных библиотеках вроде Lodash. Сущность этого приёма заключается в создании механизма, который позволяет ограничить частоту выполнения неких действий, которые, без ограничения, выполнялись бы слишком часто. Рассмотрим собственную реализацию этого механизма.

let cached = null
window.addEventListener('mousemove', event => {
    if (!cached) {
        setTimeout(() => {
            //предыдущее событие доступно через переменную cached
            console.log(cached.clientX, cached.clientY)
            cached = null
            }, 100)
    }
    cached = event
})

Теперь вычисления, выполняемые при возникновении события mousemove, производятся не чаще одного раза за 100 миллисекунд.

ES-модули

В стандарте ES6 появилась новая возможность, получившая название ES-модули. Потребность в стандартизации этой возможности назрела уже давно, что выражается в том, что и разработчики клиентских частей веб-проектов, и серверные программисты, пишущие для среды Node.js, уже давно нечто подобное используют.

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

В Node.js в качестве системы модулей долгое время использовался и продолжает использоваться стандарт CommonJS. В браузерах, до появления ES-модулей, применялись различные библиотеки и системы сборки проектов, имитирующие возможность работы с модулями. Теперь же, после стандартизации, браузеры постепенно вводят поддержку ES-модулей, что позволяет говорить о том, что, для поддержки модулей, уже довольно скоро дополнительных средств не понадобится. В частности, по информации ресурса caniuse.com, в конце ноября 2018 года уровень поддержки ES-модулей браузерами немного превышает 80%.

Работа по внедрению ES-модулей ведётся и в Node.js.

▍Синтаксис ES-модулей

В Node.js для подключения ES-модулей применяется такая запись.

import package from 'module-name'

При работе с CommonJS-модулями то же самое выглядит так.

const package = require('module-name')

Как уже было сказано, модуль представляет собой JavaScript-файл, который что-то экспортирует. Делается это с помощью ключевого слова export. Например, напишем модуль, который экспортирует функцию, преобразующую переданную ей строку к верхнему регистру, и дадим файлу с ним имя uppercase.js. Пусть его текст будет таким.

export default str => str.toUpperCase()

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

Загрузить модуль на HTML-страницу можно, используя тег <script> с атрибутом type="module".

<script type="module" src="index.js"></script>

Обратите внимание на то, что такой способ импорта модулей работает как отложенная (defer) загрузка скрипта. Кроме того, важно учитывать то, что в нашем примере в модуле uppercase.js используется экспорт по умолчанию, поэтому, при его импорте, ему можно назначить любое желаемое имя. Вот как это выглядит в коде веб-страницы. Для того чтобы у вас этот пример заработал, вам понадобится локальный веб-сервер. Например, если вы пользуетесь редактором VSCode, можно воспользоваться его расширением Live Server (идентификатор — ritwickdey.liveserver).

<!DOCTYPE html>
<html>
  <head>
  </head>
  <body>
    <script type="module">
      import toUpperCase from './uppercase.js'
      console.log(toUpperCase('hello'))
    </script>
  </body>
</html>

После загрузки этой страницы в консоль попадёт текст HELLO.

Модули можно импортировать и с использованием абсолютного URL.

import toUpperCase from 'https://flavio-es-modules-example.glitch.me/uppercase.js'

Пути в командах импорта модулей могут быть либо абсолютными, либо относительными, начинающимися с ./ или /.

▍Другие возможности импорта и экспорта

Выше мы приводили пример модуля, использующего экспорт по умолчанию.

export default str => str.toUpperCase()

Однако из модуля можно экспортировать и несколько сущностей.

const a = 1
const b = 2
const c = 3
export { a, b, c }

Если этот код поместить в файл module.js, то импортировать всё, что он экспортирует, и воспользоваться всем этим можно следующим образом.

<html>
  <head>
  </head>
  <body>
    <script type="module">
      import * as m from './module.js'
      console.log(m.a, m.b, m.c)
    </script>
  </body>
</html>

В консоль будет выведено 1 2 3.

Импортировать из модуля можно только то, что нужно, пользуясь конструкциями следующего вида.

import { a } from './module.js'
import { a, b } from './module.js'

Пользоваться тем, что импортировано, можно напрямую.

console.log(a)

Импортируемые сущности можно переименовывать:

import { a, b as two } from './module.js'

Если в модуле используется и экспорт по умолчанию, и другие команды экспорта, то одной командой можно импортировать и то, что экспортировано по умолчанию, и другие сущности. Перепишем наш модуль module.js.

const a = 1
const b = 2
const c = 3
export { a, b, c }
export default () => console.log('hi')

Вот как его импортировать и использовать.

import sayHi, { a } from './module.js'
console.log(a)
sayHi()

Пример работы с модулем можно посмотреть здесь.

▍CORS

При загрузке модулей используется CORS. Это означает, что для успешной загрузки модулей с других доменов у них должен быть задан заголовок CORS, разрешающий межсайтовую загрузку скриптов (наподобие Access-Control-Allow-Origin: *).

▍Атрибут nomodule

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

<script type="module" src="module.js"></script>
<script nomodule src="fallback.js"></script>

▍О модулях ES6 и WebPack

Модули ES6 — это замечательная технология, привлекающая тем, что она входит в стандарт ECMAScript. Но, пользуясь модулями, нужно стремиться к тому, чтобы количество загружаемых файлов было бы небольшим, иначе это может сказаться на производительности загрузки страницы. В этой связи стоит сказать, что бандлер WebPack, используемый для упаковки кода приложений, по видимому, ещё долго не потеряет актуальности.

▍Модули CommonJS

Как уже было сказано, в Node.js используется система модулей CommonJS. Эта система позволяет разработчику создавать небольшие самостоятельные фрагменты кода, подходящие для использования во множестве проектов и поддающиеся автономному тестированию. На основе CommonJS создана огромнейшая экосистема модулей npm.

Давайте напишем CommonJS-модуль, основываясь на примере, который мы уже рассматривали. А именно, поместим в файл up-node.js следующий код.

exports.uppercase = str => str.toUpperCase()

Для того чтобы воспользоваться этим модулем в некоей программе, его нужно подключить.

const up = require('./up-node.js')
console.log(up.uppercase('hello'))

После выполнения этого кода в консоль попадёт HELLO.

Обычно пакеты, загружаемые из npm, импортируют так, как показано ниже.

const package = require('module-name')

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

Из CommonJS-модуля можно экспортировать несколько сущностей.

exports.a = 1
exports.b = 2
exports.c = 3

Импортировать их можно следующим образом, используя возможности по деструктурирующему присваиванию.

const { a, b, c } = require('./up-node.js')

Математические вычисления

Математические вычисления часто встречаются в любых программах, и JavaScript — не исключение. Поговорим об арифметических операторах языка и об объекте Math, содержащем встроенные реализации некоторых полезных функций. Для того чтобы поэкспериментировать с тем, о чём мы будем тут говорить, удобно пользоваться JS-консолью браузера.

▍Арифметические операторы

Сложение (+)

Оператор + выполняет сложение чисел и конкатенацию строк. Вот примеры его использования с числами:

const three = 1 + 2 //3
const four = three + 1 //4

Вот как он ведёт себя со строками, преобразуя, при необходимости, другие типы данных к строковому типу.

'three' + 1 // three1

Вычитание (-)

const two = 4 - 2 //2

Деление (/)

При работе с обычными числами оператор деления ведёт себя вполне ожидаемым образом.

20 / 5 //4
20 / 7 //2.857142857142857

Если поделить число на 0, это не вызовет ошибку. Результатом выполнения такой операции будет значение Infinity (для положительного делимого) или -Infinity (для отрицательного делимого).

1 / 0 //Infinity
-1 / 0 //-Infinity

Остаток от деления (%)

Оператор % возвращает остаток от деления, в некоторых ситуациях это может оказаться полезным.

20 % 5 //0
20 % 7 //6

Остатком от деления на 0 является особое значение NaN (Not a Number — не число).

1 % 0 //NaN
-1 % 0 //NaN

Умножение (*)

1 * 2 //2
-1 * 2 //-2

Возведение в степень (**)

Этот оператор возводит первый операнд в степень, заданную вторым операндом.

1 ** 2 //1
2 ** 1 //2
2 ** 2 //4
2 ** 8 //256
8 ** 2 //64

▍Унарные операторы

Инкремент (++)

Унарный оператор ++ можно использовать для прибавления 1 к некоему значению. Его можно размещать до инкрементируемого значения или после него.

Если он будет поставлен перед переменной — он сначала увеличивает хранящееся в ней число на 1, после его возвращает данное число.

let x = 0
++x //1
x //1

Если же поставить его после переменной — то он сначала вернёт её предыдущее значение, а потом уже увеличит.

let x = 0
x++ //0
x //1

Декремент (--)

Унарный оператор -- похож на вышерассмотренный ++ с той разницей, что он не увеличивает значения переменных на 1, а уменьшает их.

let x = 0
x-- //0
x //-1
--x //-2

Унарный оператор (-)

Такой оператор позволяет делать положительные числа отрицательными и наоборот.

let x = 2
-x //-2
x //2

Унарный оператор (+)

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

let x = 2
+x //2
x = '2'
+x //2
x = '2a'
+x //NaN

▍Оператор присваивания и его разновидности

В JavaScript, помимо обычного оператора присваивания (=), есть несколько его разновидностей, которые упрощают выполнение часто встречающихся операций. Вот, например, оператор +=.

let x = 2
x += 3
x //5

Его можно прочитать так: «Прибавить к значению переменной, расположенной слева, то, что находится справа, и записать результат сложения в ту же переменную». Фактически, вышеприведённый пример можно переписать следующим образом.

let x = 2
x = x + 3
x //5

По такому же принципу работают и другие подобные операторы:

  • -=
  • *=
  • /=
  • %=
  • **=

▍Приоритет операторов

При работе со сложными выражениями нужно учитывать приоритет операторов. Например, рассмотрим следующее выражение.

const a = 1 * 2 + 5 / 2 % 2

Результатом его вычисления будет 2.5. Для того чтобы понять как получен результат этого выражения, нужно учитывать приоритет операторов. Следующий список операторов начинается с тех, которые имеют наивысший приоритет.

  • - + ++ -- — унарные операторы, операторы инкремента и декремента
  • / % — умножение, деление, получение остатка от деления
  • + - — сложение и вычитание
  • = += -= *= /= %= **= — операторы присваивания

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

const a = 1 * 2 + 5 / 2 % 2
const a = 2 + 2.5 % 2
const a = 2 + 0.5
const a = 2.5

Для переопределения порядка выполнения операторов соответствующие части выражения можно включить в круглые скобки. Рассмотрим следующее выражение.

const a = 1 * (2 + 5) / 2 % 2

Результатом его вычисления будет 1.5.

▍Объект Math

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

Среди его свойств можно, например, отметить Math.E — константу, содержащую число e, и Math.PI — константу, содержащую число π.

Math.E // 2.718281828459045
Math.PI //3.141592653589793

Вот список некоторых полезных методов этого объекта.

  • Math.abs() — возвращает абсолютное значение числа
  • Math.ceil() — округляет число, возвращая наименьшее целое число, большее либо равное указанному
  • Math.cos() — возвращает косинус угла, выраженного в радианах
  • Math.floor() — округляет число, возвращая наибольшее целое число, меньшее либо равное указанному
  • Math.max() — возвращает максимальное из переданных ему чисел
  • Math.min() — возвращает минимальное из переданных ему чисел
  • Math.random() — возвращает псевдослучайное число из диапазона [0, 1) (не включая 1)
  • Math.round() — округляет число до ближайшего целого числа
  • Math.sqrt() — возвращает квадратный корень из числа

▍Сравнение значений

Для сравнения значений в JavaScript используются операторы сравнения, с некоторыми из которых мы уже встречались.

  • == — оператор нестрогого равенства. Перед сравнением значений выполняет преобразование типов.
  • != — оператор нестрогого неравенства.

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

  • === — оператор строгого равенства
  • !== — оператор строгого неравенства

Вот ещё некоторые операторы сравнения.

  • < — оператор «меньше»
  • < — оператор «больше».
  • <= — оператор «меньше или равно».
  • >= — оператор «больше или равно».

Вот один из примеров, иллюстрирующих особенности работы операторов нестрогого и строгого равенства.

1 === true //false
1 == true //true

В первом случае значения сравниваются без приведения типов, в результате оказывается, что число 1 не равно true. Во втором случае число 1 приводится к true и выражение говорит нам о том, что 1 и true равны.

Таймеры и асинхронное программирование

В соответствующих разделах руководства, перевод которого мы публикуем, поднимаются темы использования таймеров и асинхронного программирования. Эти темы были рассмотрены в ранее опубликованном нами переводе курса по Node.js. Для того чтобы с ними ознакомиться, рекомендуем почитать следующие материалы:

Тут можно найти PDF-версию перевода курса по Node.js.

Итоги

В этом материале мы поговорили о строгом режиме, об особенностях ключевого слова this, о событиях, о модулях, о математических вычислениях. В следующий раз обсудим новшества, которые принёс в язык стандарт ES6.

Уважаемые читатели! Встречались ли вы с ошибками, которые вызваны использованием оператора нестрогого равенства в JavaScript?

Руководство по JavaScript, часть 7: строгий режим, ключевое слово this, события, модули, математические вычисления - 3

Автор: ru_vds

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js