Повторное введение в JavaScript (JS учебник)

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

Было бы полезно начать с истории языка. JavaScript был создан в 1995 Бренданом Айком, инженером в компании Netscape. Первый релиз состоялся вместе с выходом браузера Netscape 2 в начале 1996 года. Сначала язык назывался LiveScript, но затем был переименован в связи с маркетинговыми целями, чтобы сыграть на популярности языка Java компании Sun Microsystem — несмотря на это языки практически не имеют ничего общего друг с другом. Так было положено начало путаницы между этими языками.

Чуть позже Microsoft выпустила очень похожий и практически совместимый язык JScript, который шёл вместе с IE3. Через пару месяцев Netscape отправил язык в Ecma International, Европейскую организацию занимающуюся стандартами, которая выпустила первую версию стандарта ECMAScript в 1997. Стандарт получил значимое обновление в ECMAScript edition 3 в 1999, и остаётся самым стабильным до сегодняшнего дня. Четвёртая версия была отклонена, из-за проблем с усложнениями в языке. Многие вещи из четвёртого издания послужили основой для стандарта ECMAScript 5 (декабрь 2009) и ECMAScript 6 (июнь 2015).

В отличие от большинства языков, JavaScript не следует концепции ввода (input) и вывода (output). Он спроектирован таким образом, чтобы запускаться как язык сценариев, встроенный в среду исполнения. Самая популярная среда исполнения это браузер, однако интерпретаторы JavaScript присутствуют и в Adobe Acrobat, Photoshop, Yahoo!'s Widget engine, и даже в серверном окружении, например node.js.

Описание

JavaScript является объектно-ориентированным языком, имеющий типы и операторы, встроенные объекты и методы. Его синтаксис происходит от языков Java и C, поэтому много конструкций из этих языков применимы и к JavaScript. Одним из ключевых отличий JavaScript является отсутствие классов, вместо этого функциональность классов осуществляется прототипами объектов (смотрите ES6 Classes) . Другое главное отличие в том, что функции это объекты, в которых содержится исполняемый код и которые могут быть переданы куда-либо, как и любой другой объект.

Начнём с основы любого языка: с типов данных. Программы на JavaScript оперируют значениями, и все эти значения принадлежат к определённому типу. Типы данных в JavaScript:

Да, ещё Undefined и Null, которые немного обособлены. И Массивы, которые являются особым видом объектов. А также Даты и Регулярные выражения, тоже являющиеся объектами. И, если быть технически точным, функции это тоже особый вид объекта. Поэтому схема типов выглядит скорее так:

Также есть несколько встроенных типов Ошибок. Чтобы было проще, рассмотрим подробнее первую схему.

Числа

Числа в JavaScript — это "64-битные значения двойной точности формата IEEE 754", согласно спецификации. Это имеет интересные последствия. В JavaScript нет такой вещи, как целое число, поэтому с арифметикой нужно быть начеку, если вы привыкли к вычислениям в языках C или Java. Взгляните на пример:

js
0.1 + 0.2 == 0.30000000000000004;

На практике целые значения это 32-битные целые (и хранятся таким образом в некоторых браузерных реализациях), что может быть важно для побитовых операций.

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

js
Math.sin(3.5);
var circumference = Math.PI * (r + r);

Вы можете преобразовать строку в целое число, используя встроенную функцию parseInt(). Её необязательный второй параметр — основание системы счисления, которое следует всегда явно указывать:

js
parseInt("123", 10); // 123
parseInt("010", 10); // 10

Если вы не предоставите основание, то можете получить неожиданные результаты:

js
parseInt("010"); // 8
parseInt("0x10"); // 16

Это случилось потому, что функция parseInt() расценила строку как восьмеричную из-за начального 0, а шестнадцатеричную - из-за начального "0x".

Если хотите преобразовать двоичное число в десятичное целое, просто смените основание:

js
parseInt("11", 2); // 3

Вы можете аналогично парсить дробные числа, используя встроенную функцию parseFloat(), которая использует всегда основание 10 в отличие от родственной parseInt().

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

js
+"42"; // 42
+"0x10"; // 16

Специальное значение NaN (сокращение от "Not a Number") возвращается, если строка не является числом:

js
parseInt("hello", 10); // NaN

NaN "заразителен": любая математическая операция над NaN возвращает NaN:

js
NaN + 5; // NaN

Проверить значение на NaN можно встроенной функцией isNaN():

js
isNaN(NaN); // true

JavaScript также имеет специальные значения Infinity (бесконечность) и -Infinity:

js
1 / 0; // Infinity
-1 / 0; // -Infinity

Проверить значение на Infinity, -Infinity и NaN можно с помощью встроенной функции isFinite():

js
isFinite(1 / 0); // false
isFinite(-Infinity); // false
isFinite(NaN); // false

Строки

Строки в JavaScript - это последовательности символов Unicode (в кодировке UTF-16). Для тех, кто имеет дело с интернационализацией, это должно стать хорошей новостью. Если быть более точным, то строка - это последовательность кодовых единиц, каждая из которых представлена 16-битовым числом, а каждый символ Unicode состоит из 1 или 2 кодовых единиц.

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

Чтобы выяснить длину строки (в кодовых единицах), используйте свойство length:

js
"hello".length; // 5

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

js
"hello".charAt(0); // h
"hello, world".replace("hello", "goodbye"); // goodbye, world
"hello".toUpperCase(); // HELLO

Другие типы

JavaScript дополнительно различает такие типы, как null, который указывает на преднамеренное отсутствующее значение, и undefined, указывающий на неинициализированное значение — то есть, значение, которое даже не было назначено. Мы поговорим о переменных позже, но в JavaScript можно объявить переменную без присвоения ей значения. В этом случае тип переменной будет "undefined".

Ещё в JavaScript есть логический (булевый) тип данных, который может принимать два возможных значения true или false (оба являются ключевыми словами). Любое значение может быть преобразовано в логическое значение в соответствии со следующими правилами:

  1. false, 0, пустая строка (""), NaN, null и undefined преобразуются в false.
  2. Все остальные значения преобразуются в true.

Преобразование значений можно осуществить явно, используя функцию Boolean():

js
Boolean(""); // false
Boolean(234); // true

Этот метод используется редко, так как JavaScript может автоматически преобразовывать типы в тех случаях, когда ожидается булевое значение, например в операторе if. Из-за того, что любой тип данных может быть преобразован в булевое значение, иногда говорят, что данные "истинные" или "ложные".

Для операций с логическими данными используются логические операторы: && (логическое И), || (логическое ИЛИ), ! (логическое НЕ).

Переменные

Для объявления новых переменных в JavaScript используются ключевые слова let, const или var.

js
let a;
let name = "Simon";

let позволяет объявлять переменные, которые доступны только в блоке, в котором они объявлены:

js
// myLetVariable недоступна здесь

for (let myLetVariable = 0; myLetVariable < 5; myLetVariable++) {
  // myLetVariable доступна только здесь
}

// myLetVariable недоступна здесь

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

js
const Pi = 3.14; // в переменную Pi записано значение.
Pi = 1; // вызовет исключение, так как значение константы нельзя изменить.

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

js
var a;
var name = "Simon";

Пример кода с переменной, объявленной с помощью var:

js
// myVarVariable доступна здесь

for (var myVarVariable = 0; myVarVariable < 5; myVarVariable++) {
  // myVarVariable доступна для всей функции
}

// myVarVariable доступна и здесь

Если вы объявляете переменную без присвоения ей какого-либо значения, то её тип будет определён как undefined.

Важной особенностью языка JavaScript является то, что блоки данных не имеют своей области видимости, она есть только у функций. Поэтому, если объявить переменную через var в блоке данных (например, внутри контролирующей структуры if), то она будет доступна всей функции. Следует отметить, что в новом стандарте ECMAScript Edition 6 появились инструкции let и const, позволяющие объявлять переменные с областью видимости, ограниченной пределами блока.

Операторы

JavaScript поддерживает такие операторы, как +, -, *, / и %, который возвращает остаток от деления (не путать с модулем). Значения присваиваются с помощью оператора =, или с помощью составных операторов += и -=. Это сокращённая запись выражения x = x оператор y.

js
x += 5;
x = x + 5;

Так же используются операторы инкремента (++) и декремента (--). Которые имеют префиксную и постфиксную форму записи.

Оператор + так же выполняет конкатенацию (объединение) строк:

js
"hello" + " world"; // "hello world"

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

js
"3" + 4 + 5; // "345"
3 + 4 + "5"; // "75"

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

Для сравнения в JavaScript используются следующие операторы: <, >, <= и >=. Сравнивать можно не только числа, но и строки. Проверка на равенство немного сложнее. Для проверки используют двойной (==) или тройной (===) оператор присваивания. Двойной оператор == осуществляет автоматическое преобразование типов, что может приводить к интересным результатам:

js
123 == "123"; // true
1 == true; // true

Если преобразование нежелательно, то используют оператор строгого равенства:

js
1 === true; // false
123 === "123"; // false
true === true; // true

Для проверки на неравенство используют операторы != и !==.

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

Управляющие структуры

Управляющие структуры в JavaScript очень похожи на таковые в языках семейства C. Условные операторы выражены ключевыми словами if и else, которые можно составлять в цепочки:

js
var name = "kittens";
if (name == "puppies") {
  name += "!";
} else if (name == "kittens") {
  name += "!!";
} else {
  name = "!" + name;
}
name == "kittens!!";

В JavaScript есть три типа циклов: while, do-while и for. While используется для задания обычного цикла, а do-while целесообразно применить в том случае, если вы хотите, чтобы цикл был выполнен хотя бы один раз:

js
while (true) {
  // бесконечный цикл!
}

var input;
do {
  input = get_input();
} while (inputIsNotValid(input));

Цикл for похож на такой же в языках C и Java: он позволяет задавать данные для контроля за выполнением цикла:

js
for (var i = 0; i < 5; i++) {
  // Выполнится 5 раз
}

JavaScript также содержит две других известных конструкции: for...of

js
for (let value of array) {
  // операции с value
}

и for...in:

js
for (let property in object) {
  // операции над свойствами объекта
}

Логические операторы && и || используют "короткий цикл вычисления", это значит, что вычисление каждого последующего оператора зависит от предыдущего. Например, полезно проверить существует ли объект или нет, прежде чем пытаться получить доступ к его свойствам:

js
var name = o && o.getName();

Таким способом удобно задавать значения по умолчанию:

js
var name = otherName || "default";

К условным операторам в JavaScript принадлежит также тернарный оператор "?" :

js
var allowed = age > 18 ? "yes" : "no";

Оператор switch используется при необходимости множественного сравнения:

js
switch (action) {
  case "draw":
    drawit();
    break;
  case "eat":
    eatit();
    break;
  default:
    donothing();
}

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

js
switch (a) {
  case 1: // fallthrough
  case 2:
    eatit();
    break;
  default:
    donothing();
}

Вариант default опциональный. Допускается использование выражений как в условии switch, так и в cases. При проверке на равенство используется оператор строгого равенства ===:

js
switch (1 + 3) {
  case 2 + 2:
    yay();
    break;
  default:
    neverhappens();
}

Объекты

Объекты в JavaScript представляют собой коллекции пар имя-значение (ключ-значение). Они похожи на:

  • Словари в Python.
  • Хеши в Perl и Ruby.
  • Таблицы хешей в C и C++.
  • HashMaps в Java.
  • Ассоциативные массивы в PHP.

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

Существует два основных способа создать объект:

js
var obj = new Object();

А также:

js
var obj = {};

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

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

js
var obj = {
  name: "Carrot",
  for: "Max",
  details: {
    color: "orange",
    size: 12,
  },
};

Доступ к свойствам объекта можно получить следующими способами:

js
obj.details.color; // orange
obj["details"]["size"]; // 12

Эти два метода равнозначны. Первый метод используется, если мы точно знаем к какому методу нам нужно обратиться. Второй метод принимает в качестве имени свойства строку, и позволяет вычислять имя в процессе вычислений. Следует отметить, что последний метод мешает некоторым движкам и минимизаторам оптимизировать код. Если появится необходимость назначить в качестве имён свойств объекта зарезервированные слова, то данный метод тоже может пригодиться:

js
// Вызовет Syntax error, ведь 'for' это зарезервированное слово
obj.for = "Simon";

// А тут всё нормально
obj["for"] = "Simon";

Больше информации об объектах и прототипах: Object.prototype.

Для получения информации по прототипам объектов и цепям прототипов объектов смотрите Inheritance and the prototype chain.

Массивы

Массивы в JavaScript всего лишь частный случай объектов. Работают они практически одинаково (если именем свойства является число, то доступ к нему можно получить только через вызов в скобках []), только у массивов есть одно удивительное свойство length (длина). Оно возвращает число, равное самому большому индексу массива + 1.

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

js
var a = new Array();
a[0] = "dog";
a[1] = "cat";
a[2] = "hen";
a.length; // 3

Но гораздо удобнее использовать литерал массива:

js
var a = ["dog", "cat", "hen"];
a.length; // 3

Запомните, свойство array.length не обязательно будет показывать количество элементов в массиве. Посмотрите пример:

js
var a = ["dog", "cat", "hen"];
a[100] = "fox";
a.length; // 101

Запомните — длина массива это его самый большой индекс плюс один.

Если попытаться получить доступ к несуществующему элементу массива, то получите undefined:

js
typeof a[90]; // undefined

Для перебора элементов массива используйте такой способ:

js
for (var i = 0; i < a.length; i++) {
  // Сделать что-нибудь с элементом a[i]
}

ES2015 представляет более краткий for...of способ обхода по итерируемым объектам, в т.ч. массивам:

for (const currentValue of a) {
  // Сделать что-нибудь с currentValue
}

Перебрать элементы массива также можно с помощью цикла for...in. Но, если вдруг будет изменено какое-либо свойство Array.prototype, то оно тоже будет участвовать в выборке. Не используйте данный метод.

И самый новый способ перебора свойств массива был добавлен в ECMAScript 5 — это метод forEach():

js
["dog", "cat", "hen"].forEach(function (currentValue, index, array) {
  // Сделать что-нибудь с currentValue или array[index]
});

Для добавления данных в массив используйте метод push():

js
a.push(item);

У массивов есть ещё множество полезных методов. С их полным списком вы можете ознакомиться по ссылке.

Метод Описание
a.toString() Возвращает строковое представление массива, где все элементы разделены запятыми.
a.toLocaleString() Возвращает строковое представление массива в соответствии с выбранной локалью.
a.concat(item1[, item2[, ...[, itemN]]]) Возвращает новый массив с добавлением указанных элементов.
a.join(sep) Преобразует массив в строку, где в качестве разделителя используется параметр sep
a.pop() Удаляет последний элемент массива и возвращает его.
a.push(item1, ..., itemN) Добавляет один или более элементов в конец массива.
a.reverse() Меняет порядок элементов массива на обратный.
a.shift() Удаляет первый элемент массива и возвращает его.
a.slice(start[, end]) Возвращает новый массив.
a.sort([cmpfn]) Сортирует данные в массиве.
a.splice(start, delcount[, item1[, ...[, itemN]]]) Позволяет вырезать из массива его часть и добавлять на это место новые элементы.
a.unshift(item1[, item2[, ...[, itemN]]]) Добавляет элементы в начало массива.

Функции

Наряду с объектами функции также являются ключевыми компонентами языка JavaScript. Базовые функции очень просты:

js
function add(x, y) {
  var total = x + y;
  return total;
}

В этом примере показано практически всё, что нужно знать о функциях. Функции в JavaScript могут принимать ноль или более параметров. Тело функции может содержать любые выражения и определять свои собственные переменные, которые будут для этой функции локальными. Инструкция return используется для возврата значения и остановки выполнения функции. Если инструкции return в функции нет (или есть, но не указано возвращаемое значение), то JavaScript возвратит undefined.

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

js
add(); // NaN
// Нельзя проводить сложение undefined и undefined

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

js
add(2, 3, 4); // 5
// используются только первые два аргумента, "4" игнорируется

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

js
function add() {
  var sum = 0;
  for (var i = 0, j = arguments.length; i < j; i++) {
    sum += arguments[i];
  }
  return sum;
}

add(2, 3, 4, 5); // 14

Или создадим функцию для вычисления среднего значения:

js
function avg() {
  var sum = 0;
  for (var i = 0, j = arguments.length; i < j; i++) {
    sum += arguments[i];
  }
  return sum / arguments.length;
}
avg(2, 3, 4, 5); // 3.5

Это довольно полезно, но при этом кажется излишне подробным. Для уменьшения количества кода взглянем на замену использования массива аргументов синтаксисом остаточных параметров. В этом случае мы можем передавать в функцию любое количество аргументов, сохраняя код минималистичным. Оператор остаточных параметров используется в списке параметров функции в формате: ...variable и включает в себя целый список аргументов, с которыми функция будет вызвана. Мы будем также использовать замену цикла for циклом for...of для получения значений, которые будет содержать наша переменная.

js
function avg(...args) {
  var sum = 0;
  for (let value of args) {
    sum += value;
  }
  return sum / args.length;
}

avg(2, 3, 4, 5); // 3.5

В JavaScript можно создавать анонимные функции:

js
var avg = function () {
  var sum = 0;
  for (var i = 0, j = arguments.length; i < j; i++) {
    sum += arguments[i];
  }
  return sum / arguments.length;
};

Данная запись семантически равнозначна записи function avg(). Это даёт возможность использовать разные интересные трюки. Вот посмотрите, как можно "спрятать" локальные переменные в функции:

js
var a = 1;
var b = 2;
(function () {
  var b = 3;
  a += b;
})();
a; // 4
b; // 2

В JavaScript есть возможность рекурсивного вызова функции. Это может оказаться полезным при работе с иерархическими (древовидными) структурами данных (например такие, которые встречаются при работе с DOM).

js
function countChars(elm) {
  if (elm.nodeType == 3) {
    // TEXT_NODE
    return elm.nodeValue.length;
  }
  var count = 0;
  for (var i = 0, child; (child = elm.childNodes[i]); i++) {
    count += countChars(child);
  }
  return count;
}

Тут мы сталкиваемся с проблемой: как вызвать функцию рекурсивно, если у неё нет имени? Для этого в JavaScript есть именованные функциональные выражения IIFEs (Immediately Invoked Function Expressions). Вот пример использования именованной самовызывающейся функции:

js
var charsInBody = (function counter(elm) {
  if (elm.nodeType == 3) {
    // TEXT_NODE
    return elm.nodeValue.length;
  }
  var count = 0;
  for (var i = 0, child; (child = elm.childNodes[i]); i++) {
    count += counter(child);
  }
  return count;
})(document.body);

Имя функции в примере доступно только внутри самой функции. Это улучшает оптимизацию и читаемость кода.

Собственные объекты

В классическом Объектно-Ориентированном Программировании (ООП) объекты — это коллекции данных и методов, которые этими данными оперируют. JavaScript - это язык, основанный на прототипах, и в его определении нет понятия классов, таких, как в языках C++ или Java. (Иногда это может запутать программистов, знакомых с языками, в которых есть классы.) Вместо классов JavaScript использует функции. Давайте представим объект с личными данными, содержащий поля с именем и фамилией. Есть два типа отображения имён: "Имя Фамилия" или "Фамилия, Имя". С помощью объектов и функций можно сделать следующее:

js
function makePerson(first, last) {
  return {
    first: first,
    last: last,
  };
}

function personFullName(person) {
  return person.first + " " + person.last;
}

function personFullNameReversed(person) {
  return person.last + ", " + person.first;
}

s = makePerson("Simon", "Willison");
personFullName(s); // Simon Willison
personFullNameReversed(s); // Willison, Simon

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

js
function makePerson(first, last) {
  return {
    first: first,
    last: last,
    fullName: function () {
      return this.first + " " + this.last;
    },
    fullNameReversed: function () {
      return this.last + ", " + this.first;
    },
  };
}
s = makePerson("Simon", "Willison");
s.fullName(); // Simon Willison
s.fullNameReversed(); // Willison, Simon

А вот кое-что новенькое: ключевое слово this. Когда this используется внутри функции, оно ссылается на текущий объект. Значение ключевого слова зависит от способа вызова функции. Если вызвать функцию с обращением к объекту через точку или квадратные скобки, то this получится равным данному объекту. В ином случае this будет ссылаться на глобальный объект. Это часто приводит к ошибкам. Например:

js
s = makePerson("Simon", "Willison");
var fullName = s.fullName;
fullName(); // undefined undefined

При вызове fullName(), this получает ссылку на глобальный объект. А так как в глобальном объекте не определены переменные first и last, то имеем два undefined.

Используя особенность ключевого слова this, можно улучшить код функции makePerson:

js
function Person(first, last) {
  this.first = first;
  this.last = last;
  this.fullName = function () {
    return this.first + " " + this.last;
  };
  this.fullNameReversed = function () {
    return this.last + ", " + this.first;
  };
}
var s = new Person("Simon", "Willison");

В примере мы использовали новое ключевое слово: new. Оно тесно связано с this. Данное ключевое слово создаёт новый пустой объект, а потом вызывает указанную функцию, а this получает ссылку на этот новый объект. Функции, которые предназначены для вызова с new называются конструкторами. Существует соглашение, согласно которому все функции-конструкторы записываются с заглавной буквы.

Мы доработали наш код в предыдущем примере, но всё равно остался один неприятный момент с самостоятельным вызовом fullName().

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

js
function personFullName() {
  return this.first + " " + this.last;
}
function personFullNameReversed() {
  return this.last + ", " + this.first;
}
function Person(first, last) {
  this.first = first;
  this.last = last;
  this.fullName = personFullName;
  this.fullNameReversed = personFullNameReversed;
}

Уже лучше: мы создали функции-методы только один раз, а при новом вызове функции-конструктора просто ссылаемся на них. Можно сделать ещё лучше? Конечно:

js
function Person(first, last) {
  this.first = first;
  this.last = last;
}
Person.prototype.fullName = function fullName() {
  return this.first + " " + this.last;
};
Person.prototype.fullNameReversed = function fullNameReversed() {
  return this.last + ", " + this.first;
};

Person.prototype это объект, доступ к которому есть у всех экземпляров класса Person. Он создаёт особую цепочку прототипов. Каждый раз, когда вы пытаетесь получить доступ к несуществующему свойству объекта Person, JavaScript проверяет, существует ли свойство в Person.prototype. В результате все, что передано в Person.prototype, становится доступным и всем экземплярам этого конструктора через this объект.

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

js
s = new Person("Simon", "Willison");
s.firstNameCaps();
// TypeError on line 1: s.firstNameCaps is not a function

Person.prototype.firstNameCaps = function () {
  return this.first.toUpperCase();
};
s.firstNameCaps(); // "SIMON"

Занимательно то, что добавлять свойства в прототип можно и для встроенных объектов JavaScript. Давайте добавим новый метод reversed классу String, этот метод будет возвращать строку задом наперёд:

js
var s = "Simon";
s.reversed(); // TypeError on line 1: s.reversed is not a function

String.prototype.reversed = function reversed() {
  var r = "";
  for (var i = this.length - 1; i >= 0; i--) {
    r += this[i];
  }
  return r;
};
s.reversed(); // "nomiS"

Данный метод будет работать даже на литералах строки!

js
"This can now be reversed".reversed();
// desrever eb won nac sihT

Как уже упоминалось, prototype формирует часть цепочки. Конечным объектом этой цепочки прототипов является Object.prototype, методы которого включают и toString() — тот метод, который вызывается тогда, когда надо получить строковое отображение объекта. Вот что можно сделать с нашими объектами Person:

js
var s = new Person("Simon", "Willison");
s.toString(); // [object Object]

Person.prototype.toString = function () {
  return "<Person: " + this.fullName() + ">";
};

s.toString(); // "<Person: Simon Willison>"

Помните, мы вызывали avg.apply() с первым аргументом равным null? Теперь мы можем сделать так: первым аргументом, переданным методу apply() будет объект, который примет значение this. Вот к примеру упрощённая реализация new:

js
function trivialNew(constructor, ...args) {
  var o = {}; // Создаём новый объект
  constructor.apply(o, args);
  return o;
}

Это не точная копия new, так как она не устанавливает цепочку прототипов (это сложно ). Метод apply() применяется не очень часто, но знать его важно. В примере выше, синтаксис ...args (включая многоточие) называется остаточными параметрами, потому что они включают в себя все оставшиеся аргументы.

Вызов

js
var bill = trivialNew(Person, "William", "Orange");

практически полностью эквивалентен этому:

js
var bill = new Person("William", "Orange");

В JavaScript метод apply() имеет похожий метод call(), который тоже позволяет устанавливать this, но принимает список, а не массив аргументов.

js
function lastNameCaps() {
  return this.last.toUpperCase();
}
var s = new Person("Simon", "Willison");
lastNameCaps.call(s);
// Аналогично записи:
s.lastNameCaps = lastNameCaps;
s.lastNameCaps(); // WILLISON

Вложенные функции

Объявлять новые функции можно и внутри других функций. Мы использовали этот приём чуть выше, создавая функцию makePerson(). Главная особенность вложенных функций в том, что они получают доступ к переменным, объявленным в их функции-родителе:

js
function parentFunc() {
  var a = 1;

  function nestedFunc() {
    var b = 4; // parentFunc can't use this
    return a + b;
  }
  return nestedFunc(); // 5
}

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

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

Замыкания (Closures)

Мы подошли к одному из самых мощных и непонятных инструментов JavaScript. Давайте разберёмся.

js
function makeAdder(a) {
  return function (b) {
    return a + b;
  };
}

var x = makeAdder(5);
var y = makeAdder(20);
x(6); // ?
y(7); // ?

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

Такой же фокус мы наблюдали в предыдущем примере, когда внутренние функции получали доступ к переменным той функции, в которой были объявлены. Только в нашем примере основная функция возвращает вложенную. Поначалу может показаться, что локальные переменные при этом перестанут существовать. Но они продолжают существовать — иначе код попросту не сработал бы. Вдобавок ко всему у нас есть две разные "копии" функции makeAdder, присвоенные разным переменным (одна копия, в которой а - это 5, а во второй а - это 20). Вот что имеем в результате вызова:

js
x(6); // возвратит 11
y(7); // возвратит 27

И вот что произошло: когда JavaScript выполняет функцию, создаётся объект 'scope', который содержит в себе все локальные переменные, объявленные внутри этой функции. Он инициализируется любым значением, переданным функции в качестве параметра. 'Scope' подобен глобальному объекту, который содержит все глобальные переменные и функции, кроме нескольких важных отличий: при каждом вызове функции создаётся новый объект 'scope' и, в отличие от глобального, к объекту 'scope' нельзя получить прямой доступ из вашего кода. И нет способа пройтись по свойствам данного объекта.

Так что при вызове функции makeAdder создаётся новый объект 'scope' с единственным свойством: a, которому присваивается значение, переданное функции в качестве аргумента. Потом makeAdder возвращает новую анонимную функцию. В любом другом случае 'сборщик мусора' удалил бы объект scope, но возвращаемая функция ссылается на этот объект. В итоге объект scope не удаляется до тех пор, пока существует хотя бы одна ссылка на него.

Все объекты scope соединяются в цепочку областей видимости, которая похожа на цепочку прототипов в объектной системе JavaScript.

Замыкание это связка из функции и объекта scope, созданного при её вызове. Подробнее о замыканиях здесь.