Что делает 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?
2 ответа
Я не хочу никого обидеть, но такого рода вещи действительно не заслуживают внимания, ИМХО. Практически любая разница в скорости между браузерами связана с движком 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 сохранили всю область действия функции в памяти.
В этом фрагменте это не имеет особого значения, но для библиотек, написанных с использованием шаблона модуля (AFAIK, это почти все из них), который состоит из тысяч строк кода, это может иметь значение.
Как бы то ни было, современные JS-движки - это больше, чем просто "тупые" разборы и исполнения. Как вы сказали: идет JIT-компиляция, но есть еще много хитростей, чтобы максимально оптимизировать ваш код. Вполне возможно, что размещенный вами фрагмент написан так, что движок FF просто обожает.
Также очень важно помнить, что между Chrome и FF происходит какая-то скоростная битва за то, у кого самый быстрый двигатель. В прошлый раз, когда я проверял, что движок Mozilla Rhino превзошел Google V8, если это все еще актуально сегодня, я не могу сказать... С тех пор и Google, и Mozilla работают над своими двигателями...
Итог: различия в скорости между различными браузерами существуют - никто не может этого отрицать, но одна разница не имеет значения: вы никогда не будете писать сценарий, который делает только одну вещь снова и снова. Это общая производительность, которая имеет значение.
Вы должны иметь в виду, что 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 имеет тенденцию создавать единый объект функции для всех экземпляров, даже если эта функция является частью конструктора - или я м сказал.
В-третьих: обход длинной цепочки прототипов, как вы упомянули, не так необычен, как вы думаете, на самом деле это далеко не так. Вы постоянно пересекаете цепочки из двух или трех прототипов, как показано в примере 3. Это не должно замедлять вас, поскольку это просто присуще тому, как JS разрешает вызовы функций или разрешает выражения.
И наконец: возможно, JIT-компилируется, но утверждение, что другие библиотеки не JIT-компилированы, просто не складывается. Они могут, с другой стороны, они не могут. Как я уже говорил ранее: разные движки работают лучше в одних задачах, чем в других... может быть так, что FIT 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.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, поэтому ожидать, что все замедляется, все равно, что ожидать, что ваш мозг зажарится, потому что ваше мышление: да, вы можете устать, слишком много думая, но просто знаете, когда сделать перерыв. Как и в случае с цепочками прототипов: их здорово, просто знайте, что они немного медленнее, да...
Я не совсем уверен, но я знаю, что при программировании хорошей практикой является сделать код как можно меньше, не жертвуя функциональностью. Мне нравится это называть minimalist code
,
Это может быть хорошей причиной, чтобы запутать код. Обфускация уменьшает размер файла, используя меньшие имена методов и переменных, затрудняя обратный инжиниринг, уменьшая размер файла, ускоряя его загрузку, а также потенциальное повышение производительности. Код JavaScript Google сильно запутан, и это способствует их скорости.
Так что в JavaScript больше не всегда лучше. Когда я нахожу способ уменьшить свой код, я сразу же его реализую, потому что я знаю, что это будет способствовать повышению производительности, даже если оно будет наименьшим.
Например, используя var
Ключевое слово в функции, где переменная не нужна вне функции, помогает при сборке мусора, что обеспечивает очень небольшое увеличение скорости по сравнению с хранением переменной в памяти.
С такой библиотекой, которая производит "миллионы операций в секунду" (слова Блейза), небольшое повышение производительности может привести к заметной / ощутимой разнице.
Так что возможно, что my.class.js
"минималистский код" или оптимизирован в некотором роде. Это может быть даже var
ключевые слова.
Я надеюсь, что это помогло несколько. Если это не помогло, тогда я желаю вам удачи в получении хорошего ответа.