Существует две модели для реализации классов и экземпляров в 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 не мой любимый язык программирования.]