Простой способ создания класса

Из блога Джона Ресига:

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