Заметки 


Дуглас Крокфорд - JavaScript. Сильные стороны

Марейн Хавербек - Выразительный JavaScript

Дэвид Флэнаган - JavaScript

John Resig, Bear Bibeault, Josip Maras - Secrets of the JavaScript Ninja

Stoyan Stefanov - JavaScript Patterns

Ved Antani, Stoyan Stefanov - Object-Oriented JavaScript

ECMA-262


История

Возможности HTML-документов развивались постепенно: сначала все ограничивалось собственно текстом с тегами и изображениями. Позднее появилась возможность вставки и воспроизведения звуковых файлов и видеоклипов. Однако помимо размещения мультимедиа-контента на веб-странице существовала проблема динамической обработки запросов и управления просмотром. Для ее решения стандартных средств HTML не хватало.

При генерации страниц в Web возникает дилемма, связанная с архитектурой «клиент-сервер». Страницы можно генерировать как на стороне клиента, так и на стороне сервера. В 1995 году специалисты компании Netscape создали механизм управления страницами на клиентской стороне, разработав язык программирования JavaScript.

Таким образом, JavaScript — это язык управления сценариями просмотра гипертекстовых страниц Web на стороне клиента. Однако на самом деле JavaScript — это не только язык для программирования на стороне клиента. Liveware, прародитель JavaScript, является средством подстановок на стороне сервера Netscape. Тем не менее, наибольшую популярность JavaScript обеспечил front-end.

Основная идея JavaScript состоит в том, чтобы изменять отдельные значения атрибутов HTML-контейнеров и свойств среды отображения в процессе просмотра HTML-страницы пользователем. При этом для актуализации изменений перезагрузка страницы не требуется.

Netscape Communications Corporation представляет

Компания Netscape Communications Corporation активно участвовала в процессе развития всемирной паутины. Последняя явно многим обязана этой компании: среди прочего Netscape подарила вебу JavaScript. Изначально компании удалось достойно вступить в борьбу за первенство в интернет-отрасли благодаря созданию и бесплатному распространению (для использования в домашних условиях) браузера Netscape Navigator.

Но в апреле 1995 года Netscape наняла Брендона Эйха, на которого была возложена особая миссия. Перед ним стояла задача внедрить язык программирования Scheme (или что-то похожее) в браузер Netscape Navigator.

Scheme — это функциональный язык программирования, один из двух наиболее популярных в наши дни диалектов языка Лисп (другой популярный диалект — это Common Lisp).

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

Через некоторое время появился скриптовый язык под названием LiveScript. Этот язык был создан для реализации интерактивности в HTML-документах, которые прежде были статичными. Поддержка LiveScript была реализована в первых версиях браузера Netscape Navigator, пользовался большой популярностью и успехом.

Помимо Брендана Эйха в разработке нового языка участвовали сооснователь Netscape Communications Марк Андрессен и сооснователь Sun Microsystems Билл Джой. Чтобы успеть закончить работы над языком к релизу браузера, компании заключили соглашение о сотрудничестве. Их целью было создать «язык для склеивания» составляющих частей веб-ресурса: изображений, плагинов, Java-апплетов, который был бы удобен для веб-дизайнеров и программистов, не обладающих высокой квалификацией.

В результате соглашения между Netscape Communications и Sun Microsystems и объединения идей LiveScript со структурой Java появилась среда под названием «Mocha», предназначенная для разработки сетевых приложений и, в конце концов, для создания динамичных web-страниц. Среда выпускалась как открытое ПО и была независима от используемой программной платформы.

Проект завершился созданием спецификаций, которые были опубликованы двумя компаниями в декабре 1995 года под названием JavaScript 1.0.

Netscape vs Microsoft vs стандартизация

Первым браузером, поддерживающим JavaScript, был Netscape Navigator 2.0. Однако корпорация Microsoft быстро сообразила, куда ветер дует и разработала свой «JavaScript», который получил название JScript 1.0. Естественно, его поддержка была реализована в браузере Microsoft Internet Explorer 3.0 и Internet Information Server.

Несмотря на то, что JScript формально был независимой разработкой Microsoft, он оказался совместимым с JavaScript 1.0 компании Netscape. Более того, сценарий, написанный для одного браузера, с большой вероятностью мог быть выполнен на другом браузере.

Позже компания Netscape выпустила версию JavaScript 1.1 для Netscape Navigator 3.0 и Live Wire Web server. В данной версии были сохранены все характерные черты языка JavaScript 1.0 и добавлено множество новых возможностей.

Компания Microsoft также усовершенствовала собственный язык JScript, но решила не включать в него все нововведения JavaScript 1.1. С этого момента возникла несовместимость браузеров: при попытке запустить сценарии, написанные на JavaScript 1.1, они не распознавались или приводили к ошибкам при использовании в продуктах Microsoft.

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

Однако вскоре Netscape, Microsoft и другие компании решили, что будет лучше мирно договориться и выработать единый стандарт. Европейская ассоциация производителей компьютеров (European Computer Manufacturing Association — ЕСМА) начала работу над ним в ноябре 1996 года. В июле следующего года был создан новый язык, получивший название ECMAScript.

А пока шла работа над стандартом, конкуренты не теряли времени и выпустили новые версии собственных языков – JavaScript 1.2 для браузера Netscape Navigator 4.0, и JScript 2.0 для браузера Microsoft Internet Explorer 3.0. Таким образом проблемы совместимости языков выполнения сценариев еще больше усиливались: при доработке этих языков не был учтен общий стандарт (ЕСМА).

Жизнь Web-разработчиков сильно осложнилась. Им не только пришлось запоминать особенности каждого браузера, но и создавать Web-страницы, которые можно было бы просматривать в обоих браузерах.

Большинство из них было уверено, что ситуация никогда не изменится и будет только усугубляться. Однако оптимисты надеялись, что благодаря ЕСМА браузеры снова станут совместимыми. Конкурирующие компании прекратили упрямиться только к выходу третьей редакции стандарта ECMA 262 (ECMAScript Edition 3) и выпустили JavaScript 1.5, и JScript 5.5. Эти версии были практически на 100% совместимы с ECMAScript Edition 3.

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


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

Кроме того, JavaScript активно применяется в следующих направлениях разработки:

  • Бэкенд
  • Мобильные приложения
  • Десктоп приложения
  • Embedded
  • Холодильники/часы/чайники/IoT (Internet of Things)

Нет такого языка, или технологии, которые были бы однозначно признаны рынком как лучшее решение для разработчиков в какой-либо сфере. У каждого варианта есть свои достоинства и недостатки. Сложность современных веб-решений давно требует существенного пересмотра. Поэтому большое внимание разработчики уделили новым версиям стандарта JavaScript – ECMAScript 6 и 7.

6-я версия стандарта (который, кстати, в пику несостоявшемуся выпуску ES4 иногда называют как ES6 Harmony) содержит изменения, которые существенно облегчат создание сложных решений: классы, модули, коллекции, итераторы, генераторы, прокси, типизированные массивы, обещания, новые методы и свойства для стандартных объектов и новые синтаксические возможности и еще много чего.

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


Переменные: let и const

В ES-2015 предусмотрены новые способы объявления переменных: через let и const вместо var.

У объявлений переменной через let есть три основных отличия от var:

  • Область видимости переменной let – блок {...}

    Как мы помним, переменная, объявленная через var, видна везде в функции. Переменная, объявленная через let, видна только в рамках блока {...}, в котором объявлена. Это, в частности, влияет на объявления внутри if, while или for.

    Например, переменная через var:
    var apples = 5;
    
    if (true) {
        var apples = 10;
    
        alert(apples); // 10 (внутри блока)
    }
    
    alert(apples); // 10 (снаружи блока то же самое)

    В примере выше apples – одна переменная на весь код, которая модифицируется в if.

    То же самое с let будет работать по-другому:

    let apples = 5; // (*)
    
    if (true) {
        let apples = 10;
    
        alert(apples); // 10 (внутри блока)
    }
    
    alert(apples); // 5 (снаружи блока значение не изменилось)

    Здесь, фактически, две независимые переменные apples, одна – глобальная, вторая – в блоке if. Заметим, что если объявление let apples в первой строке (*) удалить, то в последнем alert будет ошибка: переменная не определена. Это потому что переменная let всегда видна именно в том блоке, где объявлена, и не более.

  • Переменная let видна только после объявления

    Как мы помним, переменные var существуют и до объявления. Они равны undefined. С переменными let всё проще. До объявления их вообще нет. Заметим также, что переменные let нельзя повторно объявлять.

  • При использовании в цикле, для каждой итерации создаётся своя переменная

    Переменная var – одна на все итерации цикла и видна даже после цикла:
    for(var i=0; i<10; i++) { /* … */ }
    
    alert(i); // 10

    С переменной let – всё по-другому.

    Каждому повторению цикла соответствует своя независимая переменная let. Если внутри цикла есть вложенные объявления функций, то в замыкании каждой будет та переменная, которая была при соответствующей итерации.

const

Объявление const задаёт константу, то есть переменную, которую нельзя менять.

const apple = 5;
apple = 10; // ошибка

В остальном объявление const полностью аналогично let. Заметим, что если в константу присвоен объект, то от изменения защищена сама константа, но не свойства внутри неё.


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

  • String: представляет строку
  • Number: представляет числовое значение
  • Boolean: представляет логическое значение true или false
  • undefined: указывает, что значение не установлено
  • null: указывает на неопределенное значение

Все данные, которые не попадают под вышеперечисленные пять типов, относятся к типу object.

Нередко возникает путаница между null и undefined. Итак, когда мы только определяем переменную без присвоения ей начального значения, она представляет тип undefined. Присвоение значению null означает, что переменная имеет некоторое неопределенное значение (не число, не строка, не логическое значение), но все-таки имеет значение (undefined означает, что переменная не имеет значения).

JavaScript является языком со слабой типизацией. Это значит, что переменные могут динамически менять тип.


Операторы сравнения

Как правило, для проверки условия используются операторы сравнения. Операторы сравнения сравнивают два значения и возвращают значение true или false:

  • == Оператор равенства сравнивает два значения, и если они равны, возвращает true, иначе возвращает false: x == 5
  • === Оператор тождественности также сравнивает два значения и их тип, и если они равны, возвращает true, иначе возвращает false: x === 5
  • != Сравнивает два значения, и если они не равны, возвращает true, иначе возвращает false: x != 5
  • !== Сравнивает два значения и их типы, и если они не равны, возвращает true, иначе возвращает false: x !== 5
  • > Сравнивает два значения, и если первое больше второго, то возвращает true, иначе возвращает false: x > 5
  • < Сравнивает два значения, и если первое меньше второго, то возвращает true, иначе возвращает false: x < 5
  • >= Сравнивает два значения, и если первое больше или равно второму, то возвращает true, иначе возвращает false: x >= 5
  • <= Сравнивает два значения, и если первое меньше или равно второму, то возвращает true, иначе возвращает false: x <= 5

Логические операции

Логические операции применяются для объединения результатов двух операций сравнения. В JavaScript есть следующие логические операции:

  • && Возвращает true, если обе операции сравнения возвращают true, иначе возвращает false
  • || Возвращает true, если хотя бы одна операция сравнения возвращают true, иначе возвращает false
  • ! Возвращает true, если операция сравнения возвращает false

Циклы позволяют в зависимости от определенных условий выполнять некоторое действие множество раз. В JavaScript имеются следующие виды циклов:

  • for
  • for..in
  • for..of
  • while
  • do..while

Цикл for имеет следующее формальное определение: for ([инициализация счетчика]; [условие]; [изменение счетчика]) {}

Цикл for..in предназначен для перебора массивов и объектов. Его формальное определение: for (индекс in массив) {}

Цикл for...of похож на цикл for...in и предназначен для перебора коллекций, например, массивов:

let users = ["Tom", "Bob", "Sam"];

for(let val of users) console.log(val);

Цикл while выполняется до тех пор, пока некоторое условие истинно. Его формальное определение: while (условие) {}

В цикле do сначала выполняется код цикла, а потом происходит проверка условия в инструкции while. И пока это условие истинно, цикл повторяется: do {} while ()

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

Если нам надо просто пропустить итерацию, но не выходить из цикла, мы можем применять оператор continue


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

function sum(a, b, c){
    let d = a + b + c;
    console.log(d);
}

sum(1, 2, 3);  // 6

let nums = [4, 5, 6];
sum(...nums);  // 15

Во втором случае в функцию передается числа из массива nums. Но чтобы передавался не просто массив, как одно значение, а именно числа из этого массива, применяется spread-оператор (многоточие ...).


Функция может принимать множество параметров, но при этом часть или все параметры могут быть необязательными. Если для параметров не передается значение, то по умолчанию они имеют значение "undefined".

function display(x, y) {
    if(y === undefined) y = 5;
    if(x === undefined) x = 8;
    let z = x * y;
    console.log(z);
}

display();         // 40
display(6);       // 30
display(6, 4);   // 24

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

Есть и другой способ определения значения для параметров по умолчанию:

function display(x = 5, y = 10) {
    let z = x * y;
    console.log(z);
}

display();        // 50
display(6);      // 60
display(6, 4);  // 24

Если параметрам x и y не передаются значения, то они получаются в качестве значений числа 5 и 10 соответствено. Такой способ более лаконичен и интуитивен, чем сравнение с undefined.

При этом значение параметра по умолчанию может быть производным, представлять выражение:

function display(x = 5, y = 10 + x){
    let z = x * y;
    console.log(z);
}

display();       // 75
display(6);      // 96
display(6, 4);   // 24

В данном случае значение параметра y зависит от значения x.

При необходимости мы можем получить все переданные параметры через глобально доступный массив arguments.

Функции могут выступать в качестве параметров других функций:

function sum(x, y){
    return x + y;
}

function operation(x, y, func){
    let result = func(x, y);
    console.log(result);
}

operation(10, 6, sum);  // 16

Одна функция может возвращать другую функцию.


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

Замыкание технически включает три компонента:

  • внешняя функция, в которой определена переменная и которая определяет некоторую область видимости
  • переменная, которая определена во внешней функции
  • вложенная функция, которая использует эту переменную

Рассмотрим на примере:

function multiply(n) {
    let x = n;
    return function(m){ return x * m;};
}

let fn1 = multiply(5);
let result1 = fn1(6);
console.log(result1); // 30

let fn2= multiply(4);
let result2 = fn2(6);
console.log(result2); // 24

Итак, здесь вызов функции multiply() приводит к вызову другой внутренней функции. Внутренняя же функция:

function(m){ return x * m;};

запоминает окружение, в котором она была создана, в частности, значение переменной x.

В итоге при вызове функции multiply определяется переменная fn1, которая и представляет собой замыкание, то есть объединяет две вещи: функцию и окружение, в котором функция была создана. Окружение состоит из любой локальной переменной, которая была в области действия функции multiply во время создания замыкания.

То есть fn1 - это замыкание, которое содержит и внутреннюю функцию function(m){ return x * m;}, и переменную x, которая существовала во время создания замыкания.

При создании двух замыканий: fn1 и fn2, для каждого из этих замыканий создается свое окружение.

При этом важно не запутаться в параметрах. При определении замыкания:

var fn1 = multiply(5);

Число 5 передается для параметра n функции multiply.

При вызове внутренней функции:

var result1 = fn1(6);

Число 6 передается для параметра m во внутреннюю функцию function(m){ return x * m;};.

Также мы можем использовать другой вариант для вызова замыкания:

function multiply(n) {
    let x = n;
    return function(m){ return x * m;};
}

let result = multiply(5)(6); // 30
console.log(result);

Самовызывающиеся функции

Обычно определение функции отделяется от ее вызова: сначала мы определяем функцию, а потом вызываем. Но это необязательно. Мы также можем создать такие функции, которые будут вызываться сразу при определении. Такие функции еще называют Immediately Invoked Function Expression (IIFE).

(function(){
    console.log("Привет мир");
}());

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


Переопределение функций

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

function display(){
    console.log("Доброе утро");
    display = function(){
        console.log("Добрый день");
    }
}

display(); // Доброе утро
display(); // Добрый день

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

Но при переопределении функции надо учитывать некоторые нюансы. В частности, попробуем присвоить ссылку на функцию переменной и через эту переменную вызвать функцию:

function display(){
    console.log("Доброе утро");
    display = function(){
        console.log("Добрый день");
    }
}

// присвоение ссылки на функцию до переопределения
let displayMessage = display;
display(); // Доброе утро
display(); // Добрый день
displayMessage(); // Доброе утро
displayMessage(); // Доброе утро

Здесь переменная displayMessage получает ссылку на функцию display до ее переопределения. Поэтому при вызове displayMessage() будет вызываться непереопределенная версия функции display.

Но допустим, мы определили переменную displayMessage уже после вызова функции display:

display(); // Доброе утро
display(); // Добрый день
let displayMessage = display;
displayMessage(); // Добрый день
displayMessage(); // Добрый день

В этом случае переменная displayMessage будет указывать на переопределенную версию функции display.


Hoisting

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

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

console.log(foo);

Его выполнение вызовет ошибку ReferenceError: foo is not defined

Добавим определение переменной:

console.log(foo);   // undefined
let foo = "Tom";

В этом случае консоль выведет значение "undefined". При первом проходе компилятор узнает про существование переменной foo. Она получает значение undefined. При втором проходе вызывается метод console.log(foo).

Возьмем другой пример:

var c = a * b;
var a = 7;
var b = 3;
console.log(c); // NaN

Здесь та же ситуация. Переменные a и b используются до опеределения. По умолчанию им присваиваются значения undefined. А если умножить undefined на undefined, то получим Not a Number (NaN).

Все то же самое относится и к использованию функций. Мы можем сначала вызвать функцию, а потом уже ее определить:

display();

function display() {
    console.log("Hello Hoisting");
}

Здесь функция display благополучно отработает, несмотря на то, что она определена после вызова.

Но от этой ситуации надо отличать тот случай, когда функция определяется в виде переменной:

display();

var display = function (){
    console.log("Hello Hoisting");
}

В данном случае мы получим ошибку TypeError: display is not a function. При первом проходе компилятор также получит переменную display и присвоет ей значение undefined. При втором проходе, когда надо будет вызывать функцию, на которую будет ссылаться эта переменная, компилятор увидит, что вызывать то нечего: переменная display пока еще равна undefined. И будет выброшена ошибка.

Поэтому при определении переменных и функций следует учитывать перепетии такого аспекта как hoisting.


Передача параметров по значению

Строки, числа, логические значения передаются в функцию по значению. Иными словами при передаче значения в функцию, эта функция получает копию данного значения. Рассмотрим, что это значит в практическом плане:

function change(x){
    x = 2 * x;
    console.log("x in change:", x);
}

let n = 10;
console.log("n before change:", n); // n before change: 10
change(n);                          // x in change: 20
console.log("n after change:", n);  // n after change: 10

Функция change получает некоторое число и увеличивает его в два раза. При вызове функции change ей передается число n. Однако после вызова функции мы видим, что число n не изменилось, хотя в самой функции произошло увеличение значения параметра. Потому что при вызове функция change получает копию значения переменной n. И любые изменения с этой копией никак не затрагивают саму переменную n.

Передача по ссылке

Объекты и массивы передаются по ссылке. То есть функция получает сам объект или массив, а не их копию.

function change(user) {
    user.name = "Tom";
}

let bob = {
    name: "Bob"
};

console.log("before change: ", bob.name);  // Bob
change(bob);
console.log("after change: ", bob.name);   // Tom

В данном случае функция change получает объект и меняет его свойство name. В итоге мы увидим, что после вызова функции изменился оригинальный объект bob, который передавался в функцию.

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

function change(user){
    // полная переустановка объекта
    user = {
        name:"Tom"
    };
}

let bob = {
    name: "Bob"
};

console.log("before change:", bob.name);    // Bob
change(bob);
console.log("after change:", bob.name);     // Bob

То же самое касается массивов.


Стрелочные функции

Стрелочные функции (arrow functions) представляют сокращенную версию обычных функций. Стрелочные функции образуются с помощью знака стрелки (=>), перед которым в скобках идут параметры функции, а после - собственно тело функции. Например:

let sum = (x, y) => x + y;
let a = sum(4, 5);
let b = sum(10, 5);

console.log(a);  // 9
console.log(b);  // 15

В данном случае функция (x, y) => x + y осуществляет сложение двух чисел и присваивается переменной sum. Функция принимает два параметра - x и y. Ее тело составляет сложение значений этих параметров. И поскольку после стрелки фактически идет конкретное значение, которое представляет сумму чисел, то функция возвращает это значение. И мы можем через переменную sum вызвать данную функцию и получить ее результат в переменные a и b.

Если функция принимает один параметр, то скобки вокруг него можно опустить:

let square = n => n * n;

console.log(square(5));     // 25
console.log(square(6));     // 36
console.log(square(-7));    // 49

Если тело функции представляет набор выражений, то они облекаются в фигурные скобки:

let square = n => {
    let result = n * n;
    return result;
};

console.log(square(5));     // 25

Для возвращения результата из функции в таком случае применяется стандартный оператор return.

Если стрелочная функция не принимает никаких параметров, то ставятся пустые скобки:

let hello = ()=> console.log("Hello World");
hello();  // Hello World

Кроме создания новых объектов JavaScript предоставляет нам возможность создавать новые типы объектов с помощью конструкторов. Так, одним из способов создания объекта является применение конструктора типа Object:

let tom = new Object();

После создания переменной tom она будет вести себя как объект типа Object.

Конструктор позволяет определить новый тип объекта. Тип представляет собой абстрактное описание или шаблон объекта. Можно еще провести следующую аналогию. У нас у всех есть некоторое представление о человеке - наличие двух рук, двух ног, головы, пищеварительной, нервной системы и т.д. Есть некоторый шаблон - этот шаблон можно назвать типом. Реально же существующий человек является объектом этого типа.

Определение типа может состоять из функции конструктора, методов и свойств. Для начала определим конструктор:

function User(pName, pAge) {
    this.name = pName;
    this.age = pAge;
    this.displayInfo = function() {
        document.write("Имя: " + this.name + "; возраст: " + this.age + "<br/>");
    };
}

Конструктор - это обычная функция за тем исключением, что в ней мы можем установить свойства и методы. Для установки свойств и методов используется ключевое слово this. В данном случае устанавливаются два свойства name и age и один метод displayInfo.

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

let tom = new User("Том", 26);
console.log(tom.name); // Том
tom.displayInfo();

Чтобы вызвать конструктор, то есть создать объект типа User, надо использовать ключевое слово new.Чтобы вызвать конструктор, то есть создать объект типа User, надо использовать ключевое слово new.

Оператор instanceof позволяет проверить, с помощью какого конструктора создан объект. Если объект создан с помощью определенного конструктора, то оператор возвращает true:

let isUser = tom instanceof User;
console.log(isUser);    // true

Кроме непосредственного определения свойств и методов в конструкторе мы также можем использовать свойство prototype. Каждая функция имеет свойство prototype, представляющее прототип функции. То есть свойство User.prototype представляет прототип объектов User. И любые свойства и методы, которые будут определены в User.prototype, будут общими для всех объектов User.

Например, после определения объекта User нам потребовалось добавить к нему метод и свойство:

function User(pName, pAge) {
    this.name = pName;
    this.age = pAge;
    this.displayInfo = function() {
        document.write("Имя: " + this.name + "; возраст: " + this.age + "<br/>");
    };
};

User.prototype.hello = function() {
    document.write(this.name + " говорит: 'Привет!'<br/>");
};
User.prototype.maxAge = 110;

let tom = new User("Том", 26);
tom.hello();
let john = new User("Джон", 28);
john.hello();
console.log(tom.maxAge);   // 110
console.log(john.maxAge);  // 110

Здесь добавлены метод hello и свойство maxAge, и каждый объект User сможет их использовать. Но важно заметить, что значение свойства maxAge будет одно и то же для всех объектов, это разделяемое статическое свойство. В отличие, скажем, от свойства this.name, которое хранит значение для определенного объекта.


Инкапсуляция

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

function User(pName, pAge) {
    this.name = pName;
    this.age = pAge;
    this.displayInfo = function() {
        document.write("Имя: " + this.name + "; возраст: " + this.age);
    };
}

let tom = new User("Том", 26);
tom.name = 34;
console.log(tom.name); // 34

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

function User (name, age) {
    this.name = name;
    let _age = age;
    this.displayInfo = function(){
        document.write("Имя: " + this.name + "; возраст: " + _age + "<br>");
    };
    this.getAge = function() {
        return _age;
    };
    this.setAge = function(age) {
        if(typeof age === "number" && age > 0 && age < 110){
            _age = age;
        } else {
            console.log("Недопустимое значение");
        }
    };
}

let tom = new User("Том", 26);
console.log(tom._age); // undefined - _age - локальная переменная
console.log(tom.getAge()); // 26
tom.setAge(32);
console.log(tom.getAge()); // 32
tom.setAge("54"); // Недопустимое значени

В конструкторе User объявляется локальная переменная _age вместо свойства age. Как правило, названия локальных переменных в конструкторах начинаются со знака подчеркивания. Для того, чтобы работать с возрастом пользователя извне, определяются два метода. Метод getAge() предназначен для получения значения переменной _age. Этот метод еще называется геттер (getter). Второй метод - setAge, который еще называется сеттер (setter), предназначен для установки значения переменной _age.


Функция как объект. Методы call и apply

В JavaScript функция тоже является объектом - объектом Function и тоже имеет прототип, свойства, методы. Все функции, которые используются в программе, являются объектами Function и имеют все его свойства и методы. Например, мы можем создать функцию с помощью конструктора Function:

let square = new Function('n', 'return n * n;');
console.log(square(5));

В конструктор Function может передаваться ряд параметров. Последний параметр представляет собой само тело функции в виде строки. Фактически строка содержит код javascript. Предыдущие аргументы содержат названия параметров. В данном случае определяется функция возведения числа в квадрат, которая имеет один параметр n.

Среди свойств объекта Function можно выделить следующие:

  • arguments: массив аргументов, передаваемых в функцию
  • length: определяет количество аргументов, которые ожидает функция
  • caller: определяет функцию, вызвавшую текущую выполняющуюся функцию
  • name: имя функции
  • prototype: прототип функции

С помощью прототипа мы можем определить дополнительные свойства:

function display() {
    console.log("Hello, World!");
}

Function.prototype.program = "Hello";
console.log(display.program);  // Hello

Среди методов надо отметить методы call() и apply().

Метод call() вызывает функцию с указанным значением this и аргументами:

function add(x, y){
    return x + y;
}

let result = add.call(this, 3, 8);
console.log(result); // 11

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

При передаче объекта через первый параметр, мы можем ссылаться на него через ключевое слово this:

function User (name, age) {
    this.name = name;
    this.age = age;
}

let tom = new User("Том", 26);

function display() {
    console.log("Ваше имя: " + this.name);
}

display.call(tom); // Ваше имя: Том

В данном случае передается только одно значение, поскольку функция display не принимает параметров. То есть функция будет вызываться для объекта tom.

Если нам не важен объект, для которого вызывается функция, то можно передать значение null:

function add(x, y) {
    return x + y;
}

let result = add.call(null, 3, 8);
console.log(result);  // 11

На метод call() похож метод apply(), который также вызывает функцию и в качестве первого параметра также получает объект, для которого функция вызывается. Только теперь в качестве второго параметра передается массив аргументов:

function add(x, y) {
    return x + y;
}

let result = add.apply(null, [3, 8]);

console.log(result);  // 11

Наследование

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

// конструктор пользователя
function User (name, age) {
    this.name = name;
    this.age = age;
    this.go = function() {
        document.write(this.name + " идет <br/>");
    };
    this.displayInfo = function() {
        document.write("Имя: " + this.name + "; возраст: " + this.age + "<br/>");
    };
}

User.prototype.maxage = 110;

// конструктор работника
function Employee(name, age, comp) {
    User.call(this, name, age);
    this.company = comp;
    this.displayInfo = function() {
        document.write("Имя: " + this.name + "; возраст: " + this.age + "; компания: " + this.company + "<br/>");
    };
}

Employee.prototype = Object.create(User.prototype);

let tom = new User("Том", 26);
let bill = new Employee("Билл", 32, "Google");
tom.go();  // Том идет
bill.go();  // Билл идет
tom.displayInfo();  // Имя: Том; возраст: 26
bill.displayInfo();  // Имя: Билл; возраст: 32; компания: Google
console.log(bill.maxage);  // 110

Здесь в начале определяется конструктор User и к его прототипу добавляется свойство maxage. Затем определяется тип Employee.

В конструкторе Employee происходит обращение к конструктору User с помощью вызова:

User.call(this, name, age);

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

Кроме того, необходимо унаследовать также и прототип User. Для этого служит вызов:

Employee.prototype = Object.create(User.prototype);

Метод Object.create() позволяет создать объект прототипа User, который затем присваивается прототипу Employee. При этом при необходимости в прототипе Employee мы также можем определить дополнительные свойства и методы.

При наследовании мы можем переопределять наследуемый функционал. Например, Employee переопределяет метод displayInfo(), унаследованный от User, чтобы включить в вывод этого метода новое свойство company.


Ключевое слово this

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

Глобальный контекст

В глобальном контексте this ссылается на глобальный объект. В данном случае поведение не зависит от режима (строгий или нестрогий):

this.alert("global alert");
this.console.log("global console");

var currentDocument = this.document;

Но в данном случае использвание this избыточно.

Контекст объекта

В контексте объекта, в том числе в его методах, ключевое слово this ссылается на этот же объект:

var o = {
    bar: "bar3",
    foo: function() {
        console.log(this.bar);
    }
};

var bar = "bar1";
o.foo();    // bar3

Декомпозиция

Декомпозиция (destructuring) позволяет извлечь из объекта отдельные значения в переменные:

let user = {
    name: "Tom",
    age: 24,
    phone: "+367438787",
    email: "tom@gmail.com"
};

let {name, email} = user;
console.log(name);      // Tom
console.log(email);     // tom@gmail.com

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

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

let user = {
    name: "Tom",
    age: 24,
    phone: "+367438787",
    email: "tom@gmail.com"
};

let {name: userName, email: mailAddress} = user;
console.log(userName);      // Tom
console.log(mailAddress);   // tom@gmail.com

В данном случае свойство name сопоставляется с переменной userName, а поле email - с переменной mailAddress.

Декомпозиция массивов

Также можно декомпозировать массивы:

let users = ["Tom", "Sam", "Bob"];
let [a, b, c] = users;

console.log(a);     // Tom
console.log(b);     // Sam
console.log(c);     // Bob

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

let users = ["Tom", "Sam", "Bob", "Ann", "Alice", "Kate"];
let [first,,,,fifth] = users;

console.log(first);     // Tom
console.log(fifth);     // Alice

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

Подобным образом можно получить, например, второй и четвертый элементы:

let users = ["Tom", "Sam", "Bob", "Ann", "Alice", "Kate"];
let [,second,,forth] = users;

console.log(second);        // Sam
console.log(forth);         // Ann

Можно совместить получение данных из массива и объекта:

let people = [
    {name: "Tom", age: 34},
    {name: "Bob", age: 23},
    {name: "Sam", age: 32}
];
let [,{name}] = people;

console.log(name);      // Bob

В данном случае получаем значение свойства name второго объекта в массиве.

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


Классы

С внедреием стандарта ES2015 (ES6) в JavaScript появился новый способ определения объектов - с помощью классов. Класс представляет описание объекта, его состояния и поведения, а объект является конкретным воплощением или экземпляром класса.

Для определения класса используется ключевое слово class:

class Person {
    
}

После слова class идет название класса (в данном случае класс называется Person), и затем в фигурных скобках определяется тело класса. Также можно определить анонимный класс и присвоить его переменной:

let Person = class{}

После этого мы можем создать объекты класса с помощью конструктора:

class Person {
    
}

let tom = new Person();
let bob = new Person();

Для создания объекта с помощью конструктора сначала ставится ключевое слово new. Затем собственно идет вызов конструктора - по сути вызов функции по имени класса. По умолчанию классы имеют один конструктор без параметров. Поэтому в данном случае при вызове конструктора в него не передаются аргументы.

Также мы можем определить в классе свои конструкторы. Также класс может содержать свойства и методы:

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    display() {
        console.log(this.name, this.age);
    }
}

let tom = new Person("Tom", 34);
tom.display();          // Tom 34
console.log(tom.name);  // Tom

Конструктор определяется с помощью метода с именем constructor. По сути это обычный метод, который может принимать параметры. Основная цель конструктора - инициализировать объект начальными данными. И в данном случае в конструктор передаются два значения - для имени и возраста пользователя. Для хранения состояния в классе определяются свойства. Для их определения используется ключевое слово this. В данном случае в классе два свойства: name и age. Поведение класса определяют методы. В данном случае определен метод display(), который выводит значения свойств на консоль. Поскольку мы определили конструктор, который принимает два параметра, то соответственно мы можем передать в конструктор два значения для инициализации объекта: new Person("Tom", 34).

Наследование

Одни классы могут наследоваться от других. Наследование позволяет сократить объем кода в классах-наследниках. Например, определим следующие классы:

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    display() {
        console.log(this.name, this.age);
    }
}

class Employee extends Person {
    constructor(name, age, company) {
        super(name, age);
        this.company = company;
    }
    display() {
        super.display();
        console.log("Employee in", this.company);
    }
    work() {
        console.log(this.name, "is hard working");
    }
}

let tom = new Person("Tom", 34);
let bob = new Employee("Bob", 36, "Google");
tom.display();
bob.display();
bob.work();

Для наследования одного класса от другого в определении класса применяется оператор extends, после которого идет название базового класса. То есть в данном случае класс Employee наследуется от класса Person. Класс Person еще называется базовым классом, классом-родителем, суперклассом, а класс Employee - классом-наследником, подклассом, производным классом. Производный класс, как и базовый, может определять конструкторы, свойства, методы. Вместе с тем с помощью слова super производный класс может ссылаться на функционал, определенный в базовом. Например, в конструкторе Employee можно вручную не устанавливать свойства name и age, а с помощью выражения super(name, age); вызвать конструктор базового класса и тем самым передать работу по установке этих свойств базовому классу. Подобным образом в методе display в классе Employee через вызов super.display() можно обратиться к реализации метода display класса Person.

Консольный вывод программы:

Tom 34
Bob 36
Employee in Google
Bob is hard working

Статические методы

Статические методы вызываются для всего класса вцелом, а не для отдельного объекта. Для их определения применяется оператор static. Например:

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    static nameToUpper(person) {
        return person.name.toUpperCase();
    }
    display() {
        console.log(this.name, this.age);
    }
}

let tom = new Person("Tom Soyer", 34);
let personName = Person.nameToUpper(tom);
console.log(personName);        // TOM SOYER

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