Пост « Классы в Coffeescript » содержит интересное сопоставление кода CoffeeScript и JavaScript, на который он переведен. В этом посте более подробно рассматривается результат перевода, который хорошо иллюстрирует, как работает подклассы в JavaScript. Чтобы понять следующее, вы должны быть знакомы с прототипным наследованием JavaScript (объяснено здесь ).
Перевод
Приведен следующий код CoffeeScript суперкласса Ninja и подкласса Ronin.
class Ninja
constructor: (@numOfShurikens) ->
throwShuriken: ->
@numOfShurikens--
class Ronin extends Ninja
constructor: (numOfShurikens) ->
super numOfShurikens+1 # ronins know to carry a spare
Создание подклассов в JavaScript немного утомительно, и CoffeeScript помогает. Приведенный выше код переводится на следующий JavaScript.
var Ninja, Ronin;
var __hasProp = Object.prototype.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;
};
Ninja = (function() {
function Ninja(numOfShurikens) {
this.numOfShurikens = numOfShurikens;
}
Ninja.prototype.throwShuriken = function() {
return this.numOfShurikens--;
};
return Ninja;
})();
Ronin = (function() {
__extends(Ronin, Ninja);
function Ronin(numOfShurikens) {
Ronin.__super__.constructor.call(this, numOfShurikens + 1);
}
return Ronin;
})();
Понимание сгенерированного кода
Давайте разберем код и объясним каждый фрагмент:
var Ninja, Ronin;
Объявление переменных сейчас и назначение их позже делает подъем [3] явным. Я не вижу преимущества в объявлении двух переменных позже, когда выполняется каждое из назначений.
var __hasProp = Object.prototype.hasOwnProperty,
Ярлык для hasOwnProperty немного увеличивает производительность, потому что разыменование происходит реже. С другой стороны, для этого не нужно тратить глобальную переменную, поэтому, вероятно, было бы достаточно поместить ее непосредственно перед циклом for.
__extends = function(child, parent) {
// (1)
for (var key in parent) {
if (__hasProp.call(parent, key)) {
child[key] = parent[key];
}
}
// (2)
function ctor() { this.constructor = child; }
ctor.prototype = parent.prototype;
child.prototype = new ctor;
// (3)
child.__super__ = parent.prototype;
return child;
};
Помните, что функция конструктора C слабо соответствует классу в том смысле, что она содержит имя типа и что она используется для создания новых экземпляров. Однако методы экземпляра (которые являются общими для всех экземпляров) помещаются в C.prototype. Когда вы создаете подкласс, вы должны учитывать два уровня: с одной стороны, методы класса C.classMethod (), с другой стороны, методы экземпляра C.prototype.instanceMethod (). Имейте в виду, что свойство prototype в C имеет только шляпное имя, потому что оно станет прототипом объектов, созданных с помощью new C (). Таким образом, C.prototype является
не прототипом функции C, а скорее свойством объекта функции. Шаги, выполненные выше:
- Скопируйте методы класса из суперкласса в подкласс. Другого способа наследования таких методов в JavaScript нет.
- Убедитесь, что у объекта child.prototype есть прототип parent.prototype. Вы делаете это, создавая временный конструктор ctor и давая ему правильное значение для ctor.prototype. Фрагмент кода эквивалентен следующему коду ECMAScript 5 [1]:
child.prototype = Object.create(parent.prototype); child.prototype.constructor = child;Свойство конструктора [2] правильно установлено в прототипе по умолчанию. Поскольку мы назначаем наш собственный объект-прототип, нам нужно настроить это свойство вручную.
- Учитывая суперкласс Super и субкласс Sub, вы обычно должны ссылаться на Super по имени в методах Sub. Назначая Super.prototype для Sub .__ super__, вы избегаете такого рода жесткого кодирования. Для этого функция конструктора Ronin () использует Ronin .__ super__.
Ninja = (function() {
function Ninja(numOfShurikens) {
this.numOfShurikens = numOfShurikens;
}
Ninja.prototype.throwShuriken = function() {
return this.numOfShurikens--;
};
return Ninja;
})();
Обычное определение класса JavaScript, заключенное в IIFE [3]. Здесь нет никакой пользы от использования IIFE, кроме одного назначения (что имеет значение для литерала объекта, но не здесь). Обратите внимание, что идентификатор Ninja в первой строке — это переменная, отличная от идентификатора Ninja внутри IIFE (последующие строки).
Ronin = (function() {
__extends(Ronin, Ninja);
function Ronin(numOfShurikens) {
Ronin.__super__.constructor.call(this, numOfShurikens + 1);
}
return Ronin;
})();
Опять же, IFEE не нужен. Функция __extends (), которая была определена ранее, используется для выполнения всех необходимых соединений от подкласса до суперкласса. Предостережение: первый аргумент относится к функции Ronin () ниже, потому что эта функция
поднята [3] (грубо говоря: переместилась на вершину окружающей функции). Было бы лучше вызвать __extends () после объявления Ronin (), чтобы сделать этот факт явным. Вызов супер-конструктора является типичной техникой JavaScript: вы вызываете супер-конструктор как функцию (не новая!) И присваиваете ему текущее значение this, так что он добавляет свои свойства, но не создает новый экземпляр.
Связанное чтение
- Что нового в ECMAScript 5
- Что случилось со свойством «конструктор» в JavaScript?
- Область видимости переменной JavaScript и ее подводные камни [объясняет IIFE и подъем]
- Облегченные API наследования JavaScript [различные способы упрощения наследования JavaScript]
- CoffeeScript против JavaScript без паренов