Jak utworzyć abstrakcyjną klasę bazową w JavaScript?

109

Czy można symulować abstrakcyjną klasę bazową w JavaScript? Jaki jest najbardziej elegancki sposób na zrobienie tego?

Powiedz, chcę zrobić coś takiego: -

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

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

Powinien wyświetlić: „szczekanie”, „miauczenie”

Sabya
źródło

Odpowiedzi:

127

Prosty sposób na utworzenie klasy abstrakcyjnej jest następujący:

/**
 @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„Klasa” i saymetoda są abstrakcyjne.

Utworzenie instancji spowodowałoby błąd:

new Animal(); // throws

W ten sposób „dziedziczysz” po nim:

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 wygląda właśnie tak.

A tak rozgrywa się Twój scenariusz:

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

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

Skrzypuj tutaj (spójrz na wyjście konsoli).

Jordão
źródło
Czy możesz wyjaśnić wiersz po wierszu, jeśli nie masz nic przeciwko, ponieważ jestem nowy w OOP. Dzięki!
React Developer,
2
@undefined: aby to zrozumieć, proponuję poszukać dziedziczenia prototypowego w Javascript: to dobry przewodnik.
Jordão,
Dzięki za odpowiedź ... przejdzie przez link.
React Developer,
Najważniejszym elementem jest to, że w pierwszym fragmencie kodu pojawia się błąd. Możesz również zgłosić ostrzeżenie lub zwrócić wartość null zamiast obiektu, aby kontynuować wykonywanie aplikacji, tak naprawdę zależy to od implementacji. Moim zdaniem jest to właściwy sposób implementacji abstrakcyjnych „klas” JS.
dudewad
1
@ G1P: to zwykły sposób na wykonanie „konstruktora superklasy” w Javascript i trzeba to zrobić ręcznie.
Jordão,
46

Klasy JavaScript i dziedziczenie (ES6)

Zgodnie z ES6, możesz używać klas JavaScript i dziedziczenia, aby osiągnąć to, czego potrzebujesz.

Klasy JavaScript, wprowadzone w ECMAScript 2015, są przede wszystkim lukrem syntaktycznym w stosunku do istniejącego dziedziczenia opartego na prototypach JavaScript.

Źródła: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes

Przede wszystkim definiujemy naszą klasę abstrakcyjną. Nie można utworzyć wystąpienia tej klasy, ale można ją rozszerzyć. Możemy również zdefiniować funkcje, które muszą zostać zaimplementowane we wszystkich klasach rozszerzających tę.

/**
 * 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");
  }
}

Następnie możemy stworzyć nasze konkretne klasy. Te klasy odziedziczą wszystkie funkcje i zachowania z klasy abstrakcyjnej.

/**
 * 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 {}

A wyniki ...

// 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.
Daniel Possamai
źródło
27

Czy masz na myśli coś takiego:

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
trochę
źródło
Może to przegapiłem. Gdzie jest streszczenie klasy bazowej (Animal)?
HairOfTheDog
5
@HairOfTheDog Tak, tęskniłeś za tą odpowiedzią jakieś pięć lat temu, że javascript w tamtym czasie nie miał klas abstrakcyjnych, że pytanie dotyczyło sposobu na symulację ( whattosaynie jest zdefiniowane w zwierzęciu ) i że ta odpowiedź wyraźnie pyta czy proponowana odpowiedź była tym, czego szukał pytający. Nie twierdzi, że daje rozwiązanie dla klas abstrakcyjnych w javascript. Pytający nie zadał sobie trudu, aby odpowiedzieć mnie ani nikomu innemu, więc nie mam pojęcia, czy to zadziałało dla niego. Przepraszam, jeśli pięciolatka zaproponowana przez kogoś innego nie zadziałała.
jakiś
15

Możesz sprawdzić klasę podstawową Deana Edwardsa: http://dean.edwards.name/weblog/2006/03/base/

Alternatywnie, istnieje ten przykład / artykuł autorstwa Douglasa Crockforda na temat klasycznego dziedziczenia w JavaScript: http://www.crockford.com/javascript/inheritance.html

Ian Oxley
źródło
13
Jeśli chodzi o łącze Crockford, to mnóstwo bzdur. Na końcu tego artykułu dodał uwagę: „Obecnie uważam, że moje wczesne próby wsparcia klasycznego modelu w JavaScript są błędem”.
Matt Ball,
11

Czy można symulować abstrakcyjną klasę bazową w JavaScript?

Na pewno. Istnieje około tysiąca sposobów implementacji systemów klas / instancji w JavaScript. Tutaj jest jeden:

// 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 = new Animal ('cat');

Nie jest to oczywiście abstrakcyjna klasa bazowa. Czy masz na myśli coś takiego:

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
źródło
Po co używać złego nowego konstruktu funkcji (...)? Nie var c = function () {...}; bądź lepszy?
fionbio
2
„Var c = function () {...}” utworzyłoby zamknięcie na czymkolwiek w subclass () lub innym zakresie zawierającym. Prawdopodobnie nie jest to ważne, ale chciałem uniknąć potencjalnie niechcianych zakresów nadrzędnych; konstruktor Function () czysto tekstowy unika domknięć.
bobince
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! 
Leven
źródło
czy konstruktor podklasy może wywołać konstruktor nadklasy? (jak dzwonienie super w jakimś języku ... jeśli tak, to bezwarunkowo zgłosi wyjątek
nonopolarity
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();

Nie ma gwarancji, że zadziała, ponieważ __proto__nie jest to zmienna standardowa, ale działa przynajmniej w Firefoksie i Safari.

Jeśli nie rozumiesz, jak to działa, przeczytaj o łańcuchu prototypów.

Georg Schölly
źródło
proto działa AFAIK tylko w FF i Chome (ani IE, ani Opera nie obsługują tego. Nie testowałem w Safari). BTW, robisz to źle: klasę bazową (zwierzę) należy edytować za każdym razem, gdy potrzebny jest nowy typ zwierzęcia.
około
Safari i Chrome używają tego samego silnika javascript. Nie byłem pewien, czy chciał tylko wiedzieć, jak działa dziedziczenie, dlatego starałem się jak najdokładniej naśladować jego przykład.
Georg Schölly
4
Safari i Chrome nie używają tego samego silnika JavaScript, Safari używa JavaScriptCore, a Chrome używa V8 . Cechą wspólną obu przeglądarek jest mechanizm układu, WebKit .
CMS
@ GeorgSchölly Proszę rozważyć edycję odpowiedzi, aby użyć nowej konstrukcji Object.getPrototypeOf
Benjamin Gruenbaum
@Benjamin: Według Mozilli nie ma setPrototypeOfjeszcze metody, której potrzebowałbym do mojego kodu.
Georg Schölly,
5

Możesz tworzyć klasy abstrakcyjne za pomocą prototypów obiektów, prosty przykład może wyglądać następująco:

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

Możesz zmienić powyższą metodę lub nie, to od Ciebie zależy, kiedy ją zastosujesz. Aby uzyskać szczegółową obserwację, możesz odwiedzić tutaj .

yusufaytas
źródło
5

Pytanie jest dość stare, ale stworzyłem możliwe rozwiązanie, jak stworzyć abstrakcyjną „klasę” i blokować tworzenie obiektów tego typu.

//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();

Jak widać ostatni obiekt daje nam błąd, to dlatego, że Animal w prototypie ma własność abstract. Aby mieć pewność, że jest to Animal, a nie coś, co ma Animal.prototypew łańcuchu prototypów, robię:

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

Więc sprawdzam, czy mój najbliższy obiekt prototypowy ma abstractwłaściwość, tylko obiekt utworzony bezpośrednio z Animalprototypu będzie miał ten warunek na true. Funkcja hasOwnPropertysprawdza tylko właściwości aktualnego obiektu, a nie jego prototypów, więc daje nam to 100% pewności, że właściwość jest zadeklarowana tutaj, a nie w łańcuchu prototypów.

Każdy obiekt pochodzący z Object dziedziczy metodę hasOwnProperty . Ta metoda może służyć do określenia, czy obiekt ma określoną właściwość jako bezpośrednią właściwość tego obiektu; w przeciwieństwie do operatora in, ta metoda nie sprawdza łańcucha prototypów obiektu. Więcej na ten temat:

W mojej propozycji nie musimy zmieniać za constructorkażdym razem, Object.createjak to jest w aktualnej najlepszej odpowiedzi @ Jordão.

Rozwiązanie umożliwia również tworzenie wielu abstrakcyjnych klas w hierarchii, wystarczy stworzyć abstractwłasność w prototypie.

Maciej Sikora
źródło
4

Inną rzeczą, którą możesz chcieć wymusić, jest upewnienie się, że twoja klasa abstrakcyjna nie została utworzona. Możesz to zrobić, definiując funkcję, która działa jak funkcje typu FLAG ustawione jako konstruktor klasy abstrakcyjnej. To następnie spróbuje skonstruować FLAG, który wywoła swój konstruktor zawierający wyjątek do wyrzucenia. Przykład poniżej:

(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();

})()
zwykły
źródło
2

FactoryW tym przypadku możemy użyć wzorca projektowego. Wykorzystanie JavaScript prototypedo dziedziczenia członków rodzica.

Zdefiniuj konstruktora klasy nadrzędnej.

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

A następnie utwórz klasę dla dzieci.

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

Następnie zdefiniuj konstruktora klasy podrzędnej.

// 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;
}

Sprawdź to.

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

Oto link Codepen do pełnego przykładowego kodowania.

Eric Tan
źródło
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!
Paul Orazulike
źródło
To jest polimorfizm w JavaScript, możesz sprawdzić, czy zaimplementować nadpisanie.
Paul Orazulike,
0

Myślę, że wszystkie te odpowiedzi, szczególnie pierwsze dwie (przez niektórych i jordão ) dają jasną odpowiedź na to pytanie za pomocą konwencjonalnej koncepcji prototypowej bazy JS.
Ponieważ chcesz, aby konstruktor klasy zwierząt zachowywał się zgodnie z przekazanym do konstrukcji parametrem, myślę, że jest to bardzo podobne do podstawowego zachowania Creational Patternsna przykład wzorca fabrycznego .

Tutaj zrobiłem małe podejście, aby to działało w ten sposób.

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
Saif
źródło
Odnośnik do tego: programmers.stackexchange.com/questions/219543/ ... Ten Animalkonstruktor może być postrzegany jako anty-wzorzec, nadklasa nie powinna mieć wiedzy o podklasach. (narusza zasadę
Liskova
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
źródło
Z mojego punktu widzenia jest to najbardziej elegancki sposób na uzyskanie dobrych wyników przy mniejszej ilości kodu.
Tamas Romeo
1
Proszę wyjaśnić, dlaczego jest to przydatne. Przekazywanie kodu nie jest tak przydatne, jak wyjaśnianie, dlaczego kod jest przydatny. To różnica między podaniem komuś ryby a nauczeniem go łowienia.
Tin Man
Wiele z nich nie używa w swoich technikach programowania javascript prototypów lub konstruktorów. Nawet jeśli są przydatne w wielu sytuacjach. Dla nich uważam, że kod jest przydatny. Nie dlatego, że kod jest lepiej niż inni .. ale ponieważ jest to łatwiejsze do zrozumienia
Tamas Romeo
0

Jeśli chcesz się upewnić, że twoje klasy bazowe i ich składowe są ściśle abstrakcyjne, oto klasa bazowa, która robi to za Ciebie:

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

Tryb ścisły uniemożliwia zalogowanie obiektu wywołującego w metodzie throwAbstract, ale błąd powinien wystąpić w środowisku debugowania, które pokazywałoby ślad stosu.

pasx
źródło