简单的“类”实例化

时间:2011-10-25 16:54:54

标签: javascript oop

From John Resig blog

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

argsarguments之间有什么区别? args.callee可以false吗?

5 个答案:

答案 0 :(得分:22)

你写的是现有的答案没有足够的细节,但即使在阅读了你的具体问题之后,我也不能完全确定代码的哪些方面会让你陷入困境 - 它有许多棘手的部分 - 所以如果这个答案对你已经理解的事情的详细信息过分了,我会提前道歉!

由于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,因此我们可以用{{1}替换它的所有实例给出这个:

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一如既往地是一个类似于数组的对象,其中包含所有<{em>} arguments的论点。

只有当“class”在其原型中有一个名为MyClass的函数(它将是“构造函数”)时才会询问您所询问的行,所以让我们给它一个:

init

一旦我们完成了这个,请考虑一下:

MyClass.prototype.init =
  function (prop)
  {
    this.prop = prop;
  };

在对var myInstance1 = new MyClass('value'); 的调用中,MyClass将引用正在构建的对象,因此this将为真。 this instanceof MyClass将是真的,因为我们将typeof this.init == "function"作为一个函数。所以我们达到了这条线:

MyClass.prototype.init

此处this.init.apply( this, args.callee ? args : arguments ); 等于args(第一个参数),因此它是一个字符串,因此它没有'value'属性;所以callee未定义,在布尔上下文中表示它是假的,因此args.callee等同于args.callee ? args : arguments。因此,上述内容相当于:

arguments

相当于:

this.init.apply(this, arguments);

(如果您还不知道this.init('value'); 如何运作,以及它与apply的区别,请参阅https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/apply)。

到目前为止这是否有意义?

另一个需要考虑的案例是:

call

在对var myInstance2 = MyClass('value'); 的调用中,MyClass将引用全局对象(通常为this),因此window将为false,因此我们到达此行:< / p>

this instanceof MyClass

其中return new MyClass( arguments ); 是一个包含单个元素的类数组对象:arguments。请注意,'value'相同。

术语说明:因此,对new MyClass('value')的调用会导致第二次调用MyClass('value'),这次是MyClass。我将打电话给第一个电话(没有new)“外线电话”,第二个电话(用new)打电话给“内线电话”。希望这很直观。

在对new的内部调用中,MyClass现在引用外部调用的args对象:而不是argumentsargs,它现在是一个数组类似于包含'value'的对象。而'value'未定义args.callee,现在引用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的调用中将MyClass未定义,所以args.callee上的测试会引发错误。我认为这只是Resig的一个错误;无论如何,可以通过args && args.callee上的测试轻松修复。
  • 如果我们的构造函数的第一个参数碰巧实际上有一个名为callee的属性,那么args.callee上的测试将产生误报,并且错误的参数将被传递给构造函数。这意味着,例如,我们无法设计构造函数以将arguments对象作为其第一个参数。但这个问题似乎难以解决,而且可能不值得担心。

答案 1 :(得分:3)

@ruakh:很好的分析。在原问题和答案差不多两年之后,我仍然对此事感兴趣。我希望我的观察不完全是多余的。不过,它们相当冗长。可以证明一篇不错的独立博客文章: - )。

最后提到的John Resig原始代码的两个问题都可以使用私有标志解决,以区分你所谓的内部外部调用

// 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;
}

甚至可以避免进行内部调用,就像这样,这对性能也非常有利。当我们有一个Object.create的现代JavaScript时,我们可以简化如下:

// 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类型,然后应用它。

当我们想要向后兼容时,我们可以加载一个现有的polyfill,e。 G。来自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...
}

当您决定不使用“public static final”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(){});

看看这段代码,我们可能会感到非常困惑。 每个类构造函数的每个实例,我们将来使用不带new的直接构造函数调用创建将在技术上成为同一个虚拟实例构造函数,即我们作为参数传递给我们的包装函数的function(){}
这怎么可能有用呢? 当我要解释你已经知道的事情时,请原谅我。 秘密在于我们在
之前将Void的原型更改为constructor.prototype ,我们使用new来实例化它。此时,每个新对象都获得一个由[[Prototype]]非正式表示的内部属性,其值是构造函数的prototype属性的当前值。当稍后替换构造函数的prototype属性的值时,它对刚刚创建的对象没有进一步的影响。 请参阅:ECMA标准-262第5版的Section 13.2.2 [[构造]]。

因此,以下内容适用于我们使用此工具制作的所有“类”:

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

答案 2 :(得分:2)

  

args和参数之间有什么区别?

Arguments是javascript创建的类似数组的结构,包含所有传入的参数。

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可让您覆盖由javascript创建的默认arguments

答案 3 :(得分:0)

我相信在这一点上可以重写此功能以吸引ES5及更高版本中的严格模式。如果您有任何类型的linter查看您的代码,arguments.callee会给您一些问题。我相信代码可以重写如下(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");

如果我对这个课程的重新实现有误,那么我很高兴知道如何改进它。

答案 4 :(得分:-2)

关注问题的标题,而不是关于您的示例的具体问题:

我从来没有真正理解为什么他们需要像这样复杂化它。为什么不这样做呢?这是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's only here 
    // because it's more logical for someone who is used to constructors
    // the last row of SomeClass calls init(), that's 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());