У JavaScript есть прототипы, а прототипы странные. Фактически, настолько странно, что некоторые языки (такие как CoffeeScript), которые компилируются в JavaScript, пытаются описать этот факт в попытке представить более полезный, легко усваиваемый пакет. Однако, как только вы научитесь использовать прототипы, они могут стать удивительно полезным инструментом в вашем арсенале.
Бесклассовое общество
Итак, вы находитесь в своей студии, создавая самую последнюю серию игр «Call of Murder». Вы уже сделали около восьми таких игр, поэтому вы решили изменить это и кодировать все это в JavaScript. Достаточно просто, правда? Однако есть небольшая проблема — JavaScript сказал, что он объектно-ориентированный, но вы не можете понять, как определить класс. Оказывается, вы не можете использовать классы! По крайней мере, не в традиционном смысле. Но есть определенно объекты.
Ваш первый объект
soldier = new Object()
Предыдущий код создает объект и называет его soldier
. Что вы хотите, чтобы солдат сделал? Как насчет прыжков? Вы можете сопоставить кнопку A
с функцией jump()
которую вы определили в другом месте, как показано ниже.
soldier.a = jump
По сути, вы устанавливаете свойство солдата равным функции. Обратите внимание, что мы не включили скобки. Это потому, что когда вы включаете скобки с функцией в JavaScript, она вызывает эту функцию. Если вы не включите скобки, он просто возвращает ссылку на саму функцию. Таким образом, если вы soldier.a()
, он выполнит функцию. Но, если вы напишите soldier.a
без скобок, он вернет реальную функцию.
Также обратите внимание, что вы назначаете функцию непосредственно на объект. Мы уже отметили причуды JavaScript, но они не связаны напрямую с системой-прототипом. Итак, учитывая, что вы довольно опытны в написании сценариев в JavaScript, вы переходите и назначаете функции другим кнопкам, как показано ниже.
soldier.b = punch soldier.x = reload soldier.r = machineGun
Копировать-Вставить Ад
Теперь вы хотите создать второго игрока, который будет почти таким же, как первый, за исключением того, что вместо пулемета они используют снайперскую винтовку. Есть два способа сделать это. Первый — повторить код, как показано ниже.
sniper = new Object() sniper.a = jump sniper.b = punch sniper.x = reload sniper.r = snipe
Это работает, но это утомительно. Тем более, что вы пообещали игровому дизайнеру, что он может поместить в игру 100 различных типов персонажей, каждый из которых будет иметь разные комбинации из 200 одинаковых ходов. Это много повторяющегося кода, много копий-вставок и много потенциальных ошибок.
Спасенные прототипами
Итак, вы перекодируете своего снайпера с помощью функции object()
, как показано ниже. Мы вернемся к фактической реализации object()
.
sniper = object(soldier) sniper.r = snipe
Вы загружаете свою игру, просто чтобы убедиться, и начинаете играть как снайпер. Конечно же, кнопка A
заставляет вашего персонажа прыгать. Вы нажимаете кнопку X
, и она перезагружается. Вы нажимаете кнопку R
, и она стреляет из снайперской винтовки. Похоже, что снайпер унаследовал все черты от солдата, а затем переписал функцию, сопоставленную с R
Если бы вы объясняли это по-английски, вы бы сказали: «Снайпер похож на солдата, за исключением того, что он стреляет из снайперской винтовки». Обратите внимание на сходство английского предложения с кодом, определяющим снайпера.
Кража функций
Давайте реализуем раненого снайпера. Они как обычный снайпер, за исключением того, что когда они пытаются прыгнуть, они кричат от боли.
woundedSniper = object(sniper) woundedSniper.a = function(){console.log('aaaargh my leg!')}
Здесь мы использовали анонимную функцию вместо ранее названной функции. Это хорошо, но это означает, что нам придется сделать что-то немного другое, когда мы определим класс woundedSoldier
.
woundedSoldier = object(soldier) woundedSoldier.a = woundedSniper.a
Ты это видел? Вы украли функцию сразу с другого объекта! Попробуйте сделать это на обычном объектно-ориентированном языке. Попробуй! (обратите внимание: это настоящий вызов. Я бы не удивился, если бы кто-то это сделал, но я был бы удивлен, если бы это было так же просто или так красиво.) Теперь пришло время взглянуть на функцию object()
.
За занавесом
function object(o){ function F(){} F.prototype = o; return new F(); }
object()
принимает один аргумент ( o
или то, что мы можем традиционно называть родительским объектом). Он создает фиктивную «классовую» функцию F()
, дополненную пустым конструктором. Он устанавливает свойство prototype
класса в аргумент, который вы передали. Затем он возвращает экземпляр фиктивного класса. Дуглас Крокфорд официально одобрил метод object(o)
над системой классов . На самом деле, вот где мы получили функцию.
Свойство prototype
Единственный остающийся вопрос, который у вас остался: что это за свойство prototype
? Может быть, это может быть полезно в вашем собственном коде? Если вы печатаете F.prototype
в консоли при создании снайпера, вы получите следующий вывод:
{ a: [Function], b: [Function], x: [Function], r: [Function] }
Вы узнаете их как сопоставления, которые мы сделали из различных кнопок для функций. Этого и следовало ожидать, потому что мы присвоили объект солдата прототипу фиктивного класса F
Однако затем мы создаем экземпляр класса-пустышки и назначаем его снайперу. Если вы спросите, что такое прототип снайпера, вы получите undefined
. Что произошло? И как снайпер знает, что делать, когда вы нажимаете кнопку X
?
Еще более странно — когда вы создаете woundedSniper
, прототип F
возвращает только отображение на кнопку R
Но, очевидно, woundedSniper
может перезагрузить и пробить. Как нам все это объяснить?
__proto__
Смущает, что есть два свойства прототипа. Когда вызывается конструктор, prototype
присваивается __proto__
. sniper.__proto__
возвращает следующее:
{ a: [Function], b: [Function], x: [Function], r: [Function] }
Еще интереснее то, что sniper.__proto__
имеет собственное __proto__
. sniper.__proto__.__proto__
дает вам функции, которые soldier
унаследовал от Object
. Между тем, prototype
просто используется в конструкторе для установки свойства __proto__
для реального объекта.
Вывод
При использовании функции object(o)
и открытии __proto__
мы видим, что прототипный код может делать столько же, сколько классический объектно-ориентированный код. Возможность определять уникальные экземпляры на лету, а также возможность передавать функцию напрямую из одного экземпляра в другой в любое время, делает ее в конечном итоге более мощной.
Дальнейшее чтение
Эта статья была частично вдохновлена публикацией в блоге Стива Йегге . Прочтите эту статью и обратите внимание на предстоящий пост о том, как CoffeeScript создает традиционную систему классов из прототипов JavaScript.