如何在JavaScript中创建抽象基类?

167

在JavaScript中是否有可能模拟抽象基类?最优雅的方法是什么?

比如说,我想做这样的事情:

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

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

它应该输出:

meow
bark

如何在JavaScript中创建无法实例化的抽象基类 - Bergi
18个回答

221

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.

7
这应该是截至2021年的最佳答案。 - Artur Barseghyan
4
请注意,父方法eat可以使用console.log(this.constructor.name + " eating")来输出“猫正在吃”,“狗正在吃”或“马正在吃”。 - Felix Eve
注意:如果子类需要实现构造函数,则在子类中实现构造函数时需要调用super()。如果super()抛出错误,子类将无法被构造。 - Vishala Ramasamy
是的,在这种情况下,子类必须调用super(),但它将正常构建而不会出现任何错误。无论子类是否有构造函数,都会调用超级构造函数。 https://jsfiddle.net/w7tj29xf/ - Daniel Possamai

155
创建一个抽象类的简单方法如下:

一种简单的方法是这样的:

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

看起来就像它。

以下是您的情景:

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

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

请在这里尝试(查看控制台输出)。


2
建议您查阅JavaScript中的原型继承以理解它:这里是一个很好的指南。 - Jordão
最重要的部分是,在第一个代码片段中,会抛出一个错误。你也可以抛出一个警告或返回一个空值而不是一个对象,以便继续应用程序执行,这真的取决于你的实现方式。在我看来,这是实现抽象JS“类”的正确方法。 - dudewad
@Jordão,请问为什么您在Cat对象的构造函数中使用了Animal.apply(this, arguments)? - G1P
1
@G1P:这是在Javascript中执行“超类构造函数”的常规方式,需要手动完成。 - Jordão
为什么在创建类构造函数时使用函数表达式而不是函数声明?此外,为什么是"var name = function() {}",而不是"function name(){}"? - undefined
显示剩余5条评论

28

你的意思是这样的吗:

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

我可能错过了,基类 Animal 是抽象的吗? - HairOfTheDog
7
@HairOfTheDog,您错过了五年前已经回答过这个问题的事实,当时JavaScript没有抽象类,问题是为了找到一种模拟它的方式(在animal中未定义whattosay),这个答案清楚地询问了提问者是否正在寻找所提议的答案。它并没有声称给出JavaScript中抽象类的解决方案。提问者没有回复我或其他任何人,所以我不知道它是否对他有用。如果一个五年前针对别人问题的回答对您没起作用,我很抱歉。 - some

15

14
关于Crockford链接的内容,那是一派胡言。他在文章末尾添加了一条注释:“我现在意识到我早期在JavaScript中支持经典模型的尝试是一个错误。” - Matt Ball

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 = new Animal('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!

为什么要使用邪恶的 new Function(...) 构造函数?使用 var c = function () { ... }; 不是更好吗? - fionbio
2
“var c= function() {...}” 会在 subclass() 或其他包含作用域中创建一个闭包。这可能不是很重要,但我想保持它干净,避免潜在的不必要的父级作用域;纯文本 Function() 构造函数避免了闭包。 - bobince

11
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一样...如果可以,那么它将无条件地引发异常。) - nonopolarity

6

你可能还想强制确保抽象类不会被实例化。你可以通过定义一个函数来实现,该函数作为抽象类构造函数的FLAG。这将尝试构建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();

})()

6

这个问题很旧了,但我想提供一些可能的解决方案,来创建抽象“类”并阻止该类型对象的创建。

//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原型创建的对象才会满足此条件。函数hasOwnProperty仅检查当前对象的属性,而不检查其原型,因此这可以确保该属性在此处声明而不在原型链中。

从Object继承的每个对象都继承了hasOwnProperty方法。此方法可用于确定对象是否将指定属性作为该对象的直接属性;与in运算符不同,此方法不会沿着对象的原型链向下检查。更多信息:

在我的建议中,我们不必像@Jordão提供的当前最佳答案中那样在每次Object.create之后更改constructor
解决方案还可以在层次结构中创建许多抽象类,我们只需要在原型中创建abstract属性即可。

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__仅在FF和Chrome中工作(IE和Opera都不支持它。我还没有在Safari中测试过)。顺便说一句,你做错了:每次想要新类型的动物时,应该编辑基类(动物)。 - some
Safari和Chrome都使用相同的JavaScript引擎。我不太确定他只想知道继承如何工作,因此我尽可能地按照他的示例进行了跟踪。 - Georg Schölly
4
Safari和Chrome不使用相同的JavaScript引擎,Safari使用JavaScriptCore,而Chrome使用V8。这两个浏览器共享的是排版引擎WebKit - Christian C. Salvadó
@GeorgSchölly 请考虑编辑您的答案,使用新的Object.getPrototypeOf构造。 - Benjamin Gruenbaum
@Benjamin:根据Mozilla的说法(https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/getPrototypeOf),目前还没有`setPrototypeOf`方法,而我需要这个方法来完成我的代码。 - Georg Schölly
@GeorgSchölly 嗨 :) 首先,当你使用函数构造器创建一个对象时,它会设置它的原型。当你执行 Dog.prototype=new Animal() 时,你正在设置 Dog 对象的原型 :)。其次,还有一个 setPrototypeOf,它被称为 Object.create,但那是另外一个故事了。 - Benjamin Gruenbaum

5
您可以通过使用对象原型来创建抽象类,一个简单的例子如下:
var SampleInterface = {
   addItem : function(item){}  
}

当你实现它时,你可以选择更改上述方法或不更改。如果您想进行详细观察,可以访问这里


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接