是什么让my.class.is这么快?

时间:2013-01-08 10:38:20

标签: javascript oop instantiation jsperf

我一直在查看my.class.js的源代码,以了解Firefox上的fast是什么原因。以下是用于创建类的代码片段:

my.Class = function () {
    var len = arguments.length;
    var body = arguments[len - 1];
    var SuperClass = len > 1 ? arguments[0] : null;
    var hasImplementClasses = len > 2;
    var Class, SuperClassEmpty;

    if (body.constructor === Object) {
        Class = function () {};
    } else {
        Class = body.constructor;
        delete body.constructor;
    }

    if (SuperClass) {
        SuperClassEmpty = function() {};
        SuperClassEmpty.prototype = SuperClass.prototype;
        Class.prototype = new SuperClassEmpty();
        Class.prototype.constructor = Class;
        Class.Super = SuperClass;
        extend(Class, SuperClass, false);
    }

    if (hasImplementClasses)
        for (var i = 1; i < len - 1; i++)
            extend(Class.prototype, arguments[i].prototype, false);    

    extendClass(Class, body);

    return Class;
};

extend函数仅用于将第二个对象的属性复制到第一个(可选择覆盖的现有属性):

var extend = function (obj, extension, override) {
    var prop;
    if (override === false) {
        for (prop in extension)
            if (!(prop in obj))
                obj[prop] = extension[prop];
    } else {
        for (prop in extension)
            obj[prop] = extension[prop];
        if (extension.toString !== Object.prototype.toString)
            obj.toString = extension.toString;
    }
};

extendClass函数将所有静态属性复制到类中,并将所有公共属性复制到类的原型中:

var extendClass = my.extendClass = function (Class, extension, override) {
    if (extension.STATIC) {
        extend(Class, extension.STATIC, override);
        delete extension.STATIC;
    }
    extend(Class.prototype, extension, override);
};

这一切都非常简单。创建类时,它只返回您提供的构造函数。

然而,我理解的是,创建此构造函数execute faster的实例的方法与创建Vapor.js中编写的相同构造函数的实例相比如何。

这就是我想要理解的:

  1. 像my.class.js这样的库的构造函数如何在Firefox上如此快速地创建如此多的实例?库的构造函数都非常相似。执行时间不应该相似吗?
  2. 为什么创建类的方式会影响实例化的执行速度?定义和实例化不是单独的流程吗?
  3. my.class.js从哪里获得这个速度提升?我没有看到构造函数代码的任何部分应该使它更快地执行。实际上,遍历像MyFrenchGuy.Super.prototype.setAddress.call这样的长原型链应该会显着降低它的速度。
  4. 构造函数是否正在编译JIT?如果是这样,为什么其他库的构造函数也不是JIT编译的?

2 个答案:

答案 0 :(得分:11)

我不想冒犯任何人,但这种真的并不值得关注,恕我直言。几乎所有浏览器之间的速度差异都归结为JS引擎。例如,V8引擎非常擅长内存管理;特别是当你将它与IE的旧JScript引擎进行比较时。

请考虑以下事项:

var closure = (function()
{
    var closureVar = 'foo',
    someVar = 'bar',
    returnObject = {publicProp: 'foobar'};
    returnObject.getClosureVar = function()
    {
        return closureVar;
    };
    return returnObject;
}());

上次检查时,Chrome实际上是GC&#39; someVar,因为它没有被IIFE的返回值引用(由closure引用),而两者都是FF和Opera将整个功能范围保存在内存中。
在这个片段中,它并不重要,但对于使用模块模式(AFAIK,几乎所有这些模块)编写的库,它们由数千行代码组成,它就是可以有所作为。

无论如何,现代JS引擎不仅仅是&#34; dumb&#34; 解析和执行的东西。正如你所说:JIT编译正在进行中,但是尽可能地优化代码也需要很多技巧。很可能你发布的片段是以FF的引擎喜欢的方式编写的。
同样重要的是要记住Chrome和FF之间正在进行某种速度战,关于谁拥有最快的引擎。上次我检查Mozilla的Rhino引擎据说胜过谷歌的V8,如果今天仍然如此,我不能说...从那以后,谷歌和Mozilla一直在努力他们的引擎......

底线:存在各种浏览器之间的速度差异 - 没有人可以否认这一点,但是单一的差异点是微不足道的:你永远不会写一个一遍又一遍地做一件事的脚本。重要的是整体表现 你必须记住,JS也是一个棘手的bug来进行基准测试:只需打开你的控制台,编写一些递归函数,然后在FF和Chrome中对其进行100次调整。比较每次递归所需的时间和整个运行时间。然后等待几个小时再试一次......有时候FF可能会出现在顶部,而其他时候Chrome可能会更快。我已经尝试过这个功能了:

var bench = (function()
{
    var mark = {start: [new Date()],
                end: [undefined]},
    i = 0,
    rec = function(n)
    {
        return +(n === 1) || rec(n%2 ? n*3+1 : n/2);
        //^^ Unmaintainable, but fun code ^^\\
    };
    while(i++ < 100)
    {//new date at start, call recursive function, new date at end of recursion
        mark.start[i] = new Date();
        rec(1000);
        mark.end[i] = new Date();
    }
    mark.end[0] = new Date();//after 100 rec calls, first element of start array vs first of end array
    return mark;
}());

但现在,回到最初的问题:

首先关闭:您提供的代码段并不能与jQuery的$.extend方法进行比较:没有&em>真正的克隆正在进行,更不用说深度克隆了。它根本没有检查循环引用,这是我所研究过的大多数其他库。检查循环引用确实会减慢整个过程,但它可能会不时派上用场(下面的示例1)。部分性能差异可以解释为这个代码只是做得更少,因此它需要更少的时间。

其次:声明构造函数(JS中不存在类)和创建实例确实是两个不同的事情(尽管声明构造函数本身就是创建一个对象的实例({{1}你编写构造函数的方式可以产生巨大的差异,如下面的例2所示。再次,这是一个泛化,可能不适用于某些用例某些引擎:例如,V8倾向于为所有实例创建单个函数对象,即使该函数是构造函数的一部分 - 或者我告诉过。

第三:如你所说,遍历一个长的原型链并不像你想象的那样不寻常,实际上远非如此。你不断地遍历2或3个原型的链,如例3所示。这不应该让你慢下来,因为它只是JS解析函数调用或解析表达式的固有方式。 / p>

最后:它可能是JIT编译的,但是说其他libs不是JIT编译的,只是没有堆叠。他们可能,然后他们可能不会。正如我之前所说:不同的引擎在某些任务中表现更好,然后其他......它可能可能是FF JIT编译此代码的情况,其他引擎也不是。
我可以看到为什么其他库不能进行JIT编译的主要原因是:检查循环引用,深度克隆功能,依赖性(即Function方法因各种原因而在各处使用。

示例1:

extend

此函数克隆对象的第一级,仍将共享由原始对象的属性引用的所有对象。一个简单的解决方法是简单地递归调用上面的函数,但是你必须处理所有级别的循环引用的讨厌业务:

var shallowCloneCircular = function(obj)
{//clone object, check for circular references
    function F(){};
    var clone, prop;
    F.prototype = obj;
    clone = new F();
    for (prop in obj)
    {//only copy properties, inherent to instance, rely on prototype-chain for all others
        if (obj.hasOwnProperty(prop))
        {//the ternary deals with circular references
            clone[prop] = obj[prop] === obj ? clone : obj[prop];//if property is reference to self, make clone reference clone, not the original object!
        }
    }
    return clone;
};

当然,这不是最常见的情况,但如果你想要防御性地编写代码,你必须承认许多人一直在编写疯狂的代码......

示例2:

var circulars = {foo: bar};
circulars.circ1 = circulars;//simple circular reference, we can deal with this
circulars.mess = {gotcha: circulars};//circulars.mess.gotcha ==> circular reference, too
circulars.messier = {messiest: circulars.mess};//oh dear, this is hell

理论上,声明第一个构造函数较慢而不是凌乱的方式:function CleanConstructor() {}; CleanConstructor.prototype.method1 = function() { //do stuff... }; var foo = new CleanConstructor(), bar = new CleanConstructor); console.log(foo === bar);//false, we have two separate instances console.log(foo.method1 === bar.method1);//true: the function-object, referenced by method1 has only been created once. //as opposed to: function MessyConstructor() { this.method1 = function() {//do stuff }; } var foo = new MessyConstructor(), bar = new MessyConstructor(); console.log(foo === bar);//false, as before console.log(foo.method1 === bar.method1);//false! for each instance, a new function object is constructed, too: bad performance! 引用的函数对象是在创建单个实例之前创建的。除了调用构造函数时,第二个示例不会创建method1。但缺点是巨大:忘记第一个示例中的method1关键字,所得到的只是new的返回值。当省略undefined关键字时,第二个构造函数会创建一个全局函数对象,当然也会为每个调用创建新的函数对象。你有一个构造函数(和一个原型),实际上是空转...这将我们带到示例3

示例3:

new

好的,那么幕后发生了什么:var foo = [];//create an array - empty console.log(foo[123]);//logs undefined. 引用了一个对象foo的实例,它继承了Object原型(只是尝试{{1} }})。这是合情合理的,因此Array实例的工作方式与任何对象几乎相同,所以:

Array

换句话说,像你描述的链条并不太牵强或不常见。这就是JS的工作方式,所以期待减慢速度就像期待你的大脑一样,因为你的想法:是的,你可以通过思考太多而疲惫不堪,但只知道什么时候休息一下。就像原型链一样:他们很棒,只知道它们有点慢,是的......

答案 1 :(得分:1)

我不完全确定,但我确实知道在编程时,最好在不牺牲功能的情况下使代码尽可能小。我喜欢称之为minimalist code

这可能是混淆代码的一个很好的理由。混淆通过使用较小的方法和变量名来缩小文件的大小,使得更难以进行逆向工程,缩小文件大小,使其下载速度更快,以及潜在的性能提升。 Google的javascript代码非常模糊,这有助于提高速度。

所以在JavaScript中,更大并不总是更好。当我找到一种可以缩小代码的方法时,我会立即实现它,因为我知道它会使性能受益,即使是最小的数量。

例如,在函数外部不需要变量的函数中使用var关键字有助于垃圾收集,与将变量保存在内存中相比,它提供了非常小的速度提升。

使用这样的库可以产生“每秒数百万次操作”(Blaise的话),小的性能提升可以产生明显/可衡量的差异。

因此my.class.js可能是“极简主义编码”或以某种方式进行优化。它甚至可以是var个关键字。

我希望这有所帮助。如果它没有帮助,那么我祝你好运,得到一个好的答案。