Professional Documents
Culture Documents
1. Objects
Everything you can manipulate in JavaScript is an object. This includes S t r i n g s ,A r r a y s ,N u m b e r s , F u n c t i o n s , and, obviously, the so-called O b j e c t there are primitives, but they're converted to an object when you need to operate upon them. An object in the language is simply a collection of key/value pairs (and some internal magic sometimes). There are no concepts of classes anywhere, though. That is, an object with properties n a m e :L i n d a , a g e :2 1is not an instance of any class, the O b j e c tclass. Both O b j e c tand L i n d aare instances of themselves. They define their own behaviour, directly. There are no layers of meta-data (i.e.: classes) to dictate what given object must look like. You might ask: "how?"; more so if you come from a highly classically Object Orientated language (like Java or C#). "Wouldn't having each object defining their own behaviour, instead of a common class mean that if I have 100 objects, I will have 100 different methods? Also, isn't it dangerous? How would one know if an object is really an Array, for example?" Well, to answer all those questions, we'll first need to unlearn everything about the classical OO approach and start from the ground up. But, trust me, it's worth it. The prototypical OO model brings in some new ways of solving old problems, in an more dynamic and expressive way. It also presents new and more powerful models for extensibility and code-reuse, which is what most people are interested about when they talk about Object Orientation. It does not, however, give you contracts. Thus, there are no static guarantees that an object Xwill always have a given set of properties, but to understand the trade-offs here, we'll need to know what we're talking about first.
Objects are created in JavaScript using the O b j e c t . c r e a t efunction. It takes a parent and an optional set of property descriptors and makes a brand new instance. We'll not worry much about the parameters now. An empty object is an object with no parent, and no properties. The syntax to create such object in JavaScript is the following: v a rm i k h a i l=O b j e c t . c r e a t e ( n u l l )
O b j e c t . d e f i n e P r o p e r t ywill create a new property if a property with the given key does not exist in the object, otherwise it'll update the semantics and value of the existing property.
By the way
By the way
You can also use the O b j e c t . d e f i n e P r o p e r t i e swhen you need to add more than one property to an object: O b j e c t . d e f i n e P r o p e r t i e s ( m i k h a i l ,{n a m e : {v a l u e : ' M i k h a i l ' ,w r i t a b l e : t r u e ,c o n f i g u r a b l e :t r u e ,e n u m e r a b l e : t r u e} {v a l u e : 1 9 ,w r i t a b l e : t r u e ,c o n f i g u r a b l e :t r u e ,e n u m e r a b l e : t r u e}
,a g e :
Obviously, both calls are overtly verbose albeit also quite configurable , thus not really meant for enduser code. It's better to create an abstraction layer on top of them.
1.3. Descriptors
The little objects that carry the semantics of a property are called descriptors (we used them in the previous O b j e c t . d e f i n e P r o p e r t ycalls). Descriptors can be one of two types - data descriptors or accessor descriptors. Both types of descriptor contain flags, which define how a property is treated in the language. If a flag is not set, it's assumed to be f a l s e unfortunately this is usually not a good default value for them, which adds to the verbosity of these descriptors.
writable
Whether the concrete value of the property may be changed. Only applies to data descriptors.
configurable
Whether the type of descriptor may be changed, or if the property can be removed.
enumerable
Whether the property is listed in a loop through the properties of the object. Data descriptors are those that hold concrete values, and therefore have an additional v a l u eparameter, describing the concrete data bound to the property:
value
The value of a property. Accessor descriptors, on the other hand, proxy access to the concrete value through getter and setter functions. When not set, they'll default to u n d e f i n e d .
get ()
A function called with no arguments when the property value is requested.
set (new_value)
A function called with the new value for the property when the user tries to modify the value of the property.
Where i d e n t i f i e ris the variable that holds the object containing the properties we want to access, and e x p r e s s i o nis any valid JavaScript expression that defines the name of the property. There are no constraints in which name a property can have1, everything is fair game. Thus, we could just as well rewrite our previous example as: m i k h a i l [ ' n a m e ' ] =' M i k h a i l ' m i k h a i l [ ' a g e ' ] =1 9 m i k h a i l [ ' g e n d e r ' ]=' M a l e '
Note
All property names are ultimately converted to a String, such that o b j e c t [ 1 ] ,o b j e c t [[ 1 ]] , o b j e c t [ ' 1 ' ]and o b j e c t [ v a r i a b l e ](when the variable resolves to 1 ) are all equivalent. There is another way of referring to a property called dot notation, which usually looks less cluttered and is easier to read than the bracket alternative. However, it only works when the property name is a valid JavaScript IdentifierName2, and doesn't allow for arbitrary expressions (so, variables here are a no-go). The rule for dot notation is: < d o t a c c e s s >: : =< i d e n t i f i e r >" . "< i d e n t i f i e r n a m e >
This would give us an even sweeter way of defining properties: m i k h a i l . n a m e =' M i k h a i l ' m i k h a i l . a g e =1 9 m i k h a i l . g e n d e r=' M a l e '
Both of these syntaxes are equivalent to creating a data property, with all semantic flags set to t r u e .
Trying to access a property that does not exist in the object simply returns u n d e f i n e d3: m i k h a i l [ ' a d d r e s s ' ] / /= >u n d e f i n e d
The d e l e t eoperator returns t r u eif the property was removed, f a l s eotherwise. I won't delve into details of the workings of this operator, since @kangax has already written a most awesome article on how delete works.
Then we can define a common way of accessing and setting both of those values at the same time let's call it n a m e : / /( )S t r i n g / /R e t u r n st h ef u l ln a m eo fo b j e c t . f u n c t i o ng e t _ f u l l _ n a m e ( ){ r e t u r nt h i s . f i r s t _ n a m e+''+t h i s . l a s t _ n a m e } / /( n e w _ n a m e : S t r i n g )u n d e f i n e d / /S e t st h en a m ec o m p o n e n t so ft h eo b j e c t ,f r o maf u l ln a m e . f u n c t i o ns e t _ f u l l _ n a m e ( n e w _ n a m e ){v a rn a m e s n a m e s=n e w _ n a m e . t r i m ( ) . s p l i t ( / \ s + / ) t h i s . f i r s t _ n a m e=n a m e s [' 0 ' ]| |' ' t h i s . l a s t _ n a m e =n a m e s [ ' 1 ' ]| |' ' } O b j e c t . d e f i n e P r o p e r t y ( m i k h a i l ,' n a m e ' ,{g e t :g e t _ f u l l _ n a m e ,s e t :s e t _ f u l l _ n a m e ,c o n f i g u r a b l e :t r u e ,e n u m e r a b l e : t r u e} )
Now, every-time we try to access the value of Mikhail's n a m eproperty, it'll execute the g e t _ f u l l _ n a m e getter.
m i k h a i l . n a m e / /= >' M i k h a i lW e i ' m i k h a i l . f i r s t _ n a m e / /= >' M i k h a i l ' m i k h a i l . l a s t _ n a m e / /= >' W e i ' m i k h a i l . l a s t _ n a m e=' W h i t e ' m i k h a i l . n a m e / /= >' M i k h a i lW h i t e '
We can also set the name of the object, by assigning a value to the property, this will then execute s e t _ f u l l _ n a m eto do the dirty work. m i k h a i l . n a m e=' M i c h a e lW h i t e ' m i k h a i l . n a m e / /= >' M i c h a e lW h i t e ' m i k h a i l . f i r s t _ n a m e / /= >' M i c h a e l ' m i k h a i l . l a s t _ n a m e / /= >' W h i t e '
Of course, getters and setters make property access and modification fairly slower. They do have some usecases, but while browsers don't optimise them better, methods seem to be the way to go. Also, it should be noted that while getters and setters are usually used for encapsulation in other languages, in ECMAScript 5 you still can't have such if you need the information to be stored in the object itself. All properties in an object are public.
The second way is using O b j e c t . k e y s , which returns all own properties that have been marked as enumerable when they were defined: O b j e c t . k e y s ( m i k h a i l ) / /= >[' n a m e ' ,' a g e ' ,' g e n d e r ']
Property names that are not valid identifiers must be quoted. Also note that the getter/setter notation for object literals strictly defines a new anonymous function. If you want to assign a previously declared function to a getter/setter, you need to use the O b j e c t . d e f i n e P r o p e r t yfunction. The rules for object literal can be described as the following:
< o b j e c t l i t e r a l > : : =" { "< p r o p e r t y l i s t >" } " ; < p r o p e r t y l i s t > : : =< p r o p e r t y >[ " , "< p r o p e r t y > ] * ; < p r o p e r t y > : : =< d a t a p r o p e r t y > |< g e t t e r p r o p e r t y > |< s e t t e r p r o p e r t y > ; < d a t a p r o p e r t y > : : =< p r o p e r t y n a m e >" : "< e x p r e s s i o n > ; < g e t t e r p r o p e r t y >: : =" g e t "< i d e n t i f i e r > : < f u n c t i o n p a r a m e t e r s > : < f u n c t i o n b l o c k > ; < s e t t e r p r o p e r t y >: : =" s e t "< i d e n t i f i e r > : < f u n c t i o n p a r a m e t e r s > : < f u n c t i o n b l o c k > ; < p r o p e r t y n a m e > : : =< i d e n t i f i e r > |< q u o t e d i d e n t i f i e r > ;
Object literals can only appear inside expressions in JavaScript. Since the syntax is ambiguous to block statements in the language, new-comers usually confound the two: / /T h i si sab l o c ks t a t e m e n t ,w i t hal a b e l : {f o o :' b a r '} / /= >' b a r ' / /T h i si sas y n t a xe r r o r( l a b e l sc a n ' tb eq u o t e d ) : {" f o o " :' b a r '} / /= >S y n t a x E r r o r :I n v a l i dl a b e l / /T h i si sa no b j e c tl i t e r a l( n o t et h ep a r e n t h e s i st of o r c e / /p a r s i n gt h ec o n t e n t sa sa ne x p r e s s i o n ) : ( {" f o o " :' b a r '} ) / /= >{f o o :' b a r '} / /W h e r et h ep a r s e ri sa l r e a d ye x p e c t i n ge x p r e s s i o n s , / /o b j e c tl i t e r a l sd o n ' tn e e dt ob ef o r c e d .E . g . : v a rx={f o o :' b a r '} f n ( { f o o :' b a r ' } ) r e t u r n{f o o :' b a r '} 1 ,{f o o :' b a r '} (. . .)
2. Methods
Up until now, the Mikhail object only defined slots of concrete data with the exception of the name getter/setter. Defining actions that may be performed on a certain object in JavaScript is just as simple. This is because JavaScript does not differentiate how you can manipulate a F u n c t i o n ,aN u m b e ror an O b j e c t . Everything is treated the same way (i.e.: functions in JavaScript are first-class). As such, to define an action for a given object, you just assign a function object reference to a property. Let's say we wanted a way for Mikhail to greet someone: / /( p e r s o n : S t r i n g )S t r i n g / /G r e e t sar a n d o mp e r s o n m i k h a i l . g r e e t=f u n c t i o n ( p e r s o n ){ r e t u r nt h i s . n a m e+' :W h y ,h e l l ot h e r e ,'+p e r s o n+' . ' }
After setting the property, we can use it the same way we used the concrete data that were assigned to the object. That is, accessing the property will return a reference to the function object stored there, so we can just call. m i k h a i l . g r e e t ( ' y o u ' ) / /= >' M i c h a e lW h i t e :W h y ,h e l l ot h e r e ,y o u . ' m i k h a i l . g r e e t ( ' K r i s t i n ' ) / /= >' M i c h a e lW h i t e :W h y ,h e l l ot h e r e ,K r i s t i n . '
2.1. Dynamic t h i s
One thing that you must have noticed both in the g r e e tfunction, and the functions we've used for the n a m e 's getter/setter, is that they use a magical variable called t h i s . It holds a reference to the object that the function is being applied to. This doesn't necessarily means that t h i swill equal the object where the function is stored. No, JavaScript is not so selfish. Functions are generics. That is, in JavaScript, what t h i srefers to is decided dynamically, at the time the function is called, and depending only on how such a function is called. Having t h i sdynamically resolved is an incredible powerful mechanism for the dynamism of JavaScript's object orientation and lack of strictly enforced structures (i.e.: classes), this means one can apply a function to any object that meets the requirements of the actions it performs, regardless of how the object has been constructed hack in some custom multiple dispatcher and you have CLOS.
/ /t h i s= = =g l o b a l
a d d . c a l l ( o n e ,o n e . v a l u e )/ /t h i s= = =o n e / /= >2
On the other hand, a p p l ylets you pass an array of parameters as the second parameter of the function. The array will be passed as positional arguments to the target function: a d d . a p p l y ( t w o ,[ 2 ,2 ] ) / /= >6 a d d . a p p l y ( w i n d o w ,[4] ) / /= >6 / /e q u i v a l e n tt ot w o . a d d ( 2 ,2 )
/ /e q u i v a l e n tt oa d d ( 4 )
a d d . a p p l y ( o n e ,[ o n e . v a l u e ] ) / /e q u i v a l e n tt oo n e . a d d ( o n e . v a l u e ) / /= >2
Note
Note
What t h i sresolves to when applying a function to n u l lor u n d e f i n e ddepends on the semantics used by the engine. Usually, it would be the same as explicitly applying the function to the global object. But if the engine is running on strict mode, then t h i swill be resolved as expected to the exact thing it was applied to: w i n d o w . v a l u e=2 a d d . c a l l ( u n d e f i n e d ,1 )/ /t h i s= = =w i n d o w / /= >3 v o i df u n c t i o n ( ){ " u s es t r i c t " a d d . c a l l ( u n d e f i n e d ,1 )/ /t h i s= = =u n d e f i n e d / /= >N a N / /S i n c ep r i m i t i v e sc a n ' th o l dp r o p e r t i e s . } ( )
3. Inheritance
Up to this point we have seen how objects can define their own behaviours, and how we can reuse (by explicit application) actions in other objects, however, this still doesn't give us a nice way for code reuse and extensibility. That's where inheritance comes into play. Inheritance allows for a greater separation of concerns, where objects define specialised behaviours by building upon the behaviours of other objects. The prototypical model goes further than that, though, and allows for selective extensibility, behaviour sharing and other interesting patterns we'll explore in a bit. Sad thing is: the specific model of prototypical OO implemented by JavaScript is a bit limited, so circumventing these limitations to accommodate these patterns will bring in a bit of overhead sometimes.
3.1. Prototypes
Inheritance in JavaScript revolves around cloning the behaviours of an object and extending it with specialised behaviours. The object that has it's behaviours cloned is called Prototype (not to be confounded with the p r o t o t y p eproperty of functions). A prototype is just a plain object, that happens to share it's behaviours with another object it acts as the object's parent. Now, the concepts of this behaviour cloning does not imply that you'll have two different copies of the same function, or data. In fact, JavaScript implements inheritance by delegation, all properties are kept in the parent, and access to them is just extended for the child. As mentioned previously, the parent (or [ [ P r o t o t y p e ]] ) of an object is defined by making a call to O b j e c t . c r e a t e , and passing a reference of the object to use as parent in the first parameter. This would come well in our example up until now. For example, the greeting and name actions can be well defined in a separate object and shared with other objects that need them. Which takes us to the following model:
v a rp e r s o n=O b j e c t . c r e a t e ( n u l l ) / /H e r ew ea r er e u s i n gt h ep r e v i o u sg e t t e r / s e t t e rf u n c t i o n s O b j e c t . d e f i n e P r o p e r t y ( p e r s o n ,' n a m e ' ,{g e t :g e t _ f u l l _ n a m e ,s e t :s e t _ f u l l _ n a m e ,c o n f i g u r a b l e :t r u e ,e n u m e r a b l e : t r u e} ) / /A n da d d i n gt h e` g r e e t 'f u n c t i o n p e r s o n . g r e e t=f u n c t i o n( p e r s o n ){ r e t u r nt h i s . n a m e+' :W h y ,h e l l ot h e r e ,'+p e r s o n+' . ' } / /T h e nw ec a ns h a r et h o s eb e h a v i o u r sw i t hM i k h a i l / /B yc r e a t i n gan e wo b j e c tt h a th a si t ' s[ [ P r o t o t y p e ] ]p r o p e r t y / /p o i n t i n gt o` p e r s o n ' . v a rm i k h a i l=O b j e c t . c r e a t e ( p e r s o n ) m i k h a i l . f i r s t _ n a m e=' M i k h a i l ' m i k h a i l . l a s t _ n a m e =' W e i ' m i k h a i l . a g e =1 9 m i k h a i l . g e n d e r =' M a l e ' / /A n dw ec a nt e s tw h e t h e rt h i n g sa r ea c t u a l l yw o r k i n g . / /F i r s t ,` n a m e 's h o u l db el o o k e do n` p e r s o n ' m i k h a i l . n a m e / /= >' M i k h a i lW e i ' / /S e t t i n g` n a m e 's h o u l dt r i g g e rt h es e t t e r m i k h a i l . n a m e=' M i c h a e lW h i t e ' / /S u c ht h a t` f i r s t _ n a m e 'a n d` l a s t _ n a m e 'n o wr e f l e c tt h e / /p r e v i o u s l yn a m es e t t i n g . m i k h a i l . f i r s t _ n a m e / /= >' M i c h a e l ' m i k h a i l . l a s t _ n a m e / /= >' W h i t e ' / /` g r e e t 'i sa l s oi n h e r i t e df r o m` p e r s o n ' . m i k h a i l . g r e e t ( ' y o u ' ) / /= >' M i c h a e lW h i t e :W h y ,h e l l ot h e r e ,y o u . ' / /A n dj u s tt ob es u r e ,w ec a nc h e c kw h i c hp r o p e r t i e sa c t u a l l y / /b e l o n gt o` m i k h a i l ' O b j e c t . k e y s ( m i k h a i l ) / /= >[' f i r s t _ n a m e ' ,' l a s t _ n a m e ' ,' a g e ' ,' g e n d e r ']
Note that both m i k h a i land k r i s t i ndefine their own version of g r e e t . In this case, whenever we call the g r e e tmethod on them they'll use their own version of that behaviour, instead of the one that was shared from p e r s o n . / /H e r ew es e tu pt h eg r e e t i n gf o rag e n e r i cp e r s o n / /( p e r s o n : S t r i n g )S t r i n g / /G r e e t st h eg i v e np e r s o n ,f o r m a l l y p e r s o n . g r e e t=f u n c t i o n ( p e r s o n ){ r e t u r nt h i s . n a m e+' :H e l l o ,'+( p e r s o n| |' y o u ' ) } / /A n dag r e e t i n gf o ro u rp r o t a g o n i s t ,M i k h a i l / /( p e r s o n : S t r i n g )S t r i n g
/ /G r e e t st h eg i v e np e r s o n ,l i k eab r o m i k h a i l . g r e e t=f u n c t i o n ( p e r s o n ){ r e t u r nt h i s . n a m e+' :\ ' s u p ,'+( p e r s o n| |' d u d e ' ) } / /A n dd e f i n eo u rn e wp r o t a g o n i s t ,K r i s t i n v a rk r i s t i n=O b j e c t . c r e a t e ( p e r s o n ) k r i s t i n . f i r s t _ n a m e=' K r i s t i n ' k r i s t i n . l a s t _ n a m e =' W e i ' k r i s t i n . a g e =1 9 k r i s t i n . g e n d e r =' F e m a l e ' / /A l o n g s i d ew i t hh e rs p e c i f i cg r e e t i n gm a n n e r s / /( p e r s o n : S t r i n g )S t r i n g / /G r e e t st h eg i v e np e r s o n ,s w e e t l y k r i s t i n . g r e e t=f u n c t i o n ( p e r s o n ){ r e t u r nt h i s . n a m e+' :\ ' e l l o ,'+( p e r s o n| |' s w e e t i e ' ) } / /F i n a l l y ,w et e s ti fe v e r y t h i n gw o r k sa c c o r d i n gt ot h ee x p e c t e d m i k h a i l . g r e e t ( k r i s t i n . f i r s t _ n a m e ) / /= >' M i c h a e lW h i t e :\ ' s u p ,K r i s t i n ' m i k h a i l . g r e e t ( ) / /= >' M i c h a e lW h i t e :\ ' s u p ,d u d e ' k r i s t i n . g r e e t ( m i k h a i l . f i r s t _ n a m e ) / /= >' K r i s t i nW e i :\ ' e l l o ,M i c h a e l ' / /A n dj u s ts ow ec h e c kh o wc o o lt h i s[ [ P r o t o t y p e ] ]t h i n gi s , / /l e t ' sg e tK r i s t i nb a c kt ot h eg e n e r i cb e h a v i o u r d e l e t ek r i s t i n . g r e e t / /= >t r u e k r i s t i n . g r e e t ( m i k h a i l . f i r s t _ n a m e ) / /= >' K r i s t i nW e i :H e l l o ,M i c h a e l '
3.4. Mixins
Prototypes allow for behaviour sharing in JavaScript, and although they are undeniably powerful, they aren't quite as powerful as they could be. For one, prototypes only allow that one object inherit from another single object, while extending those behaviours as they see fit. However, this approach quickly kills interesting things like behaviour composition, where we could mix-andmatch several objects into one, with all the advantages highlighted in the prototypical inheritance. Multiple inheritance would also allow the usage of data-parents objects that provide an example state that fulfils the requirements for a given behaviour. Default properties, if you will. Luckily, since we can define behaviours directly on an object in JavaScript, we can work-around these issues by using mixins and adding a little overhead at object's creation time. So, what are mixins anyways? Well, they are parent-less objects. That is, they fully define their own behaviour, and are mostly designed to be incorporated in other objects (although you could use their methods directly). Continuing with our little protagonists' scenario, let's extend it to add some capabilities to them. Let's say that every person can also be a p i a n i s tor a s i n g e r . A given person can have no such abilities, be just a pianist, just a singer or both. This is the kind of case where JavaScript's model of prototypical inheritance falls short, so we're going to cheat a little bit.
For mixins to work, we first need to have a way of combining different objects into a single one. JavaScript doesn't provide this out-of-the box, but we can easily make one by copying all own property descriptors, the ones defined directly in the object, rather than inherited, from one object to another.
Basically, what e x t e n ddoes here is taking two objects a source and a target, iterating over all properties present on the s o u r c eobject, and copying the property descriptors over to t a r g e t . Note that this is a destructive method, meaning that t a r g e twill be modified in-place. It's the cheapest way, though, and usually not a problem. Now that we have a method for copying properties over, we can start assigning multiple abilities to our objects (m i k h a i le k r i s t i n ):
/ /Ap i a n i s ti ss o m e o n ew h oc a n` p l a y 't h ep i a n o v a rp i a n i s t=O b j e c t . c r e a t e ( n u l l ) p i a n i s t . p l a y=f u n c t i o n ( ){ r e t u r nt h i s . n a m e+'s t a r t sp l a y i n gt h ep i a n o . ' } / /As i n g e ri ss o m e o n ew h oc a n` s i n g ' v a rs i n g e r=O b j e c t . c r e a t e ( n u l l ) s i n g e r . s i n g=f u n c t i o n ( ){ r e t u r nt h i s . n a m e+'s t a r t ss i n g i n g . ' } / /T h e nw ec a nm o v eo nt oa d d i n gt h o s ea b i l i t i e st o / /o u rm a i no b j e c t s : e x t e n d ( m i k h a i l ,p i a n i s t ) m i k h a i l . p l a y ( ) / /= >' M i c h a e lW h i t es t a r t sp l a y i n gt h ep i a n o . ' / /W ec a ns e et h a ta l lt h a te n d su pa sa no w np r o p e r t yo f / /m i k h a i l .I ti sn o ts h a r e d . O b j e c t . k e y s ( m i k h a i l ) [ ' f i r s t _ n a m e ' ,' l a s t _ n a m e ' ,' a g e ' ,' g e n d e r ' ,' g r e e t ' ,' p l a y ' ] / /T h e nw ec a nd e f i n ek r i s t i na sas i n g e r e x t e n d ( k r i s t i n ,s i n g e r ) k r i s t i n . s i n g ( ) / /= >' K r i s t i nW e i s t a r t ss i n g i n g . ' / /M i k h a i lc a n ' ts i n gy e tt h o u g h m i k h a i l . s i n g ( ) / /= >T y p e E r r o r :O b j e c t# < O b j e c t >h a sn om e t h o d' s i n g ' / /B u tm i k h a i lw i l li n h e r i tt h e` s i n g 'm e t h o di fw e / /e x t e n dt h eP e r s o np r o t o t y p ew i t hi t : e x t e n d ( p e r s o n ,s i n g e r ) m i k h a i l . s i n g ( ) / /= >' M i c h a e lW h i t es t a r t ss i n g i n g . '
/ /W ec a na s s e r ti t ' sr e a l l yb e i n gc a l l e do n` p e r s o n 'b y / /g i v i n g` p e r s o n 'a` f i r s t _ n a m e 'a n d` l a s t _ n a m e ' p e r s o n . f i r s t _ n a m e=' R a n d o m ' p e r s o n . l a s t _ n a m e =' P e r s o n ' O b j e c t . g e t P r o t o t y p e O f ( m i k h a i l ) . n a m e / /= >' R a n d o mP e r s o n '
So, a nave solution for applying a method stored in the [ [ P r o t o t y p e ] ]of an object to the current one, would then follow, quite naturally, by looking the property on the [ [ P r o t o t y p e ] ]of t h i s : v a rp r o t o=O b j e c t . g e t P r o t o t y p e O f / /( n a m e : S t r i n g )S t r i n g / /G r e e t ss o m e o n ei n t i m a t e l yi fw ek n o wt h e m ,o t h e r w i s eu s e / /t h eg e n e r i cg r e e t i n g m i k h a i l . g r e e t=f u n c t i o n ( n a m e ){ r e t u r nn a m e= =' K r i s t i nW e i ' ? t h i s . n a m e+' :H e y a ,K r i s t t y ' :/ *w ed u n n ot h i sg u y* / p r o t o ( t h i s ) . g r e e t . c a l l ( t h i s ,n a m e ) } m i k h a i l . g r e e t ( k r i s t i n . n a m e ) / /= >' M i c h a e lW h i t e :H e y a ,K r i s t t y ' m i k h a i l . g r e e t ( ' M a r g a r e t h ' ) / /= >' M i c h a e lW h i t e :H e l l o ,M a r g a r e t h '
This looks all good and well, but there's a little catch: it will enter in endless recursion if you try to apply this approach to more than one parent. This happens because the methods are always applied in the context of the message's first target, making the [ [ P r o t o t y p e ] ]lookup resolve always to the same object:
The simple solution to this, then, is to make all parent look-ups static, by passing the object where the current function is stored, rather than the object that the function was applied to. So, the last example becomes: v a rp r o t o=O b j e c t . g e t P r o t o t y p e O f / /( n a m e : S t r i n g )S t r i n g / /G r e e t ss o m e o n ei n t i m a t e l yi fw ek n o wt h e m ,o t h e r w i s eu s e / /t h eg e n e r i cg r e e t i n g . / / / /N o t et h a tn o ww ee x p l i c i t l ys t a t et h a tt h el o o k u ps h o u l dt a k e / /t h ep a r e n to f` m i k h a i l ' ,s ow ec a nb ea s s u r e dt h ec y c l i cp a r e n t / /r e s o l u t i o na b o v ew o n ' th a p p e n . m i k h a i l . g r e e t=f u n c t i o n ( n a m e ){ r e t u r nn a m e= =' K r i s t i nW e i ' ? t h i s . n a m e+' :H e y a ,K r i s t t y ' :/ *w ed u n n ot h i sg u y* / p r o t o ( m i k h a i l ) . g r e e t . c a l l ( t h i s ,n a m e ) } m i k h a i l . g r e e t ( k r i s t i n . n a m e ) / /= >' M i c h a e lW h i t e :H e y a ,K r i s t t y ' m i k h a i l . g r e e t ( ' M a r g a r e t h ' ) / /= >' M i c h a e lW h i t e :H e l l o ,M a r g a r e t h '
Still, this has quite some short-commings. First, since the object is hard-coded in the function, we can't just assign the function to any object and have it just work, as we did up 'till now. The function would always resolve to the parent of m i k h a i l , not of the object where it's stored. Likewise, we can't just apply the function to any object. The function is not generic anymore. Unfortunately, though, making the parent resolution dynamic would require us to pass an additional parameter to every function call, which is something that can't be achieved short of ugly hacks. The approach proposed for the next version of JavaScript only solves the first problem, which is the easiest. Here, we'll do the same, by introducing a new way of defining methods. Yes, methods, not generic functions.
Functions that need to access the properties in the [ [ P r o t o t y p e ] ]will require an additional information: the object where they are stored. This makes the lookup static, but solves our cyclic lookup problem. We do this by introducing a new function m a k e _ m e t h o d which creates a function that passes this information to the target function. / /( o b j e c t : O b j e c t ,f u n : F u n c t i o n )F u n c t i o n / /C r e a t e sam e t h o d f u n c t i o nm a k e _ m e t h o d ( o b j e c t ,f u n ){ r e t u r nf u n c t i o n ( ){v a ra r g s a r g s=s l i c e . c a l l ( a r g u m e n t s ) a r g s . u n s h i f t ( o b j e c t ) / /i n s e r t` o b j e c t 'a sf i r s tp a r a m e t e r f n . a p p l y ( t h i s ,a r g s )} }
/ /N o w ,a l lf u n c t i o n st h a ta r ee x p e c t e dt ob eu s e da sam e t h o d / /s h o u l dr e m e m b e rt or e s e r v et h ef i r s tp a r a m e t e rt ot h eo b j e c t / /w h e r et h e y ' r es t o r e d . / / / /N o t et h a t ,h o w e v e r ,t h i si sam a g i c a lp a r a m e t e ri n t r o d u c e d / /b yt h em e t h o df u n c t i o n ,s oa n yf u n c t i o nc a l l i n gt h em e t h o d / /s h o u l dp a s so n l yt h eu s u a la r g u m e n t s . f u n c t i o nm e s s a g e ( s e l f ,m e s s a g e ){v a rp a r e n t p a r e n t=O b j e c t . g e t P r o t o t y p e O f ( s e l f ) i f( p a r e n t& &p a r e n t . l o g ) p a r e n t . l o g . c a l l ( t h i s ,m e s s a g e ) c o n s o l e . l o g ( ' -A t'+s e l f . n a m e ) c o n s o l e . l o g ( t h i s . n a m e+' :'+m e s s a g e ) } / /H e r ew ed e f i n eap r o t o t y p ec h a i nC>B>A v a rA =O b j e c t . c r e a t e ( n u l l ) A . n a m e=' A ' A . l o g =m a k e _ m e t h o d ( A ,m e s s a g e ) v a rB =O b j e c t . c r e a t e ( A ) B . n a m e=' B ' B . l o g =m a k e _ m e t h o d ( B ,m e s s a g e ) v a rC =O b j e c t . c r e a t e ( B ) C . n a m e=' C ' C . l o g =m a k e _ m e t h o d ( C ,m e s s a g e ) / /A n dw ec a nt e s ti fi tw o r k sb yc a l l i n gt h em e t h o d s :
A . l o g ( ' f o o ' ) / /= >' -A tA ' / /= >' A :f o o ' B . l o g ( ' f o o ' ) / /= >' -A tA ' / /= >' B :f o o ' / /= >' -A tB ' / /= >' B :f o o ' C . l o g ( ' f o o ' ) / /= >' -A tA ' / /= >' C :f o o ' / /= >' -A tB ' / /= >' C :f o o ' / /= >' -A tC ' / /= >' C :f o o '
4. Constructors
Constructor functions are the old pattern for creating objects in JavaScript, which couple inheritance with initialisation in an imperative manner. Constructor functions are not, however, a special construct in the language. Any simple function can be used as a constructor function; just like t h i s , it all depends on how the function is called. So, what's it about constructor functions, really? Well, every function object in JavaScript automatically gets a p r o t o t y p eproperty, that is a simple object with a c o n s t r u c t o rproperty pointing back to the constructor function. And this object is used to determine the [ [ P r o t o t y p e ] ]of instances created with that constructor function. The following diagram shows the objects for the constructor function f u n c t i o nP e r s o n ( f i r s t _ n a m e , l a s t _ n a m e ) :
/ /W ec o u l dd e s u g a rt h ec o n s t r u c t o rp a t t e r ni nt h ef o l l o w i n g / /T a k i n gi n t oa c c o u n tt h a t` P e r s o n 'h e r em e a n st h e` p r o t o t y p e ' / /p r o p e r t yo ft h e` P e r s o n 'c o n s t r u c t o r . v a rP e r s o n=O b j e c t . c r e a t e ( O b j e c t . p r o t o t y p e ) / /( p e r s o n : S t r i n g )S t r i n g / /G r e e t st h eg i v e np e r s o n P e r s o n . g r e e t=f u n c t i o n ( p e r s o n ){ r e t u r nt h i s . n a m e+' :H a r r o ,'+p e r s o n+' . ' } / /H e r e ' sw h a tt h ec o n s t r u c t o rd o e sw h e nc a l l e dw i t h` n e w ' v a rp e r s o n=O b j e c t . c r e a t e ( P e r s o n ) p e r s o n . f i r s t _ n a m e=' M i k h a i l ' p e r s o n . l a s t _ n a m e =' W e i '
When a function is called with the n e wstatement, the following magic happens:
1. Create a fresh O b j e c t , inheriting from O b j e c t . p r o t o t y p e , say { } 2. Set the [ [ P r o t o t y p e ] ]internal property of the new object to point to the constructor's p r o t o t y p eproperty, so it inherits those behaviours. 3. Call the constructor in the context of this fresh object, such that t h i sinside the constructor will be the fresh object, and pass any parameters given to the function. 4. If the function returns an O b j e c t , make that be the return value of the function. 5. Otherwise, return the fresh object. This means that the resulting value of calling a f u n c t i o nwith the n e woperator is not necessarily the object that was created. A function is free to return any other O b j e c tvalue as it sees fit. This is an interesting and to a certain extent powerful behaviour, but also a confusing one for many newcomers: f u n c t i o nF o o ( ){ t h i s . f o o=' b a r ' } n e wF o o ( ) / /= >{f o o :' b a r '}
v a rp r o t o=O b j e c t . g e t P r o t o t y p e O f / /n e wM i k h a i l( a g e : N u m b e r ,g e n d e r : S t r i n g ) f u n c t i o nM i k h a i l ( a g e ,g e n d e r ){ / /F i n dt h ep a r e n to ft h i so b j e c ta n di n v o k ei t sc o n s t r u c t o r / /w i t ht h ec u r r e n tt h i s .W ec o u l dh a v eu s e d : / / P e r s o n . c a l l ( t h i s ,' M i k h a i l ' ,' W e i ' ) / /B u tw e ' dl o s es o m ef l e x i b i l i t yw i t ht h a t . p r o t o ( M i k h a i l . p r o t o t y p e ) . c o n s t r u c t o r . c a l l ( t h i s ,' M i k h a i l ' ,' W e i ' ) } / /I n h e r i t st h ep r o p e r t i e sf r o mP e r s o n . p r o t o t y p e M i k h a i l . p r o t o t y p e=O b j e c t . c r e a t e ( P e r s o n . p r o t o t y p e ) / /R e s e t st h e` c o n s t r u c t o r 'p r o p e r t yo ft h ep r o t o t y p eo b j e c t M i k h a i l . p r o t o t y p e . c o n s t r u c t o r=M i k h a i l / /( p e r s o n : S t r i n g )S t r i n g M i k h a i l . p r o t o t y p e . g r e e t=f u n c t i o n ( p e r s o n ){ r e t u r nt h i s . n a m e+' :\ ' s u p ,'+( p e r s o n| |' d u d e ' ) }
v a rx={} d e f i n e P r o p e r t y ( x ,' f o o ' ,{v a l u e :' b a r '} ) d e f i n e P r o p e r t y ( x ,' b a r ' ,{g e t :f u n c t i o n ( ){r e t u r nt h i s . f o o} ,s e t :f u n c t i o n ( v ) {t h i s . f o o=v } } ) x . f o o / /= >' b a r ' x . b a r / /= >' b a r ' x . b a r=' f o o ' x . f o o / /= >' f o o ' x . b a r / /= >' f o o '
However, in environments that don't expose the [ [ P r o t o t y p e ] ]link, things aren't quite as reliable. The only way of getting the prototype of an object, in this case, would be by the constructor's p r o t o t y p e property, but we can only access that from the object given the c o n s t r u c t o rproperty is kept intact. A fallback covering most cases would look like this: f u n c t i o np r o t o ( o b j e c t ){ r e t u r n! o b j e c t ? n u l l :' _ _ p r o t o _ _ 'i no b j e c t ? o b j e c t . _ _ p r o t o _ _ :/ *n o te x p o s e d ?* / o b j e c t . c o n s t r u c t o r . p r o t o t y p e }
Note that the actual O b j e c t . g e t P r o t o t y p e O fthrows a T y p e E r r o rwhen you pass something that is not an object to it.
6. Wrapping it up
The object orientation model chosen for JavaScript is definitely one of the things that makes the language expressive and powerful, however the really poor semantics from the before-ES5 age quite killed all the fun about it. With ECMAScript 5, we have got better ways to deal with objects and inheritance, but most of the API is pretty verbose and awkward to use out of the box, so abstracting them is the only sane way of exploring all the power of the first-class inheritance model provided by the language. Once you dwell on the depths of JavaScript's prototypical object orientation, however, you will find it lacking on aspects that would otherwise seem like the obvious thing to do like multiple inheritance and message resending, but also basic features like an easier object extension functionality. Luckily most of these issues manage to have a solution, albeit not necessarily a satisfactory one in some cases i.e.: manual mixins. Being able to reproduce semantics that are not provided straight away on the language by patterns leveraging the built-in constructs is an important part of the language, and this all is made easier because of the way functions are treated in JavaScript.
8. Acknowledgements
Thanks to @hughfdjackson, Alex and Taylor for the additional revision of the article. Thanks to -for pointing out (in the comments) that properties expect an IdentifierName , rather than a plain Identifier , which would allow reserved words (like foo.class or foo.null ) to be used unquoted.
Footnotes:
1
: Some implementations have magical names, like __proto__ , which may yield undesired and unwanted results when set. For example, the __proto__ property is used to define the parent of an object in some implementations. As such, you wouldn't be able to set a string or number to that.
2
: While an IdentifierName also allows reserved words, you should be aware that ECMAScript engines that aren't fully compliant with the ECMAScript 5 specs may choke on reserved words when they're used for property access unquoted.
3
: It should be noted that, while ECMAScript-defined native objects don't throw an error when you try to access a non-existing property, it's not guaranteed that the same will hold true for a host object (i.e. an object defined by the engine implementor). After all, host object semantics are not defined, they are dependent on the particular run-time implementation.
4
Some engines do expose the [ [Prototype] slot, usually ] through a property like __proto__ , however no such thing is described in the specifications for the language, so it's recommended that you avoid using it, unless you're well aware that all platforms you code must run on will have such means of setting the [ [Prototype] object directly. ] It should also be noted that messing with the prototype chain might defeat all look-up optimisations in the JS engine.