Революция в 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. Говоря революционным языком, перейти на глобальный, земшарный уровень

Первый герб СССР 1924 год

Нас ничто не остановит от того, чтобы, например, поменять метод 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("Чхеидзе"); 

image

Здесь есть два новшества:
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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *