Понимание функции __extends, сгенерированной машинописным текстом?

Я играю с Typescript и пытается понять скомпилированный код Javascript, сгенерированный компилятором.

Код машинописного текста:

class A { }
class B extends A { }

Сгенерированный код Javascript:

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

Наследование Javascript в соответствии с документами Mozilla таково:

B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;

Части, которые я не понимаю в сгенерированном коде Typescript, это

1. Какова цель этой строки? Похоже, он копирует все ключи A в B? Это какой-то хак для статических свойств?

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. Что это делает?

function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());

Я не понимаю эту часть: (__.prototype = b.prototype, new __())

Почему функция B() возвращает это?

return _super !== null && _super.apply(this, arguments) || this;

Если кто-то может объяснить мне это построчно, я был бы признателен.


person supersan    schedule 30.08.2017    source источник
comment
Документы Mozilla, которые вы цитируете, предназначены для наследования на основе прототипа, единственного такого варианта до недавнего времени (ES6). Используя class, вы выбираете объекты на основе классов. Все это необходимо для реализации наследования классов в терминах объектов на основе прототипов. Функция появляется, когда целевая версия JavaScript/ES не имеет встроенной поддержки для них, в противном случае она просто выдает class A {} и так далее.   -  person Jesper    schedule 30.08.2017
comment
Хорошо, это имеет смысл. Значит ли это, что код Mozilla делает все, что делает код, сгенерированный Typescript (или он делает что-то еще)? Также был бы признателен за объяснение загадочного кода, даже если он устарел для учебных целей.   -  person supersan    schedule 30.08.2017
comment
Он не устарел, это просто две разные модели. Например, я думаю, что у объектов класса все еще есть прототипы. Вам не нужно понимать каждую строку этого кода, но этого достаточно, чтобы реализовать модель класса с точки зрения модели прототипа и поддерживать некоторые особенности (например, конструкторы, вызывающие базовый конструктор (в чем заключается super) и даже разрешено возвращать другой объект в некоторых случаях). Причина, по которой он не выглядит чистым, заключается в том, что это не так — ему приходится подделывать что-то, что еще не встроено в язык для вашей версии.   -  person Jesper    schedule 30.08.2017
comment
@Jesper class по-прежнему использует наследование прототипов.   -  person Bergi    schedule 28.09.2017


Ответы (2)


Мне самому было любопытно, и я не мог найти быстрого ответа, поэтому вот моя разбивка:

Что он делает

__extends — это функция, которая имитирует наследование одного класса в объектно-ориентированных языках и возвращает новую функцию-конструктор для производной функции, которая может создавать объекты, наследуемые от базового объекта.

Примечание 1:

На самом деле я сам не знал об этом, но если вы сделаете что-то вроде следующего, где все значения верны, переменной будет присвоено значение последнего тестируемого элемента, если только один из них не является ложным, и в этом случае переменная устанавливается в значение 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;

Я упоминаю об этом, потому что это то, что больше всего смутило меня, когда я увидел этот javascript, и он использовался пару раз в определении функции __extends.

Примечание 2. Параметр d (вероятно, означает производный) и параметр b (вероятно, означает базовый) являются функциями-конструкторами, а не объектами-экземплярами.

Примечание 3:

prototype — это свойство функции и объект-прототип, используемый функциями «конструктора» (то есть объектами, созданными с использованием new <function name>()).

Когда вы используете оператор new для создания нового объекта, внутреннее [[PROTOTYPE]] нового объекта, также известное как __proto__, устанавливается в качестве свойства прототипа функции.

function Person() {  
}

// Construct new object 
var p = new Person();

// true
console.log(p.__proto__ === Person.prototype);

// true
console.log(Person.prototype.__proto__ === Object.prototype);

Это не копия. Это и есть объект.

Когда вы создаете буквальный объект, например

var o = {};

// true    
console.log(o.__proto__ === Object.prototype);

__proto__ нового объекта устанавливается равным Object.prototype (встроенная функция конструктора объекта).

Вы можете установить __prototype__ объекта на другой объект, используя Object.create.

Когда свойство или метод не найдены в текущем объекте, проверяется [[PROTOTYPE]] объекта. Если он не найден, то проверяется прототип ЭТОГО объекта. И так он проверяет прототипы, пока не достигнет конечного объекта-прототипа, Object.prototype. Имейте в виду, ничто не является копией.

Примечание 4 При моделировании наследования в Javascript устанавливаются прототипы функций-конструкторов.

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

Обратите внимание, как мы указываем конструктору Girl, потому что конструктор Person указывает на встроенный Function.

Вы можете увидеть приведенный выше код в действии по адресу: http://jsbin.com/dutojo/1/edit?js,console

Оригинал:

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 __extends = (this && this.__extends) || (function () {
   // gobbledygook
})();

Помня о моем Примечании 1 выше, эта первая часть (и конец) создает переменную с именем __extends, предназначенную для удержания функции для установки прототипа производного учебный класс.

(this && this.__extends) 

делает то, что объясняет мое Примечание 1. Если this соответствует действительности и this.__extends соответствует действительности, то переменная __extends уже существует и поэтому устанавливается в существующий экземпляр самой себя. Если нет, то устанавливается то, что идет после || который является iife (немедленно вызываемым функциональным выражением).

А теперь тарабарщина, которая является фактическим определением __extends:

var extendStatics = Object.setPrototypeOf ||

Переменной с именем extendStatics присваивается либо встроенная функция Object.setPrototypeOf среды, в которой выполняется скрипт (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/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]; };

В Примечании 3 я обсуждал __proto__, также известный как [[PROTOTYPE]], и то, как его можно установить. Код

{ __proto__: [] } instanceof Array

— это тест, позволяющий определить, позволяет ли текущая среда установить это свойство, путем сравнения набора __proto__ литерального объекта с литеральным массивом с помощью встроенной функции Array.

Возвращаясь к моему Примечанию 1 выше и помня, что оператор instanceof javascript возвращает значение true или false (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof), если среда оценивает объект с его свойством прототипа, установленным на встроенный массив, тогда для extendsStatics установлено значение

function (d, b) { d.__proto__ = b; })

Если среда не оценивает это таким образом, тогда для параметра extendStatics установлено следующее:

function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }

Это происходит потому, что __proto__ никогда не входил в официальный стандарт ECMAScript до ECMAScript 2015 (и согласно https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto) только для обратной совместимости). Если это поддерживается, используется функция __proto__, иначе используется версия «свернуть свою собственную», которая выполняет копирование из объекта b в d для определенных пользователем свойств.

Теперь, когда функциональная переменная extendStatics определена, возвращается функция, которая вызывает все, что находится внутри extendStatics (а также некоторые другие вещи). Обратите внимание, что параметр «d» — это подкласс (тот, который наследуется), а «b» — это суперкласс (тот, от которого наследуется):

return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };

Разбивая его на части, вызывается метод extendStatics, и прототип первого объекта параметра (d) имеет значение (b) (вспомните Примечание 3 выше):

extendStatics(d, b);

В следующей строке объявляется функция-конструктор с именем '__', которая присваивает своему конструктору производную (d) функцию-конструктор:

function __() { this.constructor = d; }

если базовая (b) функция constructor окажется нулевой, это гарантирует, что производная функция сохранит свою собственную prototype.

Из https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/constructor, Object.prototype.constructor (все объекты являются объектами в javascript):

Возвращает ссылку на функцию конструктора объекта, которая создала экземпляр объекта. Обратите внимание, что значение этого свойства является ссылкой на саму функцию, а не на строку, содержащую имя функции.

А также

Все объекты будут иметь свойство конструктора. Объекты, созданные без явного использования функции-конструктора (то есть литералы объекта и массива), будут иметь свойство конструктора, указывающее на тип конструктора Fundamental Object для этого объекта.

Таким образом, если constructor функция '__' была обновлена ​​как есть, она создала бы производный объект.

Наконец, есть эта строка:

d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());

который устанавливает prototype производного (d) как новый пустой объект, если базовая функция constructor оказывается нулевой

//  b is null here so creates {}
Object.create(b)

OR

устанавливает prototype функции __ constructor в качестве базового класса prototype, а затем вызывает __(), что приводит к установке производных функций constructor в качестве производных функций.

(__.prototype = b.prototype, new __()

Таким образом, возвращаемая конечная функция создает производную функцию-конструктор, которая прототипически наследуется от базовой функции-конструктора.

Почему функция B() возвращает это?

return _super !== null && _super.apply(this, arguments) || this;

Согласно: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply

Метод apply() вызывает функцию с заданным значением this и аргументами, представленными в виде массива (или объекта, подобного массиву).

Помните, что B — это функция-конструктор, и именно это возвращается в определении B.

Если бы у вас был класс Person (функция-конструктор), который принимал параметр имени в функции-конструкторе, вы могли бы вызвать производный класс Girl (функция-конструктор) с именем девушки в качестве параметра.

// 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);
person Mike Cheel    schedule 27.09.2017
comment
Вау, спасибо за подробное объяснение. Это действительно помогло мне ясно понять это, и я также узнал одну или две вещи в процессе. Я добавляю это в комментарии для всех, кто читает ваш пост: - person supersan; 01.10.2017
comment
{ __proto__: [] } instanceof Array - это устанавливает прототип пустого объекта в новый массив, а затем проверяет, является ли созданный таким образом пустой объект экземпляром массива? Это была самая сложная часть всего этого, потому что, как вы объяснили, может быть случай, когда proto тоже может не работать! - person supersan; 01.10.2017
comment
когда вы устанавливаете var a = ((function(){console.log(123);})(), 2), он отбрасывает первый аргумент и устанавливает a = 2 (хотя он все еще выполняет первый аргумент). Это то, что происходит и при настройке d.prototype. - person supersan; 01.10.2017
comment
Я обновил свой ответ разъяснениями и некоторыми исправлениями. - person Mike Cheel; 04.10.2017
comment
Помимо подробного объяснения выше, в этой статье есть полезная дополнительная информация basarat.gitbooks.io/typescript/content/docs/classes-emit.html - person Diwaker Tripathi; 11.01.2019

Это может помочь понять, что на самом деле происходит в расширителе класса TypeScript. На самом деле следующий код содержит точно такую ​​же логику, так как его можно даже использовать как замену исходного кода без всех особых ухищрений, делающих его чрезвычайно трудным для чтения.

  • Первая половина только пытается найти совместимую с браузером версию для setPrototypeOf.
  • Вторая половина реализует наследование от базового класса

Попробуйте заменить функцию TypeScript __extends следующим кодом:

// 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
    }
  }
}

В следующей версии все, что делает его универсальным, было удалено, например, обработка совместимости браузера и «нулевые» производные. Я не призываю никого использовать следующий код в качестве постоянной замены, но приведенная ниже версия действительно демонстрирует самую суть того, как наследование классов работает с 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
}

Попробуйте следующий рабочий пример:

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'

И, наконец, когда вы устали учиться на запутанном механизме наследования TypeScript, я обнаружил, что функция __extend даже не нужна, просто позволив нативной функции JavaScript «применить» работу, которая через Цепочка прототипов точно реализует наш целевой механизм наследования.

Попробуйте этот последний пример... и забудьте обо всем остальном, или я что-то пропустил?

// 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'

person Shrimpy    schedule 01.09.2019