Что делает my.class.js так быстро?

Я искал исходный код 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.

Вот что я пытаюсь понять:

  • Как конструкторы библиотек, такие как my.class.js, создают так много экземпляров так быстро в Firefox? Конструкторы библиотек очень похожи. Должно ли время выполнения быть аналогичным?
  • Почему способ создания класса влияет на скорость выполнения экземпляра? Разве это не определение и создание отдельных процессов?
  • Где my.class.js получает повышение этой скорости? Я не вижу никакой части кода конструктора, которая должна заставлять ее выполнять быстрее. Фактически, перемещение длинной прототипной цепи, например MyFrenchGuy.Super.prototype.setAddress.call, должно значительно замедлить ее.
  • Является ли функция конструктора составляющей JIT? Если да, то почему функции конструктора других библиотек также не скомпилированы JIT?

Ответы

Ответ 1

Я не хочу никого обижать, но такого рода вещи не стоит внимания, ИМХО. Почти любая разница в скорости между браузерами сводится к движку JS. Например, двигатель V8 очень хорош в управлении памятью; особенно когда вы сравниваете его с IE JScript двигателями старого.

Рассмотрим следующее:

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

В прошлый раз, когда я проверил, хром фактически GC'ed someVar, потому что на него не ссылалось возвращаемое значение IIFE (на которое ссылается closure), тогда как FF и Opera сохраняли всю функциональную область в Память.
В этом фрагменте это не имеет особого значения, но для libs, которые написаны с использованием модуля-шаблона (AFAIK, что почти все из них), которые состоят из тысяч строк кода, это может иметь значение.

Во всяком случае, современные JS-двигатели - это больше, чем просто "тупые" синтаксические и синтаксические действия. Как вы сказали: там происходит компиляция JIT, но также есть много обмана, чтобы максимально оптимизировать ваш код. Вполне возможно, что фрагмент, который вы опубликовали, написан таким образом, что двигатель FF просто обожает.
Также очень важно помнить, что между Chrome и FF происходит какое-то быстрое сражение, у которого самый быстрый двигатель. В прошлый раз, когда я проверял, что движок Mozilla Rhino, как говорили, превосходит Google V8, если это все еще сохраняется сегодня, я не могу сказать... С тех пор как Google, так и Mozilla работают над своими двигателями...

Внизу: существуют различия в скорости между различными браузерами - никто не может этого отрицать, но одна точка различия незначительна: вы никогда не напишете script, который делает одно и то же снова и снова. Это важная общая производительность.
Вы должны иметь в виду, что JS тоже сложный баггер для тестирования: просто откройте консоль, запишите некоторую рекурсивную функцию и выполните ее 100 раз, в FF и Chrome. сравнить время, необходимое для каждой рекурсии, и общий пробег. Затем подождите пару часов и повторите попытку... иногда 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. Это не должно замедлять вас, поскольку это просто связано с тем, как JS разрешает вызовы функций или разрешает выражения.

Наконец: он, вероятно, скомпилирован JIT, но говорит, что другие libs не скомпилированы 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 ссылается на объект, экземпляр Array, который, в свою очередь, наследует форму прототипа 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.

Это может быть хорошей причиной для обфускации кода. Obfuscation уменьшает размер файла, используя меньшие имена методов и переменных, что затрудняет обратное проектирование, сокращение размера файла, ускорение загрузки, а также потенциальное повышение производительности. Код javascript Google интенсивно запутывается, и это способствует их быстроте.

Итак, в JavaScript больше не всегда лучше. Когда я нахожу способ, я могу сжать свой код, я реализую его немедленно, потому что знаю, что это принесет пользу производительности, даже если наименьшая сумма.

Например, использование ключевого слова var в функции, где переменная не требуется вне функции, помогает сборку мусора, которая обеспечивает очень небольшое ускорение скорости по сравнению с сохранением переменной в памяти.

С такой библиотекой, которая производит "миллионы операций в секунду" (слова Blaise), небольшие повышения производительности могут добавить заметную/измеримую разницу.

Таким образом, возможно, что my.class.js является "минималистским кодированием" или каким-то образом оптимизировано. Это могут быть даже ключевые слова var.

Надеюсь, это несколько помогло. Если это не помогло, я желаю вам удачи в получении хорошего ответа.