Существует две модели для реализации классов и экземпляров в JavaScript: способ прототипирования и способ замыкания. Оба имеют свои преимущества и недостатки, и существует множество расширенных вариантов. Многие программисты и библиотеки используют разные подходы и функции утилит для обработки классов, чтобы наметить некоторые уродливые части языка.
В результате в смешанной компании вы получите смесь метаклассов, которые ведут себя немного по-разному. Что еще хуже, большинство учебных материалов по JavaScript ужасны и служат неким промежуточным компромиссом, охватывающим все основы, оставляя вас в замешательстве. (Возможно, автор также запутался. Объектная модель JavaScript сильно отличается от большинства языков программирования, и во многих местах она плохо спроектирована.)
Давайте начнем с прототипа . Это самый естественный JavaScript-код, который вы можете получить: минимальный объем служебного кода и instanceof будет работать с экземплярами такого типа объектов.
function Shape(x, y) {
this.x= x;
this.y= y;
}
Мы можем добавить методы к созданному экземпляру new Shape
, написав их в prototype
поиске этой функции конструктора:
Shape.prototype.toString= function() {
return 'Shape at '+this.x+', '+this.y;
};
Теперь, чтобы создать его подкласс, настолько, насколько вы можете назвать то, что JavaScript делает подклассами. Мы делаем это, полностью заменяя это странное магическое prototype
свойство:
function Circle(x, y, r) {
Shape.call(this, x, y); // invoke the base class's constructor function to take co-ords
this.r= r;
}
Circle.prototype= new Shape();
перед добавлением методов к нему:
Circle.prototype.toString= function() {
return 'Circular '+Shape.prototype.toString.call(this)+' with radius '+this.r;
}
Этот пример будет работать, и вы увидите код, подобный этому, во многих руководствах. Но, new Shape()
черт возьми , это ужасно: мы создаем экземпляр базового класса, хотя фактическая форма не должна быть создана. Случается работать в этом простом случае , поскольку JavaScript настолько неаккуратен: она позволяет нулевые аргументы, которые передаются в, в каком случае x
и y
стать undefined
и назначены на прототип this.x
и this.y
. Если бы функция конструктора делала что-то более сложное, она бы не получалась.
Поэтому нам нужно найти способ создать объект-прототип, который содержит методы и другие члены, которые нам нужны на уровне класса, без вызова функции конструктора базового класса. Для этого нам нужно начать писать вспомогательный код. Это самый простой из известных мне подходов:
function subclassOf(base) {
_subclassOf.prototype= base.prototype;
return new _subclassOf();
}
function _subclassOf() {};
Это передает членов базового класса в его прототипе новой функции конструктора, которая ничего не делает, а затем использует этот конструктор. Теперь мы можем написать просто:
function Circle(x, y, r) {
Shape.call(this, x, y);
this.r= r;
}
Circle.prototype= subclassOf(Shape);
вместо new Shape()
неправильности. Теперь у нас есть приемлемый набор примитивов для построенных классов.
Есть несколько уточнений и расширений, которые мы можем рассмотреть в рамках этой модели. Например, вот синтаксически-сахарная версия:
Function.prototype.subclass= function(base) {
var c= Function.prototype.subclass.nonconstructor;
c.prototype= base.prototype;
this.prototype= new c();
};
Function.prototype.subclass.nonconstructor= function() {};
...
function Circle(x, y, r) {
Shape.call(this, x, y);
this.r= r;
}
Circle.subclass(Shape);
У любой версии есть недостаток, заключающийся в том, что функция конструктора не может быть унаследована, как во многих языках. Таким образом, даже если ваш подкласс ничего не добавляет к процессу конструирования, он должен не забывать вызывать конструктор базы с любыми аргументами, которые требуется базе. Это может быть немного автоматизировано с помощью apply
, но все же вы должны написать:
function Point() {
Shape.apply(this, arguments);
}
Point.subclass(Shape);
Таким образом, общее расширение состоит в том, чтобы разбить материал инициализации на его собственную функцию, а не на сам конструктор. Эта функция может потом наследовать от базы просто отлично:
function Shape() { this._init.apply(this, arguments); }
Shape.prototype._init= function(x, y) {
this.x= x;
this.y= y;
};
function Point() { this._init.apply(this, arguments); }
Point.subclass(Shape);
// no need to write new initialiser for Point!
Теперь у нас есть один и тот же шаблон функций конструктора для каждого класса. Может быть, мы можем переместить это в свою собственную вспомогательную функцию, чтобы нам не приходилось постоянно печатать ее, например, вместо того Function.prototype.subclass
, чтобы переворачивать ее и позволить функции базового класса выплевывать подклассы:
Function.prototype.makeSubclass= function() {
function Class() {
if ('_init' in this)
this._init.apply(this, arguments);
}
Function.prototype.makeSubclass.nonconstructor.prototype= this.prototype;
Class.prototype= new Function.prototype.makeSubclass.nonconstructor();
return Class;
};
Function.prototype.makeSubclass.nonconstructor= function() {};
...
Shape= Object.makeSubclass();
Shape.prototype._init= function(x, y) {
this.x= x;
this.y= y;
};
Point= Shape.makeSubclass();
Circle= Shape.makeSubclass();
Circle.prototype._init= function(x, y, r) {
Shape.prototype._init.call(this, x, y);
this.r= r;
};
... который начинает больше походить на другие языки, хотя и с немного неуклюжим синтаксисом. Вы можете добавить несколько дополнительных функций, если хотите. Может быть, вы хотите makeSubclass
взять и запомнить имя класса и указать его по умолчанию toString
. Может быть, вы хотите, чтобы конструктор определял, когда он был случайно вызван без new
оператора (что в противном случае часто приводило бы к очень раздражающей отладке):
Function.prototype.makeSubclass= function() {
function Class() {
if (!(this instanceof Class))
throw('Constructor called without "new"');
...
Может быть, вы хотите передать всех новых участников и makeSubclass
добавить их в прототип, чтобы избавить вас от необходимости писать Class.prototype...
так много. Многие системы классов делают это, например:
Circle= Shape.makeSubclass({
_init: function(x, y, z) {
Shape.prototype._init.call(this, x, y);
this.r= r;
},
...
});
Существует множество потенциальных возможностей, которые вы можете посчитать желательными в объектной системе, и никто не может согласиться с одной конкретной формулой.
Путь закрытия , тогда. Это позволяет избежать проблем наследования на основе прототипов JavaScript, поскольку вообще не использует наследование. Вместо:
function Shape(x, y) {
var that= this;
this.x= x;
this.y= y;
this.toString= function() {
return 'Shape at '+that.x+', '+that.y;
};
}
function Circle(x, y, r) {
var that= this;
Shape.call(this, x, y);
this.r= r;
var _baseToString= this.toString;
this.toString= function() {
return 'Circular '+_baseToString(that)+' with radius '+that.r;
};
};
var mycircle= new Circle();
Теперь каждый экземпляр Shape
будет иметь свою собственную копию toString
метода (и любые другие методы или другие члены класса, которые мы добавим).
Плохая вещь в каждом экземпляре, имеющем собственную копию каждого члена класса, состоит в том, что он менее эффективен. Если вы имеете дело с большим количеством подклассовых экземпляров, прототипное наследование может помочь вам лучше. Кроме того, вызов метода базового класса немного раздражает, как вы можете видеть: мы должны помнить, каким был метод, до того, как конструктор подкласса перезаписал его, или он потеряется.
[Кроме того, поскольку здесь нет наследования, instanceof
оператор не будет работать; Вы должны будете предоставить свой собственный механизм для прослушивания классов, если вам это нужно. Хотя вы можете возиться с объектами-прототипами так же, как и с наследованием прототипов, это немного сложно и не стоит того, чтобы просто instanceof
работать.]
Хорошая вещь о каждом экземпляре, имеющем свой собственный метод, - то, что метод может тогда быть привязан к определенному экземпляру, которому он принадлежит. Это полезно из-за странного способа привязки JavaScript this
в вызовах методов, который приводит к тому, что если вы отсоедините метод от его владельца:
var ts= mycircle.toString;
alert(ts());
тогда this
внутри метода не будет ожидаемого экземпляра Circle (на самом деле это будет глобальный window
объект, вызывающий горе отладки). В действительности это обычно происходит , когда метод принимается и присваивается setTimeout
, onclick
или EventListener
в целом.
При использовании прототипа вы должны включать закрытие для каждого такого назначения:
setTimeout(function() {
mycircle.move(1, 1);
}, 1000);
или в будущем (или сейчас, если вы взломали Function.prototype), вы также можете сделать это с помощью function.bind()
:
setTimeout(mycircle.move.bind(mycircle, 1, 1), 1000);
если ваши экземпляры выполняются способом закрытия, привязка выполняется бесплатно закрытием по переменной экземпляра (обычно вызываемой that
или self
, хотя лично я бы посоветовал против последней, поскольку она self
уже имеет другое, другое значение в JavaScript). Вы не получаете аргументы 1, 1
в приведенном выше фрагменте бесплатно, поэтому вам все равно понадобится другое закрытие или a, bind()
если вам нужно это сделать.
Есть много вариантов метода закрытия. Вы можете предпочесть this
полностью опустить , создавая новый that
и возвращая его вместо использования new
оператора:
function Shape(x, y) {
var that= {};
that.x= x;
that.y= y;
that.toString= function() {
return 'Shape at '+that.x+', '+that.y;
};
return that;
}
function Circle(x, y, r) {
var that= Shape(x, y);
that.r= r;
var _baseToString= that.toString;
that.toString= function() {
return 'Circular '+_baseToString(that)+' with radius '+r;
};
return that;
};
var mycircle= Circle(); // you can include `new` if you want but it won't do anything
Какой путь «правильный»? Обе. Что является «лучшим»? Это зависит от вашей ситуации. FWIW Я склонен к созданию прототипов для реального наследования JavaScript, когда я сильно занимаюсь OO, и замыканий для простых одноразовых эффектов страницы.
Но оба способа довольно противоречивы для большинства программистов. У обоих есть много потенциальных беспорядочных изменений. Вы встретите обе (а также многие промежуточные и вообще неработающие схемы), если будете использовать код / библиотеки других людей. Нет единого общепринятого ответа. Добро пожаловать в удивительный мир объектов JavaScript.
[Это было частью 94 Почему JavaScript не мой любимый язык программирования.]