https://habrahabr.ru/post/324640/- Ненормальное программирование
- JavaScript
Сегодня 22 апреля юбилейного 2017 года, день рождения человека, без которого не состоялись бы события столетней давности. А значит, есть повод поговорить о революционной истории. Но причём тут Хабр? — Оказалось, всё в этом мире может иметь самую неожиданную связь.
Как мы знаем, JavaScript может рассматриваться как объектно-ориентированный язык в том смысле, что «всё сводится к объектам» (на самом деле, прототипу объекта). С другой стороны, в широком философском вопросе, мы тоже то и дело имеем дело с объектами и прототипами. Например, при рассмотрении объекта Революции и его прототипа, описанного в «Капитале». Так давайте просто поговорим о тех событиях современным языком!
Со всей революционной прямотой, сразу козыри на стол!
Данная статья является по сути кратким пояснением к механизму наследования в JavaScript, то есть одной из сторон ООП. Передовые пролетарии-разработчики не найдут в ней абсолютно ничего нового. Однако надеюсь, материал может послужить запоминающейся, образной памяткой для широкого круга интересующихся разработкой на JS. Кроме того, возможно, кто-то подтянет свои знания по отечественной истории
Создадим два объекта:
var stalin = {
gulag: true,
mustache: true,
hero: 1
}
var lenin = {
baldHead: true,
armand: false,
criticalImperialism: true
}
Укажем, что один объект является наследником другого через свойство
__proto__ (такая форма записи доступна во всех браузерах, кроме IE10-, и
включена в ES2015). Один наследник, а другой — прототип. Проверяем свойства объекта-наследника, там появилось __proto__:
stalin.__proto__ = lenin;
console.log(stalin);
Если свойство не обнаруживается непосредственно в объекте, оно ищется в родительском объекте (прототипе). Попробуем:
console.log(stalin.baldHead); // true
Да, свойство доступно, однако его значение нас не устраивает. Перезаписываем его, при этом свойство родительского объекта не меняется:
stalin.baldHead = false;
console.log(lenin.baldHead); // true - свойство в прототипе не поменялось
Кстати, а что является прототипом прототипа?
В JS объект, кроме одного случая (об этом ниже) наследует от Object.__proto__ (посмотрите в консоли). В том числе, стандартные методы, доступные по умолчанию: такие, например, как Object.toString(), Object.valueOf() и так далее.
А как нам перечислить свойства непосредственно объекта, без свойств его родителя, чтобы не выполнять лишние операции? – Для этого есть hasOwnProperty:
for (var key in stalin) {
if (stalin.hasOwnProperty(key)) console.log(key + ": " + stalin[key])
}
Кстати, если объект уже имеет своё свойство, то после присвоения прототипа оно не будет затёрто значением из прототипа, а останется как было:
var dzerjinskiy = {
mustache: true,
baldHead: false
}
dzerjinskiy.__proto__ = lenin;
console.log(dzerjinskiy.baldHead); // false - при присвоении прототипа осталось тем же
Наконец, может понадобиться простой объект-пустышка без свойств, который нужен только для записи значений. Тогда нам не придётся при перечислении его свойств проверять hasOwnProperty:
var zyuganov = Object.create(null);
zyuganov.experience = 25;
console.log(zyuganov.toString); // undefined
При проверке выясняется, что у пустого объекта нет даже стандартных методов, таких, как toString(). Кстати, выше был использован метод Object.create(prototype[, {}]) — метод, позволяющий создать объект с обязательным указанием прототипа (в т.ч. null) и свойствами (не обязательно).
F.prototype и new
Можно создать конструктор, чтобы проще было создавать экземпляры, родителем которых является единственный объект:
var marks = {
marxism: true,
engels: "friend",
beard: 80
}
function Marksist(name) {
this._name = name;
this.__proto__ = marks;
}
var kamenev = new Marksist("kamenev");
console.log(kamenev);
Видим, что тов. Каменев тоже имеет бороду
Однако что насчёт Революции? Можно добавить в прототип новое значение и метод, тогда и потомок может использовать этот метод:
marks.revolution = "future";
marks.deal = function() {return this.revolution}; // this ссылается на объект marks
Новое значение появилось в потомке:
console.log(kamenev.revolution); // "future"
Мы добавляем свойство или метод в прототип, и оно появляется у потомков без необходимости их переопределения. Сила прототипного наследования!
Естественно, значение в потомке можно модифицировать, остальных потомков прототипа это не коснётся:
kamenev.revolution = "now";
console.log(kamenev.deal()); // "now"
Как видно, у объекта изначально не было метода, однако после добавления метода в прототип, мы можем вызывать его, мало того, с модифицированными в потомке значениями.
Для поддержки во всех браузерах, в т.ч. старых, есть другой способ:
function Marksist(name) {
this._name = name;
}
Marksist.prototype = marks;
var kamenev = new Marksist("Каменев");
console.log(kamenev); // выведет объект с одним своим свойством и объектом marks в __proto__
Prototype имеет смысл только в конструкторах (пишутся в JS с большой буквы), он по сути выполняет лишь одно действие, а именно: указывает, куда ссылаться свойству __proto__ при инициализации функции-конструктора.
Если мы уже после создания первого объекта хотим создать второй, указав ему другой прототип, прототип первого объекта не изменится:
Marksist.prototype = {};
var chicherin = new Marksist("Чичерин");
console.log(chicherin.marxism); // undefined, в свойстве __proto__ будет стандартный прототип Object
console.log(kamenev.marxism); // по-прежнему true, в свойстве __proto__ будет объект marks
Видно, что у нового объекта с пустым прототипом нет унаследованных свойств, как у первого объекта. Но всё можно переиграть на лету:
Marksist.prototype = marks;
var zinovev = new Marksist("Зиновьев");
console.log(zinovev.marksizm); // true
console.log(zinovev.deal()); // future
Следует отметить, что изменение прототипов считается весьма затратной операцией, поэтому играть с прототипами “на лету” не рекомендуется!
В прототипе мы также можем задавать методы, которые будут использовать все потомки:
var marks = {
marxism: true,
engels: "friend",
beard: 80,
shout: function(){
alert("Я есть товарищ " + this._name + "!")
}
}
function Marksist(name) {
this._name = name;
}
Marksist.prototype = marks;
var dzerjinskiy = new Marksist("Дзержинский");
dzerjinskiy.shout(); // Я есть товарищ Дзержинский!
Здесь this – это объект, у которого вызывается функция из прототипа, в данном случае Дзержинский.
Правильно Феликс Эдмундович нас предупреждает: в JavaScript всегда надо быть бдительным насчёт того, куда в данный момент указывает ключевое слово this
Можно проверить, а является ли объект наследником конструктора, с помощью оператора instanceof:
var zemlyachka = function(tov) {
var res = false;
if (tov instanceof Marksist) res = true;
return res;
}
console.log(zemlyachka(zinovev)); // true
Приводим оппортуниста, который будет иметь на практике все те же свойства и методы, что и обычный марксист:
var opportunist = {
name: "Преображенский",
marxism: true,
engels: "friend",
beard: 80,
shout: function(){
alert("Я есть товарищ " + this.name + "!")
}
};
opportunist.shout();
Можем даже сообщить ему то же единственное собственное свойство, а остальные определить в его прототипе, то есть сохранить ровно ту же структуру, что и у предыдущих объектов:
var plehanov = {
marxism: true,
engels: "friend",
beard: 80,
shout: function(){
alert("Я есть товарищ " + this._name + "!")
}
}
function Socialist (name){
this._name = name;
}
Socialist.prototype = plehanov;
var opportunist = new Socialist("Попутчик");
console.log(opportunist);
// структура объекта и прототипа идентичная таковой объекта var zinovev = new Marksist, имена свойств те же
Однако на проверке он завалится:
console.log(zemlyachka(opportunist)); // false
Розалия Самойловна видит объект насквозь. Есть и другой подход к проверке объектов — Duck TypingЕсли это выглядит как утка, плавает как утка и крякает как утка, то это, возможно, и есть утка.
Если субъект объект ведёт себя так, как нам нужно, то мы считаем его коммунистом тем, кем нужно считать, несмотря на его происхождение
Однако и проверенные коммунисты иногда могут ошибаться. Оператор instanceof сравнивает только прототипы объекта и конструктора, поэтому возможны коллизии наподобие этой:
var troczkiy = new Marksist("Троцкий");
Marksist.prototype = {};
console.log(troczkiy instanceof Marksist); // опа, 1940-й год подкрался незаметно!
Ну и конечно помним, что всё в JS является объектом (а если точнее, в цепочке прототипов все объекты, кроме специальных пустых, приходят к
Object.prototype), поэтому проверка оба раза выдаст
true:
var john_reed = [10];
console.log(john_reed instanceof Array); // true
console.log(john_reed instanceof Object); // true
Свойство constructor
У функций-конструкторов (да и вообще всех функций) есть свойство
prototype, в котором записан
constructor: он возвращает ссылку на функцию, создавшую прототип экземпляра. Его можно легко потерять в дальнейших преобразованиях, т.к. JS не обязан сохранять эту ссылку. Допустим, мы решили всё-таки выяснить политические корни Льва Давыдовича:
var marks = {
marxism: true,
engels: "friend",
beard: 80
}
function Marksist(name) {
this._name = name;
}
Marksist.prototype = marks;
var troczkiy = new Marksist("Троцкий");
var Congress = troczkiy.constructor;
var retrospective = new Congress("My life");
console.log(retrospective); // чёрти что, конструктор делает явно не то, чего мы от него ожидаем!
Очевидно, мы не смогли вызвать ту же функцию-конструктор, которая создала бы нам новый объект, идентичный первому (хотя
constructor, по идее, должен был бы указывать на неё!). Чтобы получить нужный результат, просто сохраним свойство
constructor в прототипе Marksist:
var marks = {
marxism: true,
engels: "friend",
beard: 80
}
function Marksist(name) {
this._name = name;
}
Marksist.prototype = marks;
Marksist.prototype.constructor = Marksist;
var troczkiy = new Marksist("Троцкий");
var Congress = troczkiy.constructor;
var retrospective = new Congress("My life");
console.log(retrospective); // вот теперь всё как нужно!
Таким образом, нам не обязательно знать, каким конструктором создавался экземпляр, от которого теперь мы создаём новый экземпляр. Эта информация записана в самом экземпляре.
Глядя на эти преобразования, может прийти мысль переопределять свойства и методы встроенных прототипов JavaScript. Говоря революционным языком, перейти на глобальный, земшарный уровень
Нас ничто не остановит от того, чтобы, например, поменять метод Object.prototype:
Object.prototype.toString = function(){
alert("К стене!")
}
Или даже не такой экстремистский пример:
Array.prototype.manifest= function(){
return "Призрак бродит по Европе - призрак коммунизма";
}
Такой стиль изменения классов (здесь точнее было бы сказать прототипов) называется monkey patching.
Опасности две. Во-первых, расширяя или изменяя встроенные прототипы, мы делаем доступными изменения для всех объектов, лежащих ниже по цепочке наследования свойств (для Object.prototype — это вообще все сущности JS). Потом, используя такой метод, мы рискуем назвать новое свойство старым именем, тем самым затерев его. Если в пределах одной цепочки прототипов мы можем помнить имена свойств, в составе большого проекта, где, к тому же, могут подключаться другие скрипты, содержимое прототипов базовых сущностей, как и глобальный объект, лучше не трогать, иначе последствия будут непредсказуемые.
И во-вторых, переопределение в процессе выполнения программы может приводить к неочевидным результатам:
var pss = {
name: "Полное собрание сочинений",
author: "Ленин",
length: 20
}
var Books = function(){};
Books.prototype = pss;
var firstEdition = new Books;
console.log(firstEdition.length); // 20
var extendLenin = function(year){
if (!year) var year = new Date().getFullYear();
if (year > 1925 && year < 1932) pss.length = 30;
if (year > 1941 && year < 1966) pss.length = 35;
if (year > 1966) pss.length = 55;
}
extendLenin();
var fourthEdition = new Books;
console.log(fourthEdition.length); // ??
Поведение свойств объектов, наследующих от прототипа (т.н. «экземпляров класса») может быть не таким, как мы предполагаем.
Функциональное наследование
В JavaScript, кроме прототипной парадигмы наследования, используется также функциональная. На примере одного из героев Революции посмотрим, как она реализуется.
Создадим конструктор, который:
а) Принимает параметры
б) Обладает публичными методами, которые предполагаются для использования извне конструктора и его производных
в) Обладает приватными методами, которые, как предполагаются, будут использоваться только внутри конструктора и его производных
Перед нами типичный коммунист:
var Kommunist = function(principles) {
if (!principles) principles = {};
this._start = 1902;
if (principles && principles.start) this._start = principles.start;
// публичный метод, предполагается к использованию извне
this.response = function() {
alert("Наше дело правое!")
}
// приватный метод, по соглашению доступен внутри конструктора и его потомков
this._experience = function() {
return (this._start);
}
this.principles = (Object.keys(principles).length > 0 ? principles : {fidelity: 100});
this._getPrinciples = function() {
return this.principles
}
}
Приватные методы принято писать, начиная с нижнего подчёркивания.
Итак, у нас есть конструктор с набором методов, готовый принимать и обрабатывать аргументы. Создаём экземпляр класса:
function Voroshilov(principles) {
Kommunist.apply(this, arguments);
// расширяем метод конструктора
var parentExperience = this._experience;
this._experience = function() {
return ("Стаж в ВКП(б) с " + parentExperience.apply(this, arguments));
}
// публичные методы, обращаемся к ним извне
// геттеры
this.getPrinciples = function() {
var p = this._getPrinciples();
var char = {
fidelity: p.fidelity,
persistence: p.persistence || "достаточная!"
}
console.log("Верность: " + char.fidelity + ", стойкость: " + char.persistence)
}
this.getExperience = function() {
console.log(this._experience());
alert("Опыт ого-го!");
}
// сеттер
this.setExperience = function() {
this._experience = function() {
return ("Стаж в ВКП(б) со Второго съезда");
}
}
}
var ke = {fidelity: 101, persistence: 100, start: 1903}
var voroshilov = new Voroshilov(ke);
Обратите внимание: конструктор вызывается относительно
this, чтобы записать в него все свои методы, и с массивом
arguments, в котором содержатся все аргументы, заданные при вызове (объект
ke).
Дальше мы можем наблюдать, как работают геттер и сеттер, а также другие публичные методы:
voroshilov.getExperience(); // получили значение
voroshilov.setExperience(); // заменили метод предустановленным в экземпляре класса
voroshilov.getExperience(); // получили новое значение
voroshilov.getPrinciples(); // получили результат выполнения публичного метода с заданными параметрами
Для разнообразия можно вызвать конструктор без параметров.
Классовая сущность
Наконец, с выходом ES6 (ES2015) у нас появилась возможность использовать инструкцию
class непосредственно в JavaScript. По сути, в устройстве прототипного наследования ничего не изменилось, однако теперь JS поддерживает синтаксический сахар, который будет более привычен многим программистам, пришедшим из других языков.
class Marksist {
constructor(name) {
this._name = name
}
enemy() {
return "capitalism"
}
static revolution(){
return "Революция нужна"
}
}
У классов JS есть три вида методов:
— constructor (выполняется при инициализации экземпляра класса);
— static (статичные методы, доступные при вызове класса, но недоступные в экземплярах);
— обычные методы.
Теперь запомним в константе (ES6 допускает и такой тип переменных) очень важную дату, а далее определим меньшевика, который является наследником марксиста:
const cleavage = 1903;
class Menshevik extends Marksist {
constructor(name) {
super();
this._name = name;
}
static revolution() {
var r = super.revolution();
return r + ", но потом";
}
["che" + cleavage]() {
alert("Пароль верный!")
}
hurt() {
alert("Ленин был прав")
}
}
let chkheidze = new Menshevik("Чхеидзе");
Здесь есть два новшества:
—
super() в первом случае инициализирует конструктор базового класса, во втором вызывает метод, к которому мы в потомке добавляем новое поведение;
— вычисляемые имена методов (
[«che» + cleavage]), теперь нам не обязательно сразу знать имя метода.
Статический метод доступен при вызове класса, но не при вызове экземпляра:
console.log(Menshevik.revolution()); // работает
console.log(chkheidze.revolution()); // is not a function, в экземпляре её нет
Результат выполнения следующего кода уже понятен:
chkheidze.hurt(); // вызов метода класса
console.log(chkheidze.enemy()); // вызов метода базового класса
chkheidze.che1903(); // вызов метода с вычисляемым именем
Выше были показаны самые основные особенности наследования через классы (
class) в JavaScript. Сознательный пролетарий при должной революционной настойчивости найдёт в сети немало статей, более полно освещающий вопрос нововведений в ES6.