Як створити абстрактний базовий клас у JavaScript?


109

Чи можна імітувати абстрактний базовий клас у JavaScript? Який найелегантніший спосіб зробити це?

Скажіть, я хочу зробити щось на кшталт: -

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

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

Він повинен виводити: 'кора', 'мяу'


Відповіді:


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не визначене у тварин ), і що ця відповідь однозначно запитує якщо запропонована відповідь була те, що шукав запитуючий. Він не вимагає надання рішення для абстрактних класів у 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!

Навіщо використовувати конструкцію нової злісної функції (...)? Не вар c = function () {...}; бути кращим?
fionbio

2
"Var c = function () {...}" створить закриття для будь-якого підкласу () або іншого, що містить область. Напевно, це не важливо, але я хотів утримати його в чистоті від потенційно небажаних батьківських областей; конструктор чистого тексту Function () уникає закриттів.
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! 

чи може конструктор підкласу викликати конструктор суперкласу? (наприклад, дзвонити супер якоюсь мовою ... якщо так, то це безумовно призведе до виключення
неополярність

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.

Якщо ви не розумієте, як це працює, прочитайте про ланцюжок прототипу.


прото працюють 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
Бенджамін Груенбаум

@ Бенджамін: За словами 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();

Як ви бачите, останній об'єкт дає нам помилку, це тому, що тварина в прототипі має властивість abstract. Щоб бути впевненим, що це тварина - це не те, що є Animal.prototypeв прототипі ланцюга:

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

Тому я перевіряю, чи є у мого найближчого об'єкта прототипу abstractвластивість, лише об'єкт, створений безпосередньо з Animalпрототипу, матиме цю умову як істинний. Функція hasOwnPropertyперевіряє лише властивості поточного об'єкта, а не його прототипи, тому це дає нам 100% впевненість, що властивість декларується тут не в прототипі ланцюга.

Кожен об'єкт, що походить від Object, успадковує метод hasOwnProperty . Цей метод може бути використаний для визначення, чи має об'єкт вказану властивість як безпосередню властивість цього об'єкта; на відміну від оператора в, цей метод не перевіряє ланцюг прототипу об'єкта. Більше про це:

На мою думку, нам не доведеться змінювати constructorкожного разу після того, Object.createяк це є в поточній найкращій відповіді від @ Jordão.

Рішення також дозволяє створити багато абстрактних класів в ієрархії, нам потрібно лише створити abstractвластивість в прототипі.


4

Ще одна річ, яку ви можете захотіти застосувати - це переконатися, що ваш абстрактний клас не є примірником. Це можна зробити, визначивши функцію, яка виступає як FLAG, задану конструктором класу Abstract. Потім спробує сконструювати FLAG, який викличе його конструктор, що містить виняток. Приклад нижче:

(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, ви можете зробити перевірку, щоб здійснити переопределення.
Павло Оразуліке

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конструктор може розглядатися як анти-шаблон, а суперклас не повинен знати про підкласи. (порушує Лісков і принцип "Відкрити / закрити"
SirLenz0rlot

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

На мій погляд, це найелегантніший спосіб досягти хороших результатів із меншим кодом.
Тамас Ромео

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

Суворий режим унеможливлює реєстрацію абонента методом castAb абстракт, але помилка повинна виникати в середовищі налагодження, яка б показувала стеження стека.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.