问题
I am playing with Typescript and trying to understand the compiled Javascript code generated by the compiler
Typescript code:
class A { }
class B extends A { }
Generated Javascript code:
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 A = /** @class */ (function () {
function A() {
}
return A;
}());
var B = /** @class */ (function (_super) {
__extends(B, _super);
function B() {
return _super !== null && _super.apply(this, arguments) || this;
}
return B;
}(A));
The Javascript inheritance as per Mozilla docs is this:
B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;
The parts that I do not understand in the Typescript's generated code are this
1. What is the purpose of this line? Looks like it is copying all the keys of A into B? Is this some sort of a hack for static properties?
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]; };
2. What is this doing?
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
I don't understand this part: (__.prototype = b.prototype, new __())
Why does function B() return this?
return _super !== null && _super.apply(this, arguments) || this;
If someone can explain this to me line by line I would be grateful.
回答1:
I was curious about this myself and couldn't find a quick answer so here is my breakdown:
What it does
__extends is a function that simulates single class inheritance in object oriented languages and returns a new constructor function for a derived function that can create objects that inherit from a base object.
Note 1:
I wasn't actually aware of this myself but if you do something like the following where all of the values are truthy the variable is set to the value of last item being tested unless one is falsy in which case the variable is set to false:
// value1 is a function with the definition function() {}
var value1 = true && true && function() {};
// value2 is false
var value2 = true && false && function() {};
// value3 is true
var value3 = true && function() {} && true;
I mention this because this is the thing that confused me the most when I saw this javascript and it is used a couple of times in the __extends function defintion.
Note 2: Parameter d (probably stands for derived) and b (probably stands for base) are both constructor functions and not instance objects.
Note 3:
prototype
is a property of a function and it is a prototype object used by 'constructor' functions (i.e. objects created by using new <function name>()
).
When you use the new
operator to construct a new object, the new object's internal [[PROTOTYPE]]
aka __proto__
is set to be the function's prototype property.
function Person() {
}
// Construct new object
var p = new Person();
// true
console.log(p.__proto__ === Person.prototype);
// true
console.log(Person.prototype.__proto__ === Object.prototype);
It isn't a copy. It IS the object.
When you create a literal object like
var o = {};
// true
console.log(o.__proto__ === Object.prototype);
the new object's __proto__
is set to Object.prototype
(the built-in Object constructor function).
You can set an object's __prototype__
to another object however using Object.create
.
When a property or method isn't found on the current object the object's [[PROTOTYPE]]
is checked. If it isn't found then THAT object's prototype is checked. And so it goes checking prototypes until it reaches the final prototype object, Object.prototype
. Keep in mind nothing is a copy.
Note 4 When simulating inheritance in Javascript the 'constructor' functions' prototypes are set.
function Girl() {
}
Girl.prototype = Object.create(Person.prototype);
// true
console.log(Girl.prototype.__proto__ === Person.prototype);
// true
console.log(Girl.constructor === Function);
// Best practices say reset the constructor to be itself
Girl.constructor = Girl;
// points to Girl function
console.log(Girl.constructor);
Notice how we point the constructor to Girl because it Person's constructor points to the built-in Function
.
You can see the code above in action at: http://jsbin.com/dutojo/1/edit?js,console
Original:
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 __());
};
})();
The break down:
var __extends = (this && this.__extends) || (function () {
// gobbledygook
})();
Keeping my Note 1 above in mind, this first part (and end) is creating a variable called __extends that has the intention of holding a function to set the prototype of the derived class.
(this && this.__extends)
is doing what my Note 1 explains. If this is truthy and this.__extends is truthy then the variable __extends is already exists and so set to the existing instance of itself. If not it is set to what comes after the || which is an iife (immediately invoked function expression).
Now for the gobbledygook which is the actual definition of __extends:
var extendStatics = Object.setPrototypeOf ||
A variable named extendStatics is set to either the built in Object.setPrototypeOf function of the environment that the script is running in (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf)
OR
it creates its own version
({ __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]; };
In Note 3 I discussed __proto__
aka [[PROTOTYPE]]
and how it could be set. The code
{ __proto__: [] } instanceof Array
is a test to determine whether the current environment allows setting this property by comparing a literal object's __proto__
set to a literal array with the Array built-in function.
Referring back to my Note 1 from above and keeping in mind that the javascript instanceof operator returns true or false (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof) if the environment evaluates an an object with its prototype property set to the built in Array then extendsStatics is set to
function (d, b) { d.__proto__ = b; })
If the environment doesn't evaluate it that way then extendStatics is set to this:
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }
It does this because __proto__
was never part of the official ECMAScript standard until ECMAScript 2015 (and according to https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto) is there only for backwards compatibility). If it is supported the __proto__
function is used or else it uses the 'roll your own' version' which does a copy from object b to d for user defined properties.
Now that the extendStatics function variable defined, a function that calls whatever is inside extendStatics (as well as some other stuff) is returned. Note that parameter 'd' is the sub-class (the one inheriting) and 'b' is the super-class (the one being inherited from):
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
Breaking it down extendStatics is called and the first parameter object (d) has its prototype set to be (b) (recall Note 3 above):
extendStatics(d, b);
In the next line a constructor function named '__' is declared that assigns its constructor to be the derived (d) constructor function:
function __() { this.constructor = d; }
if the base (b) constructor
function happens to be null this will make sure that the derived will keep its own prototype
.
From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/constructor, the Object.prototype.constructor (all objects are Objects in javascript):
Returns a reference to the Object constructor function that created the instance object. Note that the value of this property is a reference to the function itself, not a string containing the function's name.
And
All objects will have a constructor property. Objects created without the explicit use of a constructor function (i.e. the object and array literals) will have a constructor property that points to the Fundamental Object constructor type for that object.
So if constructor
function '__' was new'd up as is it would create a derived object.
Lastly there is this line:
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
which sets the prototype
of the derived (d) to be a new empty object if the base constructor
function happens to be null
// b is null here so creates {}
Object.create(b)
OR
sets the __ constructor
function's prototype
to be the base class prototype
and then calls __() which has the effect of setting the derived functions constructor
to be the derived function.
(__.prototype = b.prototype, new __()
So basically the final function returned creates a derived constructor function that prototypically inherits from the base constructor function.
Why does function B() return this?
return _super !== null && _super.apply(this, arguments) || this;
According to: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply
The apply() method calls a function with a given this value, and arguments provided as an array (or an array-like object).
Remember B is a constructor function and that is what is being returned in B's definition.
If you had a Person class (constructor function) that accepted a name parameter in the constructor function you could then call a derived Girl class (constructor function) with the girls name as a parameter.
// Base constructor function
function Person(n) {
// Set parameter n to the name property of a Person
this.name = n;
}
function Girl() {
// Call the Person function with same arguments passed to new Girl
Person.apply(this, arguments);
// Set it so all Girl objects created inherit properties and methods from Person
Girl.prototype = Object.create(Person.prototype);
// Make sure the constructor is not set to Person
Girl.prototype.constructor = Girl;
}
var p = new Person("Sally");
var g = new Girl("Trudy");
console.log(p.name);
console.log(g.name);
回答2:
This can help understand what is really going on in the TypeScript class extender. In fact, the following code contains exactly the same logic, as one can even use it as a substitute of the original code without all the special tricks that made it extremely difficult to read.
- The first half only tries to find a browser compatible version for 'setPrototypeOf'
- The second half implements inheritance from the base class
Try to substitute the TypeScript __extends function by the following code:
// refactored version of __extends for better readability
if(!(this && this.__extends)) // skip if already exists
{
var __extends = function(derived_cl, base_cl) // main function
{
// find browser compatible substitute for implementing 'setPrototypeOf'
if(Object.setPrototypeOf) // first try
Object.setPrototypeOf(derived_cl, base_cl);
else if ({ __proto__: [] } instanceof Array) // second try
derived_cl.__proto__ = base_cl;
else // third try
for (var p in base_cl)
if (base_cl.hasOwnProperty(p)) derived_cl[p] = derived_cl[p];
// construct the derived class
if(base_cl === null)
Object.create(base_cl) // create empty base class if null
else
{
var deriver = function(){} // prepare derived object
deriver.constructor = derived_cl; // get constructor from derived class
deriver.prototype = base_cl.prototype; // get prototype from base class
derived_cl.prototype = new deriver(); // construct the derived class
}
}
}
In the following version, all the stuff that makes it general-purpose was taken out, such as browser compatibility handling and 'null' derivatives. I would not encourage anyone to put the following code as a permanent substitute, but the version below really demonstrates the bare essence of how class inheritance works with TypeScript.
// Barebone version of __extends for best comprehension
var __extends = function(derived_cl,base_cl)
{
Object.setPrototypeOf(derived_cl,base_cl);
var deriver = function(){} // prepare derived object
deriver.constructor = derived_cl; // get constructor from derived class
deriver.prototype = base_cl.prototype; // get prototype from base class
derived_cl.prototype = new deriver(); // construct derived class
}
Try the following working example:
var __extends = function(derived_cl,base_cl)
{
Object.setPrototypeOf(derived_cl,base_cl);
var deriver = function(){} // prepare derived object
deriver.constructor = derived_cl; // get constructor from derived class
deriver.prototype = base_cl.prototype; // get prototype from base class
derived_cl.prototype = new deriver(); // construct derived class
}
// define the base class, and another class that is derived from base
var Base = function()
{
this.method1 = function() { return "replace the batteries" }
this.method2 = function() { return "recharge the batteries" }
}
var Derived = function(_super) {
function Derived() {
__extends(this, _super); _super.apply(this, arguments);
this.method3 = function() { return "reverse the batteries" }
this.method4 = function() { return "read the damn manual" }
}
return Derived
}(Base)
// Let's do some testing: create the objects and call their methods
var oBase = new Base(); // create the base object
var oDerived = new Derived(); // create the derived object
console.log(oDerived.method2()); // result: 'recharge the batteries'
console.log(oDerived.method4()); // result: 'read the damn manual'
console.log(oBase.method1()) ; // result: 'replace the batteries'
try{ console.log(oBase.method3()) }
catch(e) {console.log(e.message)}; // result: 'oBase.method3 is not a function'
And finally, when you are tired learning from the obfuscated inheritance mechanism of TypeScript, I discovered that the __extend function is not even necessary, just by letting the native JavaScript function 'apply' do the work, which through prototype chaining exactly implements our aimed inheritance mechanism.
Try this last example ...and forget everything else, or did I miss something ?
// Short, readable, explainable, understandable, ...
// probably a 'best practice' for JavaScript inheritance !
var Base = function()
{
this.method1 = function() { return "replace the batteries" }
this.method2 = function() { return "recharge the batteries" }
}
var Derived = function(){
Base.apply(this, arguments); // Here we inherit all methods from Base!
this.method3 = function() { return "reverse the batteries" }
this.method4 = function() { return "read the damn manual" }
}
var oDerived = new Derived(); // create the derived object
console.log(oDerived.method2()); // result: 'recharge the batteries'
来源:https://stackoverflow.com/questions/45954157/understanding-the-extends-function-generated-by-typescript