这三种JavaScript中"class"定义模式有什么区别?

15

如果在这四种模式之间进行选择,是否在底层存在任何重要/微妙/显著的差异?另外,在使用Object.create()new操作符时,它们之间有什么区别吗?

1) 当翻译“class”定义时,CoffeeScript使用的模式:

Animal = (function() {

  function Animal(name) {
    this.name = name;
  }

  Animal.prototype.move = function(meters) {
    return alert(this.name + (" moved " + meters + "m."));
  };

  return Animal;

})();

同时

2) Knockout似乎倡导的模式:

var DifferentAnimal = function(name){

    var self = this;

    self.name = name;

    self.move = function(meters){
        return alert(this.name + (" moved " + meters + "m."));
    };

}

而且

3) 我经常看到的一个类似的简单模式:

var DifferentAnimalWithClosure = function(name){

    var name = name;

    var move = function(meters){

    };

    return {name:name, move:move};

}

并且

4) Backbone所推广的模式:

var OneMoreAnimal= ClassThatAlreadyExists.extend({

    name:'',
    move:function(){}

});

更新1:根据Elias的回复更改了第二个模式并添加了第三个模式//进行了一些小的格式调整


3
Knockout的模式实际上不能创建可原型化或可扩展的类。 - Waleed Khan
Knockout 不需要使用任何特定的模式。上面的模式不是我们通常在文档中使用的模式(返回匿名对象,而不是让它成为 this,在这里你仍然可以使用原型)。 - RP Niemeyer
Waleed - 这正是我希望揭示的底层差异类型 - 非常感谢。 - user1889765
RP - 我在原帖中犯了错误...已经进行了编辑以反映出来。谢谢! - user1889765
2个回答

8
只是为了明确:JS不知道类,只知道对象和自定义的构造函数,但这并不是重点。
简短回答你的问题:是的,在你发布的各种创建新对象的方式中,有一些小的甚至相当大的差异。
CoffeeScript:
实际上,这是创建自己的构造函数最清晰和传统的方法,但它已经被“优化”,因为它已经准备好使用(可选)闭包变量。
基本上,这段代码使用IIFE将构造函数定义和原型方法分配包装在它们自己的私有作用域中,并返回新构造函数的引用。这只是干净、简单的JS,与你自己编写的代码没有区别。
Knockout:
现在这让我有点困惑,因为对我来说,至少你提供的片段看起来像模块模式的一部分或者强制构造函数。但由于你没有使用严格模式,省略new仍然会导致危险情况,并且由于整个函数费力地创建了一个DifferentAnimal的新实例,只是为了然后构建第二个对象字面量,将所有属性分配给那个次要的对象,我想你可能错过了什么。因为,说实话,在这里省略最后的return {};语句可能根本没有任何区别。此外:正如你所看到的,你在本质上是一个构造函数中声明了一个方法(move)。这意味着每个实例都将被分配自己的函数对象move,而不是从原型中获取它。
简而言之:仔细查看你从哪里找到这个片段,并仔细检查这是否是完整版本,因为如果是,我只能看到反对这个的理由。
在构造函数内部定义变量很简单:闭包,假设你的属性有一个明显的初始状态,由传递给该构造函数的一些参数确定:
function MyConstructor(param)
{
     var paramInit = param/2;//or something
     this.p = paramInit;//this property can change later on, so:
     this.reInit = function()
     {//this method HAS to be inside constructor, every instance needs its own method
         this.p = paramInit;//var paramInit can't, it's local to this scope
     };
}
var foo = new MyConstructor(10);
console.log(foo.p);//5
foo.p = 'hi';
console.log(foo.p);//hi
foo.reInit();
console.log(foo.p);//5
console.log(foo.paramInit);//undefined, not available outside object: it's a pseudo-private property

这实际上就是全部内容了。当你看到别人使用var that = this;或其他东西时,这通常是为了创建对主对象的引用,该引用可在任何地方使用,而不必处理this的问题(this指什么?将方法应用于原始意图以外的对象时,该方法应该执行什么操作?等等...)。 Backbone:
在这里,我们面临着另一种情况:扩展对象(即:使用现有“类”(构造函数)或特定实例的方法、属性)与简单创建对象并非相同。
正如您所知,JS对象可以随时分配新属性。这些属性也可以被删除。有时,在实例本身上重新定义原型属性(掩盖原型行为)等...因此,这完全取决于您希望生成的对象(新创建的对象,它扩展给定实例)看起来像什么:您想它从实例中获取所有属性,还是您希望两个对象在某处沿着同一原型线使用?
这两件事也可以通过使用简单的JS实现,但编写自己的代码需要更多的努力。但是,如果您编写例如:
function Animal(name)
{
    this.name = name;
}
Animal.prototype.eat= function()
{
    console.log(this.name + ' is eating');
};

这可以被视为等同于写作以下内容:
var Animal = Object.extend({name:'',eat:function()
{
    console.log(this.name + ' is eating');
}});

这段文本很短,但缺少构造函数。

new vs Object.create
嗯,这很容易理解: Object.createnew 更加强大:您可以在创建对象时定义原型方法、属性(包括它们是否可枚举、可写等),而无需编写构造函数和原型或创建对象字面量并混合使用所有这些 Object.defineProperty 行。
缺点是:一些人仍在使用不符合 ECMA5 标准的浏览器(IE8 仍未完全消失)。根据我的经验:在一段时间后,调试大型脚本变得非常困难:虽然我更倾向于使用 power-constructors 而不是常规构造函数,但我仍将它们定义在脚本的顶部,具有明显、清晰且相当描述性的名称,而对象字面量则是我“即兴创建”的东西。使用 Object.create,我注意到我倾向于创建一些太复杂以至于不适合作为实际对象字面量的对象,尽管它们是对象字面量:

//fictional example, old:
var createSomething = (function()
{
    var internalMethod = function()
    {//method for new object
        console.log(this.myProperty || '');
    };
    return function(basedOn)
    {
        var prop, returnVal= {};
        returnVal.myProperty = new Date();
        returnVal.getCreated = internalMethod;//<--shared by all instances, thx to closure
        if (!basedOn || !(basedOn instanceof Object))
        {//no argument, or argument is not an object:
            return returnVal;
        }
        for (prop in basedOn)
        {//extend instance, passed as argument
            if (basedOn.hasOwnProperty(prop) && prop !== '_extends')
            {
                returnVal[prop] = basedOn[prop];
            }
        }
        returnVal._extends = basedOn;//<-- ref as sort-of-prototype
        return returnVal;
    };
}());

现在这个构造函数相当冗长,但是我已经准备好了基本的构造函数,并且我也可以使用它来扩展现有的实例。可能只写以下代码似乎更简洁:

var createSomething = Object.create(someObject, {getCreated:function()
{
    console.log(this.myProperty);
},
myProperty:new Date()});

但在我看来,这会使您更难跟踪哪个对象在何处创建(主要是因为Object.create是一个表达式,而且不会提升。)
嗯,当然这远非定论:两者都有其优点和缺点:如果您不喜欢,我更喜欢使用模块模式、闭包和强构造函数。

希望这为您解决了一些问题。


首先,感谢您详细的回复...其次,对于我在第二个问题中的示例表示歉意;我本意是创建本地变量而不是将它们挂在self上 - 例如,“var name =”而不是“self.name =”,并在返回语句中指定闭包变量...我会更新原始帖子,但如果有任何进一步的见解,请告知! - user1889765
@user1889765:更新了 knockout 部分以反映本地变量。当涉及返回闭包变量时:你不再处理一个可靠的构造函数:而是处理一个模块。它只是一个巨大的闭包,需要返回那些闭包变量以创建所需的曝光。只需查看 jQuery 的源代码,就可以看到这个真实的例子。 - Elias Van Ootegem

3
第一个例子将move函数放在原型中,这个原型将被所有Animal实例共享。
第二个例子为每个动物实例创建了一个新的move函数。
第三个例子生成了一个Animal类,其中move函数与第一个例子中的原型类似,但代码更少。(在你的示例中,名称也是所有实例共享的,这可能不是你想要的)
将函数放在原型中可以加快实例化动物的速度,并且由于JIT引擎的工作方式,甚至函数的执行速度也更快。

又有一个伟大的底层发现,通过跨实例共享功能获得性能提升。再次感谢。 - user1889765
这个答案包含了一些有用的观点,但第三个例子的描述略微有误。在第三个例子中,函数返回一个带有 namemove 属性的新对象,但没有为其分配构造函数属性或原型。看起来你可以用 var frog = new DifferentAnimalWithClosure('Roger'); 创建一个新的 DifferentAnimalWithClosure 实例,你确实可以这样做,但如果你稍后运行 frog instanceof DifferentAnimalWithClosure;,你会得到 false 的结果。像例子 #1 和 #2 这样的模式会将 "DifferentAnimalWithClosure" 的特性赋给新对象。 - Simon Dell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接