|
|
|
Всякий объект имеет связь с Object.prototype по цепи своего прототипа. Что произойдёт, если мы назначим здесь свойство? В этом случае, как вы, наверное, уже догадались, все элементы будут иметь доступ к такому свойству, все, включая и его родителя. Это обстоятельство идёт вразрез с самой концепцией объектно-ориентированного программирования, ведь мы всегда старались упорядочить информацию и функциональность нашей программы, создавая унифицированную логическую систему (объектно-ориентированные языки программирования по определению не могут иметь глобальных переменных!). Если у нас появится фрагмент информации, который необходимо сделать доступным всем типам объектов в вашей программе (включая, например "hello".xxx), то возникает другая проблема. А именно, это свойство будет также доступно всем используемым циклам for-in. Системные методы или свойства, такие как array.pop или object.toString могут иметь особое свойство don't enumerate - они относятся к разряду внутренних и не подчиняются общим правилам. Таким образом, если вы пишите for(var i in Object.prototype), что означает "покажи мне всё, что ты видишь" (enumerate), то не вернётся ничего. Эти свойства действительно находятся там и могут быть использованы, однако они невидны в листе свойств объекта. А вот свойства, которые вы сами добавляете, очень даже видны и, чем выше по цепи вы их добавляете, тем большее количество элементов их видит. Если вы думаете, что было бы очень удобно добавить что-то к Object prototype, то помните, что бесплатный сыр бывает только в мышеловке. Лучше делать всё правильно, а не как проще всего. |
Другая проблема, связанная с использованием метода extends, ещё более серьёзна. Прежде чем приступать к решению этой проблемы, давайте получше разберёмся в её сущности, с помощью слов и строчек кода постараемся максимально точно её определить, а уж потом приступим к поиску решения.
Свойства и методы могут принадлежать либо классу, либо экземпляру. Если они принадлежат классу, они идут в прототипе. Иногда у вас может возникнуть надобность устанавливать такие свойства, используя методы в том же прототипе или из прототипа классом выше. Это добавляет проблем. Во-первых, у свойства нет возможности доступа к методу в своём собственном прототипе или в прототипах более высокого уровня, на это способны только экземпляры (да и то только благодаря ключевому слову this). Во-вторых, из-за того, что в первую очередь обычно определяются свойства (и это правильно!) у них нет доступа к методам, которые ещё не определены, потому что форвард-ссылки недопустимы. Если бы мы стали в первую очередь определять методы, тогда у тех из них, которые мы вызываем, не было бы доступа к другим свойствам в прототипе (опять же потому что, возможно, мы их ещё не определили). Так дело не пойдёт! У нас возникло сразу две проблемы: отсутствие доступа к методам во время установления свойств прототипа и отсутствие форвард-ссылок.
Те, кто думает, что без этих функций можно обойтись, глубоко заблуждаются. Без них единственным выходом из создавшегося положения будет перемещение всех свойств (не методов) в экземпляры. Это типичная книжная рекомендация для пользователей JavaScript и ActionScript и даже для Flash'еров. Однако, поступая таким образом, мы лишаемся многих преимуществ объектно-ориентированного программирования. Это всё равно, что возить санки в гору, но с горы на них не кататься. Просто какой-то эксгибиционизм в безлюдном месте. Программа получается организованной неряшливо и кое-как. Именно так и выглядит большинство программ, написанных на ECMA, где по мере увеличения шкалы воцаряется всё больший и больший хаос. Наследование должно упорядочивать элементы, а не разбрасывать их где попало!
Давайте теперь попытаемся разобраться в проблеме более детально. Для того чтобы свойство прототипа в момент запуска имело доступ к методу в границах своего прототипа (или прототипа классом выше), мы должны указать точный путь. Помните, что это должно произойти до того, как создаётся экземпляр, в момент создания класса. Вот несколько строк кода:
Cat.prototype.petType = "cat"; Cat.prototype.defaultState = ???.setState("sleep"); Cat.prototype.setState = function( state ) { if( state == "sleep" ) return "The " + ???.petType + " is sleeping."; if( state == "eat" ) return "The " + ???.petType + " is eating."; }
Если делать это таким вот образом Cat.prototype.setState( ), то может, конечно, что-то получиться (хотя в данном случае не получится, догадываетесь почему?). Однако, теперь у вас появилась фиксированная ссылка на определённый участок. Если вы когда-либо измените имя класса, вам гарантировано как минимум утомительное "выкапывание", а иногда и многочасовая отладка. Вы можете возразить, что класс Cat уже фиксирован в левой части выражения (Cat.prototype.xxx), так в чём же проблема?.. Разумеется, ссылка в правой части создаёт намного больше проблем, но дело даже не в этом. Любая ссылка в коде - это плохо, так что вместо того, чтобы оправдываться, оглядываясь на чужие ошибки, лучше честно признать, что в списке наших бед появилась ещё одна. Теперь нам уже будет не так легко переименовывать классы, а ведь в процессе разработки структуры часто возникает необходимость в подобной операции. На худой конец и с этим можно было бы как-то мириться, если бы мы пришли, наконец к решению нашей проблемы. Но если теперь один из этих методов попытается вызвать свойство из своего прототипа (или из прототипа классом выше), как это делают возвратные значения, то всё окончательно запутается. Теперь, если мы напишем return Cat.prototype.petType то до поры, до времени всё это, конечно, будет работать, но что случится, если когда-нибудь свойство petType будет перезаписано? Что если подкласс или экземпляр выдадут нам что-то вроде xxx.petType = "siamese cat"? Тогда, будучи вызван из экземпляра SiameseCat, метод setState вернёт нам значение "The cat is eating", вместо "The siamese cat is eating". Это огромная проблема и нет никакой возможности её решить.
Вторая проблема - проблема форвард-ссылок. Она тоже возникает в коде, подобном вышеприведённому. Как уже упоминалось ранее, файлы SWF не поддерживают форвард-ссылки. Они не скомпилированы и просто не имеют представления о том, что за всем этим может последовать (а ведь это "последующее" часто бывает даже не загружено на тот момент). Таким образом, мы вызываем метод setState ещё до того, как создали его. Совершенно очевидно, что этот путь тупиковый. Если даже вы и определились уже с именами ваших будущих "детей", кто вам сказал, что они станут отзываться на эти имена? Вот так-то вот. Ну а что если мы просто поставим определения свойств перед методами? Тогда всё почти получится. Да, именно "почти", потому что тогда методы не будут иметь доступ к свойствам, которые ещё не определены. Какие ещё есть идеи? Разделить их на части? Или просто запоминать, "кто куда пошёл"? Всё это ни к чему не приведёт.
Итак, пришло время кратко изложить суть накопившихся проблем:
Так как же быть?
Проблема с форвард-ссылками похожа на старый как мир вопрос: "Что было раньше - курица или яйцо?". Мы знаем, что методы нужно определять раньше свойств. Мы так же знаем, что свойства нужно определять раньше методов. К счастью, на этот случай есть маленькая хитрость. Методы действительно должны быть определены первыми, но это не означает, что мы должны запускать их первыми. А вот свойства действительно должны быть обработаны в первую очередь. Вот ещё небольшая, но существенная уловка, прототип, как и любой другой элемент, это не класс, а объект. Мы знаем, что по сути объекты, это экземпляры, созданные из образца. А значит, мы можем создать образец, который будет определять прототип, точно так же, как мы делали в случае с другими экземплярами. Таким образом, все свойства и методы определяются в первую очередь, прежде, чем их вызывают. Если мы создадим такой образец внутри прототипа, то при его вызове ключевое слово this будет приравнено к прототипу класса. Так что все эти свойства и методы будут скопированы в прототип. Огромное преимущество этого способа заключается в том, что ключевое слово this приравнивается к прототипу, пока подготавливаются методы и свойства. Это означает, что свойства будут иметь доступ к методам, а методы в свою очередь будут иметь доступ к свойствам. Чтобы быть уверенным, что методы определяются до того, как свойства их вызывают, можно просто разделить их (методы и свойства) на две группы и первыми запускать методы. Потеряют ли в этом случае методы доступ к свойствам? Нет. Потому, что, как мы помним, методы в это время только определяются, а не запускаются. Таким образом, несмотря на то, что к моменту определения метода, свойство, которое он вызывает, ещё не создано, оно будет создано к тому моменту, как будет вызван метод. А как насчёт экземпляров? Если методы ссылаются на другие свойства в прототипе, не означает ли это, что, когда мы создаём экземпляр данного класса, он будет напрямую ссылаться на прототип? Нет. Потому, что значение слова this изменяется в момент доступа из экземпляра. Будучи вызвано из прототипа класса, оно ссылается на прототип. При вызове метода класса из экземпляра, this будет ссылаться на экземпляр. Красотища!
Как следствие таких ухищрений, мы получаем дополнительные удобства. Ваш код сам собой становится более организованным и лучше читается. Все свойства класса определяются вместе, равно как и все методы. Ссылок на имя класса становится всё меньше: одна для класса, одна для свойств/экземпляров, а затем ещё одна для установления наследования (если нужно, мы можем ещё уменьшить их количество). Нет никакой путаницы. Ничто ни с чем не конфликтует, ни одна пара свойств, ни одна пара определений методов, потому что после того, как они запускаются, мы можем (и даже должны) их удалять. А как же быть тем, кто любит устанавливать свойства и методы прямо в прототипе? Может быть вы просто привыкли всё делать именно так или у вас есть уже готовый код, который вы хотите использовать в дальнейшем или вы просто слишком упрямы, чтобы изменить своим привычкам? Никаких проблем! Вас никто не заставляет пользоваться этими методами, если вас не прельщают вышеперечисленные преимущества и удобства. Ведь, в конце концов, не происходит ничего кроме установления прототипа, так что если делать это напрямую, то работать всё будет точно так же, как и раньше (хотя и не настолько хорошо, как могло бы, но если вам так нравится, то пожалуйста...). Кроме тех, кто мечтал о трёх лишних дюймах, все получают что хотели.
Вот достаточно развёрнутый пример того, как могло бы выглядеть определение класса:
// Класс B = function( ){ } // Свойства B.prototype.classProperties = function ( ) { this.prop1 = 55; this.prop2 = this.double( this.prop1 ); } // Методы B.prototype.classMethods = function ( ) { this.double = function ( ) {return this.prop1 * 2;} } // так B становится подклассом A Object.extends( A, B );
Так как же тогда будет выглядеть наш специальный метод 'extends'? Попробуйте отследить его работу в нижеприведённом коде, игнорируя (пока что) ту часть, которая связана с customKeyword.
// Extends ------------------------ Object.extends = function( superClass, subClass ) { // связываем с customKeyword, если это верхний уровень // работает, даже если позднее добавить более высокие уровни if( (superClass.prototype.__proto__ == Object.prototype) && (superClass.prototype <> Object.customKeyword.prototype) ) { superClass.prototype.__proto__ = Object.customKeyword.prototype; // устанавливаем прототип сверхкласса, если он ещё не установлен if( typeof(superClass.prototype.classMethods) != undefined ) { superClass.prototype.classMethods(); delete superClass.prototype.classMethods; } if( typeof(superClass.prototype.classProperties) != undefined ) { superClass.prototype.classProperties(); delete superClass.prototype.classProperties; } } // связь с родителем subClass.prototype.__proto__ = superClass.prototype // Устанавливаем прототип подкласса. Чтобы избежать конфликтов // или повторного запуска, эти методы должны быть // удалены после того, как они своё отработают if( typeof(subClass.prototype.classMethods) != undefined ) { subClass.prototype.classMethods(); delete subClass.prototype.classMethods; } if( typeof(subClass.prototype.classProperties) != undefined ) { subClass.prototype.classProperties(); delete subClass.prototype.classProperties; } }
Ну всё, я хочу кофе! А вы? После такого определённо пора отдохнуть. Выпьем по чашечке, подождём минут пятнадцать, пока кофеин не подействует на наши усталые мозги и начнём разбираться с тем, как конструкторы передают аргументы вверх по цепи. Возможно, примеры кодов покажутся вам ещё более сложными, но зато не будет уже больше таких неожиданностей и подводных камней как в случае с extends, так что в конце вам всё это покажется простым и понятным.
|