【问题标题】:What makes my.class.js so fast? [closed]是什么让 my.class.js 如此之快? [关闭]
【发布时间】:2013-01-08 10:38:20
【问题描述】:

我一直在查看 my.class.js 的源代码,以了解是什么使它在 Firefox 上如此 fast。这是用于创建类的代码的 sn-p:

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 编译?

【问题讨论】:

  • 导致 my.class.js 更快的一个方面是扩展函数不执行hasOwnProperty 检查。 --- 但是当您考虑到它每秒执行数百万次操作的事实时,jsperf 测试中快速执行库之间的差异很小。如果您创建了这么多对象实例,那么您已经遇到了与内存占用和垃圾收集有关的其他问题。

标签: javascript oop instantiation jsperf


【解决方案1】:

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

考虑以下几点:

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

上次我检查时,chrome 实际上 GC'ed someVar,因为它没有被 IIFE 的返回值引用(被 closure 引用),而 FF 和 Opera 都将整个函数范围保持在记忆。
在这个 sn-p 中,这并不重要,但是对于使用包含数千行代码的模块模式(AFAIK,几乎是所有这些)编写的库,它可以 em> 有所作为。

无论如何,现代 JS 引擎不仅仅是 “哑” 解析和执行的东西。正如您所说:正在进行 JIT 编译,但也有很多技巧可以尽可能地优化您的代码。您发布的 sn-p 很可能是以 FF 的引擎喜欢的方式编写的。
同样重要的是要记住,Chrome 和 FF 之间正在就谁拥有最快的引擎进行某种速度之战。上次我检查 Mozilla 的 Rhino 引擎时,据说性能优于 Google 的 V8,如果今天仍然如此,我不能说……从那时起,Google 和 Mozilla 都在研究他们的引擎……

底线:不同浏览器之间存在速度差异 - 没有人可以否认这一点,但单点差异是微不足道的:您永远不会编写一遍又一遍地只做一件事的脚本。重要的是整体性能。
您必须记住,JS 也是一个难以进行基准测试的漏洞:只需打开控制台,编写一些递归函数,然后在 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;
}());

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

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

其次:声明构造函数(JS 中不存在类)和创建实例确实是两件不同的事情(尽管声明构造函数本身就是创建对象的实例(Function 实例是确切的)。您编写构造函数的方式可能会产生巨大的差异,如下面的示例 2 所示。同样,这是一个概括,可能不适用于某些引擎上的某些用例:V8 ,例如,倾向于为所有实例创建一个函数对象,即使该函数是构造函数的一部分 - 或者我被告知。

第三:遍历一个很长的原型链,正如你所提到的,并不像你想象的那么不寻常,实际上远非如此。您不断地遍历 2 或 3 个原型的链,如示例 3 所示。这不会减慢您的速度,因为它只是 JS 解析函数调用或解析表达式的方式所固有的。

最后:它可能是 JIT 编译的,但是说其他库不是 JIT 编译的只是没有叠加。他们可能,再一次,他们可能不会。正如我之前所说:不同的引擎在某些任务上比其他引擎执行得更好...... 可能是 FF JIT 编译此代码的情况,而其他引擎则没有。
我可以看到为什么其他库不会被 JIT 编译的主要原因是:检查循环引用、深度克隆功能、依赖关系(即,extend 方法在所有地方都使用,出于各种原因)。

示例 1:

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;
};

此函数克隆对象的第一层,所有被原始对象的属性引用的对象仍将被共享。一个简单的解决方法是简单地递归调用上面的函数,但随后您将不得不处理所有级别的循环引用的讨厌事务:

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

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

示例 2:

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 的返回值。当您省略 new 关键字时,第二个构造函数会创建一个全局函数对象,并且当然会为每次调用创建新的函数对象。你有一个构造函数(和一个原型),实际上是在闲置......这将我们带到 示例 3

示例 3:

var foo = [];//create an array - empty
console.log(foo[123]);//logs undefined.

好的,那么幕后会发生什么:foo 引用了一个 objectArray 的实例,它又继承了 Object 原型(试试Object.getPrototypeOf(Array.prototype))。这是有道理的,因此 Array 实例的工作方式与任何对象几乎相同,因此:

foo[123] ===> JS checks instance for property 123 (which is coerced to string BTW)
    || --> property not found @instance, check prototype (Array.prototype)
    ===========> Array.prototype.123 could not be found, check prototype
         ||
         ==========> Object.prototype.123: not found check prototype?
             ||
             =======>prototype is null, return undefined

换句话说,像您描述的这样的链条并不太牵强或不常见。这就是 JS 的工作原理,所以期待它放慢速度就像期待你的大脑因为你的想法而煎炸:是的,你可能会因为想太多而筋疲力尽,但要知道什么时候该休息一下。就像原型链的情况一样:它们很棒,只是知道它们有点慢,是的...

【讨论】:

  • 拜托,只要有动力,我不介意投反对票。
【解决方案2】:

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

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

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

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

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

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

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

【讨论】:

  • 我也是这么想的,但更小的并不总是更快。看看我在 JavaScript 中的 augment 方法(只有 7 行代码)。它与 my.class.js 和其他库所做的完全一样,但在 Firefox 上仍然比 orders of magnitude slower 好(但它是 Opera 上最快的,在 Chrome 23 上是第二快的)。我真正想知道的是为什么其他库在 Firefox 上创建实例的速度如此之快。
  • 这很有趣。然而,Firefox 运行在与 Opera 不同的引擎上(Presto 用于 HTML/CSS,Caraken 用于 JavaScript)和 Chrome(Webkit 用于 HTML/CSS,V8 用于 JavaScript)。也许是引擎。
猜你喜欢
  • 1970-01-01
  • 2010-09-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-07
  • 2020-08-29
  • 2012-11-30
  • 2023-03-31
相关资源
最近更新 更多