Статьи

Сопоставление классов CoffeeScript и прототипов JavaScript

У CoffeeScript есть классы, но поскольку CoffeeScript — это просто JavaScript, откуда эти классы? В этой статье мы разбираем код JavaScript, который выводится из класса CoffeeScript и его подкласса, чтобы увидеть, как именно происходит волшебство.

Предупреждение: впереди JavaScript

Эта статья включает в себя довольно продвинутый JavaScript. У нас не будет времени подробно объяснить каждую конструкцию. Это также предполагает, что вы прочитали мою предыдущую статью о прототипах и что вы понимаете классы CoffeeScript. Конечно, вы можете прекратить читать прямо сейчас и продолжать писать код в невежестве, точно так же, как вы можете есть, не зная ничего о своем желудке. Но на самом деле, вы должны остаться и узнать о беспорядочных внутренностях того, что вы используете.

Declassing

Возьмите следующий CoffeeScript:

class Bourgeoisie constructor: (@age, @privilegeConstant) -> 

Предыдущий код переводится в этот JavaScript:

 var Bourgeoisie; Bourgeoisie = (function() { function Bourgeoisie(age, privilegeConstant) { this.age = age; this.privilegeConstant = privilegeConstant; } return Bourgeoisie; })(); 

Внешней переменной Bourgeoisie присваивается IIFE , которая по сути является конструкцией, используемой для управления областью действия. Шаблон для IIFE показан ниже.

 (function(){ //lots of code return result })(); 

Только вещи, которые возвращаются, когда-либо попадают во внешний мир. В данном случае это внутренняя функция конструктора Bourgeoisie которая возвращается. Функция конструктора присоединяет свойства к создаваемому экземпляру. Когда он возвращается, конструктор присваивается внешней переменной Bourgeoisie . Далее мы добавим следующие функции.

 class Bourgeoisie constructor: (@age, @privilegeConstant) -> worry: -> console.log("My stocks are down 1%!") profit: (hardWork, luck) -> return (@age - 23) * hardWork * (luck + @privilegeConstant) 

Это переводит на следующий JavaScript.

 var Bourgeoisie; Bourgeoisie = (function() { function Bourgeoisie(age, privilegeConstant) { this.age = age; this.privilegeConstant = privilegeConstant; } Bourgeoisie.prototype.worry = function() { return console.log("My stocks are down 1%!"); }; Bourgeoisie.prototype.profit = function(hardWork, luck) { return (this.age - 23) * hardWork * (luck + this.privilegeConstant); }; return Bourgeoisie; })(); 

Обратите внимание, что мы используем свойство prototype конструктора, чтобы добавить больше функций. При этом функция __proto__ свойство __proto__ каждого экземпляра, чтобы ее можно было использовать по желанию. Таким образом, когда мы создаем новый экземпляр Bourgeoisie , переменные age и privilegeConstant помещаются в экземпляр, а функции worry() и profit() помещаются в прототип экземпляра. Используя этот пример в качестве родительского класса, давайте рассмотрим наследование.

наследование

Возьмите следующий класс Senator , который унаследован от Bourgeoisie . Обратите внимание, что код для Bourgeoisie не включен, потому что он не изменился.

 class Senator extends Bourgeoisie worry: -> console.log("My polls are down 1%!") 

Теперь давайте посмотрим, как этот простой класс выглядит в JavaScript.

 var Senator, __hasProp = {}.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) { child[key] = parent[key]; } } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; Senator = (function(_super) { __extends(Senator, _super); function Senator() { return Senator.__super__.constructor.apply(this, arguments); } Senator.prototype.worry = function() { return console.log("My polls are down 1%!"); }; return Senator; })(Bourgeoisie); 

Святая корова. Давайте сделаем это один шаг за один раз. Следующий код объявляет переменную Senator и создает ярлык для hasOwnProperty() .

 var Senator, __hasProp = {}.hasOwnProperty, 

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

 __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) { child[key] = parent[key]; } } ... 

Этот следующий кусок сложнее разобрать. Сначала мы создаем функцию с именем ctor() которая сначала содержит только функцию конструктора. Затем мы назначаем prototype этой функции конструктора parent , а prototype дочернего элемента — новому экземпляру конструктора.

 ... function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); ... 

Уф! Что это нам дает? Итак, прототип конструктора действует как родительский класс, что означает, что экземпляр будет иметь свойство __proto__ содержащее все свойства родительского класса. Это не слишком сложно, если вы следили за обсуждением в моем первом объяснении прототипов. Запутанная часть — казалось бы, бесконечный регресс прототипа и конструктора.

Видите ли, у ctor() есть свойство конструктора child , которое имеет новый экземпляр ctor() качестве своего прототипа. Это дает нам child.prototype.constructor = child . Если вы изучите это в Chrome Dev Tools, вы получите бесконечный регресс. К счастью, это, похоже, не влияет на производительность, но это все еще немного сбивает с толку архитектуру.

К счастью, последний кусок (показанный ниже) намного проще. __super__ присваивается атрибут __super__ , которому назначается prototype родительского __super__ . Это то, что наша реализация прототипического наследования не может легко воспроизвести, и это будет очень полезно, когда вы хотите определить новую функцию для дочернего элемента, но при этом ссылаться на версию функции родительского элемента. Мы увидим это в коде Senator .

 ... child.__super__ = parent.prototype; return child; }; 

Наконец, мы возвращаем child . Чтобы было ясно, это определение класса (или прототип) для child , а не конкретного экземпляра. Код, который мы только что обсудили, создается один раз, а затем используется для каждого наследования.

Наследие сенатора

Следующий раздел кода относится к наследованию Senator . Обратите внимание, что структура IIFE была изменена, чтобы принять аргумент. _super аргумент — Bourgeoisie , которая упоминается как _super в IIFE. Кроме того, возвращаемый Senator назначается Senator за пределами IIFE.

 Senator = (function(_super) { __extends(Senator, _super); function Senator() { return Senator.__super__.constructor.apply(this, arguments); } Senator.prototype.worry = function() { return console.log("My polls are down 1%!"); }; return Senator; })(Bourgeoisie); 

Первое, что мы делаем в блоке кода, это вызов __extends() , который принимает в качестве аргументов Senator (дочерний _super ) и _super (родительский). Функция worry() определяется здесь обычным способом, перезаписывая версию родителя. Функция profit() находится в Bourgeoisie и поэтому наследуется через __proto__ . Более интересной является функция конструктора, которую мы сейчас рассмотрим.

Построение новых экземпляров

Конструктор для Senator показан ниже.

 function Senator() { return Senator.__super__.constructor.apply(this, arguments); } 

Чтобы это было легче понять, рассмотрим следующее функционально эквивалентное утверждение. Этот код просто вызывает функцию конструктора в родительском прототипе, используя переданные аргументы. Первое определение, созданное CoffeeScript, делает то же самое, но с обобщенным числом аргументов.

 function Senator(age, privilegeConstant){ return Senator.__super__.constructor(age, privilegeConstant); } 

Переменная arguments в JavaScript помещает все аргументы, передаваемые функции, в массив, подобный объекту, даже если они не указаны явно в определении функции. Другой трюк JavaScript, который мы используем, — это функция apply() . apply() позволяет вам указать аргументы функции, а также значение this . Таким образом, мы берем произвольное количество аргументов и передаем их все в функцию-конструктор родительского прототипа. Чтобы передать произвольное количество аргументов, мы используем функцию apply() .

Вывод

Мы увидели, как классы CoffeeScript создаются и расширяются, изучая сгенерированный код JavaScript. Мы также рассмотрели все основные функции классов. Просто помните, что следующая официальная версия JavaScript будет включать собственную реализацию классов. Они будут компилироваться в прототипы способом, аналогичным (но не идентичным) тому, как классы CoffeeScript компилируются в прототипы. Будьте на связи.