Javascript: 类实例化和继承
Javascript: 类实例化和继承
以下是JavaScript中类Animal和其子类Bird的定义的示例(使用TypeScript):
class Animal { name: string; numberOfLegs: number = 4; aboutMe: string; constructor (theName: string) { this.name = theName; this.init(); } init() { this.aboutMe = `我是${this.name},有${this.numberOfLegs}条腿`; } } class Bird extends Animal { numberOfLegs: number = 2; constructor (theName: string) { super(theName); } } var bird = new Bird('Bimbo'); console.log(bird.aboutMe);
正确的`bird.aboutMe`属性的期望值应为`我是Bimbo,有2条腿`,但实际上你将得到`我是Bimbo,有4条腿`。当你将上述TypeScript代码编译成纯JavaScript代码时,[在这里](http://www.typescriptlang.org/play/index.html)就很明显为什么它不能正确工作。
我的问题是:如何正确编写JavaScript类的初始化逻辑,以使其也适用于继承,并且以我们在其他面向对象语言中习惯的方式工作?TypeScript试图弥补JavaScript和其他面向对象语言之间的差距,但即使在这样一个简单的情况下也失败了。我是否遗漏了什么?
为了证明我对正确结果的期望是有效的,我已经将上述代码重写为PHP:
class Animal { protected $name; protected $numberOfLegs = 4; public $aboutMe; public function __construct ($theName) { $this->name = $theName; $this->init(); } protected function init() { $this->aboutMe = "我是{$this->name},有{$this->numberOfLegs}条腿"; } } class Bird extends Animal { protected $numberOfLegs = 2; public function __construct ($theName) { parent::__construct($theName); } } $bird = new Bird('Bimbo'); echo $bird->aboutMe;
上述PHP代码输出的结果为`我是Bimbo,有2条腿`。
编辑1:当然,我知道如何使上述代码正常工作。我的需求不是使这段简单的代码工作,而是找到一种处理JS类实例初始化的方式,使其在复杂情况下也能正常工作。也许可以通过TypeScript来达到这个目的,我希望它的工作方式与C风格的类定义类似。有办法实现这个目标吗?
编辑2:下面由Emil S. Jørgensen提供了一种非常好的通用解决方案。即使在更长的继承链中也可以工作(例如`Bird extends Animal`和`CityBird extends Bird`)。我在他的答案中添加了一些额外的代码,以显示在每个级别上,您可以重用父(超级)类的`init()`方法,并在需要时添加自己的初始化逻辑:
// TYPESCRIPT class Animal { static _isInheritable = true; public name: string; public numberOfLegs: number = 4; public aboutMe: string; constructor(theName: string) { this.name = theName; var isInheirited = (arguments.callee.caller != null ? arguments.callee.caller._isInheritable != void 0 : false); if (!isInheirited) { console.log("In Animal is "); this.init(); } else { console.log("Skipping Animal init() because inherited"); } } init() { console.log("the Animal init() called"); this.aboutMe = `I'm ${this.name} with ${this.numberOfLegs} legs`; } } class Bird extends Animal { public numberOfLegs: number = 2; constructor(theName: string) { super(theName); var isInheirited = (arguments.callee.caller != null ? arguments.callee.caller._isInheritable != void 0 : false); if (!isInheirited) { console.log("In Bird is "); this.init(); } else { console.log("Skipping Bird init() because inherited"); } } init() { super.init(); console.log("and also some additionals in the Bird init() called"); } } class CityBird extends Bird { public numberOfLegs: number = 1; constructor(theName: string) { super(theName); var isInheirited = (arguments.callee.caller != null ? arguments.callee.caller._isInheritable != void 0 : false); if (!isInheirited) { console.log("In CityBird is "); this.init(); } else { console.log("Skipping CityBird init() because inherited"); } } init() { super.init(); console.log("and also some additionals in the CityBird init() called"); } } var bird = new CityBird('Bimbo'); console.log(bird.aboutMe);
这种解决方案的缺点是在使用严格模式(`'use strict'`)时无法使用,因为在严格模式下,不能在函数上访问`caller`、`callee`和`arguments`属性(参见[这里](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments/callee#Why_was_arguments.callee_removed_from_ES5_strict_mode))。
编辑3:与严格模式和ES6类兼容的解决方案(避免使用严格模式禁用的`callee`)基于比较`this.construct`和类(函数)本身(参见[这里](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/constructor#Description))。只有当这两者相等时,才会启动`init()`方法 - 这意味着`init()`方法只在实例化类的构造函数中调用。以下是从编辑2中重写的代码:
/* // TYPESCRIPT class Animal { public name: string; public numberOfLegs: number = 4; public aboutMe: string; constructor(theName: string) { this.name = theName; if (this.constructor === Animal) { console.log("In Animal is "); this.init(); } else { console.log("Skipping Animal init() because inherited"); } } init() { console.log("the Animal init() called"); this.aboutMe = `I'm ${this.name} with ${this.numberOfLegs} legs`; } } class Bird extends Animal { public numberOfLegs: number = 2; constructor(theName: string) { super(theName); if (this.constructor === Bird) { console.log("In Bird is "); this.init(); } else { console.log("Skipping Bird init() because inherited"); } } init() { super.init(); console.log("and also some additionals in the Bird init() called"); } } class CityBird extends Bird { public numberOfLegs: number = 1; constructor(theName: string) { super(theName); if (this.constructor === CityBird) { console.log("In CityBird is "); this.init(); } else { console.log("Skipping CityBird init() because inherited"); } } init() { super.init(); console.log("and also some additionals in the CityBird init() called"); } } var bird = new CityBird('Bimbo'); console.log(bird.aboutMe); */ var __extends = (this && this.__extends) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; var Animal = (function () { function Animal(theName) { this.numberOfLegs = 4; this.name = theName; if (this.constructor === Animal) { console.log("In Animal is "); this.init(); } else { console.log("Skipping Animal init() because inherited"); } } Animal.prototype.init = function () { console.log("the Animal init() called"); this.aboutMe = "I'm " + this.name + " with " + this.numberOfLegs + " legs"; }; return Animal; }()); var Bird = (function (_super) { __extends(Bird, _super); function Bird(theName) { var _this = _super.call(this, theName) || this; _this.numberOfLegs = 2; if (_this.constructor === Bird) { console.log("In Bird is "); _this.init(); } else { console.log("Skipping Bird init() because inherited"); } return _this; } Bird.prototype.init = function () { _super.prototype.init.call(this); console.log("and also some additionals in the Bird init() called"); }; return Bird; }(Animal)); var CityBird = (function (_super) { __extends(CityBird, _super); function CityBird(theName) { var _this = _super.call(this, theName) || this; _this.numberOfLegs = 1; if (_this.constructor === CityBird) { console.log("In CityBird is "); _this.init(); } else { console.log("Skipping CityBird init() because inherited"); } return _this; } CityBird.prototype.init = function () { _super.prototype.init.call(this); console.log("and also some additionals in the CityBird init() called"); }; return CityBird; }(Bird)); var bird = new CityBird('Bimbo'); console.log(bird.aboutMe);
这种解决方案也可以与新的ES6 `class` 语法一起使用,该语法强制在类定义中使用严格模式,并禁止使用`callee`:
class Animal { constructor (theName) { this.name = theName; this.numberOfLegs = 4; if (this.constructor === Animal) { console.log("In Animal is "); this.init(); } else { console.log("Skipping Animal init() because inherited"); } } init() { console.log("the Animal init() called"); this.aboutMe = "I'm " + this.name + " with " + this.numberOfLegs + " legs"; } } class Bird extends Animal { constructor (theName) { super(theName); this.numberOfLegs = 2; if (this.constructor === Bird) { console.log("In Bird is "); this.init(); } else { console.log("Skipping Bird init() because inherited"); } } init() { super.init(); console.log("and also some additionals in the Bird init() called"); } } class CityBird extends Bird { constructor (theName) { super(theName); this.numberOfLegs = 1; if (this.constructor === CityBird) { console.log("In CityBird is "); this.init(); } else { console.log("Skipping CityBird init() because inherited"); } } init() { super.init(); console.log("and also some additionals in the CityBird init() called"); } } var bird = new CityBird('Bimbo'); console.log(bird.aboutMe);