[removed] class instance initialization and inheri

2019-06-02 03:05发布

问题:

Here is an example of class Animal and its child class Bird definition in JavaScript (using TypeScript):

class Animal {
    name: string;
    numberOfLegs: number = 4;
    aboutMe: string;
    constructor (theName: string) {
        this.name = theName;
        this.init();
    }
    init() {
        this.aboutMe = `I'm ${this.name} with ${this.numberOfLegs} legs`;
    }
}

class Bird extends Animal {
    numberOfLegs: number = 2;
    constructor (theName: string) {
        super(theName);
    }
}

var bird = new Bird('Bimbo');
console.log(bird.aboutMe);

The correct expected value of property bird.aboutMe would be I'm Bimbo with 2 legs, but in reality you will get I'm Bimbo with 4 legs. When you compile the above TypeScript code into pure JavaScript here it is quite obvious why this works incorrectly.

My question: How to properly write initialization logic of JavaScript classes so that it works also for inheritance and in a manner as we are used to in other OO languages? TypeScript tries to resolve this gap between JavaScript and other OO languages, but even in such an trivial case it fails. Am I missing something?

Just to prove that my expectation of correct result is valid I have rewritten the above code to PHP:

class Animal {
    protected $name;
    protected $numberOfLegs = 4;
    public $aboutMe;
    public function __construct ($theName) {
        $this->name = $theName;
        $this->init();
    }
    protected function init() {
        $this->aboutMe = "I'm {$this->name} with {$this->numberOfLegs} legs";
    }
}

class Bird extends Animal {
    protected $numberOfLegs = 2;
    public function __construct ($theName) {
        parent::__construct($theName);
    }
}

$bird = new Bird('Bimbo');
echo $bird->aboutMe;

The result echoed by the above PHP code is I'm Bimbo with 2 legs

EDIT 1: Of course I know how to make the above code work correctly. My need is not to make this trivial code work but to get a way to treat JS class instance initialization in such manner that it works correctly also in complex cases.

And maybe on account of TypeScript I would add "If TypeScript tries to look like C-style class definition then it would be highly appreciable that it also works like that". Is there a way to achieve this?

EDIT 2: Very nice general solution is proposed here below by Emil S. Jørgensen. This works even in case of longer inheritance chain (e.g. Bird extends Animal and CityBird extends Bird). I have added some more code to his answer to show that on each level you can reuse the parent (super) class init() and add your own initialization logic if needed:

/*
// TYPESCIPT 
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);
*/
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;
        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");
        }
    }
    Animal.prototype.init = function () {
        console.log("the Animal init() called");
        this.aboutMe = "I'm " + this.name + " with " + this.numberOfLegs + " legs";
    };
    return Animal;
}());
Animal._isInheritable = true;
var Bird = (function (_super) {
    __extends(Bird, _super);
    function Bird(theName) {
        var _this = _super.call(this, theName) || this;
        _this.numberOfLegs = 2;
        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");
        }
        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;
        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");
        }
        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);

Drawback of this solution is that you cannot to use it in 'use strict' mode as caller, callee, and arguments properties may not be accessed on strict mode (see).

EDIT 3: Strict mode and ES6 classes compatible solution (avoiding use of stric mode prohibited callee) is based on comparing this.construct and the class (function) itself (see). The init() is launched only if these both are equal - it means init() is called only in constructor of instantianized class. Here is rewritten code from EDIT 2:

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

This solution can be used also with new ES6 class syntax which forces strict mode in class definition and so prohibits use of 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);

回答1:

The easiest solution would be to call init from both constructors.

/*
class Animal {
	public name: string;
	public numberOfLegs: number = 4;
	public aboutMe: string;
	constructor(theName: string) {
		this.name = theName;
		this.init();
	}
	init() {
		console.log("init called");
		this.aboutMe = `I'm ${this.name} with ${this.numberOfLegs} legs`;
	}
}

class Bird extends Animal {
	public name: string;
	public numberOfLegs: number = 2;
	constructor(theName: string) {
		super(theName);
		this.init();
	}
}

var bird = new Bird('Bimbo');
console.log(bird.aboutMe);
*/
var __extends = (this && this.__extends) || (function() {
  var extendStatics = Object.setPrototypeOf ||
    ({
        __proto__: []
      }
      instanceof Array && function(d, b) {
        d.__proto__ = b;
      }) ||
    function(d, b) {
      for (var p in b)
        if (b.hasOwnProperty(p)) d[p] = b[p];
    };
  return function(d, b) {
    extendStatics(d, b);

    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;
    this.init();
  }
  Animal.prototype.init = function() {
    console.log("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;
    _this.init();
    return _this;
  }
  return Bird;
}(Animal));
var bird = new Bird('Bimbo');
console.log(bird.aboutMe);

JavaScript isn't like other OO languages in that you must respect the prototype chain, and the inherent object creation rules it implies.

If you need to test for inheritance, you can add a static property to your base class, and simply test if caller has inherited said static property:

/*
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) {
			this.init();
		} else {
			console.log("Skipped because inherited");
		}
	}
	init() {
		console.log("init called");
		this.aboutMe = `I'm ${this.name} with ${this.numberOfLegs} legs`;
	}
}

class Bird extends Animal {
	public name: string;
	public numberOfLegs: number = 2;
	constructor(theName: string) {
		super(theName);

		var isInheirited = (arguments.callee.caller != null ? arguments.callee.caller._isInheritable != void 0 : false);
		if (!isInheirited) {
			this.init();
		}
	}
}

var bird = new Bird('Bimbo');
console.log(bird.aboutMe);
*/

var __extends = (this && this.__extends) || (function() {
  var extendStatics = Object.setPrototypeOf ||
    ({
        __proto__: []
      }
      instanceof Array && function(d, b) {
        d.__proto__ = b;
      }) ||
    function(d, b) {
      for (var p in b)
        if (b.hasOwnProperty(p)) d[p] = b[p];
    };
  return function(d, b) {
    extendStatics(d, b);

    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;
    var isInheirited = (arguments.callee.caller != null ? arguments.callee.caller._isInheritable != void 0 : false);
    if (!isInheirited) {
      this.init();
    } else {
      console.log("Skipped because inherited");
    }
  }
  Animal.prototype.init = function() {
    console.log("init called");
    this.aboutMe = "I'm " + this.name + " with " + this.numberOfLegs + " legs";
  };
  return Animal;
}());
Animal._isInheritable = true;
var Bird = (function(_super) {
  __extends(Bird, _super);

  function Bird(theName) {
    var _this = _super.call(this, theName) || this;
    _this.numberOfLegs = 2;
    var isInheirited = (arguments.callee.caller != null ? arguments.callee.caller._isInheritable != void 0 : false);
    if (!isInheirited) {
      _this.init();
    }
    return _this;
  }
  return Bird;
}(Animal));
var bird = new Bird('Bimbo');
console.log(bird.aboutMe);



回答2:

in typescript, this

class Bird extends Animal {
    name: string;
    numberOfLegs: number = 2;
    constructor (theName: string) {
        super(theName);
    }
}

is equivalent to

class Bird extends Animal {
    name: string;
    numberOfLegs: number;
    constructor (theName: string) {
        super(theName);
        this.numberOfLegs = 2;
    }
} 

solution:

class Animal {
    name: string;
    numberOfLegs;
    aboutMe: string;
    constructor (theName: string, theLegs: number = 4) {
        this.name = theName;
        this.numberOfLegs = theLegs;
        this.init();
    }
    init() {
        this.aboutMe = `I'm ${this.name} with ${this.numberOfLegs} legs`;
    }
}

class Bird extends Animal {
    constructor (theName: string) {
        super(theName, 2);
    }
}

var bird = new Bird('Bimbo');
console.log(bird.aboutMe);

of course it is better to treat 'aboutMe' as a property:

class Animal {
    name: string;
    numberOfLegs;
    get aboutMe(): string {
        return `I'm ${this.name} with ${this.numberOfLegs} legs`;
    }
    constructor (theName: string, theLegs: number = 4) {
        this.name = theName;
        this.numberOfLegs = theLegs;
    }
}


回答3:

The correct expected value of property bird.aboutMe would be I'm Bimbo with 2 legs, but in reality you will get I'm Bimbo with 4 legs.

You are calling a function from the constructor of a base class. And you expect this function to observe values of properties assigned by the derived class constructor. However, the derived class constructor runs only after the base class constructor returns. Hence, your expectation is incorrect.