У 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 компилируются в прототипы. Будьте на связи.