Как создать абстрактный базовый класс в JavaScript?

109

Можно ли смоделировать абстрактный базовый класс в JavaScript? Как это сделать наиболее элегантно?

Скажем, я хочу сделать что-то вроде: -

var cat = new Animal('cat');
var dog = new Animal('dog');

cat.say();
dog.say();

Он должен выводить: "лай", "мяу".

Sabya
источник

Ответы:

127

Один простой способ создать абстрактный класс:

/**
 @constructor
 @abstract
 */
var Animal = function() {
    if (this.constructor === Animal) {
      throw new Error("Can't instantiate abstract class!");
    }
    // Animal initialization...
};

/**
 @abstract
 */
Animal.prototype.say = function() {
    throw new Error("Abstract method!");
}

Animal«Класс» и sayметод являются абстрактными.

Создание экземпляра приведет к ошибке:

new Animal(); // throws

Вот как вы "наследуете" от него:

var Cat = function() {
    Animal.apply(this, arguments);
    // Cat initialization...
};
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

Cat.prototype.say = function() {
    console.log('meow');
}

Dog выглядит так же.

И вот как разворачивается ваш сценарий:

var cat = new Cat();
var dog = new Dog();

cat.say();
dog.say();

Поиграйте здесь (посмотрите на вывод консоли).

Жордау
источник
Не могли бы вы объяснить построчно, если вы не против, так как я новичок в ООП. Спасибо!
React Developer
2
@undefined: чтобы понять это, я предлагаю вам поискать прототипное наследование в Javascript: это хорошее руководство.
Жордан
Спасибо за ответ .. перейду по ссылке.
React Developer
Наиболее важной частью этого является то, что в самом первом фрагменте кода выдается ошибка. Вы также можете выдать предупреждение или вернуть нулевое значение вместо объекта, чтобы продолжить выполнение приложения, это действительно зависит от вашей реализации. Это, на мой взгляд, правильный способ реализации абстрактных «классов» JS.
dudewad
1
@ G1P: это обычный способ выполнить «конструктор суперкласса» в Javascript, и это нужно делать вручную.
Жордау,
46

Классы JavaScript и наследование (ES6)

Согласно ES6, вы можете использовать классы JavaScript и наследование для выполнения того, что вам нужно.

Классы JavaScript, представленные в ECMAScript 2015, в первую очередь являются синтаксическим сахаром по сравнению с существующим наследованием на основе прототипов в JavaScript.

Ссылка: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes

Прежде всего, мы определяем наш абстрактный класс. Этот класс не может быть создан, но может быть расширен. Мы также можем определить функции, которые должны быть реализованы во всех классах, расширяющих этот.

/**
 * Abstract Class Animal.
 *
 * @class Animal
 */
class Animal {

  constructor() {
    if (this.constructor == Animal) {
      throw new Error("Abstract classes can't be instantiated.");
    }
  }

  say() {
    throw new Error("Method 'say()' must be implemented.");
  }

  eat() {
    console.log("eating");
  }
}

После этого мы можем создавать наши конкретные классы. Эти классы наследуют все функции и поведение от абстрактного класса.

/**
 * Dog.
 *
 * @class Dog
 * @extends {Animal}
 */
class Dog extends Animal {
  say() {
    console.log("bark");
  }
}

/**
 * Cat.
 *
 * @class Cat
 * @extends {Animal}
 */
class Cat extends Animal {
  say() {
    console.log("meow");
  }
}

/**
 * Horse.
 *
 * @class Horse
 * @extends {Animal}
 */
class Horse extends Animal {}

И результаты ...

// RESULTS

new Dog().eat(); // eating
new Cat().eat(); // eating
new Horse().eat(); // eating

new Dog().say(); // bark
new Cat().say(); // meow
new Horse().say(); // Error: Method say() must be implemented.

new Animal(); // Error: Abstract classes can't be instantiated.
Даниэль Поссамай
источник
27

Вы имеете в виду что-то вроде этого:

function Animal() {
  //Initialization for all Animals
}

//Function and properties shared by all instances of Animal
Animal.prototype.init=function(name){
  this.name=name;
}
Animal.prototype.say=function(){
    alert(this.name + " who is a " + this.type + " says " + this.whattosay);
}
Animal.prototype.type="unknown";

function Cat(name) {
    this.init(name);

    //Make a cat somewhat unique
    var s="";
    for (var i=Math.ceil(Math.random()*7); i>=0; --i) s+="e";
    this.whattosay="Me" + s +"ow";
}
//Function and properties shared by all instances of Cat    
Cat.prototype=new Animal();
Cat.prototype.type="cat";
Cat.prototype.whattosay="meow";


function Dog() {
    //Call init with same arguments as Dog was called with
    this.init.apply(this,arguments);
}

Dog.prototype=new Animal();
Dog.prototype.type="Dog";
Dog.prototype.whattosay="bark";
//Override say.
Dog.prototype.say = function() {
        this.openMouth();
        //Call the original with the exact same arguments
        Animal.prototype.say.apply(this,arguments);
        //or with other arguments
        //Animal.prototype.say.call(this,"some","other","arguments");
        this.closeMouth();
}

Dog.prototype.openMouth=function() {
   //Code
}
Dog.prototype.closeMouth=function() {
   //Code
}

var dog = new Dog("Fido");
var cat1 = new Cat("Dash");
var cat2 = new Cat("Dot");


dog.say(); // Fido the Dog says bark
cat1.say(); //Dash the Cat says M[e]+ow
cat2.say(); //Dot the Cat says M[e]+ow


alert(cat instanceof Cat) // True
alert(cat instanceof Dog) // False
alert(cat instanceof Animal) // True
некоторые
источник
Возможно, я это пропустил. Где находится абстрактный базовый класс (животное)?
HairOfTheDog
5
@HairOfTheDog Да, вы пропустили, что на этот вопрос был дан ответ около пяти лет назад, что в javascript в то время не было абстрактных классов, что вопрос был о способе его имитации ( whattosayне определен в animal ), и что этот ответ четко спрашивает если предложенный ответ был тем, что искал спрашивающий. Он не претендует на то, чтобы дать решение для абстрактных классов в javascript. Спрашивающий не удосужился ответить мне или кому-либо еще, поэтому я понятия не имею, сработало ли это для него. Прошу прощения, если предложенный пятилетним ребенком ответ на чей-то вопрос не сработал для вас.
некоторые
15

Возможно, вы захотите проверить базовый класс Дина Эдвардса: http://dean.edwards.name/weblog/2006/03/base/

В качестве альтернативы есть этот пример / статья Дугласа Крокфорда о классическом наследовании в JavaScript: http://www.crockford.com/javascript/inheritance.html

Ян Оксли
источник
13
Что касается ссылки Крокфорда, это полная чушь. В конце статьи он добавил примечание: «Теперь я считаю свои ранние попытки поддержать классическую модель в JavaScript ошибкой».
Мэтт Болл,
11

Можно ли смоделировать абстрактный базовый класс в JavaScript?

Безусловно. Существует около тысячи способов реализовать системы классов / экземпляров в JavaScript. Вот один из них:

// Classes magic. Define a new class with var C= Object.subclass(isabstract),
// add class members to C.prototype,
// provide optional C.prototype._init() method to initialise from constructor args,
// call base class methods using Base.prototype.call(this, ...).
//
Function.prototype.subclass= function(isabstract) {
    if (isabstract) {
        var c= new Function(
            'if (arguments[0]!==Function.prototype.subclass.FLAG) throw(\'Abstract class may not be constructed\'); '
        );
    } else {
        var c= new Function(
            'if (!(this instanceof arguments.callee)) throw(\'Constructor called without "new"\'); '+
            'if (arguments[0]!==Function.prototype.subclass.FLAG && this._init) this._init.apply(this, arguments); '
        );
    }
    if (this!==Object)
        c.prototype= new this(Function.prototype.subclass.FLAG);
    return c;
}
Function.prototype.subclass.FLAG= new Object();

var cat = новое животное ('кошка');

Конечно, это не совсем абстрактный базовый класс. Вы имеете в виду что-то вроде:

var Animal= Object.subclass(true); // is abstract
Animal.prototype.say= function() {
    window.alert(this._noise);
};

// concrete classes
var Cat= Animal.subclass();
Cat.prototype._noise= 'meow';
var Dog= Animal.subclass();
Dog.prototype._noise= 'bark';

// usage
var mycat= new Cat();
mycat.say(); // meow!
var mygiraffe= new Animal(); // error!
bobince
источник
Зачем использовать злую конструкцию new Function (...)? Не будет var c = function () {...}; быть лучше?
fionbio
2
«Var c = function () {...}» создаст закрытие для чего-либо в subclass () или другой содержащей его области. Возможно, это не важно, но я хотел очистить его от потенциально нежелательных родительских областей; конструктор Function () в виде чистого текста избегает замыканий.
bob с
10
Animal = function () { throw "abstract class!" }
Animal.prototype.name = "This animal";
Animal.prototype.sound = "...";
Animal.prototype.say = function() {
    console.log( this.name + " says: " + this.sound );
}

Cat = function () {
    this.name = "Cat";
    this.sound = "meow";
}

Dog = function() {
    this.name = "Dog";
    this.sound  = "woof";
}

Cat.prototype = Object.create(Animal.prototype);
Dog.prototype = Object.create(Animal.prototype);

new Cat().say();    //Cat says: meow
new Dog().say();    //Dog says: woof 
new Animal().say(); //Uncaught abstract class! 
Левен
источник
может ли конструктор подкласса вызывать конструктор суперкласса? (например, вызов super на каком-то языке ... если это так, то это безусловно вызовет исключение
неполярность
5
function Animal(type) {
    if (type == "cat") {
        this.__proto__ = Cat.prototype;
    } else if (type == "dog") {
        this.__proto__ = Dog.prototype;
    } else if (type == "fish") {
        this.__proto__ = Fish.prototype;
    }
}
Animal.prototype.say = function() {
    alert("This animal can't speak!");
}

function Cat() {
    // init cat
}
Cat.prototype = new Animal();
Cat.prototype.say = function() {
    alert("Meow!");
}

function Dog() {
    // init dog
}
Dog.prototype = new Animal();
Dog.prototype.say = function() {
    alert("Bark!");
}

function Fish() {
    // init fish
}
Fish.prototype = new Animal();

var newAnimal = new Animal("dog");
newAnimal.say();

Не гарантируется, что это будет работать как __proto__ не является стандартной переменной, но работает, по крайней мере, в Firefox и Safari.

Если вы не понимаете, как это работает, прочтите о цепочке прототипов.

Георг Шёлли
источник
proto работает AFAIK только в FF и Chome (ни IE, ни Opera не поддерживают это. Я не тестировал в Safari). Кстати, вы делаете это неправильно: базовый класс (животное) следует редактировать каждый раз, когда требуется новый тип животного.
примерно
Safari и Chrome используют один и тот же движок javascript. Я не был уверен, что он просто хотел знать, как работает наследование, поэтому старался как можно точнее следовать его примеру.
Георг Шелли
4
Safari и Chrome не используют один и тот же движок JavaScript, Safari использует JavaScriptCore, а Chrome использует V8 . У обоих браузеров есть общий механизм Layout Engine, WebKit .
CMS
@ GeorgSchölly Пожалуйста, подумайте о редактировании своего ответа, чтобы использовать новую конструкцию Object.getPrototypeOf
Бенджамин Грюнбаум
@Benjamin: Согласно Mozilla, еще нет setPrototypeOfметода, который мне понадобился бы для моего кода.
Георг Шелли
5

Вы можете создавать абстрактные классы, используя прототипы объектов, простой пример может быть следующим:

var SampleInterface = {
   addItem : function(item){}  
}

Вы можете изменить вышеуказанный метод или нет, это зависит от вас, когда вы его реализуете. Для подробного наблюдения вы можете посетить здесь .

Юсуфайтас
источник
5

Вопрос довольно старый, но я создал возможное решение, как создать абстрактный «класс» и заблокировать создание объекта этого типа.

//our Abstract class
var Animal=function(){
  
    this.name="Animal";
    this.fullname=this.name;
    
    //check if we have abstract paramater in prototype
    if (Object.getPrototypeOf(this).hasOwnProperty("abstract")){
    
    throw new Error("Can't instantiate abstract class!");
    
    
    }
    

};

//very important - Animal prototype has property abstract
Animal.prototype.abstract=true;

Animal.prototype.hello=function(){

   console.log("Hello from "+this.name);
};

Animal.prototype.fullHello=function(){

   console.log("Hello from "+this.fullname);
};

//first inheritans
var Cat=function(){

	  Animal.call(this);//run constructor of animal
    
    this.name="Cat";
    
    this.fullname=this.fullname+" - "+this.name;

};

Cat.prototype=Object.create(Animal.prototype);

//second inheritans
var Tiger=function(){

    Cat.call(this);//run constructor of animal
    
    this.name="Tiger";
    
    this.fullname=this.fullname+" - "+this.name;
    
};

Tiger.prototype=Object.create(Cat.prototype);

//cat can be used
console.log("WE CREATE CAT:");
var cat=new Cat();
cat.hello();
cat.fullHello();

//tiger can be used

console.log("WE CREATE TIGER:");
var tiger=new Tiger();
tiger.hello();
tiger.fullHello();


console.log("WE CREATE ANIMAL ( IT IS ABSTRACT ):");
//animal is abstract, cannot be used - see error in console
var animal=new Animal();
animal=animal.fullHello();

Как видите, последний объект выдает ошибку, потому что у Animal в прототипе есть свойство abstract. Чтобы быть уверенным, что это Animal, а не то, что есть Animal.prototypeв цепочке прототипов, я делаю:

Object.getPrototypeOf(this).hasOwnProperty("abstract")

Поэтому я проверяю, что у моего ближайшего объекта-прототипа есть abstractсвойство, только объект, созданный непосредственно из Animalпрототипа, будет иметь это условие на true. Функция hasOwnPropertyпроверяет только свойства текущего объекта, а не его прототипы, так что это дает нам 100% уверенность, что свойство объявлено здесь не в цепочке прототипов.

Каждый объект, производный от Object, наследует метод hasOwnProperty . Этот метод можно использовать, чтобы определить, имеет ли объект указанное свойство как прямое свойство этого объекта; в отличие от оператора in, этот метод не проверяет цепочку прототипов объекта. Подробнее об этом:

В моем предложении нам не нужно менять constructorкаждый раз после того, Object.createкак это в текущем лучшем ответе @ Jordão.

Решение также позволяет создавать множество абстрактных классов в иерархии, нам нужно только создать abstractсвойство в прототипе.

Мацей Сикора
источник
4

Еще одна вещь, которую вы, возможно, захотите применить, - это убедиться, что ваш абстрактный класс не создан. Вы можете сделать это, определив функцию, которая действует как функции ФЛАГА, установленные как конструктор абстрактного класса. Затем он попытается создать ФЛАГ, который вызовет свой конструктор, содержащий исключение, которое будет выдано. Пример ниже:

(function(){

    var FLAG_ABSTRACT = function(__class){

        throw "Error: Trying to instantiate an abstract class:"+__class
    }

    var Class = function (){

        Class.prototype.constructor = new FLAG_ABSTRACT("Class");       
    }

    //will throw exception
    var  foo = new Class();

})()
обыкновенный
источник
2

В Factoryэтом случае мы можем использовать шаблон проектирования. Javascript используется prototypeдля наследования родительских членов.

Определите конструктор родительского класса.

var Animal = function() {
  this.type = 'animal';
  return this;
}
Animal.prototype.tired = function() {
  console.log('sleeping: zzzZZZ ~');
}

А затем создайте детский класс.

// These are the child classes
Animal.cat = function() {
  this.type = 'cat';
  this.says = function() {
    console.log('says: meow');
  }
}

Затем определите конструктор дочернего класса.

// Define the child class constructor -- Factory Design Pattern.
Animal.born = function(type) {
  // Inherit all members and methods from parent class,
  // and also keep its own members.
  Animal[type].prototype = new Animal();
  // Square bracket notation can deal with variable object.
  creature = new Animal[type]();
  return creature;
}

Попробуй это.

var timmy = Animal.born('cat');
console.log(timmy.type) // cat
timmy.says(); // meow
timmy.tired(); // zzzZZZ~

Вот ссылка Codepen для полного примера кодирования.

Эрик Тан
источник
1
//Your Abstract class Animal
function Animal(type) {
    this.say = type.say;
}

function catClass() {
    this.say = function () {
        console.log("I am a cat!")
    }
}
function dogClass() {
    this.say = function () {
        console.log("I am a dog!")
    }
}
var cat = new Animal(new catClass());
var dog = new Animal(new dogClass());

cat.say(); //I am a cat!
dog.say(); //I am a dog!
Пол Оразулике
источник
Это полиморфизм в JavaScript, вы можете выполнить некоторую проверку, чтобы реализовать переопределение.
Paul Orazulike
0

Я думаю, что все эти ответы, особенно первые два ( некоторыми и jordão ), четко отвечают на вопрос с традиционной концепцией прототипа базовой JS.
Теперь, когда вы хотите, чтобы конструктор класса животных вел себя в соответствии с переданным параметром конструкции, я думаю, что это очень похоже на базовое поведение, Creational Patternsнапример, Factory Pattern .

Здесь я немного попытался заставить это работать именно так.

var Animal = function(type) {
    this.type=type;
    if(type=='dog')
    {
        return new Dog();
    }
    else if(type=="cat")
    {
        return new Cat();
    }
};



Animal.prototype.whoAreYou=function()
{
    console.log("I am a "+this.type);
}

Animal.prototype.say = function(){
    console.log("Not implemented");
};




var Cat =function () {
    Animal.call(this);
    this.type="cat";
};

Cat.prototype=Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

Cat.prototype.say=function()
{
    console.log("meow");
}



var Dog =function () {
    Animal.call(this);
    this.type="dog";
};

Dog.prototype=Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.say=function()
{
    console.log("bark");
}


var animal=new Animal();


var dog = new Animal('dog');
var cat=new Animal('cat');

animal.whoAreYou(); //I am a undefined
animal.say(); //Not implemented


dog.whoAreYou(); //I am a dog
dog.say(); //bark

cat.whoAreYou(); //I am a cat
cat.say(); //meow
Саиф
источник
Ссылка на это: programmers.stackexchange.com/questions/219543/… Этот Animalконструктор можно рассматривать как анти-шаблон, суперкласс не должен знать о подклассах. (нарушает принцип Лискова и открытия /
закрытия
0
/****************************************/
/* version 1                            */
/****************************************/

var Animal = function(params) {
    this.say = function()
    {
        console.log(params);
    }
};
var Cat = function() {
    Animal.call(this, "moes");
};

var Dog = function() {
    Animal.call(this, "vewa");
};


var cat = new Cat();
var dog = new Dog();

cat.say();
dog.say();


/****************************************/
/* version 2                            */
/****************************************/

var Cat = function(params) {
    this.say = function()
    {
        console.log(params);
    }
};

var Dog = function(params) {
    this.say = function()
    {
        console.log(params);
    }
};

var Animal = function(type) {
    var obj;

    var factory = function()
    {
        switch(type)
        {
            case "cat":
                obj = new Cat("bark");
                break;
            case "dog":
                obj = new Dog("meow");
                break;
        }
    }

    var init = function()
    {
        factory();
        return obj;
    }

    return init();
};


var cat = new Animal('cat');
var dog = new Animal('dog');

cat.say();
dog.say();
Тамас Ромео
источник
На мой взгляд, это наиболее элегантный способ получить хорошие результаты с меньшим количеством кода.
Tamas Romeo
1
Объясните, пожалуйста, почему это полезно. Раздача кода не так полезна, как объяснение, почему он полезен. Это разница между тем, чтобы отдать кому-нибудь рыбу или научить ловить рыбу.
Железный Человек
Многие не используют в своих методах программирования javascript прототипы или конструкторы. Даже если они пригодятся во многих ситуациях. Для них я считаю этот код полезным. Не потому, что код лучше других .. а потому, что его легче понять,
Тамас Ромео
0

Если вы хотите убедиться, что ваши базовые классы и их члены строго абстрактны, вот базовый класс, который сделает это за вас:

class AbstractBase{
    constructor(){}
    checkConstructor(c){
        if(this.constructor!=c) return;
        throw new Error(`Abstract class ${this.constructor.name} cannot be instantiated`);
    }
    throwAbstract(){
        throw new Error(`${this.constructor.name} must implement abstract member`);}    
}

class FooBase extends AbstractBase{
    constructor(){
        super();
        this.checkConstructor(FooBase)}
    doStuff(){this.throwAbstract();}
    doOtherStuff(){this.throwAbstract();}
}

class FooBar extends FooBase{
    constructor(){
        super();}
    doOtherStuff(){/*some code here*/;}
}

var fooBase = new FooBase(); //<- Error: Abstract class FooBase cannot be instantiated
var fooBar = new FooBar(); //<- OK
fooBar.doStuff(); //<- Error: FooBar must implement abstract member
fooBar.doOtherStuff(); //<- OK

Строгий режим делает невозможным регистрацию вызывающего объекта в методе throwAbstract, но ошибка должна возникать в среде отладки, которая покажет трассировку стека.

pasx
источник