объявлена функция function f чем является f prototype
Блог о веб-программировании
Javascript, PHP, MySQL и т.д.
Функции в javascript. Урок 4. Прототипы
Как я уже оговорился в предыдущем уроке, вновь создаваемый объект не совсем пустой, у него есть, например, поле __proto__ (доступное явно не во всех браузерах), которое содержит ссылку на прототип этого объекта. Прототип — это тоже некий объект, который приходит на помощь, когда у нашего объекта нет полей и методов, нужных нашей программе. По сути, через прототипы в javascript реализуется механизм наследования. Как же задать прототип? Очень просто — через свойство prototype функции-конструктора:
Как видим, метод test(), определенный для прототипа, доступен во всех объектах, созданных этим конструктором. Более того, мы можем на лету менять содержимое прототипа (добавлять, удалять, менять поля и методы), и это сразу отражается на созданных объектах:
Можно даже так: через первый объект получаем доступ к прототипу, добавляем, а вызываем метод у второго объекта. Взгляните:
Нужно учитывать, что обращение к полю в прототипе происходит только тогда, когда у самого объекта нет поля с таким именем, например:
Как же нам узнать, является ли поле собственным для объекта или заимствуется у прототипа? Для этого у каждого объекта есть метод hasOwnProperty(), принимающий в качестве параметра строку с именем поля и возвращающий true или false, продемонстрируем на примере, как должен выглядеть перебор объекта методом for-in, учитывая возможность наличия полей в прототипе:
Как видим, попытка эмулировать ассоциативные массивы на javascript может закончиться не тем, что мы ожидали, если не учитывать «прототипную» природу этого языка. Этот пример также показывает, что мы легко можем добавлять новые поля и методы и во встроенные «типы» тоже.
То, что у всех объектов, создаваемых одним конструктором, единый объект-прототип, можно увидеть на следующем примере (учитывайте, что массив — это не примитивный тип, а тоже объект, поэтому поля разных объектов ссылаются на один и тот же массив):
Javascript: ООП, прототипы, замыкания, «класс» Timer.js
Часть 1. ООП с человеческим лицом.
Кратенько: ООП это вовсе не священная мантра, а по сути, просто методика организации приложений, структурирования кода, централизации методов и объединения сущностей в единое иерархическое семейство. Подобно тому, как строились субмарины и самолеты, перенимая опыт плавучести и летучести из живой природы, ООП приложения также используют восприятие программных сущностей, как неких «живых» объектов, перенимая известные нам из реального(offline — помните о таком?) мира характеристики и свойства.
Другими словами, создается некая сущность, которая не только имеет свои свойства и методы, но умеет порождать потомков и эволюционировать! Это называется расширением – extending. Словно бережный родитель, объект передает имущество по наследству, либо получает опыт поколений, будучи потомком другой родительской сущности – parent. Таким образом, создается единое древо поколений, в котором удобно ориентироваться и массово управлять в отличие от разрозненных библиотек — процедурный метод.
Как видите, — все как у людей! С той разницей, что разработчик являет собой бога этой системы и может переноситься по поколениям, внося изменения в самом корне, либо в отдельных ветвях развития. Войны и конфликты устраняем! Новые знания — добавляем! Или наоборот, все ломаем… Трудно быть богом! Однако сам принцип ООП обязывает разработчика структурировать приложение по правилам, а не как приспичит, что облегчает и систематизирует его поддержку, и что, впрочем, вовсе не мешает при желании запутать код даже в этом случае… 🙂
На мой взгляд, освоению ООП очень помогает именно такое «человеческое» восприятие принципов. Например, как и в жизни существуют строгие родители, которые заставляют детей уметь что-либо, что сами считают нужным! Их называют Абстрактные классы — abstract. Помните, как родители заставляли вас играть на фортепиано или учить стихи. Так вот, Абстрактные классы также как и многие родители вовсе и знать не знают зачем ребенку-потомку это будет нужно, и как он это будет использовать, но уверены, что так НАДО! Т.е. такие классы содержат абстрактные методы, которые являют собой объявление метода без самой реализации, как фантик без конфетки, тем самым обязывая потомка, этот метод реализовать. Как и в жизни, где родители нередко перекладывают на детей свои нереализованные мечты…
Вот в такой шутливо-серьезной форме, мы затронули тему абстрактных классов и семейных отношений, как способ понять… и то и другое. А если серьезно, то разумеется, в программировании не должно быть случайных методов, и любые методы и свойства являются частью продуманной иерархии классов, которая как генеалогическое дерево, может давать возможности расширять функционал от поколения к поколению. А абстрактные классы, и еще более абстрактные – интерфейсы ( interface — вообще не содержит реализаций ), помогают программисту не потерять, не забыть реализовать общие необходимые для всех потомков умения в жизни, без которых особь умрет, а с ней и приложение.
Кроме шуток, с практической стороны, при проектировании элементов приложения таких как например товары в магазине, объектное представление позволяет приблизить их к свойствам реальных объектов, которые как и в жизни инкапсулируют (т.е. содержат в себе) все необходимые аттрибуты: цену, количество, массу, срок хранения и прочие необходимые качества. Но так как и товары могут быть разными, то их классовая модель может ветвиться и развиваться, наследуя или переопределяя общие свойства.
Наследование — пожалуй, важнейшая особенность ООП. Если требуется новый виток эволюции, программист создает новый класс, расширяющий умения его родителя, а иногда и реализующий по новому, т.е. перекрывающий методы родителя — override. Ведь у каждого поколения свои понятия в жизни… Если же программисту нужен «опыт и понятия» прежних поколений — он обращается к ним. Ничто не потеряно в этой структуре, поэтому крайне важно уметь ею пользоваться.
И хотя пока для javascript нет полноценной ООП спецификации, возможность следовать принципам ООП есть, и этим удобством мы сейчас будем пользоваться. Конечно, в рамках данной статьи мы лишь коснемся основ понимания, но как известно – лиха беда начало, главное зацепиться…
Итак, наша цель сейчас написать некую управляемую сущность, которая по таймеру будет запускать нужные нам процессы. «Управляемую» – это значит такая сущность, или условно класс, будет заключать в себе – как говорят инкапсулировать, методы для управления и свойства, содержащие необходимые данные. Пример из жизни:
• свойства – это то, что объект знает( имя, цвет глаз, таблица умножения ),
• методы – это то, что объект умеет( спать, есть, строить синхрофазотрон ).
Важно! Если читатель еще не знает, что из себя представляет объект в javascript, — то рекомендую предварительно почитать об этом в любом справочнике, иначе будут возникать трудности понимания.
Создание объекта будет происходить через функцию, которая вызывается с директивой new. Именно директива new определяет, что функция эта не обычная вовсе, а специальная функция – Конструктор, которая создает и возвращает некий объект. До того же как она его возвратит, мы можем этому объекту присвоить все что душа пожелает: и знания и умения.
Итак, мы создали объект timer класса Timer. Но как творческие люди, мы можем захотеть уже при создании наделить свой объект некоторыми свойствами, или не наделять… Т.е. мы хотим универсальности, чтобы при желании можно было задать «знания и умения», или нет, но объект при этом не умер бы в муках, не умея например дышать… Мы же не звери, но как это сделать?
Для этого, первое, что мы поместим в нашем классе, — свойства по умолчанию, которые объект принимал бы от природы. И блок обработки.
… и указанные свойства будут записаны, а пропущенные — взяты из defaultOptions. Объект спасен! Универсальность и гибкость получена!
Обратим внимание и на комментарии //public
разумеется, он имеет тут условное значение (как и все ООП условности в javascript, включая понятие класс), но суть его в пометке Публичных свойств, т.е. доступных из вне. К таким свойствам можно обратиться напрямую через объект:
Как пример из жизни, это очевидные свойства объекта, которые не нуждаются в сокрытии: длина хвоста у кота.
Бывают также Личные свойства — private, и Защищенные — protected. Для доступа к Личным, если это позволительно вообще, нужно использовать специальные методы, в которых программист определяет, что и как и кому можно возвращать. Защищенные — protected, это чуть менее личные, т.к. доступны и для самого класса и для его наследников, в кругу «семьи» так сказать. Делается это для стабильности приложения, ведь не весь сор лучше выносить их избы…
Давайте и мы добавим Личные свойства — private, делается это созданием внутренних переменных внутри функции, таким образом, область их видимости ограничивается (или иначе — замыкается) самой функцией, — «никому не скажу, если не захочу»! О замыканиях мы еще поговорим… А пока, вставляем дальше в функцию Timer:
к таким свойствам, если захотим, мы позволим обращаться через публичные методы как:
Обратим внимание, что этот метод именно публичный, т.к. через this присвоен свойству объекта, к которому можно потом обратится через точку (не забудем про скобки на конце, если вызываем именно действие ):
если же нам нужен приватный метод, то он подобно приватным переменным, также создается «обычным» объявлением внутренней функции:
получилась служебная функция, которую Конструктор может вызвать для личных целей. Но из созданного конструктором объекта эта функция будет недоступна, т.к. не является его свойством.
Повторюсь, в javascript все это условности, которые помогают следовать принципам ООП, но не всегда обеспечивают точную реализацию. Например, с реализацией protected в javascript совсем туго! Дело в том, что protected частично сочетает в себе свойства и private- для недоступности из объекта, и public- для доступа из других классов, что в javascript противоречит друг другу, так как обеспечивается областью видимости — замыканием. Как вариант, можно создать public метод и внутри него проверять, является ли вызывающий его объект наследником хозяина метода и т.п. Идеального решения тут нет, все в рамках условностей.
Ну вроде, с организацией доступа чуток разобрались. А как насчет наследования, ведь это наиболее важное качество в ООП — способность перенимать и развивать умения и знания своих родителей? И тут мы подходим к важной особенности javascript — прототипному наследованию. Эта тема часто вызывает трудности понимания, и далее я сделаю свою попытку «объяснить все раз и навсегда» простым, человечеким языком.
Часть 2. Прототипы в javascript.
Итак, в javascript существует понятие прототип, скрытая ссылка [[prototype]] объекта, она же __proto__, и свойство prototype функции. Чтобы перестать путаться в этих понятиях, разберем их по одному:
Любой, уважающий себя, javascript-объект имеет скрытую ссылку [[prototype]], которая связывает его с родительским по замыслу объектом, который в свою очередь со своим и т.д. Наверху всей этой цепочки заседает встроенный объект javascript, этакий верховный прародитель, объектный адам, имеющий все необходимые встроенные методы, такие как toString, valueOf, hasOwnProperty и т.д. Благодаря этому, все объекты потомки тоже имеют этот минимально необходимый набор методов, позволяющий выжить в непростой среде javascript.
Т.е. даже если просто создать пустой объект var obj = <>, не имеющий методов и свойств, и обратиться к стандарному методу, то по цепочке ссылок [[prototype]] ( в данном случае минимально короткой цепочке ) он возьмет этот метод из встроенный объекта javascript:
Так реализуется прототипное наследование в javascript, — через цепочку ссылок [[prototype]]. Все свойства, доступные по цепочке прототипов будут открыты потомкам, как кладезь знаний и умений, что позволяет выстраивать настоящее эволюционирующее древо классов!
Обратим внимание, что прототипные свойства каждого объекта хранятся не в нем самом, а как бы в промежуточном звене цепочки прототипов, между текущим объектом и встроенным объектом javascript «в начале времен». Этот корневой объект требует уважения, и нарушать его покой не совсем прилично, поэтому для создания собственных прототипных свойств лучше создавать и встраивать в цепочку собственные же объекты с ссылкой [[prototype]].
Но, как мы помним, [[prototype]] — ссылка закрытая, как же нам выстроить свою цепочку, не имея к ней доступа? Тут нам помогает уже знакомая функция-Конструктор, с ключевым словом new, и свойство prototype, которое вполне себе открытое. Дело в том, что объект, создаваемый через Конструктор, получает ссылку [[prototype]] со значением, указанным в свойстве prototype этого Конструктора! Изначально, любая функция имеет в своем свойстве prototype ссылку на почти пустой объект(с единственным свойством constructor указывающим обратно на саму функцию),
но мы можем заменить свойство prototype, передавая свой родительский класс. Т.е. создавая функцию-Конструктор, мы просто присвоим в ее свойство prototype ссылку на объект с нужными нам свойствами, и новый создаваемый объект получит ссылку [[prototype]] на этот объект с нужными нам свойствами.
В примере выше скрытая ссылка [[prototype]] объекта obj получала указатель на некий объект имеющий свойство hi и метод sayHi. Таким образом объект obj наследовал это знание и умение.
Для упрощения этой процедуры придумана функция
Может возникнуть вопрос, зачем нужны первые три строки, почему бы сразу не сделать присвоение Child.prototype = Parent.prototype, безо всякого new F(), и дело с концом?!
Добавлять отдельные свойства в прототип Конструктора можно еще и так:
И кстати, не нужно пытаться обращаться к prototype как к свойству объекта — экземпляра класса.
У объекта нет свойства prototype, есть скрытая ссылка [[prototype]], а свойства prototype — нет!
Его конечно можно создать, но толку от него в наследовании никакого. Толк есть только от свойства prototype функции-Конструктора, благодаря ее способности передавать указатель в ссылку [[prototype]] создаваемого объекта.
Вот и все, что касается прототипного наследования. Правда просто?
Но прототипное наследование не единственно возможная схема. Хочу упомянуть также и метод вызова конструктора суперкласса, т.е. класса родителя, не даром же мы позаботились о его записи в свойства прототипа ( см. function extend ).
В конструкторе Timer нашего забытого примера, мы присваиваем объекту некоторые свойства через this. Чтобы передать эти свойства последующим поколениям, надо в конструкторе потомка сделать вызов родительского конструктора в контексте потомка т.е.:
Здесь важно помнить, что нельзя вызывать через this.superclass.constructor.apply, а именно через имя текущего конструктора( тут TimerPlayer), потому что иначе, если родительский конструктор тоже использует this, и вызывает this.superclass.constructor.apply(this, arguments), то это превратится в замкнутый вызов apply в контексте this как потомка, что вызовет ошибку.
Вызов родительского конструктора в контексте потомка создаст и присвоит потомку все его свойства и методы. Причем приватные свойства родителя, объявленные через var, а не через this, могут быть доступны только при наличии позволяющих их прочитать родительских публичных методов.
Именно этим путем мы и продолжаем строить наш Timer.
Часть 3. Javascript-класс Timer и его наследие.
Итак, у нас уже есть класс, что-то знающий, но ничего не умеющий, так что пора добавить ему умений! Чему мы желаем научить наш класс? Сделаем что-то вроде плеера:
• start
• pause
• stop
• rewind
• setToFrame
И некоторые менее важные методы. Представим, что мы их уже написали… Итак, вставляем дальше в функцию Timer:
Саму функцию timer я подробно прокомментировал. В целом идея простая:
Сперва, сохраняем ссылку на контекст нашего объекта в переменную, т.к. внутри функции вызываемой в setInterval контекст будет потерян, а переменная останется в замыкании, т.е в локальной области видимости. Возможно для понимания, следует повторить(или узнать) про замыкания, а мы о них еще поговорим ниже… Далее присваиваем нашему объекту свойство intervalId, которое возвращается методом setInterval, этот идентификатор позволит нам останавливать выполнение setInterval при паузе или стоп, смотрите эти методы.
Отдельного разбора требует свойство task, ведь именно там мы в некоем виде храним задачи для выполнения. Структура его такая:
Объект массивов объектов. Ой, лучше бы не говорил, а то сам запутался…
Но все просто, в объекте task под нужным номером кадра содержится массив заданий-объектов со свойством run. Этому свойству надо присвоить функцию, которая и вызовется при нужном кадре. При необходимости каждому заданию-объекту, можно добавить еще свойство, на то он и объект.
Также, по надобности, можно в массив добавлять новый объект-задание, пользуясь стандартными методами массивов push, unshift, splice.
Ну и разумеется самому объекту task можно присваивать свойство по номеру нужного кадра!
Таким образом, заполняя task и присваивая нашему классу методом setTask, мы определяем, что и когда ему делать. Как это можно использовать? Выполнять различные динамические сценарии на сайте или на клиенте оffline, создавать анимацию, создавать «живые» учебные пособия или тесты завязанные на времени, напоминать о важных событиях(чайник вкипел!), доставать пользователей всплывающей рекламой( мерзкая и гадкая шутка!). Или просто выводить часики в углу страницы!
Более того, у нас уже организовался простейший интерфейс, некое маленькое API для управления нашим таймером и визуализацией, и сейчас мы его используем, построив панель управления на подобие плеера! Вставляем на страницу html код, любимый и родной:
на событие onclick кнопочкам повешены методы объекта timer. Разумеется, перед вызовом которых объект следует не забыть создать. Помните как? — через функцию конструктор:
Однако, не плохо бы теперь и сценарий создать, чем управлять, а иначе — за что боролись.
Попробуем создать простую анимацию, будем перемещать картинку по странице, ну своеобразный «hello world» в мире анимации. Перемещать будем картинку
Напомню, что задача нашего таймера вызывать действие, какое нам угодно действие, при этом сам он за это действие не в ответе. Поэтому ЧТО именно делать — это наша задача, и мы ее сейчас решим, написав простенькую функцию перемещения элемента, которой передается сам элемент по ID и две его координаты:
Итак, теперь эту функцию нужно присвоить свойству run объекта-задания в массивах под номерами нужных кадров объекта task, следите за мыслью? Итак, создаем объект-сценарий, и первый его кадр определяем как массив, в этот кадр мы положим начальное положение элемента-картинки:
Почему нельзя написать run: moveElem( ball, 600, 600 )? Это неправильно, потому что синтаксис…
moveElem();
… означает вызов функции, а нам ее не надо вызывать тут и сейчас, а надо поместить в тело свойства-функции run, которая вызов и сделает. А иначе мы бы в run запихали результат выполненной moveElem() — undefined, поскольку она ничего не возвращает, и картинку нашу почем зря дернули бы.
И вуаля! Первому кадру мы добавили действие, которое помещает нашу картинку(воздушный шарик) в некую нижнюю позицию страницы. Теперь, чтобы начать подниматься, нам нужно покадрово изменять это состояние, т.е. уменьшать координату top, ну и left — с поправкой на ветер. 🙂 Для заполнения нужных кадров используем цикл. При желании, кстати, можно написать собственный метод класса Timer — который бы добавлял кадры, и действия, и распределял бы изменяющиеся параметры действий по кадрам… А пока, для примера, заполним циклом кадры со 2 по 600-й.
Здесь надо также обратить внимание на использование замыканий. Дело в том, что для передачи динамического i тут используется обертывание в функциональное выражение, которое вызывается на месте:
Если бы мы просто использовали:
То при вызов moveElem() i бы бралась из глобальной области видимости, т.е. та, которая у нас объявлена и отработана в цикле for(var i=2; i
F.prototype
If F.prototype is an object, then the new operator uses it to set [[Prototype]] for the new object.
JavaScript had prototypal inheritance from the beginning. It was one of the core features of the language.
But in the old times, there was no direct access to it. The only thing that worked reliably was a «prototype» property of the constructor function, described in this chapter. So there are many scripts that still use it.
Setting Rabbit.prototype = animal literally states the following: «When a new Rabbit is created, assign its [[Prototype]] to animal «.
That’s the resulting picture:
F.prototype property is only used when new F is called, it assigns [[Prototype]] of the new object.
Default F.prototype, constructor property
Every function has the «prototype» property even if we don’t supply it.
The default «prototype» is an object with the only property constructor that points back to the function itself.
Naturally, if we do nothing, the constructor property is available to all rabbits through [[Prototype]] :
We can use constructor property to create a new object using the same constructor as the existing one.
That’s handy when we have an object, don’t know which constructor was used for it (e.g. it comes from a 3rd party library), and we need to create another one of the same kind.
But probably the most important thing about «constructor» is that…
…JavaScript itself does not ensure the right «constructor» value.
Yes, it exists in the default «prototype» for functions, but that’s all. What happens with it later – is totally on us.
In particular, if we replace the default prototype as a whole, then there will be no «constructor» in it.
So, to keep the right «constructor» we can choose to add/remove properties to the default «prototype» instead of overwriting it as a whole:
Or, alternatively, recreate the constructor property manually:
Summary
In this chapter we briefly described the way of setting a [[Prototype]] for objects created via a constructor function. Later we’ll see more advanced programming patterns that rely on it.
Everything is quite simple, just a few notes to make things clear:
On regular objects the prototype is nothing special:
By default all functions have F.prototype = < constructor: F >, so we can get the constructor of an object by accessing its «constructor» property.
Tasks
Changing «prototype»
In the start, we have this code:
We added one more string (emphasized). What will alert show now?
…And if the code is like this (replaced one line)?
And like this (replaced one line)?
The assignment to Rabbit.prototype sets up [[Prototype]] for new objects, but it does not affect the existing ones.
So when we change its content through one reference, it is visible through the other one.
The property eats is deleted from the prototype, it doesn’t exist any more.
Create an object with the same constructor
Can we do it like that?
Give an example of a constructor function for obj which lets such code work right. And an example that makes it work wrong.
We can use such approach if we are sure that «constructor» property has the correct value.
Here’s how new user.constructor(‘Pete’) works:
(Just in case you’re curious, the new Object(. ) call converts its argument to an object. That’s a theoretical thing, in practice no one calls new Object with a value, and generally we don’t use new Object to make objects at all).
Встроенные прототипы
Свойство «prototype» широко используется внутри самого языка JavaScript. Все встроенные функции-конструкторы используют его.
Сначала мы рассмотрим детали, а затем используем «prototype» для добавления встроенным объектам новой функциональности.
Object.prototype
Давайте выведем пустой объект:
Вот что происходит:
Когда вызывается new Object() (или создаётся объект с помощью литерала <. >), свойство [[Prototype]] этого объекта устанавливается на Object.prototype по правилам, которые мы обсуждали в предыдущей главе:
Мы можем проверить это так:
Обратите внимание, что по цепочке прототипов выше Object.prototype больше нет свойства [[Prototype]] :
Другие встроенные прототипы
Вот более полная картина (для трёх встроенных объектов):
Давайте проверим прототипы:
В браузерных инструментах, таких как консоль разработчика, можно посмотреть цепочку наследования (возможно, потребуется использовать console.dir для встроенных объектов):
Примитивы
Самое сложное происходит со строками, числами и булевыми значениями.
Специальные значения null и undefined стоят особняком. У них нет объектов-обёрток, так что методы и свойства им недоступны. Также у них нет соответствующих прототипов.
Изменение встроенных прототипов
В течение процесса разработки у нас могут возникнуть идеи о новых встроенных методах, которые нам хотелось бы иметь, и искушение добавить их во встроенные прототипы. Это плохая идея.
Так что, в общем, изменение встроенных прототипов считается плохой идеей.
В современном программировании есть только один случай, в котором одобряется изменение встроенных прототипов. Это создание полифилов.
Полифил – это термин, который означает эмуляцию метода, который существует в спецификации JavaScript, но ещё не поддерживается текущим движком JavaScript.
Тогда мы можем реализовать его сами и добавить во встроенный прототип.
Заимствование у прототипов
В главе Декораторы и переадресация вызова, call/apply мы говорили о заимствовании методов.
Это когда мы берём метод из одного объекта и копируем его в другой.
Некоторые методы встроенных прототипов часто одалживают.
Например, если мы создаём объект, похожий на массив (псевдомассив), мы можем скопировать некоторые методы из Array в этот объект.
Но это будет невозможно, если obj уже наследует от другого объекта. Помните, мы можем наследовать только от одного объекта одновременно.
Заимствование методов – гибкий способ, позволяющий смешивать функциональность разных объектов по необходимости.