Understanding the __extends function generated by typescript?

爷,独闯天下 提交于 2019-11-30 04:13:57

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

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'
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!