【问题标题】:Simple “Class” Instantiation简单的“类”实例化
【发布时间】:2011-12-15 03:01:56
【问题描述】:

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吗?

【问题讨论】:

  • classic, // makeClass - 作者:John Resig(MIT 许可)

标签: javascript oop


【解决方案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 );
}

其中argsMyClassfirst 参数的标识符,而arguments 一如既往地是一个类似数组的对象,其中包含all 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 的工作原理以及它与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 对象:而不是args'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 对象作为其第一个参数。但这个问题似乎很难解决,而且可能不值得担心。

【讨论】:

  • "new MyClass.apply( itself, arguments ),但是 JavaScript 不允许这种表示法(也不允许任何等效的表示法)"——这就是重点。
  • @katspaugh:对不起,我不明白你的评论。重点是什么?链接的博客文章提到,未来版本的 JavaScript 将添加一个这样做的符号,所以我不认为这是一个特别明显的缺陷。
  • 兴趣点。顺便说一句,您也不能将初始化参数传递给Object.create(是否打包),因此init 方法应该成为标准做法。在 2007 年,仍然使用构造函数并隐藏 init 似乎是一个不错的选择。我只会在没有new 的调用上抛出异常,并在创建实例后让用户显式调用init
  • 谢谢你!很好的答案! ;)
【解决方案2】:

@ruakh:很好的分析。在最初的问题和你的回答之后将近两年,我仍然被这件事所吸引。我希望我的观察不是完全多余的。不过,它们很长。可以证明一篇不错的独立博客文章是合理的:-)。

您在最后提到的 John Resig 原始代码的两个问题都可以使用私有标志来解决,以区分您所谓的 innerouter 调用.

// 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语句来获取它,然后检查它的functiontype,然后应用它。

当我们想要向后兼容时,我们可以加载现有的 polyfill 之一,例如。 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...
}

当您决定不使用类似的“公共静态最终”Function.VOID 时, 您可以在makeClass 的顶部使用类似var VOID=function(){} 的声明。但这会导致在您要生成的 every 类构造函数中创建一个私有函数。 我们还可以使用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 的直接构造函数调用创建,在技术上将是一个相同的 void 构造函数的实例,即我们作为参数传入包装函数的function(){}
这怎么可能起作用?
请原谅我解释一些你已经知道的事情。 秘密在于我们将Void 的原型更改为constructor.prototype 之前我们使用new 来实例化它。此时,每个新对象都会获得一个由[[Prototype]] 非正式表示的内部属性,其值是构造函数原型属性的当前值。当构造函数的原型属性的值稍后被替换时,它对我们刚刚创建的对象没有进一步的影响。 请参阅:ECMA Standard-262 第 5 版的Section 13.2.2 [[Construct]]。

因此,以下适用于我们使用此工具创建的所有“类”:

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】:

    args 和 arguments 有什么区别?

    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

    【讨论】:

    • 你有点欺骗了“args.callee”部分。 考虑到这个函数应该如何被调用 args.callee 什么时候会是假的?
    • 如果 args.callee 为 false,将发送给 init 的参数是什么?它们与 args 有何不同??
    • -1:你完全误解了makeClass() 的工作原理,以及它的用途。
    • @ruakh,我实际上并不关心它的用途。我正在回答 OP 问题。但感谢您的意见。
    • 但是您实际上并没有回答 OP 的问题,因为这些问题与函数如何完成其​​任务有关。由于您不了解它的任务,因此您的答案是完全错误的——或者至少是完全不相关的。我想如果您看到有关window.onload 的问题,您会解释window 可以是局部变量,onload 可以是该变量的整数值属性?
    【解决方案4】:

    我相信在这一点上,这个函数可以被重写以吸引 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");
    

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

    【讨论】:

      【解决方案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'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());
      

      【讨论】:

      • -1;链接的博客文章提到了以“复杂的方式”做事是值得的几个原因。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-04-28
      • 2012-02-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多