-3

我一直在查看my.class.js的源代码,以了解是什么让它在 Firefox 上运行得如此之快。这是用于创建类的代码片段:

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

这一切都很简单。当你创建一个类时,它只是返回你提供给它的构造函数。

然而,让我无法理解的是,创建此构造函数的实例如何比创建用Vapor.js编写的相同构造函数的实例执行得更快

这就是我想要理解的:

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

2 回答 2

12

我并不是要冒犯任何人,但是这种事情真的不值得关注,恕我直言。几乎所有浏览器之间的速度差异都取决于 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 都将整个函数范围保存在内存中。
在这个片段中,这并不重要,但是对于使用包含数千行代码的模块模式(AFAIK,几乎是所有这些)编写的库,它可以有所作为。

无论如何,现代 JS 引擎不仅仅是“愚蠢的”解析和执行的东西。正如您所说:正在进行 JIT 编译,但也有很多技巧可以尽可能地优化您的代码。您发布的片段很可能是以 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;
}());

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

首先:您提供的代码段与 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:在创建单个实例之前创建引用的函数对象。第二个示例不创建 a method1,除非在调用构造函数时。但是缺点是巨大的:忘记new第一个例子中的关键字,你得到的只是一个返回值undefined。第二个构造函数在省略new关键字时创建一个全局函数对象,当然每次调用都会创建新的函数对象。你有一个构造函数(和一个原型),事实上,它是空闲的......这将我们带到示例 3

示例 3:

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

好的,那么幕后会发生什么:foo引用一个object, instance of Array,而后者又继承了 Object 原型(只是 try 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 的工作原理,所以期待它放慢速度就像期待你的大脑因为你的想法而煎炸:是的,你可能会因为想太多而筋疲力尽,但要知道什么时候该休息一下。就像原型链的情况一样:它们很棒,只知道它们有点慢,是的......

于 2013-01-15T10:46:11.277 回答
1

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

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

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

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

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

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

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

于 2013-01-15T00:48:46.000 回答