有没有办法利用JavaScript工厂函数中使用原型方法的性能优势?

时间:2016-07-17 22:22:58

标签: javascript performance prototype factory factory-pattern

我正在寻找以类似于Java类的方式编写面向对象的JavaScript(JS)代码的最佳方法。

工厂功能(FF)看起来像是一种非常有前途的方式,在JS中提供类似于类的功能,到目前为止,我一直在构建它们:

function FF(constructorArg)
{
   var _privateName = constructorArg;

   var publicMessage = "Hello StackOverflow";

   function publicMethodGetName() {
      return _privateName;
   }

   return {
      publicMethodGetName: publicMethodGetName,
      publicMessage: publicMessage
   };
}

然而,I've recently discovered that, unlike prototype methods, this style of FF recreates each method for every FF instance and can thus disadvantage performance

this excellent thread的第二个答案中,Eric Elliot谈到了FF:

  

如果将原型存储在父对象上,这可能是一个很好的方法   动态交换功能,并启用非常灵活   对象实例化的多态性。

我无法在网上找到这方面的任何例子。任何人都可以向我解释如何使用上面的FF进行此操作吗?

如果我知道将从同一个FF创建许多对象,这是否意味着我可以将FF切换为使用原型方法?

1 个答案:

答案 0 :(得分:5)

  

我正在寻找编写面向对象JavaScript(JS)的最佳方法   代码的方式类似于Java类。

这是你的第一个错误。 Javascript是一种非常不同的语言,您不应该尝试在Javascript中模拟其他语言。当我来自C ++时,我做了类似的事情,这是一个很大的错误。你需要做的是学习Javascript的优势以及如何最好地解决问题" Javascript方式"用Javascript写的时候。我知道,用其他语言你已经知道的方式来做事情是一种自然倾向,但这是一个错误。因此,不要试图用#Java; Java方式做事,而是要问Javascript中解决某些特定问题的最佳方法是什么。

例如,在大量对象上使用方法的最低内存方式是使用Javascript的原型。您可以使用原型手动分配原型或使用较新的ES6 class语法。两者都在原型对象上创建方法,然后在所有实例之间有效共享。

例如,您可以使用具有如下典型原型的工厂函数:

// constructor and factory function definition
function MyCntr(initialCnt) {
    if (!(this instanceof MyCntr)) {
        return new MyCntr(initialCnt);
    } else {
        // initialize properties
        this.cntr = initialCnt || 0;
    }
}

MyObject.prototype = {
    getCntr: function() {
        return this.cntr++;
    },
    resetCntr: function() {
        this.cntr = 0;
    }
};

然后,您可以使用传统的new运算符创建一个对象,如下所示:

var m = new MyCntr(10);
console.log(m.getCntr());    // 10

或者,您可以将其用作工厂功能:

var m = MyCntr(10);
console.log(m.getCntr());    // 10

请记住,使用ES6(或转换程序),您也可以使用ES6类语法:

class MyCntr {
    constructor(initialCnt) {
        if (!(this instanceof MyCntr)) {
            return new MyCntr(initialCnt);
        } else {
            // initialize properties
            this.cntr = initialCnt || 0;
        }
    }

    getCntr() {
        return this.cntr++;
    }

    resetCntr() {
        this.cntr = 0;
    } 
}

var m = new MyCntr(10);
console.log(m.getCntr());    // 10

或者,您可以将其用作工厂功能:

var m = MyCntr(10);
console.log(m.getCntr());    // 10

这两种语法都创建了完全相同的对象定义和原型。

所有这一切,不使用原型的内存消耗通常不是什么大问题,除非你有很多方法和很多对象,并且不使用原型有一些显着的优点。一个重要的一点是,您可以在构造函数创建的闭包中拥有真正的私有实例数据。这里的前一个示例是以cntr实例变量真正私有的方式实现的。

// constructor and factory function definition
function MyCntr(initialCnt) {
    // truly private instance variable
    var cntr;

    if (!(this instanceof MyCntr)) {
        return new MyCntr(initialCnt);
    } else {
        // initialize properties
        cntr = initialCnt || 0;
    }

    this.getCntr = function() {
        return cntr++;
    }

    this.resetCntr = function() {
        cntr = 0;
    }
}

这确实使用了更多内存,因为构造函数(包含cntr变量)创建了持久闭包,并且构成方法的每个函数都有新实例。但是,它在记忆方面并没有太大差异。如果你没有数以万计的cntr对象,那么内存消耗差异可能是无关紧要的。 Doug Crawford是这种Javascript编码风格的冠军之一。你可以在这里看到他关于这个主题的早期文章之一:http://javascript.crockford.com/private.html并且对Crockford的一些观点here进行了一些讨论。那里有一个Crockford视频(我现在无法找到它),他在那里捍卫非原型风格。

因此,问你是否可以结合两者的优点是合乎逻辑的。不,不是真的。为了访问构造函数闭包,必须在构造函数的词法范围中定义方法,为此,它们不在原型上。试图将它们分配给构造函数中的原型会造成一个混乱,这个混乱会受到各种错误的影响,因此也不可行。

使用ES6 weakMap对象,可以在使用原型时创建私有实例数据,但我会说它通常更麻烦,因为它复杂化只是编写代码和访问私有数据 - 但这是可能的。您可以使用Private instance members with weakmaps in JavaScriptHiding Implementation Details with ECMAScript 6 WeakMaps中的weakMap查看私有变量的实现。

我提出我的意见,隐藏你通过某种方式消除对new的需求来创建一个新对象的事实并不是真的非常像Javascript或者非常像OO。对于在创建新对象时以及刚刚调用函数时读取代码的人来说,这应该是显而易见的。使用带有大写构造函数的new在Javascript中非常明显,我认为这是一件好事。我不会故意在我的代码中避免这种明显的意图。

  

如果将原型存储在父对象上,这可能是一个很好的方法   动态交换功能,并启用非常灵活   对象实例化的多态性。

确实如果你使用原型,它是一个很好的封装对象,它包含了对象的所有方法,它可以使一些多态现象变得更容易。但是,如果你没有使用原型,你也可以做多态性。

例如,假设您有三个不使用原型的对象,并且他们在构造函数中分配了所有方法,并且您想要创建这三个方法的mixin组合。

您可以创建一个对象然后只调用其他构造函数,它们会自动将您的对象初始化为具有所有三个行为的组合对象(假设实例数据属性或方法名称没有冲突)。

function MyCombo() {
    ObjectA.call(this);
    ObjectB.call(this);
    ObjectC.call(this);
}

这将调用三个构造函数中的每一个,并且它们将各自初始化它们的方法和实例变量。在某些方面,这比使用原型更简单。

如果您有使用原型的对象,那么您可以这样做:

function MyCombo() {
    ObjectA.call(this);
    ObjectB.call(this);
    ObjectC.call(this);
}

Object.assign(MyCombo.prototype, ObjectA.prototype, ObjectB.prototype, 
              ObjectC.prototype, {myComboMethod: function() {...}});

var x = new MyCombo();
x.methodA();
x.methodB();
  

如果我知道将从同一个FF创建许多对象   这是否意味着我可以将FF切换为使用原型方法?

这取决于哪些权衡对您的代码最有效。如果您有1000个方法并且正在创建20,000个该类型的对象,那么我说您可能想要使用原型,以便您可以共享所有这些方法。如果您没有那么多方法或者没有创建大量这类对象或者您有足够的内存,那么您可能希望优化其他一些特性(如私有数据)而不使用原型。这是一个权衡空间。没有一个正确的答案。