Введение
Как вы, должно быть, знаете, в 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();
Вот в принципе и всё. То, что многие разработчики считают архисложным, записывается в
Вариант 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 Chrome | Mozilla Firefox | Opera | IE6 | IE7 | IE8 | IE9 | IE10 | |
---|---|---|---|---|---|---|---|---|
Добавление методов в конструкторе | ||||||||
Копирование прототипа через for..in | ||||||||
Временный конструктор | ||||||||
Object.create | (4+) | (12+) | ||||||
__proto__ |
Производительность
Память (Мб) | Скорость определения (оп/с) | Скорость создания (оп/c) | |
---|---|---|---|
Добавление методов в конструкторе | 74.91 | 12 152 481 (34375,7%) | 31 716 (0,2%) |
Копирование прототипа через for..in | 5.61 | 7 946 (22,5%) | 13 880 306 (96,7%) |
Временный конструктор | 5.03 | 20 484 (57,9%) | 14 361 306 (100%) |
Object.create | 4.98 | 35 352 (100%) | 14 103 497 (98,2%) |
__proto__ | 4.98 | 22 474 (63,6%) | 13 893 309 (96,7%) |
Как видно, первый способ можно смело исключить
Второй способ уступает по скорости определения, но всё ещё вполне годен для использования, если не принимать в расчёт тот факт, что это не совсем наследование. На самом деле, он активно используется, когда вопрос заходит об аналоге mixin или traits.
Остальные способы примерно равны по производительности и потреблению памяти.
Использование памяти измерялось в Google Chrome. В нем есть инструмент Profiles, который позволяет создавать снимки памяти и изучать, на что она была потрачена. Вы можете и сами провести исследование и сравнить с моим. Вот для этого вам несколько ссылок: генератор страниц для проверки потребления памяти, тестирование скорости определения классов на jsperf и тестирование скорости создания объектов на jsperf.
Лично я продолжу применять способ с использованием временного конструктора
На самом деле, при создании массива будет еще создано поле
length
, в котором содержится длина.Поддерживается начиная с IE9.
Это не верно, если
функция-конструктор возвращает объект. В этом случае результатом будет возвращённый объект, а не полученный из этого конструктора.Т. е. в этом случае нет большой разницы между вызовом функции сnew
и вызовом безnew
.Кстати, именно так реализуются статические методы и свойства.
А вы знаете, что в Internet Explorer вплоть до 8-го у document
нет метода getElementsByClassName
? А следовательно, все ваши верстки, основанные только на классах, будут сильно проигрывать в производительности. Все фреймворки эмулируют метод вызовом getElementsByTagName('*')
с дальнейшей ручной фильтрацией.
var els = document.getElementsByTagName("*"), filtered = [], len = els.length, i;
for (i = 0; i < len; ++i) {
el = els[i];
el.className.search(/^(.* |)some-class( .*|)$/) !== -1 && filtered.push(el);
}
Ну это код на случай, если вам придется использовать vanilla.js. Обратите внимание, что в цикле нельзя изменять DOM-дерево. Нужно сначала скопировать все подходящие элементы в отдельный массив и только потом их обрабатывать. Все потому, что getElementsByTagName
(как и все остальные похожие функции) возвращает NodeList
, который динамически изменяется вместе с деревом.
May the Force be with you.
Я всё-таки дописал этот функционал, хотя он отнял приличное количество времени. Попытаюсь поделиться своим опытом. Не буду рассказывать как реализовать получение списка файлов, т.к. в большей части это перекликается с самой загрузкой.
Oсторожно, сейчас будет простыня.
Во-первых, нам нужна форма загрузки:
<form class="well form-inline upload-form" action="/vault/fileupload" enctype="multipart/form-data" method="post">
<strong>Загрузить: </strong>
<input type="hidden" name="container" id="file-container" />
<input type="file" name="file" id="file-upload"
style="position: absolute; top: -100px; left: -100px;" />
<button class="btn" type="button" id="file-button"> Выбрать </button>
<input type="text" name="name" id="file-name" placeholder="Под именем" />
<button class="btn" type="submit"> Отправить </button>
</form>
Мне захотелось кастомную кнопку выбора файлов, для этого оригинальную кнопку мы прячем, а клик по ней эмулируем:
$('#file-button').click(function () {
$('#file-upload').click();
return false;
});
Стоит обратить внимание на способ, которым кнопка спрятана. Если просто указать display: none
, то опера перестанет воспринимать поддельные клики и окошко с выбором файла перестанет появляться. А если указать visibility: hidden
, то старое поле будет занимать положенное ему место и получится дырка на форме.
Итак, форму нарисовали, можно писать обработчик:
vaultFileUpload :: AppHandler ()
vaultFileUpload = do
tmpDir <- liftIO getTemporaryDirectory
response <- handleFileUploads tmpDir uploadPolicy partUploadPolicy processForm
writeBS $ pack $ encode response
Тут всё просто: получаем имя временной папки, обрабатываем форму и выводим результат, поэтому посмотрим внимательнее на функцию обработки:
response <- handleFileUploads tmpDir uploadPolicy partUploadPolicy processForm
Функция handleFileUploads
принимает на вход 4 параметра: временная папка, глобальная политика загрузки, функция, возвращающая по описанию части локальную политику загрузки, и, наконец, функция для обработки полученных данных. Т.е. мы загружаем все или только некоторые файлы во временную папку и вызываем обработчик:
processForm ((_, Left uploadError) : []) =
return $ ServiceError $ show uploadError
processForm ((_, Right path) : []) = do
container <- getPostParam "container"
name <- getPostParam "name"
if container == Nothing || name == Nothing
then return $ ServiceError "Container or name not defined"
else liftIO $ curlDo $ UploadFile path (unpack $ fromMaybe "" container) (unpack $ fromMaybe "" name)
processForm [] = return $ ServiceError "No files were transmitted"
processForm _ = return $ ServiceError "Too many files"
Интересен только второй случай, когда успешно загружен только один файл. Мы получаем два POST-параметра (имя контейнера Rackspace CloudFiles и имя, под которым файл должен быть загружен) и вызываем нашу функцию curlDo
с описанием нужной операции. UploadFile
— это конструктор моего типа ServiceAction
, где просто перечислены все необходимые операции:
data ServiceAction
= GetContainers
| GetContainerItems String
| UploadFile FilePath String String
Переходим к curlDo
.
curlDo :: ServiceAction -> IO ServiceResponse
curlDo action = withCurlDo $ do
h <- initialize
response <- curl h "https://auth.api.rackspacecloud.com/v1.0"
[CurlHttpHeaders
[ "X-Auth-Key: " ++ rackspaceAuthKey
, "X-Auth-User: " ++ rackspaceAuthUser
]
]
let headers = M.map (dropWhile (== ' ')) $ M.fromList $ respHeaders response
case respStatus response of
204 -> processAction action
(headers M.! "X-Storage-Url") (headers M.! "X-CDN-Management-Url")
(headers M.! "X-Auth-Token")
_ -> return $ ServiceError "Can't authenticate"
Мы отправляем аутентификационные данные на Cloud-сервер и проверяем их правильность. В случае удачи вызывается processAction
с действием, которое нужно выполнить. Ну и кроме того передаются некоторые заголовки из ответа сервера — там указано, к каким серверам дальше обращаться.
В функции processAction
несколько паттернов, по одному на каждую выполняемую операцию из типа ServiceAction
. Вот тот, что используется для операции загрузки файла:
processAction (UploadFile filePath container name) url _ =
uploadFile filePath container name url
Как видите, ничего особенного, просто вызов. Переходим к самому интересному — uploadFile
. Каркас этой функции выглядит так:
uploadFile filePath container name url token = do
h <- initialize
response <- withBinaryFile filePath ReadMode processFile
case respStatus response of
201 -> return ServiceSuccess
_ -> return $ ServiceDebug $ show $ respStatus response
Сначала мы инициализируем handle от curl c помощью initialize
, затем оборачиваем в withBinaryFile
работу с файлом (отправку) и отдаем результат в зависимости от кода, возвращенного сервером. Отправка файла реализуется через processFile
:
processFile fh = do
fileSize <- hFileSize fh
curl h (url ++ "/" ++ container ++ "/" ++ name)
[ CurlPut True
, CurlHttpHeaders ["X-Auth-Token: " ++ token]
, CurlReadFunction readFunction
, CurlInFileSize $ fromInteger fileSize
]
Тут задаются опции для вызова curl. Самое интересное и сложное — это функция readFunction
, которая отвечает за чтение файла и передачу данных в curl. Т.к. функция будет вызываться из libcurl, написана она весьма специфически с использованием библиотеки Foreign
:
readFunction :: Ptr CChar -> CInt -> CInt -> Ptr () -> IO (Maybe CInt)
readFunction ptr size nmemb _ = do
actualSize <- hGetBuf fh ptr $ fromInteger $ toInteger (size * nmemb)
return $ if (actualSize > 0) then Just $ fromInteger $ toInteger actualSize else Nothing
hGetBuf
читает из файла fh
в область по указателю pts
(size * nmemb)
байт и возвращает количество действительно прочитанных байт. Ну и сама функция должна вернуть Maybe CInt
, причем Nothing
возвращается в случае, если ничего не прочитано и читать дальше не надо.
Если собрать весь код функции в одно место, получится как-то так:
uploadFile filePath container name url token = do
h <- initialize
let
processFile fh = do
let
readFunction :: Ptr CChar -> CInt -> CInt -> Ptr () -> IO (Maybe CInt)
readFunction ptr size nmemb _ = do
actualSize <- hGetBuf fh ptr $ fromInteger $ toInteger (size * nmemb)
return $ if (actualSize > 0) then Just $ fromInteger $ toInteger actualSize else Nothing
fileSize <- hFileSize fh
curl h (url ++ "/" ++ container ++ "/" ++ name)
[ CurlPut True
, CurlHttpHeaders ["X-Auth-Token: " ++ token]
, CurlReadFunction readFunction
, CurlInFileSize $ fromInteger fileSize
]
response <- withBinaryFile filePath ReadMode processFile
case respStatus response of
201 -> return ServiceSuccess
_ -> return $ ServiceDebug $ show $ respStatus response
Вот такими нехитрыми действиями можно пересылать файлы с клиента через промежуточный сервер на сервера Rackspace CloudFiles. Все исходники лежат на GitHub, где и подлежат и вдумчивому изучению (trollface).
На рынке браузеров сложилась довольно интересная ситуация с поддержкой тега <audio>
. Оставим в стороне IE6–8, в которых вообще нет поддержки нативного воспроизведения аудио, и посмотрим на весь остальной зоопарк. Самый распространенный формат mp3 поддерживают Google Chrome, Safari, IE9-10. Firefox и Opera решили отказаться от него, видимо, из-за лицензионных ограничений. Зато эти браузеры поддерживают ogg, а IE и Safari — нет. Т.е. если хочешь поддерживать все браузеры, то будь добр сконвертировать свое аудио в mp3 и ogg. А еще не забудь прикрутить flash-плеер для совсем старых браузеров, ну или хотя бы ссылочку приложи на файлы, чтобы пользователи могли тебя услышать.
Кстати, точно так же обстоит дело с поддержкой еще двух форматов: WebM и AAC. WebM поддерживают Chrome, Firefox и Opera, а AAC — Chrome, IE и Safari. Получаем две воинствующие группировки IE-Safari и Firefox–Opera, которые работают с непересекающимися множествами форматов. Ну и Chrome, которому всё равно и он играет все, что ему подсунешь.
Табличка для тех, кто совсем ничего не понял:
MP3 | OGG | AAC | WebM | |
---|---|---|---|---|
Chrome | ||||
Firefox | ||||
Opera | ||||
Safari | ||||
Internet Explorer |
HTML5 audio вы можете видеть в предыдущем посте.