Сегодня 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.
ссылка на оригинал статьи https://habrahabr.ru/post/324640/
Добавить комментарий