自执行函数中的闭包与对象的区别

15

假设我有以下内容:

var foo = (function(){
    var bar = 0;
    return {
       getBar: function(){
           return bar;
       },
       addOne: function(){
           bar++;
       },
       addRandom: function(rand){
           bar += rand;
       }
    }
})();

我有如下内容:

var foo2 = function(){
    var bar = 0;
    this.getBar = function(){
           return bar;
       };
    this.addOne = function(){
           bar++;
       };
    this.addRandom = function(rand){
           bar += rand;
       }
};

执行这些函数唯一的区别是使用new吗?

alert(foo.getBar()); //0
foo.addOne();
foo.addRandom(32);
alert(foo.getBar()); //33

var foo2_obj = new foo2;
alert(foo2_obj.getBar());//0
foo2_obj.addOne();
foo2_obj.addRandom(32);
alert(foo2_obj.getBar());//33

它们都输出了完全相同的东西。

那么从长远来看有什么区别呢?

它们之间有什么一个可以做而另一个不能做的事情吗?

上述示例的演示:http://jsfiddle.net/maniator/YtBpe/


1
在一个句子中:第一个是对象,第二个是函数。 - Zirak
4
纯粹从编程角度出发关注能否实现某些事情是不正确的。JavaScript的语言设计,就像所有代码一样,都是不完美的。特别是考虑到最初设计的时间匆忙。所以,要专注于最佳实践。new通常用于构造函数调用模式(Crockford认为这是一个糟糕的做法)。它将this设置为原型对象,并以有趣的方式修改方法的返回值。因此,构造函数应该大写以表示它们的预期用途。 - P.Brian.Mackey
你的例子是不相关的,因为它们没有更大的上下文存在。如果你想谈论闭包,你必须在一个上下文中谈论跨越作用域边界的结果。 - austincheney
'foo' 将运行一次并生成一个对象。可以使用 'new' 运行 'foo2' 来创建任意数量的对象。就像 Brian 所说,将 'Foo2' 首字母大写。 - Billbad
7个回答

14
在第一个中,您只能创建对象一次,而在第二个中,您可以创建任意数量的对象。即第一个实际上是一个单例。
请注意,对于第二个来说,闭包并不适用。每次实例化它时,您都要重新创建函数,并浪费大量内存。原型对象旨在解决这个问题,您可以在函数范围之外仅创建一次函数,并且不会创建意外的闭包。
function foo2(){
    this._bar = 0;
}

foo2.prototype = {

    constructor: foo2,

    getBar: function(){
        return this._bar;
    },

    addOne: function(){
        this._bar++;
    },

    addRandom:function(rand){
        this._bar += rand;
    }

};

那么:

var a = new foo2, b = new foo2, c = new foo2;

创建三个实例,它们拥有自己的_bar但共享相同的功能。

jsperf

你可以将所有这些与PHP进行“比较”,其中一些代码甚至无法运行,但在原则上是“等效”的:


var foo = (function(){
    var bar = 0;
    return {
       getBar: function(){
           return bar;
       },
       addOne: function(){
           bar++;
       },
       addRandom: function(rand){
           bar += rand;
       }
    }
})();

在 PHP 中,这大致相当于:

$foo = new stdClass;

$foo->bar = 0;

$foo->getBar = function(){
    return $this->bar;
};

$foo->addOne = function(){
    $this->bar++;
}

$foo->addRandom = function($rand){
    $this->bar += $rand;
}

var foo2 = function(){
    var bar = 0;
    this.getBar = function(){
        return bar;
    };
    this.addOne = function(){
        bar++;
    };
    this.addRandom = function(rand){
        bar += rand;
    }
};

在 PHP 中,大致等同于:

Class foo2 {


    public function __construct(){
    $bar = 0;

        $this->getBar = function(){
            return $bar;
        };
        $this->addOne = function(){
            $bar++;
        };
        $this->addRandom = function($rand){
            $bar += rand;
        };


    }

}

function foo2(){
    this._bar = 0;
}

foo2.prototype = {

    constructor: foo2,

    getBar: function(){
        return this._bar;
    },

    addOne: function(){
        this._bar++;
    },

    addRandom:function(rand){
        this._bar += rand;
    }

};

在PHP中,大致等同于以下代码:

Class foo2 {

    public $_bar;

    public function __construct(){
        $this->_bar = 0;    
    }

    public function getBar(){
        return $this->_bar;    
    }

    public function addOne(){
        $this->_bar++
    }

    public function addRandom($rand){
        $this->_bar += $rand;
    }

}

...并且是上述三个示例中唯一接近面向对象编程的示例



没有单例模式。在 OP 的帖子中,foo 是一个返回具有三个函数(getBar, addOne, addRandom)的动态对象的函数。你可以创建任意多个该对象的实例,它不是单例。 - Halcyon
2
@FritsvanCampen,请仔细查看原始帖子,该函数立即执行,因此foo只是创建一次的对象,而不是创建“动态”对象的函数。您甚至可以在控制台中按原样运行它... - Esailija
1
+1 是因为提到了私有变量的共享特性,这使得第一个例子中的 bar 成为静态变量,而在第二个例子中,它是一个唯一于对象实例的私有变量。 - wheresrhys

5
唯一的区别是foo将成为一个通用的Object,而foo2_obj在检查其类型时将被识别为foo2(即foo2_obj.constructor == foo2将为true,而在foo上的等效项是foo.constructor == Object)。
当然,foofoo2之间有一个重要区别-foo是一个对象,而foo2是一个函数(用于作为构造函数使用)。因此,可以轻松地创建许多foo2实例(其中foo2_obj是其中之一),而创建“实例”foo的想法并不真正有意义-最好的方式是复制(这比调用构造函数更困难)。
由于复制/创建实例的区别,第二种方法允许使用原型链进行真正的面向对象编程,而第一种方法使这些事情变得更加困难(且不可取)。

这不是真的。Object.create允许您使用任何东西创建“真正”的面向对象编程。 - Zirak
你也可以使用第一种形式创建出很好的继承模式和“强力构造函数”。 - jAndy
@Zirak,你只是一般地说吗?Object.create对于第一种形式和第二种形式都没有什么有用的作用。第二种形式也不是面向对象编程,所有函数都必须手动在每个实例化中创建,而不能从原型继承。它也无法被扩展。 :( - Esailija
@jAndy,你完全知道你不能使用第一种方式进行继承。除非你完全模拟它,但是在BF中你可以实现继承。 - Raynos
我认为这是第一种方法的主要缺点 - 创建的对象没有“类型”。但是,可以通过手动设置构造函数属性来纠正此问题(虽然我无法立即想起其完美的语法)。 - wheresrhys
显示剩余4条评论

1

[1]首先,但不是最重要的:效率

function Foo1() {
    var bar = 0;
    return {
        getBar: function () {
            return bar;
        }
    }
}
var o = Foo1();
o.getBar();


function Foo2() {
    var bar = 0;
    this.getBar = function () {
        return bar;
    }
}
var o = new Foo2();
o.getBar();

哪个更快?看看对象字面量 vs new 操作符

[2]编程模式:前者没有编程模式,但后者将从原型继承中受益。如果现在我们想要添加一个名为“logBar”的方法,

前者:

1:扩展每个 Foo1 实例:

o.logBar = function () {
    console.log(this.getBar());
}
o.logBar();

不好的方式!

2:找到Foo1定义的位置并添加:

function Foo1() {
    var bar = 0;
    return {
        getBar: function () {
            return bar;
        },
        logBar:function () {
            console.log(this.getBar());
        }
    }
}
var o = Foo1();
o.logBar = o.logBar();

你想在每次添加更多方法时都返回这样做吗?

后者:

Foo2.prototype.logBar = function () {
    console.log(this.getBar());
}

var o = Foo2();
o.logBar = o.logBar();

这将会很好地工作。

[3] 回到效率问题: 在 Foo1 的方式中,每当创建一个 Foo1 实例时,它都会生成一个 logBar 函数实例。对象字面量 vs new 操作符


能够使用(原型)继承,我认为是最决定性的因素。+1 - Halcyon
原型是JavaScript中重要的特性。 - island205
1
在OP中不存在原型继承,通过在构造函数中定义可共享的函数,你就是在蔑视原型(或任何形式的)继承。 - Esailija
非常抱歉。我在jsperf上有一个没有prototype的清晰版本,但当我将其更改以显示prototype更好时,它没有保存旧版本。 - island205
我添加了第三个方法到基准测试中,只是为了看看它与其他方法的比较情况 http://jsperf.com/object-literal-vs-new-operate/3 - zatatatata
显示剩余2条评论

0

我个人认为这两种类型如下:

1- Singleton 单例模式

2- Object 对象

假设我们有一个页面,它使用对象(第二种)的 JavaScript,并且有许多使用单例模式(第一种)的实用程序,并且运行良好。

但是有一天,我们需要通过 AJAX 调用第一页的新页面,这个新页面使用对象(第二种)的 JavaScript,并且使用相同的单例模式的实用程序,但我们在单例模式的实用程序中添加了一些新功能。

结果,新页面中的单例模式实用程序被加载到第一页中的单例模式实用程序中,因此当新页面执行其中一些新函数时,会出现错误...

我认为这就是我的观点,当您遇到这种情况时,单例模式会被覆盖,并且在这种情况下查找错误很难...与具有唯一实例的对象不同。

干杯。


0

主要的区别实际上是,foo是一个对象,而foo2是一个函数。

这意味着,除非你复制/粘贴它的代码,否则你将无法创建像foo那样的另一个对象,这个对象实际上不是foo本身。

另一方面,您可以创建另一个foo2对象,并在使用foo2_obj进行其他操作时对其进行操作。

简而言之,foo是一个实例,而foo2可以被看作是一个(即使它只是构造对象的函数)。

这取决于您在程序中想做什么,但我肯定建议使用第二种形式,它允许通过创建其他实例来重用您的代码。


在JavaScript中,从对象而不是类继承(例如使用Douglas Crockford的object()方法http://javascript.crockford.com/prototypal.html)实际上是相当标准的做法。 - wheresrhys
我没有提到继承,而是创建另一个与我们最初定义的对象相同的对象。使用你指出的方法,第二个对象要么为空,要么在复制时具有与第一个对象相同的状态(这可能已经发生了改变,因为它的初始化)。 - ghusse

0

foofoo2_obj是相同的。在这两种情况下,您都有一个函数,它创建一个新对象,引用闭包范围内的变量并返回该对象。

您有4个东西

  • 匿名函数是“foos”的工厂
  • 从匿名工厂创建的对象foo
  • 名称工厂“foo2”,用于“foo2_objs”
  • 从foo2工厂创建的对象foo2_obj

如果您不触及<Function>.prototype,则使用new和从函数返回函数文字之间的确切差异可以忽略不计。

您可能想进行比较

var foo2 = function(){
    var bar = 0;
    this.getBar = function(){
           return bar;
       };
    this.addOne = function(){
           bar++;
       };
    this.addRandom = function(rand){
           bar += rand;
       };
};

var Foo = {
  addOne: function () { this.bar++; },
  addRandom: function (x) { this.bar+=x; }
};

var foo3 = function () {
  return Object.create(Foo, { bar: { value: 0 } });
}

foo3使用原型面向对象编程。这意味着您不必一遍又一遍地重新创建那些函数。


那么使用 new 创建对象的意义是什么?我不能自己实例化所有东西吗? - Naftali
@Neal:当然可以。许多优秀和有才华的JS程序员甚至不使用new。调用Object.create()Object.defineProperties()可能是一个更好的主意。顺便说一下,你最好希望Crockford永远不要看到那段代码片段。不给一个被new调用的构造函数大写字母开头就像是在寻求缓慢而痛苦的死亡:p - jAndy
@Neal,“new”是一种原型面向对象的机制。它是ES3混乱构造函数的一部分。 - Raynos

0
简单来说,如果您创建了10个foofoo2实例,则foogetBar函数将在内存中存在10次,而foo2getBar函数只会存在一次。
此外,像Chrome这样的现代浏览器使用V8编译器,它将js编译为机器代码...在这种情况下,foo2将被转换为本地类对象,速度快约20倍(当您在循环中创建1000个实例时)。
通常情况下,当某个类/模块只需要一个实例时,我会使用简单对象方法。我遵循的结构是:
var myInstance = function(){
   var self = {};
   self.bar = null;
   self.gerBar = function(){
      return self.bar
   }
   return self;
}();

这与foo方法非常相似,但我发现这种结构更方便。


在实际使用中,我通常遇到的另一个区别是当我在类内部有回调函数或超时时。

var foo2 = function(){
    this.doSomething = function(){
        var temp = this;
        $.someAsyncCall(function(){
           // 'this' in current scope is the inline function itself, not the class instance
           // so have to store the instance ref in a local var in outer scope and then use that to get the class instance
           temp.callAfterAsyncCall();
        });
    };
    this.callAfterAsyncCall = function(){
    };
};

正如您所看到的,当您有许多这些情况时,本地临时变量并不美观。

而在另一种方法中,您始终可以在模块范围内的任何地方使用self引用。

var myInstance = function(){
   var self = {};
   self.doSomething = function(){
      $.someAsyncCall(function(){
          self.callAfterAsyncCall();
      });
   }
   self.callAfterAsyncCall = function(){
   };
   return self;
}();

我不确定这对你有多大的重要性,但我想提一下。

此外,我相信“如果您创建了10个foo和foo2实例,则foo的getBar函数将在内存中存在10次,而foo2的getBar函数只会存在一次”的说法是不正确的。如果您使用foo2.prototype.doSomething,那么这肯定是正确的,但我认为直接在新的foo2实例上设置属性并没有任何效率上的节省(考虑到foo2可能会分支并在不同的实例上设置不同的属性,具体取决于条件,因此它不能假设所有对象共享相同的方法和属性)。 - wheresrhys

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