Javascript原型运算符性能:节省内存,但速度更快?

IT技术 javascript performance function-prototypes
2021-03-12 09:46:03

在这里阅读(Douglas Crockford)使用原型运算符将方法添加到 Javascript 类也可以节省内存

然后我在这篇 John Resig 的文章中 读到了“用一堆原型属性实例化一个函数非常、非常、快,但他是在谈论以标准方式使用原型,还是在他的文章中谈论他的具体例子?

例如,正在创建这个对象:

function Class1()
{
   this.showMsg = function(string) { alert(string); }
}
var c = new Class1();
c.showMsg();

创建这个对象还,然后呢?

function Class1() {}
Class1.prototype.showMsg = function(string) { alert(string); }
var c = new Class1();
c.showMsg();

聚苯乙烯

我知道原型用于创建继承和单例对象等。但是这个问题与这些主题没有任何关系。


编辑:可能对JS 对象和 JS 静态对象之间的性能比较感兴趣的人可以阅读下面的这个答案静态对象肯定更快,显然只有当您不需要多个对象实例时才能使用它们。

6个回答

2021年编辑:

这个问题是在 2010 年class在 JS 中不可用时提出的如今,class已经如此优化,没有理由不使用它。如果需要使用new,请使用class. 但早在 2010 年,将方法绑定到它们的对象构造函数时,您有两个选择——一个是在函数构造函数内部使用绑定函数this,另一个是在构造函数外部使用绑定它们prototype@MarcoDemaio 的问题有非常简洁的例子。class被添加到 JS 时,早期的实现在性能上接近,但通常更慢。这不再是真实的了。只需使用class. 我想不出prototype今天没有理由使用


这是一个有趣的问题,所以我运行了一些非常简单的测试(我应该重新启动浏览器以清除内存,但我没有;以它的value为例)。看起来至少在 Safari 和 Firefox 上,prototype运行速度明显更快 [编辑:不是前面所说的 20 倍]。我确信对功能齐全的对象进行真实世界的测试会是一个更好的比较。我运行的代码是这样的(我分别运行了几次测试):

var X,Y, x,y, i, intNow;

X = function() {};
X.prototype.message = function(s) { var mymessage = s + "";}
X.prototype.addition = function(i,j) { return (i *2 + j * 2) / 2; }

Y = function() {
  this.message = function(s) { var mymessage = s + "";}
  this.addition = function(i,j) { return (i *2 + j * 2) / 2; }
};


intNow = (new Date()).getTime();
for (i = 0; i < 10000000; i++) {
  y = new Y();
  y.message('hi');
  y.addition(i,2)
}
console.log((new Date()).getTime() - intNow); //FF=5206ms; Safari=1554

intNow = (new Date()).getTime();
for (i = 0; i < 10000000; i++) {
  x = new X();
  x.message('hi');
  x.addition(i,2)
}
console.log((new Date()).getTime() - intNow);//FF=3894ms;Safari=606

这真是一种耻辱,因为我真的很讨厌使用prototype. 我喜欢我的目标代码是自封装的,并且不允许漂移。不过,我想当速度很重要时,我别无选择。该死的。

[编辑] 非常感谢@Kevin,他指出我之前的代码是错误的,大大提高了报告的prototype方法速度修复后,原型仍然明显更快,但差异并不大。

@David Wolever - 我的 Safari 是 5.0.6553.16,最近被推出了。虽然我在 Mac 上。我不知道为什么我不更多地使用 Safari。它的速度快得离谱,而且调试器比 Firebug 优越得多,但愚蠢的是,除非必须使用,否则我从不使用它。我尝试了你的建议。单个属性查找比仅实例化增加了大约 10%。两种方法都显示出大致相同的增长。因此,使用这两种方法似乎都没有任何额外的成本或节省。这让我很惊讶。同样,用一个真实世界的例子来尝试这个会好得多。
2021-04-29 09:46:03
我还可以验证此基准测试是否正确……尽管我对 Safari 的低数量感到好奇。你用的是什么版本?在我的机器上,我看到:FireFox 3.6.8: 5030/377、Safari 5.0: 3037/264。
2021-04-30 09:46:03
刚刚在 Node.js v0.4.7 上运行了这个。结果是原型(相对于构造函数)快了大约6 倍
2021-05-03 09:46:03
你也可以这样做: X.prototype = { message: function() { ... }, additional: function() { ... } } 可能会让你看起来更“自我封装”。
2021-05-07 09:46:03
看到时间细分也很有趣——即,实例化每个XY需要多少时间,查找属性需要多少时间(例如,var tmp = x.message;)以及调用属性需要多少时间(例如, x.message('hi'))
2021-05-09 09:46:03

我猜这取决于您要创建的对象类型。我运行了与 Andrew 类似的测试,但使用的是静态对象,并且静态对象赢得了胜利。这是测试:

var X,Y,Z,x,y,z;

X = function() {};
X.prototype.message = function(s) { var mymessage = s + "";}
X.prototype.addition = function(i,j) { return (i *2 + j * 2) / 2; }

Y = function() {
    this.message = function(s) { var mymessage = s + "";}
    this.addition = function(i,j) { return (i *2 + j * 2) / 2; }
};

Z = {
 message: function(s) { var mymessage = s + "";}
 ,addition: function(i,j) { return (i *2 + j * 2) / 2; }
}

function TestPerformance()
{
  var closureStartDateTime = new Date();
  for (var i = 0; i < 100000; i++)
  {
 y = new Y();
    y.message('hi');
    y.addition(i,2);
  }
  var closureEndDateTime = new Date();
 
  var prototypeStartDateTime = new Date();
  for (var i = 0; i < 100000; i++)
  {
    x = new X();
    x.message('hi');
    x.addition(i,2);
  }
  var prototypeEndDateTime = new Date();

  var staticObjectStartDateTime = new Date();
  for (var i = 0; i < 100000; i++)
  {
 z = Z; // obviously you don't really need this
    z.message('hi');
    z.addition(i,2);
  }
  var staticObjectEndDateTime = new Date();
  var closureTime = closureEndDateTime.getTime() - closureStartDateTime.getTime();
  var prototypeTime = prototypeEndDateTime.getTime() - prototypeStartDateTime.getTime();
  var staticTime = staticObjectEndDateTime.getTime() - staticObjectStartDateTime.getTime();
  console.log("Closure time: " + closureTime + ", prototype time: " + prototypeTime + ", static object time: " + staticTime);
}

TestPerformance();

此测试是对我在以下位置找到的代码的修改:

关联

结果:

IE6:关闭时间:1062,原型时间:766,静态对象时间:406

IE8:关闭时间:781,原型时间:406,静态对象时间:188

FF:关闭时间:233,原型时间:141,静态对象时间:94

Safari:关闭时间:152,原型时间:12,静态对象时间:6

Chrome:关闭时间:13,原型时间:8,静态对象时间:3

吸取的教训是,如果你不要有一个需要从同一个类实例化许多不同的对象,然后创建它作为一个静态对象手中夺了下来。所以仔细想想你真正需要什么样的课程。

如果有人感兴趣,向 jsperf.com 添加了一个测试:jsperf.com/closure-prototype-static-performance
2021-04-24 09:46:03
@momomo 的评论非常重要。想知道为什么它没有得到更多的支持。对于单个实例,闭包/原型比 Chrome 49.x 上的静态对象快约 2 倍
2021-04-29 09:46:03
这个测试只是一遍又一遍地重用 Z,这不是一个公平的测试,因为其他测试在 for 循环中都有他们的 new X() 和 new Y() 。更公平的是创建一次 new X() 并在 for 循环中做同样的事情。考试有点问题。
2021-05-02 09:46:03
好吧,这里至少应该有一个 +1!感谢您分享您的想法和测试!!!当我不需要实例化一个对象的多个实例时,我通常在 JS 中使用静态对象(并且通常也在其他语言中使用,而不是使用单例)。我希望静态对象会更快,但我很高兴你在这里通过测试清楚地说明了这一点。再次感谢!
2021-05-09 09:46:03
不错的测试,这回答了我的 Javascript 设计问题!
2021-05-09 09:46:03

所以我也决定测试一下。我测试了创建时间、执行时间和内存使用情况。我使用了 Nodejs v0.8.12 和在 Mac Book Pro 上运行的 mocha 测试框架,并启动到 Windows 7。“快”的结果是使用原型,而“慢”的结果是使用module模式。我为每种类型的对象创建了 100 万个,然后访问了每个对象中的 4 个方法。结果如下:

c:\ABoxAbove>mocha test/test_andrew.js

Fast Allocation took:170 msec
·Fast Access took:826 msec
state[0] = First0
Free Memory:5006495744

·Slow Allocation took:999 msec
·Slow Access took:599 msec
state[0] = First0
Free Memory:4639649792

Mem diff:358248k
Mem overhead per obj:366.845952bytes

? 4 tests complete (2.6 seconds)

代码如下:

var assert = require("assert"), os = require('os');

function Fast (){}
Fast.prototype = {
    state:"",
    getState:function (){return this.state;},
    setState:function (_state){this.state = _state;},
    name:"",
    getName:function (){return this.name;},
    setName:function (_name){this.name = _name;}
};

function Slow (){
    var state, name;
    return{
        getState:function (){return this.state;},
        setState:function (_state){this.state = _state;},
        getName:function (){return this.name;},
        setName:function (_name){this.name = _name;}
    };
}
describe('test supposed fast prototype', function(){
    var count = 1000000, i, objs = [count], state = "First", name="Test";
    var ts, diff, mem;
    it ('should allocate a bunch of objects quickly', function (done){
        ts = Date.now ();
        for (i = 0; i < count; ++i){objs[i] = new Fast ();}
        diff = Date.now () - ts;
        console.log ("Fast Allocation took:%d msec", diff);
        done ();
    });
    it ('should access a bunch of objects quickly', function (done){
        ts = Date.now ();
        for (i = 0; i < count; ++i){
            objs[i].setState (state + i);
            assert (objs[i].getState () === state + i, "States should be equal");
            objs[i].setName (name + i);
            assert (objs[i].getName () === name + i, "Names should be equal");
        }
        diff = Date.now() - ts;
        console.log ("Fast Access took:%d msec", diff);
        console.log ("state[0] = " + objs[0].getState ());
        mem = os.freemem();
        console.log ("Free Memory:" + mem + "\n");
        done ();
    });
    it ('should allocate a bunch of objects slowly', function (done){
        ts = Date.now ();
        for (i = 0; i < count; ++i){objs[i] = Slow ();}
        diff = Date.now() - ts;
        console.log ("Slow Allocation took:%d msec", diff);
        done ();
    });
    it ('should access a bunch of objects slowly', function (done){
        ts = Date.now ();
        for (i = 0; i < count; ++i){
            objs[i].setState (state + i);
            assert (objs[i].getState () === state + i, "States should be equal");
            objs[i].setName (name + i);
            assert (objs[i].getName () === name + i, "Names should be equal");
        }
        diff = Date.now() - ts;
        console.log ("Slow Access took:%d msec", diff);
        console.log ("state[0] = " + objs[0].getState ());
        var mem2 = os.freemem();
        console.log ("Free Memory:" + mem2 + "\n");
        console.log ("Mem diff:" + (mem - mem2) / 1024 + "k");
        console.log ("Mem overhead per obj:" + (mem - mem2) / count + 'bytes');
        done ();
    });
});

结论:这支持了这篇文章中其他人的发现。如果您不断地创建对象,那么原型机制显然会更快。如果您的代码大部分时间都花在访问对象上,那么module模式会更快。如果您对内存使用很敏感,原型机制每个对象使用的字节数少约 360 字节。

我不是 JavaScript 老师,但我创建了这个测试来测试访问时的性能,这里是:jsperf.com/accessing-prototyped-and-static-objects
2021-04-18 09:46:03
360 bytes是不固定的-这取决于有多少属性您设置和他们的value观,以及什么样的任何优化的引擎可能运行。
2021-04-24 09:46:03

直觉上,在原型上创建函数似乎更节省内存,速度更快:函数只创建一次,而不是每次创建新实例时。

但是,当需要访问该函数时,性能会略有不同c.showMsg被引用时,JavaScript 运行时首先检查 上的属性c如果未找到,c则检查 的原型。

因此,在实例上创建属性会导致访问时间稍微加快 - 但这可能只是一个非常深的原型层次结构的问题。

我们需要将对象的构造和使用分开。

在原型上声明函数时,它在所有实例之间共享。在构造函数中声明函数时,每次创建新实例时都会重新创建。鉴于此,我们需要分别对构建和使用进行基准测试以获得更好的结果。这就是我所做的,并希望与您分享结果。此基准测试不测试构建速度。

function ThisFunc() {
    this.value = 0;
    this.increment = function(){
        this.value++;
    }
}

function ProtFunc() {
    this.value = 0;
}

ProtFunc.prototype.increment = function (){
    this.value++;
}

function ClosFunc() {
    var value = 0;

    return {
        increment:function(){
            value++;
        }
    };
}

var thisInstance = new ThisFunc;

var iterations = 1000000;
var intNow = (new Date()).getTime();
for (i = 0; i < iterations; i++) {
    thisInstance.increment();
}
console.log(`ThisFunc: ${(new Date()).getTime() - intNow}`); // 27ms node v4.6.0

var protInstance = new ProtFunc;
intNow = (new Date()).getTime();
for (i = 0; i < iterations; i++) {
    protInstance.increment();
}
console.log(`ProtFunc: ${(new Date()).getTime() - intNow}`); // 4ms node v4.6.0

var closInstance = ClosFunc();
intNow = (new Date()).getTime();
for (i = 0; i < iterations; i++) {
    closInstance.increment();
}
console.log(`ClosFunc: ${(new Date()).getTime() - intNow}`); // 7ms node v4.6.0

从这些结果我们可以看出,原型版本最快(4ms),但闭包版本非常接近(7ms)。您可能仍需要针对您的特定情况进行基准测试。

所以:

  • 当我们需要在实例之间拥有所有性能或共享功能时,我们可以使用原型版本。
  • 当我们想要的是他们提供的功能时,我们可以使用其他版本。(私有状态封装,可读性等)

PS:我用安德鲁的回答作为参考。使用相同的循环和符号。