-
Notifications
You must be signed in to change notification settings - Fork 46
...введение
Скрипты, которые должны подгрузиться до отрисовки основных элементов страницы, добавляются
в тэг head. Остальные скрипты чаще всего помещают в конец тэга body, чтобы они не перегружали
основной поток и не мешали отрисовки других элементов.
Специальные атрибуты async и defer используются для того, чтобы внешний скрипт загружался асинхронно, не препятствуя показу оставшейся части страницы.
Разница между async и defer:
- defer сохраняет относительную последовательность скриптов, а async – нет.
- defer всегда ждёт, пока весь HTML-документ будет готов, а async – нет.
Переменная состоит из имени и выделенной области памяти, которая ему соответствует.
Имя переменной может содержать буквы, цифры, $, _.
Регистр имеет значение.
Константы называют следующим образом: ANY_NAME.
Есть 5 примитивных типов и объекты:
- number // 1, 2.17
- string // 'str', "str"
- boolean // true, false
- null
- undefined
- object // { ... }
Значение null не является «ссылкой на нулевой адрес/объект» или чем-то подобным.
Значение null специальное и имеет смысл «ничего» или «значение неизвестно».
Значение undefined означает «переменная не присвоена».
Оператор typeof возвращает тип аргумента. Его результатом являетя строка.
typeof undefined // "undefined"
typeof 0 // "number"
typeof true // "boolean"
typeof "foo" // "string"
typeof {} // "object"
typeof null // "object" (ошибка в языке)
typeof function(){} // "function"
Функции не являются отдельным базовым типом в JavaScript, а подвидом объектов. Но typeof
выделяет функции отдельно, потому что легко определить функцию.
Операнд (аргумент оператора) – то, к чему применяется оператор.
Унарным называется оператор, который применяется к одному операнду.
Бинарным называется оператор, который применяется к двум операндам.
Бинарный "+" приводит выражение к строке, если хотя бы один его операнд является строкой.
Унарный "+" приводит операнд к числу.
Ассоциативность определяет порядок, в котором обрабатываются операторы с одинаковым приоритетом.
Левая ассоциативность (слева направо) означает, что оно обрабатывается как (a OP b) OP c.
Правая ассоциативность (справа налево) означает, что они интерпретируются как a OP (b OP c).
Пример: операторы присваивания являются право-ассоциативными
Приоритет операторов:
- 20 - группировка (не определено)
- 19 - доступ к свойствам (.), доступ к свойствам ([]), new c аргументами, вызов функции
- 18 - new без аргументов
- 17 - постфиксные инкремент и декремент
- 16 - унарный плюс/минус, отрицание, await, typeof, delete (справа налево)
- 15 - возведение в степень (**, справа налево)
- 14 - умножение, деление, остаток (слева направо)
- 13 - сложение, вычитание (слева направо)
- 12 - побитовые сдвиги (слева направо)
- 11 - сравнения (слева направо)
- 10 - равенства и неравенства (слева направо)
- 7-9 - побитовые операторы (слева направо)
- 6 - логическое && (слева направо)
- 5 - логическое || (слева направо)
- 4 - тернарный (справа налево)
- 3 - присваивание (=, справа налево)
- 2 - yeld (справа налево)
- 1 - запятая (слева направо)
Все операторы без исключения возвращают значение.
Оператор запятая позволяет перечислять выражения, разделяя их запятой ','. Каждое из них
вычисляется и отбрасывается, за исключением последнего, которое возвращается.
У запятой приоритет ниже, чем у оператора присваивания, поэтому:
a = 5, 6 --> a = 5
b = (5, 6) --> b = 6
Операторы сравнения возвращают значение логического типа.
Сравнение строк производится побуквенно и называется лексикографическим.
Для сравнения строк используются численные коды символов.
При сравнении разных типов происходит приведение операндов к числу.
Для проверки равенства без преобразования типов используются **операторы строгого **
равенства === и !==.
При преобразовании в число null становится 0, а undefined становится NaN.
При проверке равенства значения null и undefined равны друг другу, но не равны чему-то ещё.
В остальных случаях сравнение с undefined всегда даст false.
parseInt("11000", 2) – переводит строку с двоичной записью числа в число
n.toString(2) – получает для числа n запись в двоичной системе в виде строки
Можно использовать битовые маски для того, чтобы хранить информацию о доступе пользователя к
ресурсам в виде последовательности нулей и единиц (101101 = 45), что экономит память.
Оператор if (...) вычисляет и преобразует выражение в скобках к логическому типу.
В логическом контексте число 0, пустая строка "", null и undefined,
а также NaN являются false, остальные значения – true.
Вопросительный знак – единственный оператор, у которого есть три аргумента, поэтому
его называют «тернарный оператор»
Логические операторы || (ИЛИ), && (И) и ! (НЕ) могут применяться к значениям любого типа
и возвращают также значения любого типа.
Для экономии ресурсов используется короткий цикл вычислений:
|| запинается на «правде», && запинается на «лжи».
То есть || возвращает первое правдивое или последнее значение, && возвращает первое ложное
или последнее значение.
Приоритет у && больше, чем у ||.
Оператор ! сперва приводит аргумент к логическому типу, а затем возвращает **противоположное **
значение.
По этой причине !! используют для преобразования значений к логическому типу.
...циклы, switch, функции
Модальная функция, модальное окно называются так, потому что не позволяют посетителю
взаимодействовать со страницей, пока он не ответит.
Директива 'use strict' нужна для того, чтобы интерпретатор работал в режиме масимального
соответствия современному стандарту (строгом режиме). Есть возможность указать её в начале функции.
Пример: можно просто пофиксив один баг, создать новый и не заметить этого. Тесты нужны, чтобы
этого избежать.
Автоматизированное тестирование – это когда тесты написаны отдельно от кода, и можно в любой
момент запустить их и проверить все важные случаи использования.
Behavior Driven Development (разработка через поведение) - это методология разработки ПО,
являющаяся ответвлением от методологии разработки через тестирование (TDD).
Пусть, нужно написать функцию возведения в степень.
Мы можем ещё до разработки представить, что функция будет делать и описать это по методике BDD.
Это описания называется спецификацией (спекой).
У спецификации есть три основных строительных блока:
-
describe(название, function() { ... })
Задаёт, что именно мы описываем, используется для группировки блоков it. -
it(название, function() { ... })
В названии блока it человеческим языком описывается, что должна делать функция, далее следует тест,
который проверяет это. -
assert.equal(<результат выполнения функции>, <ожидаемый ответ>)
Код внутри it, если реализация верна, должен выполняться без ошибок.
Пример:
describe("pow", function() {
it("возводит в n-ю степень", function() {
assert.equal(pow(2, 3), 8);
assert.equal(pow(3, 4), 81);
});
it("при возведении в отрицательную степень результат NaN", function() {
assert(isNaN(pow(2, -1)));
});
});
Примечание: Лучше делать только одну проверку в каждом it, потому что иначе после ошибки код не
будет проверен.
Примечание: Блоки describe могут быть вложенными, что может пригодиться, чтобы определить
функцию для вычисления ожидаемого значения. Вложенный describe объявляет новую подгруппу тестов.
assert(value) – проверяет что value является true в логическом контексте.
Алгоритм:
- Пишется спецификация, которая описывает самый базовый функционал.
- Делается начальная реализация.
- Для проверки соответствия спецификации мы задействуем фреймворк Mocha, который запускает все
тесты it и выводит ошибки, если они возникнут. При ошибках вносятся исправления. -
Спецификация расширяется, в неё добавляются новые возможности, которые могут быть ещё не
реализованы. - Снова делается реализация. Так образуется цикл.
Разработка ведётся итеративно: один проход за другим, пока спецификация и реализация не будут
завершены.
Mocha – эта библиотека содержит общие функции для тестирования, включая describe и it.
Chai – библиотека поддерживает разнообразные функции для проверок.
Sinon – библиотека для эмуляции и хитрой подмены функций «заглушками».
Все значения за исключением null и undefined содержат набор вспомогательных функций и значений,
доступных через оператор точка. Такие функции называют методами, а значения - свойствами.
Все числа (целые и дробные) имеют тип Number и хранятся в 64-битном формате double precision.
Infinity – особенное численное значение, которое ведет себя в точности как математическая
бесконечность ∞.
Её можно получить с помощью деления на ноль.
Для проверки на конечность числа можно использовать isFinite(x), где x приводится к числу.
Для проверки на число можно использовать:
const isNumeric = x => !isNaN(parseFloat(x)) && isFinite(x);
Значение NaN используется для обозначения математической ошибки.
Его можно получить при делении 0 / 0.
Это единственное значение, которое не равно ничему, включая себя.
Для проверки можно использовать isNaN(x), где x приводится к числу.
Для приведения к числу используется унарный +, при этом если приводится строка и она не
является в точности числом (исключая пробельные символы), то результатом будет NaN.
Функции parseInt и parseFloat преобразуют строку символ за символом, пока это возможно.
Они возвращают NaN, если первый символ не является числом.
Также можно указать систему счисления:
parseInt('FF', 16) --> 255
Дробные числа дают ошибку вычислений.
При необходимости ее можно отсечь округлением до нужного знака c помощью x.toFixed(<знак>).
Внутренний формат строк вне зависимости от кодировки страницы - Unicode.
Cтроки могут содержать специальные символы, которые нужно экранировать с помощью ****.
Содержимое строки в JavaScript нельзя изменять. Как только строка создана – она такая навсегда.
Строки сравниваются в лексикографическом порядке (посимвольно).
Символы сравниваются не по алфавиту, а по их числовому коду в Unicode.
Если на каком-то шаге символы не совпали, то поиск прекращается и выдаётся результат.
Если символы до момента окончания первого слова совпали со вторым, а второе слово не закончилось,
тогда оно больше первого.
Объект в JS - ассоциативный массив.
Ассоциативный массив - структура данных, в которой можно хранить любые данные в формате ключ-значение.
... объекты, массивы
Глобальными называют переменные и функции, которые не находятся внутри конструкции function.
В JS все глобальные переменные и функции являются свойствами специального объекта, который
называется «глобальный объект» (global object). В браузере это window.
Интерпритация происходит в две фазы:
-
Инициализация, подготовка к запуску.
Во время инициализации скрипт ищет объявления функций вида Function Declaration, затем ищет объявления переменных var. Каждое объявление добавляется в window. -
Выполнение.
Построчное присваивание значений переменным (до этого они undefined).
// window = { f: function, a: undefined, g: undefined }
var a = 5;
// window = { f: function, a: 5, g: undefined }
function f(arg) { /*...*/ }
// window = { f: function, a: 5, g: undefined } без изменений, f обработана ранее
var g = function(arg) { /*...*/ };
// window = { f: function, a: 5, g: function }
Переменную var можно объявлять сколько угодно раз, но она будет обработана только один раз
при инициализации.
Циклы и условные операторы не влияют на видимость переменных var.
Все переменные внутри функции – это свойства специального внутреннего объекта LexicalEnvironment, который создаётся при её запуске. Его называют «лексическим окружением», объектом переменных».
При запуске функция создает объект LexicalEnvironment, записывает туда аргументы, функции и переменные.
Процесс инициализации выполняется в том же порядке, что и для глобального объекта, который является
частным случаем лексического окружения.
В отличие от window, объект LexicalEnvironment является внутренним, он скрыт от прямого доступа.
При вызове функции:
- интерпретатор создаёт пустой объект лексического окружения и заносит туда нужные переменные
- во время выполнения функции значения переменных присваиваются/изменяются
- в конце выполнения функции объект обычно выбрасывается и память очищается
Интерпретатор, при доступе к переменной, сначала пытается найти переменную в текущем LexicalEnvironment,
а затем, если её нет – ищет во внешнем объекте переменных. Самым верхним является window.
Такой порядок поиска возможен потому, что ссылка на внешний объект переменных хранится в специальном
внутреннем свойстве функции Scope, закрытым от прямого доступа.
При создании функция получает скрытое свойство Scope, которое ссылается на лексическое окружение,
в котором она была создана.
Это свойство никогда не меняется.
Значение переменной из внешней области берётся всегда текущее.
Оно может быть уже не то, что было на момент создания функции.
Внутри функций можно объявлять другие функции, а также возвращать их.
Так как функция в JS является объектом, ей можно присваивать свойства (статические переменные):
function f() {};
f.something = "smth";
Принципиальное отличие статической переменной от переменной из замыкания: к свойству функции есть
доступ у всех, кто имеет объект функции.
Замыкание – это функция вместе со всеми внешними переменными, которые ей доступны.
Обычно, говоря «замыкание функции», подразумевают именно внешние переменные функции.
Иногда говорят «переменная берётся из замыкания» (из внешнего объекта переменных).
При создании функции с использованием new Function, её свойство Scope ссылается не на текущий LexicalEnvironment, а на window.
Поэтому такие функции не могут использовать замыкания.
Это связано с особенностями минификации кода: минификатор переименовывает локальные переменные, которые
могли бы использоваться в объявленной функции, что вызвало бы ошибку: искомых переменных нет.
Устаревшая конструкция with позволяет использовать в качестве области видимости для переменных
произвольный объект.
with(obj) { /***/ }
Приём проектирования «Модуль» позволяет скрыть внутренние детали реализации.
Он наделяет скрипт собственной областью видимости.
Реализовать модуль можно, поместив всё содержимое скрипта в новую функцию, которая сразу же будет вызвана:
(function() { /.../ }());
Скобки вокруг нужны для того, чтобы показать интерпретатору, что это Function Expression, а не Function
Declaration, что поможет избежать ошибки: вызывать на месте можно только Function Expression.
Все функции модуля имеют доступ к другим переменным и внутренним функциям этого же модуля через замыкание.
Снаружи можно обращаться лишь к экспортированным из модуля переменным и функциям.
Главная концепция управления памятью в JS - принцип достижимости (reachability).
Значения, которые всегда хранятся в памяти (достижимые значения, корни):
-
Значения, ссылки на которые содержатся в стеке вызова (т.е. все локальные переменные и параметры
функций, которые в настоящий момент выполняются или находятся в ожидании окончания вложенного вызова). - Все глобальные переменные.
Любое другое значение сохраняется в памяти лишь до тех пор, пока доступно из корня по ссылке или
цепочке ссылок.
Для очистки памяти от недостижимых значений в браузерах используется автоматический Сборщик мусора
(Garbage collection), встроенный в интерпретатор, который наблюдает за объектами и время от времени
удаляет недостижимые.
Сборщик мусора идёт от корня по ссылкам и запоминает все найденные объекты.
По окончанию – он смотрит, какие объекты в нём отсутствуют и удаляет их.
Совершенно неважно, что из объекта выходят какие-то ссылки, они не влияют на достижимость этого объекта.
http://jayconrod.com/posts/55/a-tour-of-v8-garbage-collection
Объект переменных внешней функции существует в памяти до тех пор, пока существует хоть одна внутренняя
функция, ссылающаяся на него через свойство Scope.
При объявлении объекта или даже после можно указать его свойство-функцию (метод):
const obj = {
method: function() {},
}
obj.method = function() {};
Для доступа к текущему объекту из метода используется ключевое слово this.
Значением this является объект перед «точкой», в контексте которого вызван метод.
Через this метод можно передать куда-то ссылку на сам объект целиком:
const user = {
method: function() {
outerFunction(this);
}
};
function outerFunction(obj) { /.../ };
Любая функция может иметь в себе this.
Значение this называется контекстом вызова и определяется в момент вызова функции.
Если функция имеет this, но вызвана не в контексте какого-либо объекта, то в старом стандарте она
укажет на window, а в строгом режиме выдаст undefined.
Контекст this никак не привязан к функции.
Чтобы this передался, нужно вызвать функцию через точку или квадратные скобки, иначе
контекст теряется.
Любой объект в логическом контексте – true, даже если это пустой массив [] или объект {}.
При строковом преобразовании объекта используется его метод toString.
Он должен возвращать примитивное значение, причём не обязательно строку.
Для численного преобразования объекта используется метод valueOf, а если его нет то toString.
Исключение: Date.
Вызов состоит из двух независимых операций:
- точка . – получение свойства
- скобки () – вызов свойства
Функция не запоминает контекст, поэтому чтобы «донести его» до скобок, JavaScript делает следующее:
точка возвращает не функцию, а значение специального «ссылочного» типа Reference Type.
Этот тип представляет собой связку «base-name-strict»:
- base – объект,
- name – имя свойства,
- strict – вспомогательный флаг для передачи use strict.
Ссылочный тип существует исключительно для целей спецификации, мы его не видим, поскольку любой оператор
тут же от него избавляется:
Скобки () получают из base значения требуемых свойств и вызывают в контексте base.
Другие операторы получают из base значения требуемых свойств и используют, а остальное игнорируют.
Поэтому любая операция над результатом операции получения свойства (кроме вызова) приводит к потере
контекста.
Аналогично работает и получение свойства через квадратные скобки obj[method].
Чтобы "сконструировать" несколько однотипных объектов, используют функцию-конструктор и оператор new.
Конструктором становится любая функция, вызванная через new.
Действия функции, запущенной через new:
- создаётся новый пустой объект.
- ключевое слово this получает ссылку на этот объект.
- функция выполняется и модифицирует this, добавляет методы, свойства.
- возвращается this.
function Animal(name) {
// this = {};
// модификация this
this.name = name;
this.method = function() { /*...*/ };
// return this;
}
Конструкторы ничего не возвращают (то есть return не пишется).
Их задача – записать всё в this, который автоматически станет результатом.
Если есть явный вызов return:
- при вызове return с объектом, вернётся он вместо this.
- при вызове return с примитивом, оно будет отброшено и вернётся this.
В конструктор можно передавать параметры (как и в обычные функции).
В конструкторе можно объявить вспомогательные локальные переменные и вложенные функции,
которые будут видны только внутри (как и в обычных функциях).
...дескрипторы
Методы и свойства, которые не привязаны к конкретному экземпляру объекта, называют «статическими».
Они хранят общие данные для всех объектов, так как не используют this.
function Article() {
Article.count++;
}
Article.count = 0; // статическое свойство-переменная
Article.DEFAULT_FORMAT = "html"; // статическое свойство-константа
Article.method = function() { /*...*/ }
Фабричный статический метод – статический метод, который служит для создания новых объектов.
Неплохая альтернатива конструктору с параметрами.
Знаем: this – это текущий объект при вызове «через точку» и новый объект при конструировании
через new.
Явно указать this можно при помощи методов call и apply.
Синтаксис метода call:
func.call(context, arg1, arg2, ...)
Вызывается функция func, первый аргумент call становится её this, остальные передаются как есть.
function logField(fieldName) {
console.log(this[fieldName]);
}
logField.call({ name: "Max" }, 'name');
В качестве this для call может выступать примитив. Он и будет своим this.
При помощи call можно взять метод одного объекта и вызвать в контексте другого.
Это называется «одалживание метода» (method borrowing).
function printArgs() {
// arguments - псевдомассив (обычный объект, не имеющий методов массива)
const args = [].slice.call(arguments); // скопирует все элементы из this в новый массив
console.log( args.join(', ') ); // теперь args - полноценный массив из аргументов
}
Иногда можно взаимствовать методы и без call, но так делать не стоит, так как это может
перезаписать уже существующий метод:
const obj = {
0: "a",
1: "b",
2: "c",
length: 3,
join: function() { /*...*/ }
};
obj.join = [].join;
alert( obj.join('') ); // "abc"
Вызов функции c apply работает аналогично случаю с call, но принимает массив аргументов
вместо списка.
Иногда бывает важным привязать контекст к функции на постоянной основе.
Напишем такую функцию-обёртку:
function bind(func, context) {
return function() {
return func.apply(context, arguments);
};
}
Синтаксис встроенного метода bind:
const wrapper = func.bind(context[, arg1, arg2...]);
Важнейшее отличие между bind и call/apply:
- call/apply вызывают функцию с заданным контекстом и аргументами.
- bind не вызывает функцию,а возвращает «обёртку», которую мы можем вызвать позже.
Карринг или каррирование – термин функционального программирования, который означает
создание новой функции путём фиксирования аргументов существующей.
Привязывать можно не только контекст, но и аргументы.
Это можно сделать с помощью bind.
function mul(a, b) { return a * b };
const double = mul.bind(null, 2);
double(3); // 6
Говорят, что double является «частичной функцией» (partial function) от mul.
Декоратор – приём программирования, который позволяет взять существующую функцию и
изменить/расширить ее поведение.
Функция bind является декоратором.
Декоратор получает функцию и возвращает обертку, которая делает что-то своё «вокруг»
вызова основной функции.
Декораторы можно комбинировать.
Типы данных: Class, instanceof и утки
Иногда удобно создавать «полиморфные» функции, которые по-разному обрабатывают аргументы,
в зависимости от их типа.
Одной из таких функций является typeof, но она не различает массивы и объекты:
console.log( typeof {} ); // 'object'
console.log( typeof [] ); // 'object'
console.log( typeof new Date ); // 'object'
Во всех встроенных объектах есть специальное свойство Class, в котором хранится информация
о его типе или конструкторе.
Оно взято в квадратные скобки, так как это свойство – внутреннее. Явно получить его нельзя, но
можно прочитать его «в обход», воспользовавшись методом toString стандартного объекта Object.
Его внутренняя реализация выводит Class в небольшом обрамлении, как "[object значение]".
const toString = {}.toString;
console.log( toString.call([1, 2]) ); // [object Array]
console.log( toString.call(new Date) ); // [object Date]
console.log( toString.call({}) ); // [object Object]
console.log( toString.call(123) ); // [object Number]
console.log( toString.call("строка") ); // [object String]
Фигурные скобки считаются объектом, только если они находятся в контексте выражения или в ().
В противном случае они воспринимаются как пустой блок кода.
Метод Array.isArray() позволяет узнать, является ли объект массивом.
Оператор instanceof позволяет проверить, создан ли объект данной функцией, причём
работает для любых функций – как встроенных, так и новых.
function Animal() {}
const animal = new Animal();
console.log( animal instanceof Animal ); // true
Альтернативный подход к типу – «утиная типизация», которая основана на одной известной
пословице: «Если это выглядит как утка, плавает как утка и крякает как утка, то, вероятно,
это и есть утка (какая разница, что это на самом деле)».
Смысл утиной типизации – в проверке необходимых методов и свойств.
...json, settimeout, eval
Производится в блоках try-catch.
При ошибке в try скрипт не «падает», и мы получаем возможность обработать ошибку внутри catch.
Синтаксическая ошибка - ошибка, при которой грубо нарушена структура кода (не закрыта фигурная
скобка или где-то стоит лишняя запятая).
Такие ошибки называются синтаксическими, интерпретатор не может понять такой код.
Семантическая ошибка - ошибка в корректном коде, в процессе выполнения.
При таких ошибках скрипт в try не «падает», а переходит в catch.
Объект ошибки имеет три основных свойства:
- name - тип ошибки (несуществующая переменная: "ReferenceError").
- message - текстовое сообщение о деталях ошибки.
- stack - строку с информацией о последовательности вызовов, которая привела к ошибке
Оператор throw генерирует ошибку.
В качестве конструктора ошибок можно использовать встроенный конструктор new Error(message)
или любой другой (SyntaxError, ReferenceError, ...).
Ошибку, о которой catch не знает, он не должен обрабатывать.
В этом случае в catch генерируется новое исключение.
Этот приём называется пробросом исключений.
Приём оборачивания исключений:
function MyError(message, reason) {
this.message = message; // наш текст ошибки
this.reason = reason; // исходный текст ошибки
this.name = 'ReadError';
this.stack = cause.stack;
}
/*...*/
throw new MyError("My reason", e);
Блок finally содержит код, который выполнится вне зависимости от того, что произошло в try-catch.
В браузере существует специальное свойство window.onerror, если в него записать функцию,
то она выполнится и получит в аргументах сообщение ошибки, текущий URL и номер строки,
откуда «выпала» ошибка.
Очень долго в программировании применялся процедурный подход.
Программы состояли из функций, вызывающих друг друга.
Гораздо позже появилось объектно-ориентированное программирование (ООП), которое
позволяет группировать функции и данные в единой сущности – «объекте».
Используя ООП мы описываем происходящее на уровне объектов, которые создаются,
меняют свои свойства, взаимодействуют друг с другом и со своим окружением.
Класс - в ООП называют шаблон/программный код, предназначенный для создания
объектов и методов.
Один из важнейших принципов ООП – отделение внутреннего интерфейса от внешнего.
Внутренний интерфейс – это свойства и методы, доступные только из других методов
объекта, их также называют «приватными».
Внешний интерфейс – это свойства и методы, доступные снаружи объекта, их
называют «публичными».
Локальные переменные (включая параметры конструктора) можно считать приватными свойствами.
Свойства, записанные в this, можно считать публичными.
Внутренние методы можно объявлять как вложенные функции.
При этом нужно учитывать, что может произойти потеря контекста.
Этого можно избежать двумя способами:
function CoffeeMachine(power) {
this.time = 0;
// потеря контекста
let getTime = function() { return this.time / 60 };
// привязывание контекста
getTime = function() { return this.time / 60 }.bind(this);
// сохранение this в замыкании
let self = this;
getTime = function() { return self.time / 60 };
this.showTime = function() { console.log(getTime()) };
}
В терминологии ООП отделение и защита внутреннего интерфейса называется инкапсуляция.
Для большего контроля над присвоением и чтением значения вместо свойства делают
«функцию-геттер» и «функцию-сеттер», геттер возвращает значение, сеттер – устанавливает.
Cамо свойство делают приватным.
Если свойство предназначено только для чтения, то может быть только геттер,
только для записи – только сеттер.
Наследование – это создание новых «классов» на основе существующих.
Рассмотрим алгоритм функционального наследования.
Объявляется конструктор родителя Machine. В нём могут быть приватные (private),
публичные (public) и защищённые (protected) свойства:
function Machine(params) {
// локальные переменные и функции доступны только внутри Machine
var privateProperty;
// публичные доступны снаружи
this.publicProperty = ...;
// защищённые доступны внутри Machine и для потомков
// мы договариваемся не трогать их снаружи
this._protectedProperty = ...;
}
Для наследования конструктор потомка вызывает родителя в своём контексте через apply.
После этого может добавить свои переменные и методы:
function CoffeeMachine(params) {
// универсальный вызов с передачей любых аргументов
Machine.apply(this, arguments);
this.coffeePublicProperty = ...;
}
Свойства, полученные от родителя, можно перезаписать своими или расширить.
Для расширения метод предварительно копируется в переменную, чтобы не потерять.
function CoffeeMachine(params) {
Machine.apply(this, arguments);
var parentProtected = this._protectedProperty;
this._protectedProperty = function(args) {
parentProtected.apply(this, args);
// ...
};
}
Если в методе родительского класса используется this, то можно потерять контекст.
Чтобы этого избежать, нужно использовать либо .bind(this) в дочернем классе, либо
сохранить контекст в замыкании с помощью self в родительском классе.
Объекты в JavaScript можно организовать в цепочки так, чтобы свойство, не найденное
в одном объекте, автоматически искалось бы в другом.
Связующим звеном выступает специальное свойство proto.
const animal = { eats: true };
const rabbit = { jumps: true };
rabbit.__proto__ = animal;
console.log( rabbit.jumps ); // true
console.log( rabbit.eats ); // true
Объект, на который указывает ссылка proto, называется «прототипом».
В данном случае animal является прототипом для rabbit.
Говорят, что объект rabbit «прототипно наследует» от animal.
Можно сказать, что прототип – это «резервное хранилище свойств и методов» объекта,
автоматически используемое при поиске.
Прототип задействуется только при чтении свойства. Операции присваивания (=)
и удаления (delete ...) совершаются всегда над самим объектом obj.
Обычный цикл for..in не делает различия между свойствами объекта и его прототипа.
Для проверки можно использовать obj.hasOwnProperty(prop).
Чтобы создать коллекцию, можно использовать Object.create(null).
Такой объект не будет иметь прототипа, а соответственно и лишних свойств.
Указать прототип для функций-конструкторов можно с помощью свойства proto.
var animal = { eats: true };
function Rabbit(name) {
this.name = name;
this.__proto__ = animal;
}
const rabbit = new Rabbit("Кроль");
alert( rabbit.eats ); // true
Но можно сделать то же самое с помощью свойства prototype.
При создании объекта через new, в его прототип proto записывается ссылка из prototype
функции-конструктора.
Свойство с именем prototype можно указать на любом объекте, но особый смысл оно имеет лишь
для функции-конструкторов.
Без вызова оператора new, оно вообще ничего не делает, его единственное назначение –
указывать proto для новых объектов.
При работе new, свойство prototype используется лишь в том случае, если это объект.
У каждой функции по умолчанию уже есть свойство prototype.
JavaScript никак не использует свойство constructor. Оно создаётся автоматически, а что с ним
происходит дальше уже наша забота. В стандарте прописано только его создание.
Есть два способа его не потерять:
// явно указать
Rabbit.prototype = {
jumps: true,
constructor: Rabbit
};
// перезаписать только свойство, а не весь прототип
Rabbit.prototype.jumps = true
Откуда новый объект obj получает такой proto ?
- Запись obj = {} - краткая форма от obj = new Object, где Object – встроенная
функция-конструктор для объектов. - При выполнении new Object, создаваемому объекту ставится proto по prototype
конструктора, который в данном случае равен встроенному Object.prototype. - В дальнейшем необходимые методы будет взяты из Object.prototype.
Аналогично Array.prototype и Function.prototype.
Объект Object.prototype – вершина иерархии, единственный, у которого proto равно null.
Все объекты наследуют от Object, а если более точно, то от Object.prototype.
«Псевдоклассом» («классом»), называют функцию-конструктор вместе с её prototype.
Такой способ объявления классов называют «прототипным стилем ООП».
Примитивы не являются объектами, но методы берут из соответствующих прототипов:
Number.prototype, Boolean.prototype, String.prototype.
Обращение к свойству примитива:
- создаётся объект соответствующего типа с помощью new.
- производится операция со свойством или вызов метода с поиском в прототипе.
- объект уничтожается.
Конструкторы String/Number/Boolean – только для внутреннего использования!
function Animal(name) {
this.speed = 0;
// объявление метода в функцональном стиле
this.run = function(speed) {
this.speed += speed;
console.log(this.speed);
};
}
// методы в прототипе
Animal.prototype.run = function(speed) {
this.speed += speed;
console.log(this.speed);
};
Достоинства:
- Функциональный стиль записывает в каждый объект и свойства и методы, а прототипный –
только свойства. Поэтому прототипный стиль – быстрее и экономнее по памяти. Недостатки: - При создании методов через прототип, мы теряем возможность использовать локальные
переменные (в том числе и параметры, поэтому всё должно быть в this) как приватные
свойства (нет общей области видимости с конструктором).
Ранее с помощью свойства proto наследовались от объектов, теперь попробуем унаследовать
от класса.
// I способ
Rabbit.prototype.__proto__ = Animal.prototype;
// II способ
Rabbit.prototype = Object.create(Animal.prototype);
// чтобы не потерять конструктор
Rabbit.prototype.constructor = Rabbit;
Чтобы переопределить метод родителя, нужно взять его из прототипа и вызвать в текущем
контексте с текущими аргументами:
Rabbit.prototype.run = function() {
Animal.prototype.run.apply(this, arguments);
this.jump();
}