Видео с Frontend Dev Conf ’13
Frontend DEV Conf '13

Готово видео с моим выступлением на Frontend Dev Conf ’13. Я не тешу себя мыслью, что я идеальный рассказчик: слишком многое хочется в этом выступлении исправить. Но значит, мне есть к чему стремиться и над чем работать. В следующий раз я постараюсь, чтобы мой доклад звучал более гладко.

Я рекомендую всё-таки прочитать текстовую версию доклада (часть 1, часть 2), так как она более логичная и я потратил на нее значительно больше времени, чтобы донести и разъяснить сложные моменты.

ООП в JavaScript

ООП в JavaScript. Часть 2

Часть 1

private

Теперь, когда мы разобрались с наследованием, пришла пора посмотреть и на другие концепции из объектно ориентированного программирования. И начнём с ограничения доступа. Вероятно, вы догадались, что полноценных приватных методов и полей в JS нет и не планировалось. Но есть два различных подхода, чтобы ими обзавестись.

Замыкания

Я надеюсь, рассказывать, что такое замыкания, не требуется. В крайнем случае можете почитать какой-нибудь умный учебник.

Идея способа заключается в том, что мы замыкаем все необходимые приватные данные и функции внутри конструктора. Все публичные методы, которые работают с приватными данными, тоже должны быть объявлены в конструкторе.

var SomeClass = function (value) {
  var someVar = value;

  this.getSomeVar = function () {
    return someVar;
  }
};

Помните наш первый медленный вариант наследования? Это как раз он. Медленно, зато работает.

Аннотации

Второй подход активно используется, например, в Google Closure. Он не требует каких-то особых ухищрений, просто небольшая договорённость. Например, пусть все приватные поля и методы начинаются с подчёркивания (или заканчиваются подчёркиванием). И больше ничего, доступ к этим методам будет на нашей совести. Никаких предосторожностей, зато будет быстро работать.

Этот метод отлично работает со статическим анализатором кода, который, например, есть в Google Closure Compiler. Помечаем метод приватным — и компилятор будет выдавать предупреждения при попытке обратиться из-вне. Для этой цели используется специальные JSDoc-аннотации. Предыдущий пример с учётом всего сказанного будет выглядеть так:

/**
 * @param {number} value
 * @constructor
 */
var SomeClass = function (value) {
  this._someVar = value;
};

/**
 * @type {number}
 * @private
 */
SomeClass.prototype._someVar = 0;

/**
 * @returns {number}
 */
SomeClass.prototype.getSomeVar = function () {
  return this._someVar;
};

static

Статические члены класса обычно создаются путём добавления их к объекту конструктора. Пожалуй, проще показать на примере.

var SomeClass = function () {};
SomeClass.staticVar = 0;
SomeClass.staticMethod = function () {};

Так как данные поля принадлежат объекту конструктора, их можно считать статическими. Таким образом, получается вот такая структура:

Обращаться к таким полям/методам придётся тоже через имя конструктора: SomeClass.staticMethod().

Однако это ещё не всё. JavaScript достаточно гибок, чтобы дать нам возможность самим организовать позднее статическое связывание (late static binding). Это, конечно, не так легко, но мы справимся.

Для начала определимся, что такое позднее статическое связывание. Поправьте меня, если я ошибаюсь, но с таким определением сталкиваются в основном программисты на PHP: в других языках подобная функциональность была заложена ещё в самом начале.

Итак, допустим, у нас есть два класса: ParentClass и ChildClass. В ParentClass объявлено статическое свойство NAME, содержащее имя класса, и статический метод getName(), который это имя возвращает. В ChildClass мы переопределяем свойство NAME и присваиваем ему другое имя.

Так вот, в случае раннего связывания ChildClass.getName() будет возвращать ParentClass.NAME, потому что в этом методе стоит явная ссылка на класс, которому это свойство принадлежит. Т.е. getName() определена как-то так:

ParentClass.getName = function () {
  return ParentClass.NAME;
};

А в случае позднего статического связывания класс, который вызвал данный метод, определяется на этапе выполнения функции и поэтому ChildClass.getName() вернет ChildClass.NAME.

ParentClass.getName = function () {
  var ctor = <каким-то образом определенный класс>;
  return ctor.NAME;
};

А теперь попробуем позднее связывание реализовать.

Вариант 1

Первый вариант будет работать, если у нас доступно свойство __proto__.

var ParentClass = function () {};
ParentClass.NAME = 'ParentClass';
ParentClass.getClassName = function () {
  return this.NAME;
}

var ChildClass = function () {};
ChildClass.NAME = 'ChildClass';
ChildClass.__proto__ = ParentClass;

Когда мы вызываем ChildClass.getClassName(), интерпретатор ищет в дереве наследования getClassName и выполняет этот метод в контексте ChildClass. Т.е. this.NAME == ChildClass.NAME, что и требовалось получить. Если бы NAME был не определён, то интерпретатор искал бы и его в дереве наследования.

Вариант 2

В случае, если __proto__ не доступно, придётся воспользоваться более сложными методами. В таком случае можно вынести все статические свойства в отдельный объект и наследовать их параллельно.

var ParentClass = function () {};
ParentClass.static = {};
ParentClass.static.NAME = 'ParentClass';
ParentClass.static.getClassName = function () {
  return this.NAME;
};
ParentClass.prototype.static = ParentClass.static;

var ChildClass = function () {};

// Главная цепочка наследования
var tempCtor = function () {};
tempCtor.prototype = ParentClass.prototype;
ChildClass.prototype = new tempCtor();

// Наследование статических свойств
tempCtor.prototype = ParentClass.static;
ChildClass.static = new tempCtor();
ChildClass.prototype.static = ChildClass.static;

ChildClass.static.NAME = 'ChildClass';

Попробуем разобрать с той «простыней», которая у нас получилась.

Получаются 2 цепочки прототипов: одна для объектов, другая для статических полей и методов. Временная переменная tempCtop используется для тех же целей, что и в методе наследования с временным конструктором.

Получить доступ к статическим свойствам можно, например, так: ChildClass.static.NAME. Если нужно получить доступ из какого-либо метода класса, то это можно сделать через this.static (именно для этого у нас есть конструкция ChildClass.prototype.static = ChildClass.static). И, наконец, внутри статического метода все остальные статические методы и поля будут доступны просто через this, например, this.NAME. Весьма неплохо, как мне кажется.

private static

Лучший способ получить private static свойство — замкнуть переменную внутри анонимной функции. Хотя никто не мешает использовать способ с аннотациями и Google Closure Compiler. Покажу на примере синглтона:

var getClassInstance = function () {
  var instance;

  var Class = function () {
  };
  Class.prototype.method = function () {};

  return function () {
    if (!instance) {
      instance = new Class();
    }

    return instance;
  }
}();

superclass

Со ссылкой на родительский класс вообще всё просто — её нет. Поэтому, если вам нужна такая, добавьте её самостоятельно. Расширим пример с правильным вариантом наследования ещё одной строчкой:

var tempCtor = function () {};
tempCtor.prototype = ParentClass.prototype;
ChildClass.prototype = new tempCtor();
ChildClass.prototype.superclass = ParentClass;

Результат будет выглядеть так:

Вызвать метод предка можно так: this.superclass.prototype.method.call(this, arg0, arg1, ...).

Есть ещё один вариант, который больше подходит для использования в какой-нибудь библиотеке. Заключается он в том, чтобы добавить в прототип класса метод, скажем, inherited, который будет смотреть, кто его вызвал, а затем искать в дереве наследования метод с таким же именем и вызывать его. Работать такой метод, несомненно, будет медленнее, но и избавит от необходимости писать много букв и помнить, к каком классу принадлежит перекрываемый метод. Достаточно будет просто написать this.inherited(arg0, arg1, ...). В простом варианте данный метод будет выглядеть, например, так:

Object.prototype.inherited = function () {
    var proto = this.constructor.prototype;
    var method;
    var caller = arguments.callee.caller; 
    for (var i in proto) {
        if (proto[i] === caller) {
            method = i;
            break;
        }
    }

    if (method) {
        if (proto.superclass && 
            proto.superclass.prototype[method] !== undefined) {
            return proto.superclass.prototype[method].apply(this, arguments);
        }    
    }
};

Обратите внимание на переменную proto. Чтобы данная функция заработала, нужно самим добавлять в прототип объекта свойство constructor, указывающее на класс, к которому принадлежит данный объект. Так ведь свойство constructor уже есть, можете сказать вы. Но нужно помнить, что не все браузеры его поддерживают, поэтому лучше перестраховаться.

А если у нас новый браузер или node.js, то можно воспользоваться свойством __proto__.

Ключевое слово this

С ним всё на самом деле просто. В отличии от всех обычных объектно ориентированных языков, в которых this является указателем на текущий экземпляр класса, в JavaScript this указывает на контекст вызова функции. Т.е. если мы написали obj.method(), то внутри функции method значение this = obj. А если мы скопировали функцию в какой-нибудь другой объект (obj2.method = obj.method) и вызвали obj2.method(), то внутри this == obj2.

А что получится, если мы вызываем просто функцию, не принадлежащую никакому объекту? В этом случае this будет равен корневому объекту. Это тот же объект, в который попадают все наши глобальные переменные, и он называется window в том случае, когда речь заходит о браузерном JavaScript.

У объекта функции есть 2 стандартных метода, которые позволяют изменить контекст вызова: call и apply. Вы видели пример использования call, когда речь шла о вызове метода предка. Эти два метода различаются только способом передачи параметров: call ожидает первым параметром новый контекст, а потом все необходимые для передачи параметры. Для вызова же apply нужно 2 параметра: новый контекст и массив с параметрами, передаваемыми в функцию.

Пространства имён

Как таковых пространств имён в JavaScript нет. Но многие библиотеки применяют некую эмуляцию. Для этого просто используются глобальные объекты, в которые уже помещают необходимые классы. При этом обращение выглядит приблизительно так: some.namespace.SomeClass. Понятно, что объявить конструктор прямо так не получится:

some.namespace.SomeClass = function () {};

А не получится это потому, что some может быть не определено или some.namespace может не существовать. Значит, перед использованием пространства имен его нужно определить. Обычно это выглядит так:

some = some || {};
some.namespace = some.namespace || {};

Или же можно использовать какую-нибудь специальную функцию, которая сделает это за нас. Пример из Ext.js:

Ext.ns(‘some.namespace’);

Или вот пример из Google Closure Library:

goog.provide(‘some.namespace’);

Пример библиотеки

Чтобы всё обобщить попробуем создать свою небольшую библиотеку для работы с классами. Посмотреть конечный результат можно на GitHub. Сразу оговорюсь, что данная библиотека не предназначена для использования в реальных системах, а служит скорее примером, как можно собрать все те данные, что я привёл, в единое целое. Если увидите там неточность, скажите мне, я поправлю.

Расширение стандартных объектов

Расскажу про ещё одну интересную возможность JavaScript, которую трудно встретить в каком-нибудь другом нединамическом языке — расширение стандартных объектов. Связана она с тем, что прототип любого объекта — тоже объект, а значит его можно изменять. Т.е. когда нам нужен ещё один метод в классе Array, то мы можем его легко добавить:

Array.prototype.doNothing = function () {};

Эта конструкция добавит doNothing во всё массивы, включая уже созданные. А все потому, что прототип массива — это ссылка на Array.prototype. Даже больше, если мы создадим наследника от Array, то его экземпляры тоже получат изменения.

На этом эффекте основаны различные так называемые shim-библиотеки. Они добавляют в прототипы классов методы, которых почему-то там не хватает.

if (!Array.prototype.indexOf) {
    Array.prototype.indexOf = function () {...};
}

Google Closure

И напоследок хочу сказать пару слов о таком наборе инструментов от компании Google, как Google Closure, и в первую очередь о компиляторе. Дело в том, что он выполняет статическую проверку типов и позволяет на этапе компиляции увидеть проблемы кода. Так же работают компиляторы статических языков вроде Java. Например, он может следить, чтобы вы не пользовались приватными членами класса ниоткуда, кроме как из самого класса. Или же смотреть, чтобы вы не смогли передать параметры неверных типов в функцию. Конечно же это ограничивает вас в использовании языка, но вместе с тем уберегает от множества ошибок.

Ещё одна немаловажная часть Google Closure — это огромная библиотека. Если кого-то отпугивает её размер и сложность, учтите, что она разрабатывалась для написания очень больших JavaScript-приложений. На её основе сделаны большинство сервисов Google, в том числе Gmail, Google Reader и Google+1. Ещё кому-то библиотека может показаться слишком многословной, но не забываем, что написано так для того, чтобы Closure Compiler генерировал наиболее компактный и быстрый код.

Так вот, чтобы более эффективно использовать Google Closure Compiler, нам будет достаточно всего одного файла из библиотеки — base.js. В нем описаны функции для наследования, вызова предков, управления зависимостями между js-файлами, а также некоторое количество дополнительных вспомогательных функций, большую часть из которых следует воспринимать скорее как инструкции для компилятора.

В любом случае попробуйте. Если вы любите красивый код и у вас есть страсть к статической типизации, то это для вас.

Вместо заключения

Каждый для себя решает: использовать ли ему объектно-ориентированное программирование или нет. Конечно, если вы создаёте небольшую страничку с парой несложных эффектов, то лучше будет воспользоваться jQuery и не заморачиваться. Но если вы под действием новостей пишете замену медленно уходящему на покой Google Reader, то без объектно-ориентированного подхода будет трудно. Я же в свою очередь всего лишь показал вам способ, как можно перенять лучшие практики вроде шаблонов проектирования и начать, наконец, писать понятный и структурированный код.


  1. Информация с официального сайта библиотеки.

ООП в JavaScript. Часть 1

Введение

Как вы, должно быть, знаете, в JavaScript используется особенная модель объектно-ориентированного программирования. Оставим философские размышления о правильности или неправильности подобного подхода за рамками данного доклада и попытаемся понять, как с таким подходом жить. Тем более, что выбора у нас особо и нет.

Итак, обо всем по порядку.

Объекты

Объекты в JavaScript представляют собой, в первую очередь, ассоциативный массив. В нем строковым или числовым (а при большом желании и типа boolean) ключам соответствует одно любое значение. Значением массива может быть и функция, в таком случае она является методом этого объекта.

Для наглядности я буду обозначать объекты таким вот образом:

Кстати, многие знают, что у обыкновенных массивов и объектов практически никаких различий, кроме набора доступных методов и небольшого количества синтаксического сахара. Например, нет никакой разницы между obj.property и obj['property'] и следующий код:

var obj = {};
var arr = [];

генерирует идентичные объекты1. Единственной разница — в указателе на прототип. Кстати, о прототипах.

Прототипы

Прототип примерно соответствует классу в стандартной объектно-ориентированной модели. Особо обратите внимание на слово «примерно», потому что эти понятия всё-таки не тождественны: есть множество различий.

Прототип — это то место, где будет искаться поле или метод в том случае, если его нет в самом объекте. Например, когда мы пишем a.method(), то сначала наличие метода method будет проверено в объекте a, и, если там его нет, то в прототипе объекта a. Каждый объект имеет свой прототип.

«[[прототип]]» на рисунке выше — это внутреннее свойство объекта, его нельзя прочитать напрямую. Но есть стандартная функция Object.getPrototypeOf(obj)2, которая позволяет всё-таки посмотреть, что именно находится в прототипе.

Но по своей сути прототип тоже является объектом, т. е. ассоциативным массивом. А это значит, что у него тоже есть свой прототип. Таким образом строятся цепочки наследования. Например, если мы напишем arr = [], то у нас будет сформирована вот такая структура:

И даже у Object есть прототип, но он равен null, а потому никуда не ссылается.

Конструктор

Идем дальше. Как вы, должно быть, знаете из ООП, конструктор — это специальная функция, которая вызывается при создании объекта. В JavaScript тоже есть такая функция. Вернее даже так: в JavaScript любая функция может исполнять роль конструктора. Я понимаю, это сложно сразу воспринять, особенно учитывая тот факт, что функции зачастую возвращают какие-то данные с помощью конструкции return. Так вот, если вы воспользуетесь подобной функцией как конструктором, возвращаемое значение будет успешно отброшено и конструктор вернёт сконструированный объект3.

Для того чтобы вызвать какую-либо функцию как конструктор, достаточно написать перед ней слово new:

var f = new F();

Необходимо упомянуть ещё об одной детали перед тем, как мы перейдём к связи конструкторов и прототипов. Любая функция может вести себя как объект. Вы же помните, что в JavaScript всё (почти) может вести себя как объект? Т. е. ничто не мешает нам написать4:

F.someValue = 1;
F.someMethod = function () {};

У объекта функции есть несколько стандартных свойств/методов. Одно из них называется prototype. Это свойство и является будущим прототипом нашего объекта, сконструированного из этой функции.

Т. е. в момент создания объекта внутренняя ссылка этого объекта на прототип начинает ссылаться на тот же объект, что и свойство prototype у его конструктора. Важно понимать, что у объектов нет свойства prototype, это свойство есть только у функций. И по-умолчанию оно равно пустому объекту {}, а это то же самое, что и new Object().

Пример создания объекта

var F = function () {
  this.a = 1;
  this.b = 2;
}
F.prototype.a = 3;
F.prototype.c = 4;
var f = new F();
f.a = 5;
f.c = 6;

Разберём происходящее в этом примере более подробно.

К моменту создания объекта в строке 7 мы приходим с такой структурой:

Затем происходит вызов конструктора и создаётся новый пустой объект с прототипом, взятым из функции F:

Вслед за этим наш свежесозданный объект заполняется свойствами в конструкторе F.

И уже затем конструктор возвращает объект. Дальше он присваивается переменной f и дозаполняется.

Свойство __proto__

Помните, я сказал, что значение прототипа нельзя получить напрямую? Я несколько слукавил. Большая часть браузеров (кроме Internet Explorer) предоставляют подобную возможность. Для этого у каждого объекта есть свойство __proto__.

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

Некоторые считают свойство __proto__ устаревшим (deprecated), но это не так. Оно скорее нестандартное, но и это будет в относительно скором времени исправлено. В разрабатываемой спецификации ECMAScript 6 оно уже описано. Если вдруг кто не знает, JavaScript — это один из диалектов ECMAScript (наряду с ActionScript), а потому должен подчиняться общей спецификации.

Наследование

Теперь, когда основные концепции разобраны, пора переходить к наследованию. Начнём с неправильных вариантов, которые зачастую выдаются за верные.

Вариант №1

Простое копирование прототипов.

var ParentClass = function () {};
ParentClass.prototype.method1 = function () {};
var ChildClass = function () {};
ChildClass.prototype = ParentClass.prototype;
ChildClass.prototype.method2 = function () {};

Вы, наверное, уже и сами видите, почему этот вариант неправильный. Но всё равно давайте разберём.

В строке 1 объявляем конструктор родителя:

Дальше добавляем в прототип ParentClass метод method1:

Строка 3. Объявляем дочерний конструктор:

Присваиваем дочернему прототипу родительский:

«Добавляем» ещё один метод к дочернему прототипу:

Как видите, получилось совсем не то, что ожидалось.

Вариант 2

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

var ParentClass = function () {}; // ParentClass contructor
ParentClass.prototype.method1 = function () {};
var ChildClass = function () {}; // ChildClass constructor
ChildClass.prototype = ParentClass.prototype;
var child = new ChildClass();
child.method2 = function () {};

Различие только в двух последних строчках. Мы создаём экземпляр класса и добавляем в уже созданный объект ещё один метод.

Но ведь в таком случае нужно каждый раз писать включение дополнительных методов в объект! Согласен, можно вынести инициализацию в конструктор, ведь именно для этого он и предназначен.

var ChildClass = function () { // ChildClass constructor
  this.method2 = function () {};
};

Большая (и не всегда очевидная) проблема с этим методом — количество используемой памяти. Чуть позже я покажу результаты тестирования, они играют не в пользу данного метода. Ещё одна проблема в том, что наследование от ChildClass неосуществимо прямыми методами, придётся самостоятельно вызывать конструктор родителя, чтобы он добавил недостающие методы.

Вариант 3

В этом варианте будем простым копированием добавлять необходимые поля и методы в прототип:

var ParentClass = function () {}; // ParentClass contructor
ParentClass.prototype.method1 = function () {};
var ChildClass = function () {}; // ChildClass constructor
for (var prop in ParentClass.prototype) {
  if (ParentClass.prototype.hasOwnProperty(prop)) {
    ChildClass.prototype[prop] = ParentClass.prototype[prop];
  }
}
ChildClass.prototype.method2 = function () {};

Тут стоит обратить внимание на условие ParentClass.prototype.hasOwnProperty(prop). Эта конструкция for..in перебирает не только то, что находится в самом объекте, но ещё и то, что находится в его прототипе (строка [[прототип]] на диаграммах выше). Поэтому получается, что будут скопированы и те методы, которые достались по наследству от класса Object. А зачем нам их копировать, если они и так будут у дочернего класса? Метод hasOwnProperty как раз и позволяет избежать подобного поведения. Это стандартный метод класса Object и он возвращает true, если свойство находится в самом объекте, а не в его прототипе.

В результате выполнения кода выше мы получим 2 класса, как и задумывали:

Однако в таком способе, помимо необходимости вручную копировать поля и методы, есть и ещё один недостаток: невозможно использовать оператор instanceof. Т. е. если в любом объектно-ориентированном языке childObject instanceof ParentClass вернёт true, то здесь это не так. Кстати, то же самое касается и предыдущего варианта.

Вариант 4. Правильный

Что же, самое время разобраться с правильным вариантом. Как вообще должна выглядеть правильная структура объекта? На мой взгляд, так:

Так как же получить подобную красоту? Начнём с того, что у нас есть 2 конструктора: ChildClass и ParentClass.

Требуется, чтобы ChildClass.prototype был равен пустому объекту, у которого в прототипе будет стоять ParentClass.prototype.

Чтобы получить такой результат, достаточно одной строки:

ChildClass.prototype = new ParentClass();

Хотя в этом случае будет выполнен и конструктор ParentClass, что не всегда желательно. Поэтому лучшим решением будет создать временный класс с таким же прототипом, как в ParentClass, но пустым конструктором. И уже экземпляр этого временного конструктора присваивать дочернему прототипу:

var tempCtor = function () {};
tempCtor.prototype = ParentClass.prototype;
ChildClass.prototype = new tempCtor();

Вот в принципе и всё. То, что многие разработчики считают архисложным, записывается в 3-х строчках, которые, по-своей сути, можно заменить вообще одной.

Вариант 5. Используем Object.create

Идем дальше. Вы же помните, что в JavaScript есть несколько способов сделать одно и то же? Тоже и с наследованием. Всё шаманство с созданием временного класса и последующим созданием объекта можно выразить с помощью вызова всего лишь одного метода — Object.create.

Посмотрим описание этого метода. Он принимает 2 параметра: прототип объекта и описание свойств. Свойства выходят за рамки этой статьи, можете посмотреть их сами, а вот первый параметр — это именно то что нужно. Получается, что если вызвать Object.create только с одним параметром (прототипом создаваемого объекта), то результат будет аналогичным предыдущему способу.

ChildClass.prototype = Object.create(ParentClass.prototype);

К сожалению, Object.create объявлен только в спецификации ECMAScript 5, поэтому данный способ наследования работает только в относительно новых браузерах (IE9+, FF4+, O12+).

Вариант 6. Магия с prototype.__proto__

Ну и напоследок, есть совсем простой способ получить то, что требуется. Я рассказывал про наличие в некоторых браузерах свойства __proto__, которое представляет собой ссылку на прототип и которое в остальных браузерах недоступно. Получается, что мы можем просто указать прототипу класса, от кого он унаследован, безо всяких дополнительных действий.

ChildClass.prototype.__proto__ =  ParentClass.prototype;

Приведу ещё раз для наглядности схему с тем, что делает эта инструкция:

Вот так, легко и просто. Но посмотрим на список браузеров, где подобная конструкция будет работать правильно: Chrome, Firefox, Safari >= 5, Opera >= 10.50. Ни одна из версий Internet Explorer сюда не входит.

Упомяну так же, что данный способ очень часто используется в node.js, т. к. в этом случае нет необходимости поддерживать целый зоопарк различных браузеров.

Сравнение различных вариантов

Итак, у нас получилось 3 рабочих варианта, 2 более-менее и один совсем не рабочий. Неплохо бы их сравнить.

Поддержка браузерами

 Google ChromeMozilla FirefoxOperaIE6IE7IE8IE9IE10
Добавление методов в конструкторе
Копирование прототипа через for..in
Временный конструктор
Object.create
(4+)

(12+)
__proto__

Производительность

 Память (Мб)Скорость определения (оп/с)Скорость создания (оп/c)
Добавление методов в конструкторе74.9112 152 481 (34375,7%)31 716 (0,2%)
Копирование прототипа через for..in5.617 946 (22,5%)13 880 306 (96,7%)
Временный конструктор5.0320 484 (57,9%)14 361 306 (100%)
Object.create4.9835 352 (100%)14 103 497 (98,2%)
__proto__4.9822 474 (63,6%)13 893 309 (96,7%)

Как видно, первый способ можно смело исключить из-за ужасно медленного создания объектов. Не смотрите, что у него скорость определения классов в 35000 раз выше, это разовая операция, а вот создание будет использоваться постоянно.

Второй способ уступает по скорости определения, но всё ещё вполне годен для использования, если не принимать в расчёт тот факт, что это не совсем наследование. На самом деле, он активно используется, когда вопрос заходит об аналоге mixin или traits.

Остальные способы примерно равны по производительности и потреблению памяти.

Использование памяти измерялось в Google Chrome. В нем есть инструмент Profiles, который позволяет создавать снимки памяти и изучать, на что она была потрачена. Вы можете и сами провести исследование и сравнить с моим. Вот для этого вам несколько ссылок: генератор страниц для проверки потребления памяти, тестирование скорости определения классов на jsperf и тестирование скорости создания объектов на jsperf.

Лично я продолжу применять способ с использованием временного конструктора из-за того, что он поддерживается всеми браузерами.

Часть 2


  1. На самом деле, при создании массива будет еще создано поле length, в котором содержится длина.

  2. Поддерживается начиная с IE9.

  3. Это не верно, если функция-конструктор возвращает объект. В этом случае результатом будет возвращённый объект, а не полученный из этого конструктора. Т. е. в этом случае нет большой разницы между вызовом функции с new и вызовом без new.

  4. Кстати, именно так реализуются статические методы и свойства.

Тестирование

Написание доклада подошло к своей финальной стадии. Но чтобы его завершить, мне нужно собрать немножко данных о производительности. А поэтому просьба зайти по следующий двум ссылкам и нажать Run tests. Если вы это сделаете из нескольких различных браузеров, я буду вообще счастлив.

Вот тесты: скорость определения классов и скорость создания объектов.

Заранее спасибо.

Frontend DEV Conf ’13
Frontend DEV Conf '13

В апреле состоится Frontend DEV Conf ’13, на которую меня пригласили прочитать доклад. Я уже активно работаю над ним, чтобы многие смогли вынести для себя чего-нибудь полезное. Самое сложное — это уложить такую большую и сложную тему, как ООП в JavaScript, в связное повествование. В скором времени, когда я закончу активное написание, попрошу кого-нибудь проверить весь текст на адекватность.

Буду рад любым комментариям. И еще больше буду рад всем, кто соберется меня послушать.