Введение в JavaScript

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

Значения и типы

В JavaScript есть типизированные значения, а не типизированные переменные. Доступны следующие встроенные типы:

  • string (строка)

  • number (число)

  • boolean (логическое значение)

  • null и undefined (пустое значение)

  • object (объект)

  • symbol (символ, новое в ES6)

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

var a;
typeof a;				// "undefined"

a = "hello world";
typeof a;				// "string"

a = 42;
typeof a;				// "number"

a = true;
typeof a;				// "boolean"

a = null;
typeof a;				// "object" — черт, ошибка

a = undefined;
typeof a;				// "undefined"

a = { b: "c" };
typeof a;				// "object"

a = Symbol();
typeof a;               // "symbol"

Значение, возвращаемое операцией typeof, всегда одно из шести (семи в ES6! - тип "symbol") строковых значений. Это значит, что typeof "abc" вернет "string", а не string.

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

typeof null — это интересный случай, так как он ошибочно возвращает "object", тогда как вы ожидали бы, что он вернет "null".

Предупреждение: Это давнишняя ошибка в JS, но она похоже никогда не будет исправлена. Слишком много кода в интернет полагается на нее и ее исправление соответственно повлечет за собой намного больше ошибок!

А еще, обратите внимание на a = undefined. Мы явно установили a в значение undefined, но это по поведению не отличается от переменной, у которой еще не установлено значение, например как тут var a;, в строке в начале блока кода. Переменная может получать такое состояние значения "undefined" разными путями, включая функции, которые не возвращают значения, и использование операции void.

Объекты

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

var obj = {
	a: "hello world",
	b: 42,
	c: true,
	d: { a: 1, b: "Hello world" }
};

obj.a;		// "hello world"
obj.b;		// 42
obj.c;		// true
obj.d;      // { a: 1, b: "Hello world" }

obj["a"];	// "hello world"
obj["b"];	// 42
obj["c"];	// true
obj["d"];   // { a: 1, b: "Hello world" }

Полезно представить значение этого obj визуально:

Свойства могут быть доступны либо через точечную нотацию (т.е., obj.a), либо через скобочную нотацию (т.е., obj["a"]). Точечная нотация короче и в целом легче для чтения и следовательно ей нужно отдавать предпочтение по возможности.

Скобочная нотация полезна, если у вас есть имя свойства, содержащее спецсимволы, например obj["hello world!"] — такие свойства часто называют ключами, когда к ним обращаются с помощью скобочной нотации. Нотация [ ] требует либо переменную (поясняется ниже), либо строковый литерал (который должен быть заключен в " .. " или ' .. ').

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

var obj = {
	a: "hello world",
	b: 42
};

var b = "a";

obj[b];			// "hello world"
obj["b"];		// 42

Есть пара других типов значений, с которыми вы обычно взаимодействуете в JavaScript программах: array(массив) и function (функция). Точнее, вместо того, чтобы быть полноценными встроенными типами, о них следует думать скорее как о подтипах — особых версиях типа object.

Массивы

Массив — это объект, который хранит значения (любого типа) не отдельно в именованных свойствах/ключах, а в ячейках, доступных по числовому индексу. Например:

var arr = [
	"hello world",
	42,
	true
];

arr[0];			// "hello world"
arr[1];			// 42
arr[2];			// true
arr.length;		// 3

typeof arr;		// "object"

Примечание: Языки, которые начинают счет с нуля, как и JS, используют 0 как индекс первого элемента массива.

Полезно представить arr визуально:

Поскольку массивы — это особые объекты (как намекает typeof), то у них могут быть свойства, включая автообновляемое свойство length (длина).

Теоретически, вы можете использовать массив как обычный объект со своими собственными свойствами или использовать object, но дать ему числовые свойства (0, 1 и т.д.) как у массива. Однако, в общем это было бы использование соответствующих типов не по назначению.

Лучший и самый естественный подход — использование массивов для значений, расположенных по числовым позициям и использовать object для именованных свойств.

Функции

Еще один подтип object, которым вы будете пользоваться во всех ваших JS программах — это функция :

function foo() {
	return 42;
}

foo.bar = "hello world";

typeof foo;			// "function"
typeof foo();		// "number"
typeof foo.bar;		// "string"

И еще раз, функции — это подтипы объектов, typeof вернет "function", что говорит о том, что function — основной тип и поэтому у него могут быть свойства, но обычно вы будете пользоваться свойствами функций (как например foo.bar) в редких случаях.

Методы встроенных типов

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

Например:

var a = "hello world";
var b = 3.14159;

a.length;				// 11
a.toUpperCase();		// "HELLO WORLD"
b.toFixed(4);			// "3.1416"

Вопрос "Как?", возникающий о возможности вызова a.toUpperCase() более сложен, чем то, что этот метод существует у значения.

Коротко говоря, есть форма обертки объекта String (заглавная S), обычно называемая "родной," которая связывается с примитивным типом string, и именно эта обертка определяет метод toUpperCase() в своем прототипе.

Когда вы используете примитивное значение, такое как "hello world", как object ссылаясь на свойство или метод (к примеру, a.toUpperCase() в предыдущем кусочке кода), JS автоматически "упаковывает" значение в его обертку-двойника (скрытую внутри).

Значение типа string может быть обернуто объектом String, значение типа number может быть обернуто объектом Number и boolean может быть обернуто объектом Boolean. В основном, вам не нужно беспокоиться о прямом использовании этих оберток значений — отдавайте предпочтение примитивным формам значений практически во всех случаях, а об остальном позаботится JavaScript.

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

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

Приведение типов (coercion)

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

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

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

Вот пример явного приведения:

var a = "42";

var b = Number( a );

a;				// "42"
b;				// 42 — число!

А вот пример неявного приведения:

var a = "42";

var b = a * 1;	// здесь "42" неявно приводится к 42

a;				// "42"
b;				// 42 — число!

Истинный и ложный

Ранее, мы кратко рассмотрели "истинную" и "ложную" природу значений: когда не-boolean значение приводится к boolean, становится ли оно true или false, соответственно?

Особый список "ложных" значений в JavaScript таков:

  • "" (пустая строка)

  • 0, -0, NaN (некорректное число)

  • null, undefined

  • false

Любое значение, не входящее в этот "ложный" список — "истинно." Вот несколько примеров:

  • "hello"

  • 42

  • true

  • [ ], [ 1, "2", 3 ] (массивы)

  • { }, { a: 42 } (объекты)

  • function foo() { .. } (функции)

Важно помнить, что не-boolean значение следует такому приведению "истинный"/"ложный" только если оно действительно приводится к boolean. Это — не единственная трудность, которая может смутить вас в ситуации, когда кажется что есть приведение значения к boolean, когда на самом деле его нет.

Равенство

Есть четыре операции равенства: ==, ===, != и !==. Формы с ! — конечно же, симметричные версии "не равно" своих противоположностей; не равно не следует путать с неравенством.

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

Посмотрите на пример неявного приведения, которое допускается нестрогим равенством == и не допускается строгим равенством ===:

var a = "42";
var b = 42;

a == b;			// true
a === b;		// false

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

Если подумать, то есть два возможных пути, когда a == b может стать true через приведение. Либо сравнение может закончится на 42 == 42, либо на "42" == "42". Так какое же из них?

Ответ: "42" становится 42, чтобы сделать сравнение 42 == 42. В таком простом примере не так уж важно по какому пути пойдет сравнение, в конце результат будет один и тот же. Есть более сложные случаи, где важно не только каков конечный результат сравнения, но и как вы к нему пришли.

Сравнение a === b даст false, так как приведение не разрешено, поэтому простое сравнение значений очевидно не завершится успехом. Многие разработчики чувствуют, что операция === — более предсказуема, поэтому они советуют всегда использовать эту форму и держаться подальше от ==. Думаю, такая точка зрения очень недальновидна. Я верю, что операция == — мощный инструмент, который помогает вашей программе, если вы уделите время на изучение того, как это работает.

Мы не собираемся рассматривать все скучные мельчайшие подробности того, как работает приведение в сравнениях ==. Многие из них очень разумные, но есть несколько важных тупиковых ситуаций, с которыми надо быть осторожным. Чтобы посмотреть точные правила, можно заглянуть в раздел 11.9.3 спецификации ES5 (http://www.ecma-international.org/ecma-262/5.1/) и вы будете удивлены тем, насколько этот механизм прямолинейный, по сравнению со всей этой негативной шумихой вокруг него.

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

  • Если одно из значений (т.е. сторона) в сравнении может быть значением true или false, избегайте == и используйте ===.

  • Если одно из значений в сравнении может быть одним из этих особых значений (0, "" или [] — пустой массив), избегайте == и используйте ===.

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

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

Форма не-равно != идет в паре с ==, а форма !== — в паре с ===. Все правила и утверждения, которые мы только что обсудили также применимы для этих сравнений на не равно.

Вам следует особо обратить внимение на правила сравнения == и ===, если вы сравниваете два непримитивных значения, таких как object (включая function и array). Так как эти значения на самом деле хранятся по ссылке, оба сравнения == и === просто проверят равны ли ссылки, но ничего не сделают касаемо самих значений.

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

var a = [1,2,3];
var b = [1,2,3];
var c = "1,2,3";

a == c;		// true
b == c;		// true
a == b;		// false

Неравенство

Операции <, >, <= и >=, использующиеся для неравенств, упоминаются в спецификации как "относительное сравнение." Обычно они используются со значениями, сравниваемыми порядками, как числа. Легко понять, что 3 < 4.

Но строковые значения в JavaScript тоже могут участвовать в неравенствах, используя типичные алфавитные правила ("bar" < "foo").

Как насчет приведения? Тут всё похоже на правила в сравнении == (хотя и не совсем идентично!). Примечательно, что нет операций "строгого неравенства", которые бы запрещали приведение таким же путем как и "строгое равенство" ===.

Пример:

var a = 41;
var b = "42";
var c = "43";

a < b;		// true
b < c;		// true

Что здесь происходит? В разделе 11.8.5 спецификации ES5 говорится, что если оба значения в сравнении <являются строками, как это было в случае с b < c, сравнение производится лексикографически (т.е. в алфавитном порядке, как в словаре). но если одно или оба значения не являются строкой, как в случае с a < b, то оба значения приводятся к числу и происходит типичное числовое сравнение.

Самое большое затруднение, в которое вы можете попасть со сравнениями между потенциально разными типами значений (помните, что нет формы "строгого неравенства"?) — это когда одно из значений не может быть превращено в корректное число, например:

var a = 42;
var b = "foo";

a < b;		// false
a > b;		// false
a == b;		// false

Подождите-ка, как это все эти три сравнения могут быть false? Так как значение b приводится к "некорректному числовому значению" NaN в сравнениях < и >, а спецификация говорит, что NaN не больше и не меньше чем любое другое значение.

Сравнение == не проходит по другой причине. a == b может быть некорректным если оно интерпретируется как 42 == NaN или "42" == "foo" — как мы объяснили ранее, первый вариант — наш случай.

Переменные

В JavaScript имена переменных (включая имена функций) должны быть корректными идентификаторами. Строгие и полные правила о корректных символах в идентификаторах — немного сложны, когда вы хотите использовать нестандартные символы, такие как Unicode-символы. Если вы предполагаете использовать только типичные буквенно-цифровые ASCII-символы, то правила просты.

Идентификатор должен начинаться с a-z, A-Z, $ или _. Дальше он может содержать любые из этих же символов плюс цифры 0-9.

В общем-то, те же правила, как и к идентификатору переменной, применяются и к имени свойства. Однако, определенные слова не могут использоваться как переменные, но могут как имена свойств. Эти слова называются "зарезервированными словами," и включают ключевые слова JS (for, in, if и т.д.), так же как и null, true и false.

Примечание: Более детальная информация о зарезервированных словах есть тут

Области видимости функций

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

Поднятие переменной (hoisting)

Где бы ни появлялось var внутри области видимости, это объявление принадлежит всей области видимости и доступно везде в ней.

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

Пример:

var a = 2;

foo();					// работает, так как определение `foo()`
						// "всплыло"

function foo() {
	a = 3;

	console.log( a );	// 3

	var a;				// определение "всплыло"
						// наверх `foo()`
}

console.log( a );	// 2

Предупреждение: Не общепринято и не так уж здраво полагаться на поднятие переменной, чтобы использовать переменную раньше в ее области видимости, чем появится ее объявление var, такое может сбить с толку. Общепринято и приемлемо использовать всплытие объявлений функций, что мы и делали с вызовом foo(), появившемся до ее объявления.

Вложенные области видимости

Когда вы объявляете переменную, она доступна везде в ее области видимости, так же как и в более нижних/внутренних областях видимости. Например:

function foo() {
	var a = 1;

	function bar() {
		var b = 2;

		function baz() {
			var c = 3;

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

		baz();
		console.log( a, b );		// 1 2
	}

	bar();
	console.log( a );				// 1
}

foo();

Заметьте, что c не доступна внутри bar(), потому что она объявлена только внутри внутренней области видимости baz() и b не доступна в foo() по той же причине.

Если вы попытаетесь получить доступ к значению переменной в области видимости, где она уже недоступна, вы получите ReferenceError. Если вы попытаетесь установить значение переменной, которая еще не объявлена, у вас либо закончится тем, что переменная создастся в самой верхней глобальной области видимости (плохо!), либо получите ошибку в зависимости от "строгого режима" (см. "Строгий режим"). Давайте взглянем:

function foo() {
	a = 1;	// `a` формально не объявлена
}

foo();
a;			// 1 — упс, автоматическая глобальная переменная :(

Это очень плохая практика. Не делайте так! Всегда явно объявляйте свои переменные.

В дополнение к созданию объявлений переменных на уровне функций, ES6 позволяет вам объявлять переменные, принадлежащие отдельным блокам (пара { .. }), используя ключевое слово let. Кроме некоторых едва уловимых деталей, правила области видимости будут вести себя точно так же, как мы видели в функциях:

function foo() {
	var a = 1;

	if (a >= 1) {
		let b = 2;

		while (b < 5) {
			let c = b * 2;
			b++;

			console.log( a + c );
		}
	}
}

foo();
// 5 7 9

Из-за использования let вместо var, b будет принадлежать только оператору if и следовательно не всей области видимости функции foo(). Точно так же c принадлежит только циклу while. Блочная область видимости очень полезна для управления областями ваших переменных более точно, что может сделать ваш код более легким в обслуживании в долгосрочной перспективе.

Условные операторы

В дополнение к оператору if, который мы кратко представили в главе 1, JavaScript предоставляет несколько других механизмов условных операторов, на которые нам следует взглянуть.

Иногда вы ловите себя на том, что пишете серию операторов if..else..if примерно как тут:

if (a == 2) {
	// сделать что-то
}
else if (a == 10) {
	// сделать что-то еще
}
else if (a == 42) {
	// сделать еще одну вещь
}
else {
	// резервный вариант
}

Эта структура работает, но она немного слишком подробна, поскольку вам нужно указать проверку для a в каждом случае. Вот альтернативная возможность, оператор switch:

switch (a) {
	case 2:
    // сделать что-то
		break;
	case 10:
    // сделать что-то еще
		break;
	case 42:
    // сделать еще одну вещь
		break;
	default:
    // резервный вариант
}

Оператор break важен, если вы хотите, чтобы выполнились операторы только одного case. Если вы опустите break в case и этот case подойдет или выполнится, выполнение продолжится в следующем операторе caseнезависимо то того, подходит ли этот case. Этот так называемый "провал (fall through)" иногда полезен/желателен:

switch (a) {
	case 2:
	case 10:
		// какие-то крутые вещи
		break;
	case 42:
		// другие вещи
		break;
	default:
		// резерв
}

Здесь если a будет либо 2, либо 10, то выполнятся операторы "какие-то крутые вещи".

Еще одна форма условного оператора в JavaScript — это "условная операция", часто называемая "тернарная операция." Это примерно как более краткая форма отдельного оператора if..else, например:

var a = 42;

var b = (a > 41) ? "hello" : "world";

// эквивалентно этому:

// if (a > 41) {
//    b = "hello";
// }
// else {
//    b = "world";
// }

Если проверяемое выражение (здесь a > 41) вычисляется как true, результатом будет первая часть ("hello"), в противном случае результатом будет вторая часть ("world") и затем независимо от результата он будет присвоен переменной b.

Условная операция не обязательно должна использоваться в присваивании, но это самое распространенное ее использование.

Строгий режим (Strict Mode)

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

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

function foo() {
	"use strict";

	// этот код в строгом режиме

	function bar() {
		// этот код в строгом режиме
	}
}

// этот код в нестрогом режиме

Сравните с:

"use strict";

function foo() {
	// этот код в строгом режиме

	function bar() {
		// этот код в строгом режиме
	}
}

// этот код в строгом режиме

Всего одно ключевое отличие (улучшение!) строго режима — запрет автоматического неявного объявления глобальных переменных из-за пропускаvar:

function foo() {
	"use strict";	// включить строгий режим
	a = 1;			// `var` missing, ReferenceError
}

foo();

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

Строгий режим не только способствует большей безопасности вашего кода и не только делает ваш код более оптимизируемым, но и заодно показывает будущее направление языка. Вам будет легче привыкнуть к строгому режиму сейчас, чем продолжать откладывать его в сторону — потом код будет сложнее сконвертировать!

Примечание: Более детальная информация о строгом режиме есть тут

Функции как значения

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

function foo() {
	// ..
}

Хотя это может показаться очевидным из синтаксиса, foo — по сути просто переменная во внешней окружающей области видимости, у которой есть ссылка на объявляемую функцию. То есть, функция сама является значением, так же как 42 или [1,2,3].

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

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

Пример:

var foo = function() {
	// ..
};

var x = function bar(){
	// ..
};

Первое функциональное выражение, присваиваемое переменной foo, называется анонимным поскольку у него нет имени.

Второе функциональное выражение именованное (bar), несмотря на то, что является ссылкой, также присваивается переменной x. Выражения с именованными функциями как правило более предпочтительны, хотя выражения с анонимными функциями все еще чрезвычайно употребительны.

Более детальная информация есть в книге Область видимости и замыкания этой серии.

Выражения немедленно вызываемых функций (Immediately Invoked Function Expressions (IIFEs))

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

Есть еще один путь выполнить выражение с функцией, на который обычно ссылаются как на immediately invoked function expression (IIFE):

(function IIFE(){
	console.log( "Hello!" );
})();
// "Hello!"

Внешние ( .. ), которые окружают выражение функции (function IIFE(){ .. }), — это всего лишь нюанс грамматики JS, необходимый для предотвращения того, чтобы это выражение воспринималось как объявление обычной функции.

Последние () в конце выражения, строка })(); — это то, что и выполняет выражение с функцией, указанное сразу перед ним.

Может показаться странным, но это не так уж чужеродно, как кажется на первый взгляд. Посмотрите на сходства между foo и IIFE тут:

function foo() { .. }

// `foo` выражение со ссылкой на функцию,
// затем `()` выполняют ее
foo();

// Выражение с функцией `IIFE`,
// затем `()` выполняют ее
(function IIFE(){ .. })();

Как видите, содержимое (function IIFE(){ .. }) до ее вызова в () фактически такое же, как включение fooдо его вызова после (). В обоих случаях ссылка на функцию выполняется с помощью () сразу после них.

Так как IIFE — просто функция, а функции создают область видимости переменных, то использование IIFE таким образом обычно происходит, чтобы объявлять переменные, которые не будут влиять на код, окружающий IIFE снаружи:

var a = 42;

(function IIFE(){
	var a = 10;
	console.log( a );	// 10
})();

console.log( a );		// 42

Функции IIFE также могут возвращать значения:

var x = (function IIFE(){
	return 42;
})();

x;	// 42

Значение 42 возвращается из выполненной IIFE функции, а затем присваивается в x.

Замыкание

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

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

Проиллюстрируем:

function makeAdder(x) {
	// параметр `x` - внутренняя переменная

	// внутренняя функция `add()` использует `x`, поэтому
	// у нее есть "замыкание" на нее
	function add(y) {
		return y + x;
	};

	return add;
}

Ссылка на внутреннюю функцию add(..), которая возвращается с каждым вызовом внешней makeAdder(..), умеет запоминать какое значение x было передано в makeAdder(..). Теперь давайте используем makeAdder(..):

// `plusOne` получает ссылку на внутреннюю функцию `add(..)`
// с замыканием на параметре `x` 
// внешней `makeAdder(..)`
var plusOne = makeAdder( 1 );

// `plusTen` получает ссылку на внутреннюю функцию `add(..)`
// с замыканием на параметре `x`
// внешней `makeAdder(..)`
var plusTen = makeAdder( 10 );

plusOne( 3 );		// 4  <-- 1 + 3
plusOne( 41 );		// 42 <-- 1 + 41

plusTen( 13 );		// 23 <-- 10 + 13

Теперь подробней о том, как работает этот код:

  1. Когда мы вызываем makeAdder(1), мы получаем обратно ссылку на ее внутреннюю add(..), которая запоминает x как 1. Мы назвали эту ссылку на функцию plusOne(..).

  2. Когда мы вызываем makeAdder(10), мы получаем обратно ссылку на ее внутреннюю add(..), которая запоминает x как 10. Мы назвали эту ссылку на функцию plusTen(..).

  3. Когда мы вызываем plusOne(3), она прибавляет 3 (свою внутреннюю y) к 1 (которая запомнена в x) и мы получаем в качестве результата 4.

  4. Когда мы вызываем plusTen(13), она прибавляет 13 (свою внутреннюю y) к 10 (которая запомнена в x), и мы получаем в качестве результата 23.

Не волнуйтесь, если всё это кажется странным и сбивающим с толку поначалу — это нормально! Понадобится много практики, чтобы всё это полностью понять.

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

Модули

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

Представим:

function User(){
	var username, password;

	function doLogin(user,pw) {
		username = user;
		password = pw;

		// сделать остальную часть работы по логину
	}

	var publicAPI = {
		login: doLogin
	};

	return publicAPI;
}

// создать экземпляр модуля `User`
var fred = User();

fred.login( "fred", "12Battery34!" );

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

Предупреждение: Мы не вызываем тут new User() намеренно, несмотря на тот факт, что это будет более естественно для большинства читателей. User() — просто функция, а не класс, поэтому она вызывается обычным образом. Использование new было бы неуместным и еще и тратой попусту ресурсов.

При выполнении User() создается экземпляр модуля User и создается целая новая область видимости и также совершенно новая копия каждой из этих внутренних переменных/функций. Мы присваиваем этот экземпляр в fred. Если мы запустим User() снова, мы получим новый экземпляр, целиком отдельный от fred.

У внутренней функции doLogin() есть замыкание на username и password, что значит, что она сохранит свой доступ к ним даже после того, как функция User() завершит свое выполнение.

publicAPI — это объект с одним свойством/методом, login, который является ссылкой на внутреннюю функцию doLogin(). Когда мы возвращаем publicAPI из User(), он становится экземпляром, который мы назвали fred.

На данный момент внешняя функция User() закончила выполнение. Как правило, вы думаете, что внутренние переменные, такие как username и password, при этом исчезают. Но они никуда не деваются, потому что есть замыкание в функции login(), хранящее их.

Вот поэтому мы можем вызвать fred.login(..), что то же самое, что вызвать внутреннюю doLogin(..) и у нее все еще будет доступ ко внутренним переменным username и password.

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

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

Идентификатор this

Еще одна очень часто неверно понимаемая концепция в JavaScript — это идентификатор this. Здесь мы только кратко его рассмотрим.

При том что может часто казаться, что этот this связан с "объектно-ориентированным шаблонами," в JS this — это другой механизм.

Если у функции есть внутри ссылка this, эта ссылка this обычно указывает на объект. Но на какой объект она указывает зависит от того, как эта функция была вызвана.

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

Вот краткая иллюстрация:

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

var bar = "global";

var obj1 = {
	bar: "obj1",
	foo: foo
};

var obj2 = {
	bar: "obj2"
};

//--------

foo();				// "global"
obj1.foo();			// "obj1"
foo.call( obj2 );	// "obj2"
new foo();			// undefined

Есть четыре правила того, как устанавливается this и они показаны в этих четырех последних строках кода.

  1. foo() заканчивается установкой this в глобальный объект в нестрогом режиме. В строгом режиме, thisбудет undefined и вы получите ошибку при доступе к свойству bar, поэтому "global" — это значение для this.bar.

  2. obj1.foo() устанавливает this в объект obj1.

  3. foo.call(obj2) устанавливает this в объект obj2.

  4. new foo() устанавливает this в абсолютно новый пустой объект.

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

Прототипы

Механизм прототипов в JavaScript довольно сложен. Здесь мы только взглянем на него немного.

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

Связывание ссылки на внутренний прототип от объекта к его резервному варианту происходит в момент когда объект создается. Простейший путь проиллюстрировать это — с помощью вызова встроенной функции Object.create(..).

Пример:

var foo = {
	a: 42
};

// создаем `bar` и связываем его с `foo`
var bar = Object.create( foo );

bar.b = "hello world";

bar.b;		// "hello world"
bar.a;		// 42 <-- делегируется в `foo`

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

Свойство a в действительности не существует в объекте bar, но поскольку bar прототипно связан с foo, JavaScript автоматически прибегает к поиску a в объекте foo, где оно и находится.

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

Но более естественный путь применения прототипов — шаблон, называемый "делегирование поведения", когда вы намеренно проектируете свои связанные объекты так, чтобы они могли делегировать от одного к другому части необходимого поведения.

Старый и новый

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

Так что же вам делать со всеми этими новыми вещами? Нужно ли ждать годы или десятилетия, чтобы все старые браузеры канули в лету?

Именно так думают многие люди об этой ситуации, но это совсем не здравый подход к JS.

Есть две основные техники, которыми можно пользоваться, чтобы "привнести" более новые возможности JavaScript в старые браузеры: полифиллинг (polyfilling) и транспиляция (transpiling).

Полифиллинг (polyfilling)

Слово "polyfill" — изобретенный термин (Реми Шарпом) (https://remysharp.com/2010/10/08/what-is-a-polyfill), используется для указания на взятие определения новой возможности и генерации кода, эквивалентного этому поведению, но с возможностью запуска в более старых окружениях JS.

Например, ES6 определяет функцию, называемую Number.isNaN(..) для обеспечения точной безошибочной проверки на значения NaN, отмечая как устаревшую исходную функцию isNaN(..). Но очень легко заполифиллить эту функцию, чтобы вы могли пользоваться ею в вашем коде независимо от того, поддерживает браузер ES6 или нет.

Пример:

if (!Number.isNaN) {
	Number.isNaN = function isNaN(x) {
		return x !== x;
	};
}

Оператор if защищает против применения полифильного определения в браузерах с ES6, где функция уже есть. Если она еще не существует, мы определяем Number.isNaN(..).

Примечание: Проверка, которую мы тут выполняем, использует преимущество причудливости значения NaN, которое заключается в том, что оно является единственным значением во всем языке, которое не равно самому себе. Поэтому значение NaN — единственное, которое может сделать условие x !== x истинным.

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

Или даже лучше используйте уже проверенный набор полифиллов, которому вы можете доверять, вроде тех, что предоставляются ES5-Shim (https://github.com/es-shims/es5-shim) и ES6-Shim (https://github.com/es-shims/es6-shim).

Транспиляция (Transpiling)

Не существует возможности полифиллить новый синтаксис, который был добавлен в язык. Новый синтаксис вызовет ошибку в старом движке JS как нераспознанный/невалидный.

Поэтому лучшим выбором будет использовать утилиту, которая конвертирует ваш более новый код в эквивалент более старого. Этот процесс обычно называют "транспиляцией", как объединение терминов трансформация и компиляция (transforming + compiling).

По большому счету, ваш исходный код написан в новом синтаксисе, но то, что вы развертываете в браузере — это транспилированный код со старым синтаксисом. Вы обычно вставляете транспилятор в ваш процесс сборки, примерно так же как linter или minifier.

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

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

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

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

  • Использование нового синтаксиса как можно раньше позволяет ему быть протестированным более тесно в реальном мире, что обеспечивает более ранние отзывы в комитет JavaScript (TC39). Если проблемы обнаружены достаточно рано, их можно изменить/устранить до того, как эти ошибки дизайна языка станут постоянными.

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

function foo(a = 2) {
	console.log( a );
}

foo();		// 2
foo( 42 );	// 42

Просто, правда? Еще и полезно! Но это как раз новый синтаксис, который будет считаться невалидным в до-ES6 движках. Так что же транспилятор сделает с этим кодом, чтобы заставить его работать в более старых движках?

function foo() {
	var a = arguments[0] !== (void 0) ? arguments[0] : 2;
	console.log( a );
}

Как видите, он проверяет, что значение arguments[0]void 0 (т.е. undefined) и если да, то предоставляет значение по умолчанию 2, иначе он присваивает то, что было передано.

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

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

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

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

Есть довольно много отличных транспиляторов для выбора. Вот несколько из них на момент написания этого текста:

Не-JavaScript

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

Самый распространенный не-JavaScript JavaScript, с которым вы столкнетесь — это DOM API. Например:

var el = document.getElementById( "foo" );

Переменная document существует как глобальная переменная, когда ваш код выполняется в браузере. Она не обеспечивается ни движком JS, ни особенно не контролируется спецификацией JavaScript. Она принимает форму чего-то очень похожего на обычный JS объект, но не является им на самом деле. Это — специальный объект, часто называемый "хост-объектом."

Более того, метод getElementById(..) в document выглядит как обычная функция JS, но это всего лишь кое-как открытый интерфейс к встроенному методу, предоставляемому DOM из вашего браузера. В некоторых (нового поколения) браузерах этот слой может быть на JS, но традиционно DOM и его поведение реализовано на чем-то вроде C/C++.

Еще один пример с вводом/выводом (I/O).

Всеобщее любимое всплывающее окно alert(..) в пользовательском окне браузера. alert(..)предоставляется вашей программе на JS браузером, а не самим движком JS. Вызов, который вы делаете, отправляет сообщение во внутренности браузера и они обрабатывают отрисовку и отображение окна с сообщением.

То же происходит и с console.log(..) — ваш браузер предоставляет такие механизмы и подключает их к средствам разработчика.

Last updated