Skip to content

Latest commit

 

History

History
965 lines (625 loc) · 76.6 KB

File metadata and controls

965 lines (625 loc) · 76.6 KB

Вы не знаете JS: Начните и Совершенствуйтесь

Глава 2: Введение в JavaScript

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

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

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

Здесь начинается ваше путешествие к серьезному изучению JavaScript.

Примечание: Как я упоминал в главе 1, вам определенно стоит попробовать весь этот код самим, пока вы читаете и работаете над этой главой. Имейте в виду, что здесь есть код, который предполагает возможности, представленные в последней на момент написания версии JavaScript (обычно упоминаемой как "ES6" из-за 6-ой версии ECMAScript — официального названия JS спецификации). Если вы вдруг используете более старый, пред-ES6 браузер, код может не заработать. Следует использовать последние версии современных браузеров (такие как Chrome, Firefox или IE).

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

Как мы условились в главе 1, в 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
};

obj.a;		// "hello world"
obj.b;		// 42
obj.c;		// true

obj["a"];	// "hello world"
obj["b"];	// 42
obj["c"];	// true

Полезно представить значение этого 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 объектах есть в книге this и прототипы объектов этой серии, особенно в главе 3.

Есть пара других типов, с которыми вам предстоит взаимодействовать в 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).

Примечание: Более детальная информация о значениях в JS и их типах есть в первых двух главах книги Типы и синтаксис этой серии.

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

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

Например:

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", подобно объекту, ссылаясь на свойство или метод (к примеру, a.toUpperCase() в предыдущем кусочке кода), JS автоматически "упаковывает" значение в его обертку-двойника (скрытую внутри).

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

Примечание: Более детальная информация о родных типах в JS и "упаковке" есть в главе 3 книги Типы и синтаксис этой серии. Для лучшего понимания прототипов объектов см. главу 5 книги this и прототипы объектов этой серии.

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

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

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

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

Приведение в 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 — число!

Как бы истинный и ложный

В главе 1, мы кратко рассмотрели "как бы истинную" и "как бы ложную" природу значений: когда не-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

Примечание: Детальную информацию о правилах сравнения равенства == можно посмотреть в спецификации ES5 (раздел 11.9.3) а также свериться с главой 4 книги Типы и синтаксис этой серии; см. главу 2 для детальной информации о значениях в сравнении с ссылками.

Неравенство

Операции <, >, <= и >=, использующиеся для неравенств, упоминаются в спецификации как "относительное сравнение." Обычно они используются со значениями, сравниваемыми порядками, как числа. Легко понять, что 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" — как мы объяснили ранее, первый вариант — наш случай.

Примечание: Более детальная информация о правилах сравнения в неравенствах есть в разделе 11.8.5 спецификации ES5, также сверьтесь с главой 4 книги Типы и синтаксис этой серии.

Переменные

В 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. Блочная область видимости очень полезна для управления областями ваших переменных более точно, что может сделать ваш код более легким в обслуживании в долгосрочной перспективе.

Примечание: Более детальная информация об области видимости есть в книге Область видимости и замыкания этой серии. См. книгу ES6 и за его пределами этой серии, чтобы узнать больше о блочной области видимости let.

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

В дополнение к оператору 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.

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

Примечание: Более детальная информация об условиях проверки и других шаблонах для switch и ? : есть в книге Типы и синтаксис этой серии.

Строгий режим (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();

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

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

Примечание: Более детальная информация о строгом режиме есть в главе 5 книги Типы и синтаксис этой серии.

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

До сих пор мы обсуждали функции как основной механизм области видимости в 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 и прототипы объектов этой серии, поэтому здесь мы только кратко его рассмотрим.

При том что может часто казаться, что ключевое слово 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.

Примечание: Более детальная информация о ключевом слове this есть в главах 1 и 2 книги this и прототипы объектов этой серии.

Прототипы

Механизм прототипов в JavaScript довольно сложен. Здесь мы только немного взглянем на него. Вам потребуется потратить много времени, изучая главы 4-6 книги this и прототипы объектов этой серии, чтобы получить детальную информацию.

Если вы ссылаетесь на свойство объекта, и этого свойства не существует, то 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, где оно и находится.

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

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

Примечание: Более детальная информация о прототипах и делегировании поведения есть в главах 4-6 книги this & прототипы объектов этой серии.

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

Некоторые из возможностей 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 — единственное значение, которое не может быть явно задано значением по умолчанию для параметра, но транспилированный код показывает это гораздо яснее.

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

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

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

Не-JavaScript

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

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

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

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

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

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

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

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

Эта книга, да и вся эта серия, фокусируется на языке JavaScript. Поэтому вы не увидите какого-либо подробного раскрытия деталей об этих не-JavaScript механизмах JavaScript. Как бы то ни было, вам не нужно забывать о них, поскольку они будут в каждой написанной вами JS программе!

Обзор

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

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

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