Простой способ создания класса
Из блога Джона Ресига:
// makeClass - By John Resig (MIT Licensed)
function makeClass(){
return function(args){
if ( this instanceof arguments.callee ) {
if ( typeof this.init == "function" )
this.init.apply( this, args.callee ? args : arguments );
} else
return new arguments.callee( arguments );
};
}
особенно эта строка this.init.apply( this, args.callee ? args : arguments );
В чем разница между args
и arguments
? Может ли args.callee
быть false
?
Ответы
Ответ 1
Вы пишете, что в существующих ответах недостаточно подробностей, но даже после прочтения ваших конкретных вопросов я не совсем уверен, какие именно аспекты кода бросают вас за цикл - у него есть несколько сложных частей - поэтому я заранее извиняюсь, если этот ответ выходит за борт с подробностями о вещах, которые вы уже поняли!
Так как makeClass
всегда называется так же, его немного легче рассуждать, если мы удалим один уровень косвенности. Это:
var MyClass = makeClass();
эквивалентно этому:
function MyClass(args)
{
if ( this instanceof arguments.callee )
{
if ( typeof this.init == "function" )
this.init.apply( this, args.callee ? args : arguments );
}
else
return new arguments.callee( arguments );
}
Поскольку мы больше не имеем дело с анонимной функцией, нам больше не нужна нотация arguments.callee
: она обязательно относится к MyClass
, поэтому мы можем заменить все ее экземпляры на MyClass
, указав это:
function MyClass(args)
{
if ( this instanceof MyClass )
{
if ( typeof this.init == "function" )
this.init.apply( this, args.callee ? args : arguments );
}
else
return new MyClass( arguments );
}
где args
является идентификатором для первого аргумента MyClass
, а arguments
, как всегда, представляет собой массивный объект, содержащий все аргументы MyClass
.
Строка, о которой вы спрашиваете, достигается только в том случае, если в своем прототипе "конструктор" имеет функцию с именем init
, которая будет "конструктором" ), поэтому давайте ее:
MyClass.prototype.init =
function (prop)
{
this.prop = prop;
};
Как только мы это сделаем, рассмотрим следующее:
var myInstance1 = new MyClass('value');
Внутри вызова MyClass
, this
будет ссылаться на объект, который будет построен, поэтому this instanceof MyClass
будет true. И typeof this.init == "function"
будет истинным, потому что мы сделали функцию MyClass.prototype.init
. Итак, мы достигаем этой строки:
this.init.apply( this, args.callee ? args : arguments );
Здесь args
равен 'value'
(первый аргумент), поэтому это строка, поэтому она не имеет свойства callee
; поэтому args.callee
- undefined, который в булевом контексте означает, что он ложный, поэтому args.callee ? args : arguments
эквивалентен arguments
. Поэтому приведенная выше строка эквивалентна этому:
this.init.apply(this, arguments);
что эквивалентно этому:
this.init('value');
(если вы еще не знаете, как работает apply
и как он отличается от call
, см. https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/apply).
Означает ли это до сих пор?
Другим рассмотренным случаем является следующее:
var myInstance2 = MyClass('value');
Внутри вызова MyClass
, this
будет ссылаться на глобальный объект (обычно window
), поэтому this instanceof MyClass
будет ложным, поэтому мы дойдем до этой строки:
return new MyClass( arguments );
где arguments
- объект, подобный массиву, содержащий один элемент: 'value'
. Обратите внимание, что это не то же самое, что new MyClass('value')
.
Терминологическое примечание. Таким образом, вызов MyClass('value')
вызывает второй вызов MyClass
, на этот раз с new
. Я назову первый вызов (без new
) "внешний вызов", а второй вызов (с new
) - "внутренний вызов". Надеюсь, что интуитивно.
Внутри внутреннего вызова MyClass
, args
теперь ссылается на внешний вызов arguments
object: вместо args
, являющийся 'value'
, теперь он представляет собой объект, похожий на массив, содержащий 'value'
. И вместо args.callee
, являющегося undefined, теперь он относится к MyClass
, поэтому args.callee ? args : arguments
эквивалентен args
. Поэтому внутренний вызов MyClass
вызывает this.init.apply(this, args)
, что эквивалентно this.init('value')
.
Таким образом, тест на args.callee
предназначен для того, чтобы отличить внутренний вызов (MyClass('value')
→ new MyClass(arguments)
) от обычного прямого вызова (new MyClass('value')
). В идеале мы могли бы устранить этот тест, заменив эту строку:
return new MyClass( arguments );
с чем-то гипотетическим, который выглядел так:
return new MyClass.apply( itself, arguments );
но JavaScript не позволяет эту нотацию (или любую эквивалентную нотацию).
Вы можете видеть, кстати, что с кодом Resig есть несколько небольших проблем:
- Если мы определим конструктор
MyClass.prototype.init
, а затем мы создадим экземпляр "класса", написав var myInstance3 = new MyClass();
, тогда args
будет undefined внутри вызова MyClass
, поэтому тест на args.callee
вызовет ошибку. Я думаю, что это просто ошибка на Resig part; во всяком случае, он легко устанавливается путем тестирования на args && args.callee
.
- Если наш первый аргумент конструктора фактически имеет свойство с именем
callee
, тогда тест на args.callee
приведет к ложному положительному результату, и неправильные аргументы будут переданы в конструктор. Это означает, что, например, мы не можем создать конструктор, чтобы в качестве первого аргумента принять объект arguments
. Но эта проблема кажется трудной для работы, и, вероятно, ее не стоит беспокоиться.
Ответ 2
@ruakh: Отличный анализ. Почти через два года после первоначального вопроса и вашего ответа меня все еще привлекают к этому вопросу. Надеюсь, мои наблюдения не совсем лишние. Однако они довольно длинные. Обосновал бы хорошую автономную статью в блоге: -).
Обе проблемы с исходным кодом Джона Ресига, о которых вы упоминаете в конце, могут быть решены с использованием частного флага, чтобы отличить то, что вы называете внутренним от внешнего вызова.
// makeClass - By Hubert Kauker (MIT Licensed)
// original by John Resig (MIT Licensed).
function makeClass(){
var isInternal;
return function(args){
if ( this instanceof arguments.callee ) {
if ( typeof this.init == "function" ) {
this.init.apply( this, isInternal ? args : arguments );
}
} else {
isInternal = true;
var instance = new arguments.callee( arguments );
isInternal = false;
return instance;
}
};
}
Мы даже можем избавиться от использования arguments.callee
вообще, назначив анонимную функцию локальной переменной перед ее возвратом.
// makeClass - By Hubert Kauker (MIT Licensed)
// original by John Resig (MIT Licensed).
function makeClass(){
var isInternal;
var constructor = function(args){
if ( this instanceof constructor ) {
if ( typeof this.init == "function" ) {
this.init.apply( this, isInternal ? args : arguments );
}
} else {
isInternal = true;
var instance = new constructor( arguments );
isInternal = false;
return instance;
}
};
return constructor;
}
Можно даже избежать внутреннего вызова, как это, что также очень хорошо для производительности. Когда у нас есть современный JavaScript, который имеет Object.create
, мы можем упростить следующее:
// makeClass - By Hubert Kauker (MIT Licensed)
// original by John Resig (MIT Licensed).
function makeClass(){
var constructor = function(){
if ( this instanceof constructor ) {
if ( typeof this.init == "function" ) {
this.init.apply( this, arguments );
}
} else {
var instance = Object.create(constructor.prototype);
if ( typeof instance.init == "function" ) {
instance.init.apply( instance, arguments );
}
return instance;
}
};
return constructor;
}
Это не самое быстрое решение. Мы можем избежать поиска цепочек прототипов, начиная с объекта экземпляра, потому что мы знаем, что init
должен быть в прототипе.
Поэтому мы можем использовать оператор var init=constructor.prototype.init
для его получения, а затем проверить его на тип function
, а затем применить его.
Если мы хотим быть обратно совместимыми, мы можем либо загрузить один из существующих полиполков, например. г. из Mozilla Developer Network или используйте следующий подход:
// Be careful and check whether you really want to do this!
Function.VOID = function(){};
function makeClass(){
// same as above...
Function.VOID.prototype = constructor.prototype;
var instance = new Function.VOID();
// same as above...
}
Если вы решите отказаться от использования "публичного статического финала" Function.VOID
вы можете использовать объявление типа var VOID=function(){}
в верхней части makeClass
. Но это приведет к созданию частной функции внутри каждого конструктора классов, который вы собираетесь создать.
Мы также можем определить "статический" метод для нашей утилиты, используя makeClass.VOID=function(){}
.
Другой популярный шаблон - передать один экземпляр этой маленькой функции в makeClass
, используя сразу называемую оболочку.
// makeClass - By Hubert Kauker (MIT Licensed)
// original by John Resig (MIT Licensed).
var makeClass = (function(Void) {
return function(){
var constructor = function(){
var init=constructor.prototype.init,
hasInitMethod=(typeof init == "function"),
instance;
if ( this instanceof constructor ) {
if(hasInitMethod) init.apply( this, arguments );
} else {
Void.prototype = constructor.prototype;
instance = new Void();
if(hasInitMethod) init.apply( instance, arguments );
return instance;
}
};
return constructor;
};
})(function(){});
Глядя на этот код, мы можем смущаться. Каждый экземпляр каждого конструктора классов, который мы будем создавать в будущем с помощью прямого конструктора invokation без new
, будет технически быть экземпляром одного и того же конструктора void, а именно function(){}
, который мы передали в качестве аргумента нашей обертке функция.
Как это может работать?
Простите меня, когда я объясню то, что вы уже знаете.
Секрет заключается в том, что мы меняем прототип Void
на constructor.prototype
, прежде чем использовать new
для его создания. На этом этапе каждый новый объект получает внутреннее свойство, неофициально обозначаемое [[Prototype]]
, значение которого является текущим значением свойства прототипа конструктора. Когда значение свойства прототипа конструктора будет заменено позже, оно больше не влияет на наш только что созданный объект.
См. Раздел 13.2.2 [[Construct]] в ECMA Standard-262 5th Edition.
Поэтому для всех "классов", которые мы делаем с помощью этого инструмента, мы разработаем следующее:
var MyClass = makeClass();
var obj1 = new MyClass();
var obj2 = MyClass();
alert( obj1 instanceof MyClass ); // true
alert( obj2 instanceof MyClass ); // true
alert( obj1.constructor == MyClass ); // true
alert( obj2.constructor == MyClass ); // true
Ответ 3
Какая разница между аргументами и аргументами?
Аргументы - это структура, подобная массиву, созданная javascript, содержащая все переданные в paremeters.
Args является параметром самой функции.
Может ли args.callee когда-либо быть ложным?
Абсолютно,
function makeClass(){
return function(args){
if ( this instanceof arguments.callee ) {
if ( typeof this.init == "function" )
this.init.apply( this, args.callee ? args : arguments );
} else
return new arguments.callee( arguments );
};
}
var class = makeClass();
class({callee: false});
Итак, в приведенном выше примере:
function makeClass(){
return function(args){
if ( this instanceof arguments.callee ) {
if ( typeof this.init == "function" )
this.init.apply( this, args.callee ? args : arguments );
} else
return new arguments.callee( arguments );
};
}
возвращает следующую функцию, сохраненную в переменной class
function (args) {
if ( this instanceof arguments.callee ) {
if ( typeof this.init == "function" )
this.init.apply( this, args.callee ? args : arguments );
} else
return new arguments.callee( arguments );
}
поэтому, когда я вызываю class({args: false});
arguments.callee == makeClass
поэтому args
дает вам возможность переопределить значение по умолчанию arguments
, созданное javascript
Ответ 4
Я считаю, что в этот момент эту функцию можно переписать, чтобы обратиться к строгому режиму в ES5 и далее. arguments.callee
даст вам проблемы, если у вас есть какой-то linter, смотрящий на ваш код. Я считаю, что код можно переписать следующим образом (http://jsfiddle.net/skipallmighty/bza8qwmw/):
function makeClass() {
return function f(args) {
console.log(this)
if(this instanceof f){
if(typeof this.init === "function") {
this.init.apply(this, args);
}
} else {
return new f(arguments);
}
};
}
Вы можете создать наследование следующим образом:
var BaseClass = makeClass();
BaseClass.prototype.init = function(n){
console.log("baseClass: init:" + n);
}
var b = BaseClass("baseClass");
var SubClass = makeClass();
SubClass.prototype = Object.create(BaseClass.prototype);
SubClass.prototype.init = function(n) {
BaseClass.prototype.init.call(this,n); // calling super();
console.log("subClass: init:" + n);
}
var s = SubClass("subClass");
Если я ошибаюсь в переоценке этого класса, я был бы очень рад узнать, как я могу улучшить его.
Ответ 5
Следуя вашему названию вопроса, а не конкретному вопросу о вашем примере:
Я никогда не понимаю, почему они должны усложнять это. Почему бы просто не сделать это? Это лучший пример (по мне) "простого" экземпляра класса в js:
function SomeClass(argument1, argument2) {
// private variables of this object.
var private1, private2;
// Public properties
this.public1 = 4;
this.public2 = 10;
// Private method that is invoked very last of this instantionation, it only here
// because it more logical for someone who is used to constructors
// the last row of SomeClass calls init(), that the actual invokation
function init() {
}
// Another private method
var somePrivateMethod() {
// body here
}
// Public methods, these have access to private variables and other private methods
this.publicMethod = function (arg1, arg2) {
// arguments are only accessible within this method
return argument1 + argument2;
}
init();
}
// And then instantiate it like this:
var x = new SomeClass(1, 2);
// Arguments are always optional in js
alert(x.publicMethod());