- Chapter 17. Objects and Inheritance
- Layer 1: Single Objects
- Converting Any Value to an Object
- this as an Implicit Parameter of Functions and Methods
- Layer 2: The Prototype Relationship Between Objects
- Iteration and Detection of Properties
- Best Practices: Iterating over Own Properties
- Accessors (Getters and Setters)
- Property Attributes and Property Descriptors
- Protecting Objects
- Layer 3: Constructors—Factories for Instances
- Data in Prototype Properties
- Keeping Data Private
- Layer 4: Inheritance Between Constructors
- Inheriting Instance Properties
- Inheriting Prototype Properties
- Confused by the two kinds of prototypes?
- Ensuring That instanceof Works
- Overriding a Method
- Making a Supercall
- Avoiding Hardcoding the Name of the Superconstructor
- Tip
- Example: Constructor Inheritance in Use
- Example: The Inheritance Hierarchy of Built-in Constructors
- Antipattern: The Prototype Is an Instance of the Superconstructor
- Methods of All Objects
- Generic Methods: Borrowing Methods from Prototypes
- Pitfalls: Using an Object as a Map
- Cheat Sheet: Working with Objects
buy the book to support the author.
Chapter 17. Objects and Inheritance
There are several layers to object-oriented programming (OOP) in JavaScript:
- Layer 1: Object-orientation with single objects (covered in Layer 1: Single Objects)
- Layer 2: Prototype chains of objects (described in Layer 2: The Prototype Relationship Between Objects)
- Layer 3: Constructors as factories for instances, similar to classes in other languages (discussed in Layer 3: Constructors—Factories for Instances)
- Layer 4: Subclassing, creating new constructors by inheriting from existing ones (covered in Layer 4: Inheritance Between Constructors)
Layer 1: Single Objects
Roughly, all objects in JavaScript are maps (dictionaries) from strings to values. A (key, value) entry in an object is called a property. The key of a property is always a text string. The value of a property can be any JavaScript value, including a function. Methods are properties whose values are functions.
Kinds of Properties
There are three kinds of properties:
- Properties (or named data properties)
- Normal properties in an object—that is, mappings from string keys to values. Named data properties include methods. This is by far the most common kind of property.
- Accessors (or named accessor properties)
- Special methods whose invocations look like reading or writing properties.Normal properties are storage locations for property values; accessors allow you to compute the values of properties. They are virtual properties, if you will. See Accessors (Getters and Setters) for details.
- Internal properties
- Exist only in the ECMAScript language specification. They are not directly accessible from JavaScript, but there might be indirect ways of accessing them. The specification writes the keys of internal properties in brackets. For example,
[[Prototype]]
holds the prototype of an object and is readable viaObject.getPrototypeOf()
.
Object Literals
JavaScript’s object literals allow you to directly create plain objects (direct instances of Object
). The following code uses an object literal to assign an object to the variable jane
. The object has the two properties: name
and describe
. describe
is a method:
var
jane
=
{
name
:
'Jane'
,
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
// (1)
},
// (2)
};
- Use
this
in methods to refer to the current object (also called the receiver of a method invocation). - ECMAScript 5 allows a trailing comma (after the last property) in an object literal. Alas, not all older browsers support it. A trailing comma is useful, because you can rearrange properties without having to worry which property is last.
You may get the impression that objects are only maps from strings to values. But they are more than that: they are real general-purpose objects. For example, you can use inheritance between objects (see Layer 2: The Prototype Relationship Between Objects), and you can protect objects from being changed. The ability to directly create objects is one of JavaScript’s standout features: you can start with concrete objects (no classes needed!) and introduce abstractions later. For example, constructors, which are factories for objects (as discussed in Layer 3: Constructors—Factories for Instances), are roughly similar to classes in other languages.
Dot Operator (.): Accessing Properties via Fixed Keys
The dot operator provides a compact syntax for accessing properties. The property keys must be identifiers (consult Legal Identifiers).If you want to read or write properties with arbitrary names, you need to use the bracket operator (see Bracket Operator ([]): Accessing Properties via Computed Keys).
The examples in this section work with the following object:
var
jane
=
{
name
:
'Jane'
,
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
}
};
Getting properties
The dot operator lets you “get” a property (read its value). Here are some examples:
- > jane.name // get property `name`
- 'Jane'
- > jane.describe // get property `describe`
- [Function]
Getting a property that doesn’t exist returns undefined
:
- > jane.unknownProperty
- undefined
Calling methods
The dot operator is also used to call methods:
- > jane.describe() // call method `describe`
- 'Person named Jane'
Setting properties
You can use the assignment operator (=
) to set the value of a property referred to via the dot notation. For example:
- > jane.name = 'John'; // set property `name`
- > jane.describe()
- 'Person named John'
If a property doesn’t exist yet, setting it automatically creates it. If a property already exists, setting it changes its value.
Deleting properties
The delete
operator lets you completely remove a property (the whole key-value pair) from an object. For example:
- > var obj = { hello: 'world' };
- > delete obj.hello
- true
- > obj.hello
- undefined
If you merely set a property to undefined
, the property still exists and the object still contains its key:
- > var obj = { foo: 'a', bar: 'b' };
- > obj.foo = undefined;
- > Object.keys(obj)
- [ 'foo', 'bar' ]
If you delete the property, its key is gone, too:
- > delete obj.foo
- true
- > Object.keys(obj)
- [ 'bar' ]
delete
affects only the direct (“own,” noninherited) properties of an object. Its prototypes are not touched (see Deleting an inherited property).
Tip
Use the delete
operator sparingly. Most modern JavaScript engines optimize the performance of instances created by constructors if their “shape” doesn’t change (roughly: no properties are removed or added). Deleting a property prevents that optimization.
The return value of delete
delete
returns false
if the property is an own property, but cannot be deleted. It returns true
in all other cases. Following are some examples.
As a preparation, we create one property that can be deleted and another one that can’t be deleted (Getting and Defining Properties via Descriptors explains Object.defineProperty()
):
var
obj
=
{};
Object
.
defineProperty
(
obj
,
'canBeDeleted'
,
{
value
:
123
,
configurable
:
true
});
Object
.
defineProperty
(
obj
,
'cannotBeDeleted'
,
{
value
:
456
,
configurable
:
false
});
delete
returns false
for own properties that can’t be deleted:
- > delete obj.cannotBeDeleted
- false
delete
returns true
in all other cases:
- > delete obj.doesNotExist
- true
- > delete obj.canBeDeleted
- true
delete
returns true
even if it doesn’t change anything (inherited properties are never removed):
- > delete obj.toString
- true
- > obj.toString // still there
- [Function: toString]
Unusual Property Keys
While you can’t use reserved words (such as var
and function
) as variable names, you can use them as property keys:
- > var obj = { var: 'a', function: 'b' };
- > obj.var
- 'a'
- > obj.function
- 'b'
Numbers can be used as property keys in object literals, but they are interpreted as strings. The dot operator can only access properties whose keys are identifiers. Therefore, you need the bracket operator (shown in the following example) to access properties whose keys are numbers:
- > var obj = { 0.7: 'abc' };
- > Object.keys(obj)
- [ '0.7' ]
- > obj['0.7']
- 'abc'
Object literals also allow you to use arbitrary strings (that are neither identifiers nor numbers) as property keys, but you must quote them. Again, you need the bracket operator to access the property values:
- > var obj = { 'not an identifier': 123 };
- > Object.keys(obj)
- [ 'not an identifier' ]
- > obj['not an identifier']
- 123
Bracket Operator ([]): Accessing Properties via Computed Keys
While the dot operator works with fixed property keys, the bracket operator allows you to refer to a property via an expression.
Getting properties via the bracket operator
The bracket operator lets you compute the key of a property, via an expression:
- > var obj = { someProperty: 'abc' };
- > obj['some' + 'Property']
- 'abc'
- > var propKey = 'someProperty';
- > obj[propKey]
- 'abc'
That also allows you to access properties whose keys are not identifiers:
- > var obj = { 'not an identifier': 123 };
- > obj['not an identifier']
- 123
Note that the bracket operator coerces its interior to string. For example:
- > var obj = { '6': 'bar' };
- > obj[3+3] // key: the string '6'
- 'bar'
Calling methods via the bracket operator
Calling methods works as you would expect:
- > var obj = { myMethod: function () { return true } };
- > obj['myMethod']()
- true
Setting properties via the bracket operator
Setting properties works analogously to the dot operator:
- > var obj = {};
- > obj['anotherProperty'] = 'def';
- > obj.anotherProperty
- 'def'
Deleting properties via the bracket operator
Deleting properties also works similarly to the dot operator:
- > var obj = { 'not an identifier': 1, prop: 2 };
- > Object.keys(obj)
- [ 'not an identifier', 'prop' ]
- > delete obj['not an identifier']
- true
- > Object.keys(obj)
- [ 'prop' ]
Converting Any Value to an Object
It’s not a frequent use case, but sometimes you need to convert an arbitrary value to an object. Object()
, used as a function (not as a constructor), provides that service. It produces the following results:
Value | Result |
(Called with no parameters) |
{}
|
undefined
|
{}
|
null
|
{}
|
A boolean bool
|
new Boolean(bool)
|
A number num
|
new Number(num)
|
A string str
|
new String(str)
|
An object obj
|
obj (unchanged, nothing to convert)
|
Here are some examples:
- > Object(null) instanceof Object
- true
- > Object(false) instanceof Boolean
- true
- > var obj = {};
- > Object(obj) === obj
- true
The following function checks whether value
is an object:
function
isObject
(
value
)
{
return
value
===
Object
(
value
);
}
Note that the preceding function creates an object if value
isn’t an object.You can implement the same function without doing that, via typeof
(see Pitfall: typeof null).
You can also invoke Object
as a constructor, which produces the same results as calling it as a function:
- > var obj = {};
- > new Object(obj) === obj
- true
- > new Object(123) instanceof Number
- true
Tip
Avoid the constructor; an empty object literal is almost always a better choice:
var
obj
=
new
Object
();
// avoid
var
obj
=
{};
// prefer
this as an Implicit Parameter of Functions and Methods
When you call a function, this
is always an (implicit) parameter:
- Normal functions in sloppy mode
- Even though normal functions have no use for
this
, it still exists as a special variable whose value is always the global object (window
in browsers; see The Global Object):
- > function returnThisSloppy() { return this }
- > returnThisSloppy() === window
- true
- Normal functions in strict mode
this
is alwaysundefined
:
- > function returnThisStrict() { 'use strict'; return this }
- > returnThisStrict() === undefined
- true
- Methods
this
refers to the object on which the method has been invoked:
- > var obj = { method: returnThisStrict };
- > obj.method() === obj
- true
In the case of methods, the value of this
is called the receiver of the method call.
Calling Functions While Setting this: call(), apply(), and bind()
Remember that functions are also objects. Thus, each function has methods of its own. Three of them are introduced in this section and help with calling functions. These three methods are used in the following sections to work around some of the pitfalls of calling functions. The upcoming examples all refer to the following object, jane
:
var
jane
=
{
name
:
'Jane'
,
sayHelloTo
:
function
(
otherName
)
{
'use strict'
;
console
.
log
(
this
.
name
+
' says hello to '
+
otherName
);
}
};
Function.prototype.call(thisValue, arg1?, arg2?, …)
The first parameter is the value that this
will have inside the invoked function; the remaining parameters are handed over as arguments to the invoked function. The following three invocations are equivalent:
jane
.
sayHelloTo
(
'Tarzan'
);
jane
.
sayHelloTo
.
call
(
jane
,
'Tarzan'
);
var
func
=
jane
.
sayHelloTo
;
func
.
call
(
jane
,
'Tarzan'
);
For the second invocation, you need to repeat jane
, because call()
doesn’t know how you got the function that it is invoked on.
Function.prototype.apply(thisValue, argArray)
jane
.
sayHelloTo
(
'Tarzan'
);
jane
.
sayHelloTo
.
apply
(
jane
,
[
'Tarzan'
]);
var
func
=
jane
.
sayHelloTo
;
func
.
apply
(
jane
,
[
'Tarzan'
]);
For the second invocation, you need to repeat jane
, because apply()
doesn’t know how you got the function that it is invoked on.
apply() for Constructors explains how to use apply()
with constructors.
Function.prototype.bind(thisValue, arg1?, …, argN?)
This method performs partial function application—meaning it creates a new function that calls the receiver of bind()
in the following manner: the value of this
is thisValue
and the arguments start with arg1
until argN
, followed by the arguments of the new function. In other words, the new function appends its arguments to arg1, …, argN
when it calls the original function.Let’s look at an example:
function
func
()
{
console
.
log
(
'this: '
+
this
);
console
.
log
(
'arguments: '
+
Array
.
prototype
.
slice
.
call
(
arguments
));
}
var
bound
=
func
.
bind
(
'abc'
,
1
,
2
);
The array method slice
is used to convert arguments
to an array, which is necessary for logging it (this operation is explained in Array-Like Objects and Generic Methods). bound
is a new function. Here’s the interaction:
- > bound(3)
- this: abc
- arguments: 1,2,3
The following three invocations of sayHelloTo
are all equivalent:
jane
.
sayHelloTo
(
'Tarzan'
);
var
func1
=
jane
.
sayHelloTo
.
bind
(
jane
);
func1
(
'Tarzan'
);
var
func2
=
jane
.
sayHelloTo
.
bind
(
jane
,
'Tarzan'
);
func2
();
apply() for Constructors
Let’s pretend that JavaScript has a triple dot operator (…
) that turns arrays into actual parameters. Such an operator would allow you to use Math.max()
(see Other Functions) with arrays. In that case, the following two expressions would be equivalent:
Math
.
max
(...[
13
,
7
,
30
])
Math
.
max
(
13
,
7
,
30
)
For functions, you can achieve the effect of the triple dot operator via apply()
:
- > Math.max.apply(null, [13, 7, 30])
- 30
The triple dot operator would also make sense for constructors:
new
Date
(...[
2011
,
11
,
24
])
// Christmas Eve 2011
Alas, here apply()
does not work, because it helps only with function or method calls, not with constructor invocations.
Manually simulating an apply() for constructors
We can simulate apply()
in two steps.
- Step 1
- Pass the arguments to
Date
via a method call (they are not in an array—yet):
new
(
Date
.
bind
(
null
,
2011
,
11
,
24
))
The preceding code uses bind()
to create a constructor without parameters and invokes it via new
.
- Step 2
- Use
apply()
to hand an array tobind()
. Becausebind()
is a method call, we can useapply()
:
new
(
Function
.
prototype
.
bind
.
apply
(
Date
,
[
null
,
2011
,
11
,
24
]))
The preceding array contains null
, followed by the elements of arr
. We can use concat()
to create it by prepending null
to arr
:
var
arr
=
[
2011
,
11
,
24
];
new
(
Function
.
prototype
.
bind
.
apply
(
Date
,
[
null
].
concat
(
arr
)))
A library method
The preceding manual workaround is inspired by a library method published by Mozilla. The following is a slightly edited version of it:
if
(
!
Function
.
prototype
.
construct
)
{
Function
.
prototype
.
construct
=
function
(
argArray
)
{
if
(
!
Array
.
isArray
(
argArray
))
{
throw
new
TypeError
(
"Argument must be an array"
);
}
var
constr
=
this
;
var
nullaryFunc
=
Function
.
prototype
.
bind
.
apply
(
constr
,
[
null
].
concat
(
argArray
));
return
new
nullaryFunc
();
};
}
Here is the method in use:
- > Date.construct([2011, 11, 24])
- Sat Dec 24 2011 00:00:00 GMT+0100 (CET)
An alternative approach
An alternative to the previous approach is to create an uninitialized instance via Object.create()
and then call the constructor (as a function) via apply()
. That means that you are effectively reimplementing the new
operator (some checks are omitted):
Function
.
prototype
.
construct
=
function
(
argArray
)
{
var
constr
=
this
;
var
inst
=
Object
.
create
(
constr
.
prototype
);
var
result
=
constr
.
apply
(
inst
,
argArray
);
// (1)
// Check: did the constructor return an object
// and prevent `this` from being the result?
return
result
?
result
:
inst
;
};
Warning
The preceding code does not work for most built-in constructors, which always produce new instances when called as functions. In other words, the step in line (1) doesn’t set up inst
as desired.
Pitfall: Losing this When Extracting a Method
If you extract a method from an object, it becomes a true function again. Its connection with the object is severed, and it usually doesn’t work properly anymore. Take, for example, the following object, counter
:
var
counter
=
{
count
:
0
,
inc
:
function
()
{
this
.
count
++
;
}
}
Extracting inc
and calling it (as a function!) fails:
- > var func = counter.inc;
- > func()
- > counter.count // didn’t work
- 0
Here’s the explanation: we have called the value of counter.inc
as a function. Hence, this
is the global object and we have performed window.count++
. window.count
does not exist and is undefined
. Applying the ++
operator to it sets it to NaN
:
- > count // global variable
- NaN
How to get a warning
If method inc()
is in strict mode, you get a warning:
- > counter.inc = function () { 'use strict'; this.count++ };
- > var func2 = counter.inc;
- > func2()
- TypeError: Cannot read property 'count' of undefined
The reason is that when we call the strict mode function func2
, this
is undefined
, resulting in an error.
How to properly extract a method
Thanks to bind()
, we can make sure that inc
doesn’t lose the connection with counter
:
- > var func3 = counter.inc.bind(counter);
- > func3()
- > counter.count // it worked!
- 1
Callbacks and extracted methods
In JavaScript, there are many functions and methods that accept callbacks. Examples in browsers are setTimeout()
and event handling. If we pass in counter.inc
as a callback, it is also invoked as a function, resulting in the same problem just described. To illustrate this phenomenon, let’s use a simple callback-invoking function:
function
callIt
(
callback
)
{
callback
();
}
Executing counter.count
via callIt
triggers a warning (due to strict mode):
- > callIt(counter.inc)
- TypeError: Cannot read property 'count' of undefined
As before, we fix things via bind()
:
- > callIt(counter.inc.bind(counter))
- > counter.count // one more than before
- 2
Warning
Each call to bind()
creates a new function. That has consequences when you’re registering and unregistering callbacks (e.g., for event handling). You need to store the value you registered somewhere and use it for unregistering, too.
Pitfall: Functions Inside Methods Shadow this
You often nest function definitions in JavaScript, because functions can be parameters (e.g., callbacks) and because they can be created in place, via function expressions. This poses a problem when a method contains a normal function and you want to access the former’s this
inside the latter, because the method’s this
is shadowed by the normal function’s this
(which doesn’t even have any use for its own this
).In the following example, the function at (1) tries to access the method’s this
at (2):
var
obj
=
{
name
:
'Jane'
,
friends
:
[
'Tarzan'
,
'Cheeta'
],
loop
:
function
()
{
'use strict'
;
this
.
friends
.
forEach
(
function
(
friend
)
{
// (1)
console
.
log
(
this
.
name
+
' knows '
+
friend
);
// (2)
}
);
}
};
This fails, because the function at (1) has its own this
, which is undefined
here:
- > obj.loop();
- TypeError: Cannot read property 'name' of undefined
There are three ways to work around this problem.
Workaround 1: that = this
We assign this
to a variable that won’t be shadowed inside the nested function:
loop
:
function
()
{
'use strict'
;
var
that
=
this
;
this
.
friends
.
forEach
(
function
(
friend
)
{
console
.
log
(
that
.
name
+
' knows '
+
friend
);
});
}
Here’s the interaction:
- > obj.loop();
- Jane knows Tarzan
- Jane knows Cheeta
Workaround 2: bind()
We can use bind()
to give the callback a fixed value for this
—namely, the method’s this
(line (1)):
loop
:
function
()
{
'use strict'
;
this
.
friends
.
forEach
(
function
(
friend
)
{
console
.
log
(
this
.
name
+
' knows '
+
friend
);
}.
bind
(
this
));
// (1)
}
Workaround 3: a thisValue for forEach()
A workaround that is specific to forEach()
(see Examination Methods) is to provide a second parameter after the callback that becomes the this
of the callback:
loop
:
function
()
{
'use strict'
;
this
.
friends
.
forEach
(
function
(
friend
)
{
console
.
log
(
this
.
name
+
' knows '
+
friend
);
},
this
);
}
Layer 2: The Prototype Relationship Between Objects
The prototype relationship between two objects is about inheritance: every object can have another object as its prototype. Then the former object inherits all of its prototype’s properties.An object specifies its prototype via the internal property [[Prototype]]
. Every object has this property, but it can be null
. The chain of objects connected by the [[Prototype]]
property is called the prototype chain (Figure 17-1).
Figure 17-1. A prototype chain.
To see how prototype-based (or prototypal) inheritance works, let’s look at an example (with invented syntax for specifying the [[Prototype]]
property):
var
proto
=
{
describe
:
function
()
{
return
'name: '
+
this
.
name
;
}
};
var
obj
=
{
[[
Prototype
]]
:
proto
,
name
:
'obj'
};
The object obj
inherits the property describe
from proto
. It also has a so-called own (noninherited, direct) property, name
.
Inheritance
obj
inherits the property describe
; you can access it as if the object itself had that property:
- > obj.describe
- [Function]
Whenever you access a property via obj
, JavaScript starts the search for it in that object and continues with its prototype, the prototype’s prototype, and so on. That’s why we can access proto.describe
via obj.describe
. The prototype chain behaves as if it were a single object. That illusion is maintained when you call a method: the value of this
is always the object where the search for the method began, not where the method was found. That allows the method to access all of the properties of the prototype chain. For example:
- > obj.describe()
- 'name: obj'
Inside describe()
, this
is obj
, which allows the method to access obj.name
.
Overriding
In a prototype chain, a property in an object overrides a property with the same key in a “later” object: the former property is found first. It hides the latter property, which can’t be accessed anymore.As an example, let’s override the method proto.describe()
in obj
:
- > obj.describe = function () { return 'overridden' };
- > obj.describe()
- 'overridden'
That is similar to how overriding of methods works in class-based languages.
Sharing Data Between Objects via a Prototype
Prototypes are great for sharing data between objects: several objects get the same prototype, which holds all shared properties. Let’s look at an example. The objects jane
and tarzan
both contain the same method, describe()
. That is something that we would like to avoid by using sharing:
var
jane
=
{
name
:
'Jane'
,
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
}
};
var
tarzan
=
{
name
:
'Tarzan'
,
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
}
};
Both objects are persons. Their name
property is different, but we could have them share the method describe
. We do that by creating a common prototype called PersonProto
and putting describe
into it (Figure 17-2).
The following code creates objects jane
and tarzan
that share the prototype PersonProto
:
var
PersonProto
=
{
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
}
};
var
jane
=
{
[[
Prototype
]]
:
PersonProto
,
name
:
'Jane'
};
var
tarzan
=
{
[[
Prototype
]]
:
PersonProto
,
name
:
'Tarzan'
};
And here is the interaction:
- > jane.describe()
- Person named Jane
- > tarzan.describe()
- Person named Tarzan
This is a common pattern: the data resides in the first object of a prototype chain, while methods reside in later objects. JavaScript’s flavor of prototypal inheritance is designed to support this pattern: setting a property affects only the first object in a prototype chain, whereas getting a property considers the complete chain (see Setting and Deleting Affects Only Own Properties).
Getting and Setting the Prototype
So far, we have pretended that you can access the internal property [[Prototype]]
from JavaScript. But the language does not let you do that. Instead, there are functions for reading the prototype and for creating a new object with a given prototype.
Creating a new object with a given prototype
This invocation:
Object
.
create
(
proto
,
propDescObj
?
)
creates an object whose prototype is proto
. Optionally, properties can be added via descriptors (which are explained in Property Descriptors). In the following example, object jane
gets the prototype PersonProto
and a mutable property name
whose value is 'Jane'
(as specified via a property descriptor):
var
PersonProto
=
{
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
}
};
var
jane
=
Object
.
create
(
PersonProto
,
{
name
:
{
value
:
'Jane'
,
writable
:
true
}
});
Here is the interaction:
- > jane.describe()
- 'Person named Jane'
But you frequently just create an empty object and then manually add properties, because descriptors are verbose:
var
jane
=
Object
.
create
(
PersonProto
);
jane
.
name
=
'Jane'
;
Reading the prototype of an object
This method call:
Object
.
getPrototypeOf
(
obj
)
returns the prototype of obj
. Continuing the preceding example:
- > Object.getPrototypeOf(jane) === PersonProto
- true
Checking whether one object a prototype of another one
This syntax:
Object
.
prototype
.
isPrototypeOf
(
obj
)
checks whether the receiver of the method is a (direct or indirect) prototype of obj
. In other words: are the receiver and obj
in the same prototype chain, and does obj
come before the receiver? For example:
- > var A = {};
- > var B = Object.create(A);
- > var C = Object.create(B);
- > A.isPrototypeOf(C)
- true
- > C.isPrototypeOf(A)
- false
Finding the object where a property is defined
The following function iterates over the property chain of an object obj
. It returns the first object that has an own property with the key propKey
, or null
if there is no such object:
function
getDefiningObject
(
obj
,
propKey
)
{
obj
=
Object
(
obj
);
// make sure it’s an object
while
(
obj
&&
!
{}.
hasOwnProperty
.
call
(
obj
,
propKey
))
{
obj
=
Object
.
getPrototypeOf
(
obj
);
// obj is null if we have reached the end
}
return
obj
;
}
In the preceding code, we called the method Object.prototype.hasOwnProperty
generically (see Generic Methods: Borrowing Methods from Prototypes).
The Special Property proto
Some JavaScript engines have a special property for getting and setting the prototype of an object: proto
. It brings direct access to [[Prototype]]
to the language:
- > var obj = {};
- > obj.__proto__ === Object.prototype
- true
- > obj.__proto__ = Array.prototype
- > Object.getPrototypeOf(obj) === Array.prototype
- true
There are several things you need to know about proto
:
proto
is pronounced “dunder proto,” an abbreviation of “double underscore proto.” That pronunciation has been borrowed from the Python programming language (as suggested by Ned Batchelder in 2006). Special variables with double underscores are quite frequent in Python.proto
is not part of the ECMAScript 5 standard. Therefore, you must not use it if you want your code to conform to that standard and run reliably across current JavaScript engines.- However, more and more engines are adding support for
proto
and it will be part of ECMAScript 6. - The following expression checks whether an engine supports
proto
as a special property:
Object
.
getPrototypeOf
({
__proto__
:
null
})
===
null
Setting and Deleting Affects Only Own Properties
Only getting a property considers the complete prototype chain of an object. Setting and deleting ignores inheritance and affects only own properties.
Setting a property
Setting a property creates an own property, even if there is an inherited property with that key. For example, given the following source code:
var
proto
=
{
foo
:
'a'
};
var
obj
=
Object
.
create
(
proto
);
obj
inherits foo
from proto
:
- > obj.foo
- 'a'
- > obj.hasOwnProperty('foo')
- false
Setting foo
has the desired result:
- > obj.foo = 'b';
- > obj.foo
- 'b'
However, we have created an own property and not changed proto.foo
:
- > obj.hasOwnProperty('foo')
- true
- > proto.foo
- 'a'
The rationale is that prototype properties are meant to be shared by several objects. This approach allows us to nondestructively “change” them—only the current object is affected.
Deleting an inherited property
You can only delete own properties. Let’s again set up an object, obj
, with a prototype, proto
:
var
proto
=
{
foo
:
'a'
};
var
obj
=
Object
.
create
(
proto
);
Deleting the inherited property foo
has no effect:
- > delete obj.foo
- true
- > obj.foo
- 'a'
For more information on the delete
operator, consult Deleting properties.
Changing properties anywhere in the prototype chain
If you want to change an inherited property, you first have to find the object that owns it (see Finding the object where a property is defined) and then perform the change on that object. For example, let’s delete the property foo
from the previous example:
- > delete getDefiningObject(obj, 'foo').foo;
- true
- > obj.foo
- undefined
Iteration and Detection of Properties
Operations for iterating over and detecting properties are influenced by:
- Inheritance (own properties versus inherited properties)
- An own property of an object is stored directly in that object. An inherited property is stored in one of its prototypes.
- Enumerability (enumerable properties versus nonenumerable properties)
- The enumerability of a property is an attribute (see Property Attributes and Property Descriptors), a flag that can be
true
orfalse
. Enumerability rarely matters and can normally be ignored (see Enumerability: Best Practices).
You can list own property keys, list all enumerable property keys, and check whether a property exists. The following subsections show how.
Listing Own Property Keys
You can either list all own property keys, or only enumerable ones:
Object.getOwnPropertyNames(obj)
returns the keys of all own properties ofobj
.Object.keys(obj)
returns the keys of all enumerable own properties ofobj
.
Note that properties are normally enumerable (see Enumerability: Best Practices), so you can use Object.keys()
, especially for objects that you have created.
Listing All Property Keys
If you want to list all properties (both own and inherited ones) of an object, then you have two options.
Option 1 is to use the loop:
for
(
«
variable
»
in
«
object
»
)
«
statement
»
to iterate over the keys of all enumerable properties of object
. See for-in for a more thorough description.
Option 2 is to implement a function yourself that iterates over all properties (not just enumerable ones). For example:
function
getAllPropertyNames
(
obj
)
{
var
result
=
[];
while
(
obj
)
{
// Add the own property names of `obj` to `result`
result
=
result
.
concat
(
Object
.
getOwnPropertyNames
(
obj
));
obj
=
Object
.
getPrototypeOf
(
obj
);
}
return
result
;
}
Checking Whether a Property Exists
You can check whether an object has a property, or whether a property exists directly inside an object:
propKey in obj
- Returns
true
ifobj
has a property whose key ispropKey
. Inherited properties are included in this test. Object.prototype.hasOwnProperty(propKey)
- Returns
true
if the receiver (this
) has an own (noninherited) property whose key ispropKey
.
Warning
Avoid invoking hasOwnProperty()
directly on an object, as it may be overridden (e.g., by an own property whose key is hasOwnProperty
):
- > var obj = { hasOwnProperty: 1, foo: 2 };
- > obj.hasOwnProperty('foo') // unsafe
- TypeError: Property 'hasOwnProperty' is not a function
Instead, it is better to call it generically (see Generic Methods: Borrowing Methods from Prototypes):
- > Object.prototype.hasOwnProperty.call(obj, 'foo') // safe
- true
- > {}.hasOwnProperty.call(obj, 'foo') // shorter
- true
Examples
The following examples are based on these definitions:
var
proto
=
Object
.
defineProperties
({},
{
protoEnumTrue
:
{
value
:
1
,
enumerable
:
true
},
protoEnumFalse
:
{
value
:
2
,
enumerable
:
false
}
});
var
obj
=
Object
.
create
(
proto
,
{
objEnumTrue
:
{
value
:
1
,
enumerable
:
true
},
objEnumFalse
:
{
value
:
2
,
enumerable
:
false
}
});
Object.defineProperties()
is explained in Getting and Defining Properties via Descriptors, but it should be fairly obvious how it works: proto
has the own properties protoEnumTrue
and protoEnumFalse
and obj
has the own properties objEnumTrue
and objEnumFalse
(and inherits all of proto
’s properties).
Note
Note that objects (such as proto
in the preceding example) normally have at least the prototype Object.prototype
(where standard methods such as toString()
and hasOwnProperty()
are defined):
- > Object.getPrototypeOf({}) === Object.prototype
- true
The effects of enumerability
Among property-related operations, enumberability only influences the for-in
loop and Object.keys()
(it also influences JSON.stringify()
, see JSON.stringify(value, replacer?, space?)).
The for-in
loop iterates over the keys of all enumerable properties, including inherited ones (note that none of the nonenumerable properties of Object.prototype
show up):
- > for (var x in obj) console.log(x);
- objEnumTrue
- protoEnumTrue
Object.keys()
returns the keys of all own (noninherited) enumerable properties:
- > Object.keys(obj)
- [ 'objEnumTrue' ]
If you want the keys of all own properties, you need to use Object.getOwnPropertyNames()
:
- > Object.getOwnPropertyNames(obj)
- [ 'objEnumTrue', 'objEnumFalse' ]
The effects of inheritance
Only the for-in
loop (see the previous example) and the in
operator consider inheritance:
- > 'toString' in obj
- true
- > obj.hasOwnProperty('toString')
- false
- > obj.hasOwnProperty('objEnumFalse')
- true
Computing the number of own properties of an object
Objects don’t have a method such as length
or size
, so you have to use the following workaround:
Object
.
keys
(
obj
).
length
Best Practices: Iterating over Own Properties
To iterate over property keys:
- Combine
for-in
withhasOwnProperty()
, in the manner described in for-in. This works even on older JavaScript engines. For example:
for
(
var
key
in
obj
)
{
if
(
Object
.
prototype
.
hasOwnProperty
.
call
(
obj
,
key
))
{
console
.
log
(
key
);
}
}
- Combine
Object.keys()
orObject.getOwnPropertyNames()
withforEach()
array iteration:
var
obj
=
{
first
:
'John'
,
last
:
'Doe'
};
// Visit non-inherited enumerable keys
Object
.
keys
(
obj
).
forEach
(
function
(
key
)
{
console
.
log
(
key
);
});
To iterate over property values or over (key, value) pairs:
- Iterate over the keys, and use each key to retrieve the corresponding value. Other languages make this simpler, but not JavaScript.
Accessors (Getters and Setters)
ECMAScript 5 lets you write methods whose invocations look like you are getting or setting a property. That means that a property is virtual and not storage space. You could, for example, forbid setting a property and always compute the value returned when reading it.
Defining Accessors via an Object Literal
The following example uses an object literal to define a setter and a getter for property foo
:
var
obj
=
{
get
foo
()
{
return
'getter'
;
},
set
foo
(
value
)
{
console
.
log
(
'setter: '
+
value
);
}
};
Here’s the interaction:
- > obj.foo = 'bla';
- setter: bla
- > obj.foo
- 'getter'
Defining Accessors via Property Descriptors
An alternate way to specify getters and setters is via property descriptors (see Property Descriptors). The following code defines the same object as the preceding literal:
var
obj
=
Object
.
create
(
Object
.
prototype
,
{
// object with property descriptors
foo
:
{
// property descriptor
get
:
function
()
{
return
'getter'
;
},
set
:
function
(
value
)
{
console
.
log
(
'setter: '
+
value
);
}
}
}
);
Accessors and Inheritance
Getters and setters are inherited from prototypes:
- > var proto = { get foo() { return 'hello' } };
- > var obj = Object.create(proto);
- > obj.foo
- 'hello'
Property Attributes and Property Descriptors
Tip
Property attributes and property descriptors are an advanced topic. You normally don’t need to know how they work.
In this section, we’ll look at the internal structure of properties:
- Property attributes are the atomic building blocks of properties.
- A property descriptor is a data structure for working programmatically with attributes.
Property Attributes
All of a property’s state, both its data and its metadata, is stored in attributes. They are fields that a property has, much like an object has properties. Attribute keys are often written in double brackets. Attributes matter for normal properties and for accessors (getters and setters).
The following attributes are specific to normal properties:
[[Value]]
holds the property’s value, its data.[[Writable]]
holds a boolean indicating whether the value of a property can be changed.
The following attributes are specific to accessors:
[[Get]]
holds the getter, a function that is called when a property is read. The function computes the result of the read access.[[Set]]
holds the setter, a function that is called when a property is set to a value. The function receives that value as a parameter.
All properties have the following attributes:
[[Enumerable]]
holds a boolean. Making a property nonenumerable hides it from some operations (see Iteration and Detection of Properties).[[Configurable]]
holds a boolean. If it isfalse
, you cannot delete a property, change any of its attributes (except[[Value]]
), or convert it from a data property to an accessor property or vice versa. In other words,[[Configurable]]
controls the writability of a property’s metadata. There is one exception to this rule—JavaScript allows you to change an unconfigurable property from writable to read-only, for historic reasons; the propertylength
of arrays has always been writable and unconfigurable. Without this exception, you wouldn’t be able to freeze (see Freezing) arrays.
Default values
If you don’t specify attributes, the following defaults are used:
Attribute key | Default value |
[[Value]]
|
undefined
|
[[Get]]
|
undefined
|
[[Set]]
|
undefined
|
[[Writable]]
|
false
|
[[Enumerable]]
|
false
|
[[Configurable]]
|
false
|
These defaults are important when you are creating properties via property descriptors (see the following section).
Property Descriptors
{
value
:
123
,
writable
:
false
,
enumerable
:
true
,
configurable
:
false
}
You can achieve the same goal, immutability, via accessors. Then the descriptor looks as follows:
{
get
:
function
()
{
return
123
},
enumerable
:
true
,
configurable
:
false
}
Getting and Defining Properties via Descriptors
Property descriptors are used for two kinds of operations:
- Getting a property
- All attributes of a property are returned as a descriptor.
- Defining a property
- Defining a property means something different depending on whether a property already exists:
- If a property does not exist, create a new property whose attributes are as specified by the descriptor. If an attribute has no corresponding property in the descriptor, then use the default value. The defaults are dictated by what the attribute names mean. They are the opposite of the values that are used when creating a property via assignment (then the property is writable, enumerable, and configurable).For example:
- > var obj = {};
- > Object.defineProperty(obj, 'foo', { configurable: true });
- > Object.getOwnPropertyDescriptor(obj, 'foo')
- { value: undefined,
- writable: false,
- enumerable: false,
- configurable: true }
I usually don’t rely on the defaults and explicitly state all attributes, to be completely clear.
- If a property already exists, update the attributes of the property as specified by the descriptor. If an attribute has no corresponding property in the descriptor, then don’t change it. Here is an example (continued from the previous one):
- > Object.defineProperty(obj, 'foo', { writable: true });
- > Object.getOwnPropertyDescriptor(obj, 'foo')
- { value: undefined,
- writable: true,
- enumerable: false,
- configurable: true }
The following operations allow you to get and set a property’s attributes via property descriptors:
Object.getOwnPropertyDescriptor(obj, propKey)
- Returns the descriptor of the own (noninherited) property of
obj
whose key ispropKey
. If there is no such property,undefined
is returned:
- > Object.getOwnPropertyDescriptor(Object.prototype, 'toString')
- { value: [Function: toString],
- writable: true,
- enumerable: false,
- configurable: true }
- > Object.getOwnPropertyDescriptor({}, 'toString')
- undefined
Object.defineProperty(obj, propKey, propDesc)
- Create or change a property of
obj
whose key ispropKey
and whose attributes are specified viapropDesc
. Return the modified object. For example:
var
obj
=
Object
.
defineProperty
({},
'foo'
,
{
value
:
123
,
enumerable
:
true
// writable: false (default value)
// configurable: false (default value)
});
Object.defineProperties(obj, propDescObj)
- The batch version of
Object.defineProperty()
. Each property ofpropDescObj
holds a property descriptor. The keys of the properties and their values tellObject.defineProperties
what properties to create or change onobj
. For example:
var
obj
=
Object
.
defineProperties
({},
{
foo
:
{
value
:
123
,
enumerable
:
true
},
bar
:
{
value
:
'abc'
,
enumerable
:
true
}
});
Object.create(proto, propDescObj?)
- First, create an object whose prototype is
proto
. Then, if the optional parameterpropDescObj
has been specified, add properties to it—in the same manner asObject.defineProperties
. Finally, return the result. For example, the following code snippet produces the same result as the previous snippet:
var
obj
=
Object
.
create
(
Object
.
prototype
,
{
foo
:
{
value
:
123
,
enumerable
:
true
},
bar
:
{
value
:
'abc'
,
enumerable
:
true
}
});
Copying an Object
To create an identical copy of an object, you need to get two things right:
- The copy must have the same prototype (see Layer 2: The Prototype Relationship Between Objects) as the original.
- The copy must have the same properties, with the same attributes as the original.
The following function performs such a copy:
function
copyObject
(
orig
)
{
// 1. copy has same prototype as orig
var
copy
=
Object
.
create
(
Object
.
getPrototypeOf
(
orig
));
// 2. copy has all of orig’s properties
copyOwnPropertiesFrom
(
copy
,
orig
);
return
copy
;
}
The properties are copied from orig
to copy
via this function:
function
copyOwnPropertiesFrom
(
target
,
source
)
{
Object
.
getOwnPropertyNames
(
source
)
// (1)
.
forEach
(
function
(
propKey
)
{
// (2)
var
desc
=
Object
.
getOwnPropertyDescriptor
(
source
,
propKey
);
// (3)
Object
.
defineProperty
(
target
,
propKey
,
desc
);
// (4)
});
return
target
;
};
These are the steps involved:
- Get an array with the keys of all own properties of
source
. - Iterate over those keys.
- Retrieve a property descriptor.
- Use that property descriptor to create an own property in
target
.
Note that this function is very similar to the function _.extend()
in the Underscore.js library.
Properties: Definition Versus Assignment
The following two operations are very similar:
- Defining a property via
defineProperty()
anddefineProperties()
(see Getting and Defining Properties via Descriptors). - Assigning to a property via
=
.
There are, however, a few subtle differences:
- Defining a property means creating a new own property or updating the attributes of an existing own property. In both cases, the prototype chain is completely ignored.
- Assigning to a property
prop
means changing an existing property. The process is as follows:
- If
prop
is a setter (own or inherited), call that setter. - Otherwise, if
prop
is read-only (own or inherited), throw an exception (in strict mode) or do nothing (in sloppy mode). The next section explains this (slightly unexpected) phenomenon in more detail. - Otherwise, if
prop
is own (and writable), change the value of that property. - Otherwise, there either is no property
prop
, or it is inherited and writable. In both cases, define an own propertyprop
that is writable, configurable, and enumerable. In the latter case, we have just overridden an inherited property (nondestructively changed it). In the former case, a missing property has been defined automatically. This kind of autodefining is problematic, because typos in assignments can be hard to detect.
Inherited Read-Only Properties Can’t Be Assigned To
If an object, obj
, inherits a property, foo
, from a prototype and foo
is not writable, then you can’t assign to obj.foo
:
var
proto
=
Object
.
defineProperty
({},
'foo'
,
{
value
:
'a'
,
writable
:
false
});
var
obj
=
Object
.
create
(
proto
);
obj
inherits the read-only property foo
from proto
. In sloppy mode, setting the property has no effect:
- > obj.foo = 'b';
- > obj.foo
- 'a'
In strict mode, you get an exception:
- > (function () { 'use strict'; obj.foo = 'b' }());
- TypeError: Cannot assign to read-only property 'foo'
This fits with the idea that assignment changes inherited properties, but nondestructively. If an inherited property is read-only, you want to forbid all changes, even nondestructive ones.
Note that you can circumvent this protection by defining an own property (see the previous subsection for the difference between definition and assignment):
- > Object.defineProperty(obj, 'foo', { value: 'b' });
- > obj.foo
- 'b'
Enumerability: Best Practices
- > Object.keys([])
- []
- > Object.getOwnPropertyNames([])
- [ 'length' ]
- > Object.keys(['a'])
- [ '0' ]
This is especially true for the methods of the built-in instance prototypes:
- > Object.keys(Object.prototype)
- []
- > Object.getOwnPropertyNames(Object.prototype)
- [ hasOwnProperty',
- 'valueOf',
- 'constructor',
- 'toLocaleString',
- 'isPrototypeOf',
- 'propertyIsEnumerable',
- 'toString' ]
The main purpose of enumerability is to tell the for-in
loop which properties it should ignore. As we have seen just now when we looked at instances of built-in constructors, everything not created by the user is hidden from for-in
.
The only operations affected by enumerability are:
- The
for-in
loop Object.keys()
(Listing Own Property Keys)JSON.stringify()
(JSON.stringify(value, replacer?, space?))
Here are some best practices to keep in mind:
- For your own code, you can usually ignore enumerability and should avoid the
for-in
loop (Best Practices: Iterating over Arrays). - You normally shouldn’t add properties to built-in prototypes and objects. But if you do, you should make them nonenumerable to avoid breaking existing code.
Protecting Objects
There are three levels of protecting an object, listed here from weakest to strongest:
- Preventing extensions
- Sealing
- Freezing
Preventing Extensions
Preventing extensions via:
Object
.
preventExtensions
(
obj
)
makes it impossible to add properties to obj
. For example:
var
obj
=
{
foo
:
'a'
};
Object
.
preventExtensions
(
obj
);
Now adding a property fails silently in sloppy mode:
- > obj.bar = 'b';
- > obj.bar
- undefined
and throws an error in strict mode:
- > (function () { 'use strict'; obj.bar = 'b' }());
- TypeError: Can't add property bar, object is not extensible
You can still delete properties, though:
- > delete obj.foo
- true
- > obj.foo
- undefined
You check whether an object is extensible via:
Object
.
isExtensible
(
obj
)
Sealing
Sealing via:
Object
.
seal
(
obj
)
prevents extensions and makes all properties “unconfigurable.” The latter means that the attributes (see Property Attributes and Property Descriptors) of properties can’t be changed anymore. For example, read-only properties stay read-only forever.
The following example demonstrates that sealing makes all properties unconfigurable:
- > var obj = { foo: 'a' };
- > Object.getOwnPropertyDescriptor(obj, 'foo') // before sealing
- { value: 'a',
- writable: true,
- enumerable: true,
- configurable: true }
- > Object.seal(obj)
- > Object.getOwnPropertyDescriptor(obj, 'foo') // after sealing
- { value: 'a',
- writable: true,
- enumerable: true,
- configurable: false }
You can still change the property foo
:
- > obj.foo = 'b';
- 'b'
- > obj.foo
- 'b'
but you can’t change its attributes:
- > Object.defineProperty(obj, 'foo', { enumerable: false });
- TypeError: Cannot redefine property: foo
You check whether an object is sealed via:
Object
.
isSealed
(
obj
)
Freezing
Freezing is performed via:
Object
.
freeze
(
obj
)
var
point
=
{
x
:
17
,
y
:
-
5
};
Object
.
freeze
(
point
);
Once again, you get silent failures in sloppy mode:
- > point.x = 2; // no effect, point.x is read-only
- > point.x
- 17
- > point.z = 123; // no effect, point is not extensible
- > point
- { x: 17, y: -5 }
And you get errors in strict mode:
- > (function () { 'use strict'; point.x = 2 }());
- TypeError: Cannot assign to read-only property 'x'
- > (function () { 'use strict'; point.z = 123 }());
- TypeError: Can't add property z, object is not extensible
You check whether an object is frozen via:
Object
.
isFrozen
(
obj
)
Pitfall: Protection Is Shallow
Protecting an object is shallow: it affects the own properties, but not the values of those properties. For example, consider the following object:
var
obj
=
{
foo
:
1
,
bar
:
[
'a'
,
'b'
]
};
Object
.
freeze
(
obj
);
Even though you have frozen obj
, it is not completely immutable—you can change the (mutable) value of property bar
:
- > obj.foo = 2; // no effect
- > obj.bar.push('c'); // changes obj.bar
- > obj
- { foo: 1, bar: [ 'a', 'b', 'c' ] }
Additionally, obj
has the prototype Object.prototype
, which is also mutable.
Layer 3: Constructors—Factories for Instances
A constructor function (short: constructor) helps with producing objects that are similar in some way. It is a normal function, but it is named, set up, and invoked differently. This section explains how constructors work. They correspond to classes in other languages.
We have already seen an example of two objects that are similar (in Sharing Data Between Objects via a Prototype):
var
PersonProto
=
{
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
}
};
var
jane
=
{
[[
Prototype
]]
:
PersonProto
,
name
:
'Jane'
};
var
tarzan
=
{
[[
Prototype
]]
:
PersonProto
,
name
:
'Tarzan'
};
The objects jane
and tarzan
are both considered “persons” and share the prototype object PersonProto
.Let’s turn that prototype into a constructor Person
that creates objects like jane
and tarzan
. The objects a constructor creates are called its instances. Such instances have the same structure as jane
and tarzan
, consisting of two parts:
- Data is instance-specific and stored in the own properties of the instance objects (
jane
andtarzan
in the preceding example). - Behavior is shared by all instances—they have a common prototype object with methods (
PersonProto
in the preceding example).
A constructor is a function that is invoked via the new
operator. By convention, the names of constructors start with uppercase letters, while the names of normal functions and methods start with lowercase letters. The function itself sets up part 1:
function
Person
(
name
)
{
this
.
name
=
name
;
}
The object in Person.prototype
becomes the prototype of all instances of Person
. It contributes part 2:
Person
.
prototype
.
describe
=
function
()
{
return
'Person named '
+
this
.
name
;
};
Let’s create and use an instance of Person
:
- > var jane = new Person('Jane');
- > jane.describe()
- 'Person named Jane'
We can see that Person
is a normal function. It only becomes a constructor when it is invoked via new
. The new
operator performs the following steps:
- First the behavior is set up: a new object is created whose prototype is
Person.
prototype
. - Then the data is set up:
Person
receives that object as the implicit parameterthis
and adds instance properties.
Figure 17-3 shows what the instance jane
looks like. The property constructor
of Person.prototype
points back to the constructor and is explained in The constructor Property of Instances.
The instanceof
operator allows us to check whether an object is an instance of a given constructor:
- > jane instanceof Person
- true
- > jane instanceof Date
- false
The new Operator Implemented in JavaScript
If you were to manually implement the new
operator, it would look roughly as follows:
function
newOperator
(
Constr
,
args
)
{
var
thisValue
=
Object
.
create
(
Constr
.
prototype
);
// (1)
var
result
=
Constr
.
apply
(
thisValue
,
args
);
if
(
typeof
result
===
'object'
&&
result
!==
null
)
{
return
result
;
// (2)
}
return
thisValue
;
}
In line (1), you can see that the prototype of an instance created by a constructor Constr
is Constr.prototype
.
Line (2) reveals another feature of the new
operator: you can return an arbitrary object from a constructor and it becomes the result of the new
operator. This is useful if you want a constructor to return an instance of a subconstructor (an example is given in Returning arbitrary objects from a constructor).
Terminology: The Two Prototypes
Unfortunately, the term prototype is used ambiguously in JavaScript:
- Prototype 1: The prototype relationship
- An object can be the prototype of another object:
- > var proto = {};
- > var obj = Object.create(proto);
- > Object.getPrototypeOf(obj) === proto
- true
In the preceding example, proto
is the prototype of obj
.
- Prototype 2: The value of the property
prototype
- Each constructor
C
has aprototype
property that refers to an object. That object becomes the prototype of all instances ofC
:
- > function C() {}
- > Object.getPrototypeOf(new C()) === C.prototype
- true
Usually the context makes it clear which of the two prototypes is meant. Should disambiguation be necessary, then we are stuck with prototype to describe the relationship between objects, because that name has made it into the standard library via getPrototypeOf
and isPrototypeOf
. We thus need to find a different name for the object referenced by the prototype
property. One possibility is constructor prototype, but that is problematic because constructors have prototypes, too:
- > function Foo() {}
- > Object.getPrototypeOf(Foo) === Function.prototype
- true
Thus, instance prototype is the best option.
The constructor Property of Instances
By default, each function C
contains an instance prototype object C.prototype
whose property constructor
points back to C
:
- > function C() {}
- > C.prototype.constructor === C
- true
Because the constructor
property is inherited from the prototype by each instance, you can use it to get the constructor of an instance:
- > var o = new C();
- > o.constructor
- [Function: C]
Use cases for the constructor property
- Switching over an object’s constructor
- In the following
catch
clause, we take different actions, depending on the constructor of the caught exception:
try
{
...
}
catch
(
e
)
{
switch
(
e
.
constructor
)
{
case
SyntaxError
:
...
break
;
case
CustomError
:
...
break
;
...
}
}
Warning
This approach detects only direct instances of a given constructor. In contrast, instanceof
detects both direct instances and instances of all subconstructors.
- Determining the name of an object’s constructor
- For example:
- > function Foo() {}
- > var f = new Foo();
- > f.constructor.name
- 'Foo'
Warning
Not all JavaScript engines support the property name
for functions.
- Creating similar objects
- This is how you create a new object,
y
, that has the same constructor as an existing object,x
:
function
Constr
()
{}
var
x
=
new
Constr
();
var
y
=
new
x
.
constructor
();
console
.
log
(
y
instanceof
Constr
);
// true
This trick is handy for a method that must work for instances of subconstructors and wants to create a new instance that is similar to this
. Then you can’t use a fixed constructor:
SuperConstr
.
prototype
.
createCopy
=
function
()
{
return
new
this
.
constructor
(...);
};
- Referring to a superconstructor
- Some inheritance libraries assign the superprototype to a property of a subconstructor. For example, the YUI framework provides subclassing via
Y.extend
:
function
Super
()
{
}
function
Sub
()
{
Sub
.
superclass
.
constructor
.
call
(
this
);
// (1)
}
Y
.
extend
(
Sub
,
Super
);
The call in line (1) works, because extend
sets Sub.superclass
to Super.prototype
. Thanks to the constructor
property, you can call the superconstructor as a method.
Note
The instanceof
operator (see The instanceof Operator) does not rely on the property constructor
.
Best practice
Make sure that for each constructor C
, the following assertion holds:
C
.
prototype
.
constructor
===
C
By default, every function f
already has a property prototype
that is set up correctly:
- > function f() {}
- > f.prototype.constructor === f
- true
You should thus avoid replacing this object and only add properties to it:
// Avoid:
C
.
prototype
=
{
method1
:
function
(...)
{
...
},
...
};
// Prefer:
C
.
prototype
.
method1
=
function
(...)
{
...
};
...
If you do replace it, you should manually assign the correct value to constructor
:
C
.
prototype
=
{
constructor
:
C
,
method1
:
function
(...)
{
...
},
...
};
Note that nothing crucial in JavaScript depends on the constructor
property; but it is good style to set it up, because it enables the techniques mentioned in this section.
The instanceof Operator
The instanceof
operator:
value
instanceof
Constr
determines whether value
has been created by the constructor Constr
or a subconstructor. It does so by checking whether Constr.prototype
is in the prototype chain of value
. Therefore, the following two expressions are equivalent:
value
instanceof
Constr
Constr
.
prototype
.
isPrototypeOf
(
value
)
Here are some examples:
- > {} instanceof Object
- true
- > [] instanceof Array // constructor of []
- true
- > [] instanceof Object // super-constructor of []
- true
- > new Date() instanceof Date
- true
- > new Date() instanceof Object
- true
As expected, instanceof
is always false
for primitive values:
- > 'abc' instanceof Object
- false
- > 123 instanceof Number
- false
Finally, instanceof
throws an exception if its right side isn’t a function:
- > [] instanceof 123
- TypeError: Expecting a function in instanceof check
Pitfall: objects that are not instances of Object
Almost all objects are instances of Object
, because Object.prototype
is in their prototype chain. But there are also objects where that is not the case. Here are two examples:
- > Object.create(null) instanceof Object
- false
- > Object.prototype instanceof Object
- false
The former object is explained in more detail in The dict Pattern: Objects Without Prototypes Are Better Maps.The latter object is where most prototype chains end (and they must end somewhere).Neither object has a prototype:
- > Object.getPrototypeOf(Object.create(null))
- null
- > Object.getPrototypeOf(Object.prototype)
- null
But typeof
correctly classifies them as objects:
- > typeof Object.create(null)
- 'object'
- > typeof Object.prototype
- 'object'
This pitfall is not a deal-breaker for most use cases for instanceof
, but you have to be aware of it.
Pitfall: crossing realms (frames or windows)
In web browsers, each frame and window has its own realm with separate global variables. That prevents instanceof
from working for objects that cross realms. To see why, look at the following code:
if
(
myvar
instanceof
Array
)
...
// Doesn’t always work
If myvar
is an array from a different realm, then its prototype is the Array.prototype
from that realm. Therefore, instanceof
will not find the Array.prototype
of the current realm in the prototype chain of myvar
and will return false
. ECMAScript 5 has the function Array.isArray()
, which always works:
<head>
<script>
function
test
(
arr
)
{
var
iframe
=
frames
[
0
];
console
.
log
(
arr
instanceof
Array
);
// false
console
.
log
(
arr
instanceof
iframe
.
Array
);
// true
console
.
log
(
Array
.
isArray
(
arr
));
// true
}
</script>
</head>
<body>
<iframe
srcdoc=
"<script>window.parent.test([])</script>"
>
</iframe>
</body>
Obviously, this is also an issue with non-built-in constructors.
Apart from using Array.isArray()
, there are several things you can do to work around this problem:
- Avoid objects crossing realms. Browsers have the
postMessage()
method, which can copy an object to another realm instead of passing a reference. - Check the name of the constructor of an instance (only works on engines that support the property
name
for functions):
someValue
.
constructor
.
name
===
'NameOfExpectedConstructor'
- Use a prototype property to mark instances as belonging to a type
T
. There are several ways in which you can do so. The checks for whethervalue
is an instance ofT
look as follows:
value.isT()
:The prototype ofT
instances must returntrue
from this method; a common superconstructor should return the default value,false
.'T' in value
:You must tag the prototype ofT
instances with a property whose key is'T'
(or something more unique).value.TYPE_NAME === 'T'
:Every relevant prototype must have aTYPE_NAME
property with an appropriate value.
Tips for Implementing Constructors
This section gives a few tips for implementing constructors.
Protection against forgetting new: strict mode
If you forget new
when you use a constructor, you are calling it as a function instead of as a constructor. In sloppy mode, you don’t get an instance and global variables are created. Unfortunately, all of this happens without a warning:
function
SloppyColor
(
name
)
{
this
.
name
=
name
;
}
var
c
=
SloppyColor
(
'green'
);
// no warning!
// No instance is created:
console
.
log
(
c
);
// undefined
// A global variable is created:
console
.
log
(
name
);
// green
In strict mode, you get an exception:
function
StrictColor
(
name
)
{
'use strict'
;
this
.
name
=
name
;
}
var
c
=
StrictColor
(
'green'
);
// TypeError: Cannot set property 'name' of undefined
Returning arbitrary objects from a constructor
In many object-oriented languages, constructors can produce only direct instances. For example, consider Java: let’s say you want to implement a class Expression
that has the subclasses Addition
and Multiplication
. Parsing produces direct instances of the latter two classes. You can’t implement it as a constructor of Expression
, because that constructor can produce only direct instances of Expression
. As a workaround, static factory methods are used in Java:
class
Expression
{
// Static factory method:
public
static
Expression
parse
(
String
str
)
{
if
(...)
{
return
new
Addition
(...);
}
else
if
(...)
{
return
new
Multiplication
(...);
}
else
{
throw
new
ExpressionException
(...);
}
}
}
...
Expression
expr
=
Expression
.
parse
(
someStr
);
In JavaScript, you can simply return whatever object you need from a constructor. Thus, the JavaScript version of the preceding code would look like:
function
Expression
(
str
)
{
if
(...)
{
return
new
Addition
(..);
}
else
if
(...)
{
return
new
Multiplication
(...);
}
else
{
throw
new
ExpressionException
(...);
}
}
...
var
expr
=
new
Expression
(
someStr
);
That is good news: JavaScript constructors don’t lock you in, so you can always change your mind as to whether a constructor should return a direct instance or something else.
Data in Prototype Properties
This section explains that in most cases, you should not put data in prototype properties. There are, however, a few exceptions to that rule.
Avoid Prototype Properties with Initial Values for Instance Properties
A constructor usually sets instance properties to initial values. If one such value is a default, then you don’t need to create an instance property. You only need a prototype property with the same key whose value is the default. For example:
/**
* Anti-pattern: don’t do this
*
* @param data an array with names
*/
function
Names
(
data
)
{
if
(
data
)
{
// There is a parameter
// => create instance property
this
.
data
=
data
;
}
}
Names
.
prototype
.
data
=
[];
The parameter data
is optional. If it is missing, the instance does not get a property data
, but inherits Names.prototype.data
instead.
This approach mostly works: you can create an instance n
of Names
. Getting n.data
reads Names.prototype.data
. Setting n.data
creates a new own property in n
and preserves the shared default value in the prototype. We only have a problem if we change the default value (instead of replacing it with a new value):
- > var n1 = new Names();
- > var n2 = new Names();
- > n1.data.push('jane'); // changes default value
- > n1.data
- [ 'jane' ]
- > n2.data
- [ 'jane' ]
In the preceding example, push()
changed the array in Names.prototype.data
. Sincethat array is shared by all instances without an own property data
,n2.data
’s initial value has changed, too.
Best practice: don’t share default values
Given what we’ve just discussed, it is better to not share default values and to always createnew ones:
function
Names
(
data
)
{
this
.
data
=
data
||
[];
}
Obviously, the problem of modifying a shared default value does not arise if that value is immutable (as all primitives are; see Primitive Values). But for consistency’s sake, it’s best to stick to a single way of setting up properties. I also prefer to maintain the usual separation of concerns (see Layer 3: Constructors—Factories for Instances): the constructor sets up the instance properties, and the prototype contains the methods.
ECMAScript 6 will make this even more of a best practice, because constructor parameters can have default values and you can define prototype methods via classes, but not prototype properties with data.
Creating instance properties on demand
Occasionally, creating a property value is an expensive operation (computationally or storage-wise). In that case, you can create an instance property on demand:
function
Names
(
data
)
{
if
(
data
)
this
.
data
=
data
;
}
Names
.
prototype
=
{
constructor
:
Names
,
// (1)
get
data
()
{
// Define, don’t assign
// => avoid calling the (nonexistent) setter
Object
.
defineProperty
(
this
,
'data'
,
{
value
:
[],
enumerable
:
true
,
configurable
:
false
,
writable
:
false
});
return
this
.
data
;
}
};
We can’t add the property data
to the instance via assignment, because JavaScript would complain about a missing setter (which it does when it only finds a getter). Therefore, we add it via Object.defineProperty()
. Consult Properties: Definition Versus Assignment to review the differences between defining and assigning.In line (1), we are ensuring that the property constructor
is set up properly (see The constructor Property of Instances).
Obviously, that is quite a bit of work, so you have to be sure it is worth it.
Avoid Nonpolymorphic Prototype Properties
If the same property (same key, same semantics, generally different values), exists in several prototypes, it is called polymorphic. Then the result of reading the property via an instance is dynamically determined via that instance’s prototype. Prototype properties that are not used polymorphically can be replaced by variables (which better reflects their nonpolymorphic nature).
For example, you can store a constant in a prototype property and access it via this
:
function
Foo
()
{}
Foo
.
prototype
.
FACTOR
=
42
;
Foo
.
prototype
.
compute
=
function
(
x
)
{
return
x
*
this
.
FACTOR
;
};
This constant is not polymorphic. Therefore, you can just as well access it via a variable:
// This code should be inside an IIFE or a module
function
Foo
()
{}
var
FACTOR
=
42
;
Foo
.
prototype
.
compute
=
function
(
x
)
{
return
x
*
FACTOR
;
};
Polymorphic Prototype Properties
Here is an example of polymorphic prototype properties with immutable data. Tagging instances of a constructor via prototype properties enables you to tell them apart from instances of a different constructor:
function
ConstrA
()
{
}
ConstrA
.
prototype
.
TYPE_NAME
=
'ConstrA'
;
function
ConstrB
()
{
}
ConstrB
.
prototype
.
TYPE_NAME
=
'ConstrB'
;
Thanks to the polymorphic “tag” TYPE_NAME
, you can distinguish the instances of ConstrA
and ConstrB
even when they cross realms (then instanceof
does not work; see Pitfall: crossing realms (frames or windows)).
Keeping Data Private
JavaScript does not have dedicated means for managing private data for an object. This section will describe three techniques for working around that limitation:
- Private data in the environment of a constructor
- Private data in properties with marked keys
- Private data in properties with reified keys
Additionally, I will explain how to keep global data private via IIFEs.
Private Data in the Environment of a Constructor (Crockford Privacy Pattern)
When a constructor is invoked, two things are created: the constructor’s instance and an environment (see Environments: Managing Variables). The instance is to be initialized by the constructor. The environment holds the constructor’s parameters and local variables. Every function (which includes methods) created inside the constructor will retain a reference to the environment—the environment in which it was created. Thanks to that reference, it will always have access to the environment, even after the constructor is finished. This combination of function and environment is called a closure (Closures: Functions Stay Connected to Their Birth Scopes). The constructor’s environment is thus data storage that is independent of the instance and related to it only because the two are created at the same time. To properly connect them, we must have functions that live in both worlds. Using Douglas Crockford’s terminology, an instance can have three kinds of values associated with it (see Figure 17-4):
- Public properties
- Values stored in properties (either in the instance or in its prototype) are publicly accessible.
- Private values
- Data and functions stored in the environment are private—only accessible to the constructor and to the functions it created.
- Privileged methods
- Private functions can access public properties, but public methods in the prototype can’t access private data. We thus need privileged methods—public methods in the instance.Privileged methods are public and can be called by everyone, but they also have access to private values, because they were created in the constructor.
The following sections explain each kind of value in more detail.
Public properties
Remember that given a constructor Constr
, there are two kinds of properties that are public, accessible to everyone. First, prototype properties are stored in Constr.prototype
and shared by all instances. Prototype properties are usually methods:
Constr
.
prototype
.
publicMethod
=
...;
Second, instance properties are unique to each instance. They are added in the constructor and usually hold data (not methods):
function
Constr
(...)
{
this
.
publicData
=
...;
...
}
Private values
The constructor’s environment consists of the parameters and local variables. They are accessible only from inside the constructor and thus private to the instance:
function
Constr
(...)
{
...
var
that
=
this
;
// make accessible to private functions
var
privateData
=
...;
function
privateFunction
(...)
{
// Access everything
privateData
=
...;
that
.
publicData
=
...;
that
.
publicMethod
(...);
}
...
}
Privileged methods
Private data is so safe from outside access that prototype methods can’t access it. But then how else would you use it after leaving the constructor? The answer is privileged methods: functions created in the constructor are added as instance methods. That means that, on one hand, they can access private data; on the other hand, they are public and therefore seen by prototype methods. In other words, they serve as mediators between private data and the public (including prototype methods):
function
Constr
(...)
{
...
this
.
privilegedMethod
=
function
(...)
{
// Access everything
privateData
=
...;
privateFunction
(...);
this
.
publicData
=
...;
this
.
publicMethod
(...);
};
}
An example
The following is an implementation of a StringBuilder
, using the Crockford privacy pattern:
function
StringBuilder
()
{
var
buffer
=
[];
this
.
add
=
function
(
str
)
{
buffer
.
push
(
str
);
};
this
.
toString
=
function
()
{
return
buffer
.
join
(
''
);
};
}
// Can’t put methods in the prototype!
Here is the interaction:
- > var sb = new StringBuilder();
- > sb.add('Hello');
- > sb.add(' world!');
- > sb.toString()
- ’Hello world!’
The pros and cons of the Crockford privacy pattern
Here are some points to consider when you are using the Crockford privacy pattern:
- It’s not very elegant
- Mediating access to private data via privileged methods introduces an unnecessary indirection. Privileged methods and private functions both destroy the separation of concerns between the constructor (setting up instance data) and the instance prototype (methods).
- It’s completely secure
- There is no way to access the environment’s data from outside, which makes this solution secure if you need that (e.g., for security-critical code). On the other hand, private data not being accessible to the outside can also be an inconvenience. Sometimes you want to unit-test private functionality. And some temporary quick fixes depend on the ability to access private data. This kind of quick fix cannot be predicted, so no matter how good your design is, the need can arise.
- It may be slower
- Accessing properties in the prototype chain is highly optimized in current JavaScript engines. Accessing values in the closure may be slower. But these things change constantly, so you’ll have to measure should this really matter for your code.
- It consumes more memory
- Keeping the environment around and putting privileged methods in instances costs memory. Again, be sure it really matters for your code and measure.
Private Data in Properties with Marked Keys
For most non-security-critical applications, privacy is more like a hint to clients of an API: “You don’t need to see this.” That’s the key benefit of encapsulation—hiding complexity. Even though more is going on under the hood, you only need to understand the public part of an API. The idea of a naming convention is to let clients know about privacy by marking the key of a property. A prefixed underscore is often used for this purpose.
Let’s rewrite the previous StringBuilder
example so that the buffer is kept in a property _buffer
, which is private, but by convention only:
function
StringBuilder
()
{
this
.
_buffer
=
[];
}
StringBuilder
.
prototype
=
{
constructor
:
StringBuilder
,
add
:
function
(
str
)
{
this
.
_buffer
.
push
(
str
);
},
toString
:
function
()
{
return
this
.
_buffer
.
join
(
''
);
}
};
Here are some pros and cons of privacy via marked property keys:
- It offers a more natural coding style
- Being able to access private and public data in the same manner is more elegant than using environments for privacy.
- It pollutes the namespace of properties
- Properties with marked keys can be seen everywhere. The more people use IDEs, the more it will be a nuisance that they are shown alongside public properties, in places where they should be hidden. IDEs could, in theory, adapt by recognizing naming conventions and by hiding private properties where possible.
- Private properties can be accessed from “outside”
- That can be useful for unit tests and quick fixes. Additionally, subconstructors and helper functions (so-called “friend functions”) can profit from easier access to private data. The environment approach doesn’t offer this kind of flexibility; private data can be accessed only from within the constructor.
- It can lead to key clashes
- Keys of private properties can clash. This is already an issue for subconstructors, but it is even more problematic if you work with multiple inheritance (as enabled by some libraries). With the environment approach, there are never any clashes.
Private Data in Properties with Reified Keys
One problem with a naming convention for private properties is that keys might clash (e.g., a key from a constructor with a key from a subconstructor, or a key from a mixin with a key from a constructor). You can make such clashes less likely by using longer keys, that, for example, include the name of the constructor. Then, in the previous case, the private property buffer
would be called _StringBuilder_buffer
. If such a key is too long for your taste, you have the option of _reifying it, of storing it in a variable:
var
KEY_BUFFER
=
'_StringBuilder_buffer'
;
We now access the private data via this[KEY_BUFFER]
:
var
StringBuilder
=
function
()
{
var
KEY_BUFFER
=
'_StringBuilder_buffer'
;
function
StringBuilder
()
{
this
[
KEY_BUFFER
]
=
[];
}
StringBuilder
.
prototype
=
{
constructor
:
StringBuilder
,
add
:
function
(
str
)
{
this
[
KEY_BUFFER
].
push
(
str
);
},
toString
:
function
()
{
return
this
[
KEY_BUFFER
].
join
(
''
);
}
};
return
StringBuilder
;
}();
We have wrapped an IIFE around StringBuilder
so that the constant KEY_BUFFER
stays local and doesn’t pollute the global namespace.
Reified property keys enable you to use UUIDs (universally unique identifiers) in keys. For example, via Robert Kieffer’s node-uuid:
var
KEY_BUFFER
=
'_StringBuilder_buffer_'
+
uuid
.
v4
();
KEY_BUFFER
has a different value each time the code runs. It may, for example, look like this:
- _StringBuilder_buffer_110ec58a-a0f2-4ac4-8393-c866d813b8d1
Long keys with UUIDs make key clashes virtually impossible.
Keeping Global Data Private via IIFEs
This subsection explains how to keep global data private to singleton objects, constructors, and methods, via IIFEs (see Introducing a New Scope via an IIFE). Those IIFEs create new environments (refer back to Environments: Managing Variables), which is where you put the private data.
Attaching private global data to a singleton object
You don’t need a constructor to associate an object with private data in an environment. The following example shows how to use an IIFE for the same purpose, by wrapping it around a singleton object:
var
obj
=
function
()
{
// open IIFE
// public
var
self
=
{
publicMethod
:
function
(...)
{
privateData
=
...;
privateFunction
(...);
},
publicData
:
...
};
// private
var
privateData
=
...;
function
privateFunction
(...)
{
privateData
=
...;
self
.
publicData
=
...;
self
.
publicMethod
(...);
}
return
self
;
}();
// close IIFE
Keeping global data private to all of a constructor
Some global data is relevant only for a constructor and the prototype methods. By wrapping an IIFE around both, you can hide it from public view. Private Data in Properties with Reified Keys gave an example: the constructor StringBuilder
and its prototype methods use the constant KEY_BUFFER
, which contains a property key. That constant is stored in the environment of an IIFE:
var
StringBuilder
=
function
()
{
// open IIFE
var
KEY_BUFFER
=
'_StringBuilder_buffer_'
+
uuid
.
v4
();
function
StringBuilder
()
{
this
[
KEY_BUFFER
]
=
[];
}
StringBuilder
.
prototype
=
{
// Omitted: methods accessing this[KEY_BUFFER]
};
return
StringBuilder
;
}();
// close IIFE
Note that if you are using a module system (see Chapter 31), you can achieve the same effect with cleaner code by putting the constructor plus methods in a module.
Attaching global data to a method
Sometimes you only need global data for a single method. You can keep it private by putting it in the environment of an IIFE that you wrap around the method. For example:
var
obj
=
{
method
:
function
()
{
// open IIFE
// method-private data
var
invocCount
=
0
;
return
function
()
{
invocCount
++
;
console
.
log
(
'Invocation #'
+
invocCount
);
return
'result'
;
};
}()
// close IIFE
};
Here is the interaction:
- > obj.method()
- Invocation #1
- 'result'
- > obj.method()
- Invocation #2
- 'result'
Layer 4: Inheritance Between Constructors
In this section, we examine how constructors can be inherited from: given a constructor Super
, how can we write a new constructor, Sub
, that has all the features of Super
plus some features of its own? Unfortunately, JavaScript does not have a built-in mechanism for performing this task. Hence, we’ll have to do some manual work.
Figure 17-5 illustrates the idea: the subconstructor Sub
should have all of the properties of Super
(both prototype properties and instance properties) in addition to its own. Thus, we have a rough idea of what Sub
should look like, but don’t know how to get there. There are several things we need to figure out, which I’ll explain next:
- Inheriting instance properties.
- Inheriting prototype properties.
- Ensuring that
instanceof
works: ifsub
is an instance ofSub
, we also wantsub instanceof Super
to be true. - Overriding a method to adapt one of
Super
’s methods inSub
. - Making supercalls: if we have overridden one of
Super
’s methods, we may need to call the original method fromSub
.
Inheriting Instance Properties
Instance properties are set up in the constructor itself, so inheriting the superconstructor’s instance properties involves calling that constructor:
function
Sub
(
prop1
,
prop2
,
prop3
,
prop4
)
{
Super
.
call
(
this
,
prop1
,
prop2
);
// (1)
this
.
prop3
=
prop3
;
// (2)
this
.
prop4
=
prop4
;
// (3)
}
When Sub
is invoked via new
, its implicit parameter this
refers to a fresh instance. It first passes that instance on to Super
(1), which adds its instance properties. Afterward, Sub
sets up its own instance properties (2,3). The trick is not to invoke Super
via new
, because that would create a fresh superinstance. Instead, we call Super
as a function and hand in the current (sub)instance as the value of this
.
Inheriting Prototype Properties
Shared properties such as methods are kept in the instance prototype. Thus, we need to find a way for Sub.prototype
to inherit all of Super.prototype
’s properties. The solution is to give Sub.prototype
the prototype Super.prototype
.
Confused by the two kinds of prototypes?
Yes, JavaScript terminology is confusing here. If you feel lost, consult Terminology: The Two Prototypes, which explains how they differ.
This is the code that achieves that:
Sub
.
prototype
=
Object
.
create
(
Super
.
prototype
);
Sub
.
prototype
.
constructor
=
Sub
;
Sub
.
prototype
.
methodB
=
...;
Sub
.
prototype
.
methodC
=
...;
Object.create()
produces a fresh object whose prototype is Super.prototype
. Afterward, we add Sub
’s methods. As explained in The constructor Property of Instances, we also need to set up the property constructor
, because we have replaced the original instance prototype where it had the correct value.
Figure 17-6 shows how Sub
and Super
are related now. Sub
’s structure does resemble what I have sketched in Figure 17-5. The diagram does not show the instance properties, which are set up by the function call mentioned in the diagram.
Ensuring That instanceof Works
“Ensuring that instanceof
works” means that every instance of Sub
must also be an instance of Super
. Figure 17-7 shows what the prototype chain of subInstance
, an instance of Sub
, looks like: its first prototype is Sub.prototype
, and its second prototype is Super.prototype
.
Let’s start with an easier question: is subInstance
an instance of Sub
? Yes, it is, because the following two assertions are equivalent (the latter can be considered the definition of the former):
subInstance
instanceof
Sub
Sub
.
prototype
.
isPrototypeOf
(
subInstance
)
As mentioned before, Sub.prototype
is one of the prototypes of subInstance
, so both assertions are true. Similarly, subInstance
is also an instance of Super
, because the following two assertions hold:
subInstance
instanceof
Super
Super
.
prototype
.
isPrototypeOf
(
subInstance
)
Overriding a Method
We override a method in Super.prototype
by adding a method with the same name to Sub.prototype
. methodB
is an example and in Figure 17-7, we can see why it works: the search for methodB
begins in subInstance
and finds Sub.prototype.methodB
before Super.prototype.methodB
.
Making a Supercall
To understand supercalls, you need to know the term home object. The home object of a method is the object that owns the property whose value is the method. For example, the home object of Sub.prototype.methodB
is Sub.prototype
. Supercalling a method foo
involves three steps:
- Start your search “after” (in the prototype of) the home object of the current method.
- Look for a method whose name is
foo
. - Invoke that method with the current
this
. The rationale is that the supermethod must work with the same instance as the current method; it must be able to access the same instance properties.
Therefore, the code of the submethod looks as follows. It supercalls itself, it calls the method it has overridden:
Sub
.
prototype
.
methodB
=
function
(
x
,
y
)
{
var
superResult
=
Super
.
prototype
.
methodB
.
call
(
this
,
x
,
y
);
// (1)
return
this
.
prop3
+
' '
+
superResult
;
}
One way of reading the supercall at (1) is as follows: refer to the supermethod directly and call it with the current this
. However, if we split it into three parts, we find the aforementioned steps:
Super.prototype
: Start your search inSuper.prototype
, the prototype ofSub.prototype
(the home object of the current methodSub.prototype.methodB
).methodB
: Look for a method with the namemethodB
.call(this, …)
: Call the method found in the previous step, and maintain the currentthis
.
Avoiding Hardcoding the Name of the Superconstructor
Until now, we have always referred to supermethods and superconstructors by mentioning the superconstructor name. This kind of hardcoding makes your code less flexible. You can avoid it by assigning the superprototype to a property of Sub
:
Sub
.
_super
=
Super
.
prototype
;
Then calling the superconstructor and a supermethod looks as follows:
function
Sub
(
prop1
,
prop2
,
prop3
,
prop4
)
{
Sub
.
_super
.
constructor
.
call
(
this
,
prop1
,
prop2
);
this
.
prop3
=
prop3
;
this
.
prop4
=
prop4
;
}
Sub
.
prototype
.
methodB
=
function
(
x
,
y
)
{
var
superResult
=
Sub
.
_super
.
methodB
.
call
(
this
,
x
,
y
);
return
this
.
prop3
+
' '
+
superResult
;
}
Setting up Sub._super
is usually handled by a utility function that also connects the subprototype to the superprototype. For example:
function
subclasses
(
SubC
,
SuperC
)
{
var
subProto
=
Object
.
create
(
SuperC
.
prototype
);
// Save `constructor` and, possibly, other methods
copyOwnPropertiesFrom
(
subProto
,
SubC
.
prototype
);
SubC
.
prototype
=
subProto
;
SubC
.
_super
=
SuperC
.
prototype
;
};
This code uses the helper function copyOwnPropertiesFrom()
, which is shown and explained in Copying an Object.
Tip
Read “subclasses” as a verb: SubC
subclasses SuperC
.Such a utility function can take some of the pain out of creating a subconstructor: there are fewer things to do manually, and the name of the superconstructor is never mentioned redundantly. The following example demonstrates how it simplifies code.
Example: Constructor Inheritance in Use
As a concrete example, let’s assume that the constructor Person
already exists:
function
Person
(
name
)
{
this
.
name
=
name
;
}
Person
.
prototype
.
describe
=
function
()
{
return
'Person called '
+
this
.
name
;
};
We now want to create the constructor Employee
as a subconstructor of Person
. We do so manually, which looks like this:
function
Employee
(
name
,
title
)
{
Person
.
call
(
this
,
name
);
this
.
title
=
title
;
}
Employee
.
prototype
=
Object
.
create
(
Person
.
prototype
);
Employee
.
prototype
.
constructor
=
Employee
;
Employee
.
prototype
.
describe
=
function
()
{
return
Person
.
prototype
.
describe
.
call
(
this
)
+
' ('
+
this
.
title
+
')'
;
};
Here is the interaction:
- > var jane = new Employee('Jane', 'CTO');
- > jane.describe()
- Person called Jane (CTO)
- > jane instanceof Employee
- true
- > jane instanceof Person
- true
The utility function subclasses()
from the previous section makes the code of Employee
slightly simpler and avoids hardcoding the superconstructor Person
:
function
Employee
(
name
,
title
)
{
Employee
.
_super
.
constructor
.
call
(
this
,
name
);
this
.
title
=
title
;
}
Employee
.
prototype
.
describe
=
function
()
{
return
Employee
.
_super
.
describe
.
call
(
this
)
+
' ('
+
this
.
title
+
')'
;
};
subclasses
(
Employee
,
Person
);
Example: The Inheritance Hierarchy of Built-in Constructors
Built-in constructors use the same subclassing approach described in this section. For example, Array
is a subconstructor of Object
. Therefore, the prototype chain of an instance of Array
looks like this:
- > var p = Object.getPrototypeOf
- > p([]) === Array.prototype
- true
- > p(p([])) === Object.prototype
- true
- > p(p(p([]))) === null
- true
Antipattern: The Prototype Is an Instance of the Superconstructor
Before ECMAScript 5 and Object.create()
, an often-used solution was to create the subprototype by invoking the superconstructor:
Sub
.
prototype
=
new
Super
();
// Don’t do this
This is not recommended under ECMAScript 5. The prototype will have all of Super
’s instance properties, which it has no use for. Therefore, it is better to use the aforementioned pattern (involving Object.create()
).
Methods of All Objects
Almost all objects have Object.prototype
in their prototype chain:
- > Object.prototype.isPrototypeOf({})
- true
- > Object.prototype.isPrototypeOf([])
- true
- > Object.prototype.isPrototypeOf(/xyz/)
- true
The following subsections describe the methods that Object.prototype
provides for its prototypees.
Conversion to Primitive
The following two methods are used to convert an object to a primitive value:
Object.prototype.toString()
- Returns a string representation of an object:
- > ({ first: 'John', last: 'Doe' }.toString())
- '[object Object]'
- > [ 'a', 'b', 'c' ].toString()
- 'a,b,c'
Object.prototype.valueOf()
- This is the preferred way of converting an object to a number. The default implementation returns
this
:
- > var obj = {};
- > obj.valueOf() === obj
- true
valueOf
is overridden by wrapper constructors to return the wrapped primitive:
- > new Number(7).valueOf()
- 7
The conversion to number and string (whether implicit or explicit) builds on the conversion to primitive (for details, see Algorithm: ToPrimitive()—Converting a Value to a Primitive). That is why you can use the aforementioned two methods to configure those conversions. valueOf()
is preferred by the conversion to number:
- > 3 * { valueOf: function () { return 5 } }
- 15
toString()
is preferred by the conversion to string:
- > String({ toString: function () { return 'ME' } })
- 'Result: ME'
The conversion to boolean is not configurable; objects are always considered to be true
(see Converting to Boolean).
Object.prototype.toLocaleString()
This method returns a locale-specific string representation of an object. The default implementation calls toString()
. Most engines don’t go beyond this support for this method. However, the ECMAScript Internationalization API (see The ECMAScript Internationalization API), which is supported by many modern engines, overrides it for several built-in constructors.
Prototypal Inheritance and Properties
The following methods help with prototypal inheritance and properties:
Object.prototype.isPrototypeOf(obj)
- Returns
true
if the receiver is part of the prototype chain ofobj
:
- > var proto = { };
- > var obj = Object.create(proto);
- > proto.isPrototypeOf(obj)
- true
- > obj.isPrototypeOf(obj)
- false
Object.prototype.hasOwnProperty(key)
- Returns
true
ifthis
owns a property whose key iskey
. “Own” means that the property exists in the object itself and not in one of its prototypes.
Warning
You normally should invoke this method generically (not directly), especially on objects whose properties you don’t know statically. Why and how is explained in Iteration and Detection of Properties:
- > var proto = { foo: 'abc' };
- > var obj = Object.create(proto);
- > obj.bar = 'def';
- > Object.prototype.hasOwnProperty.call(obj, 'foo')
- false
- > Object.prototype.hasOwnProperty.call(obj, 'bar')
- true
Object.prototype.propertyIsEnumerable(propKey)
- Returns
true
if the receiver has a property with the keypropKey
that is enumerable andfalse
otherwise:
- > var obj = { foo: 'abc' };
- > obj.propertyIsEnumerable('foo')
- true
- > obj.propertyIsEnumerable('toString')
- false
- > obj.propertyIsEnumerable('unknown')
- false
Generic Methods: Borrowing Methods from Prototypes
Sometimes instance prototypes have methods that are useful for more objects than those that inherit from them. This section explains how to use the methods of a prototype without inheriting from it.For example, the instance prototype Wine.prototype
has the method incAge()
:
function
Wine
(
age
)
{
this
.
age
=
age
;
}
Wine
.
prototype
.
incAge
=
function
(
years
)
{
this
.
age
+=
years
;
}
The interaction is as follows:
- > var chablis = new Wine(3);
- > chablis.incAge(1);
- > chablis.age
- 4
The method incAge()
works for any object that has the property age
. How can we invoke it on an object that is not an instance of Wine
? Let’s look at the preceding method call:
chablis
.
incAge
(
1
)
There are actually two arguments:
chablis
is the receiver of the method call, passed toincAge
viathis
.1
is an argument, passed toincAge
viayears
.
We can’t replace the former with an arbitrary object—the receiver must be an instance of Wine
. Otherwise, the method incAge
is not found. But the preceding method call is equivalent to (refer back to Calling Functions While Setting this: call(), apply(), and bind()):
Wine
.
prototype
.
incAge
.
call
(
chablis
,
1
)
With the preceding pattern, we can make an object the receiver (first argument of call
) that is not an instance of Wine
, because the receiver isn’t used to find the method Wine.prototype.incAge
. In the following example, we apply the method incAge()
to the object john
:
- > var john = { age: 51 };
- > Wine.prototype.incAge.call(john, 3)
- > john.age
- 54
A function that can be used in this manner is called a generic method; it must be prepared for this
not being an instance of “its” constructor. Thus, not all methods are generic; the ECMAScript language specification explicitly states which ones are (see A List of All Generic Methods).
Accessing Object.prototype and Array.prototype via Literals
Calling a method generically is quite verbose:
Object
.
prototype
.
hasOwnProperty
.
call
(
obj
,
'propKey'
)
You can make this shorter by accessing hasOwnProperty
via an instance of Object
, as created by an empty object literal {}
:
{}.
hasOwnProperty
.
call
(
obj
,
'propKey'
)
Similarly, the following two expressions are equivalent:
Array
.
prototype
.
join
.
call
(
str
,
'-'
)
[].
join
.
call
(
str
,
'-'
)
The advantage of this pattern is that it is less verbose. But it is also less self-explanatory. Performance should not be an issue (at least long term), as engines can statically determine that the literals should not create objects.
Examples of Calling Methods Generically
These are a few examples of generic methods in use:
- Use
apply()
(see Function.prototype.apply(thisValue, argArray)) to push an array (instead of individual elements; see Adding and Removing Elements (Destructive)):
- > var arr1 = [ 'a', 'b' ];
- > var arr2 = [ 'c', 'd' ];
- > [].push.apply(arr1, arr2)
- 4
- > arr1
- [ 'a', 'b', 'c', 'd' ]
This example is about turning an array into arguments, not about borrowing a method from another constructor.
- Apply the array method
join()
to a string (which is not an array):
- > Array.prototype.join.call('abc', '-')
- 'a-b-c'
- Apply the array method
map()
to a string:[17]
- > [].map.call('abc', function (x) { return x.toUpperCase() })
- [ 'A', 'B', 'C' ]
Using map()
generically is more efficient than using split('')
, which creates an intermediate array:
- > 'abc'.split('').map(function (x) { return x.toUpperCase() })
- [ 'A', 'B', 'C' ]
- Apply a string method to nonstrings.
toUpperCase()
converts the receiver to a string and uppercases the result:
- > String.prototype.toUpperCase.call(true)
- 'TRUE'
- > String.prototype.toUpperCase.call(['a','b','c'])
- 'A,B,C'
Using generic array methods on plain objects gives you insight into how they work:
- Invoke an array method on a fake array:
- > var fakeArray = { 0: 'a', 1: 'b', length: 2 };
- > Array.prototype.join.call(fakeArray, '-')
- 'a-b'
- See how an array method transforms an object that it treats like an array:
- > var obj = {};
- > Array.prototype.push.call(obj, 'hello');
- 1
- > obj
- { '0': 'hello', length: 1 }
Array-Like Objects and Generic Methods
There are some objects in JavaScript that feel like an array, but actually aren’t. That means that while they have indexed access and a length
property, they don’t have any of the array methods (forEach()
, push
, concat()
, etc.). This is unfortunate, but as we will see, generic array methods enable a workaround. Examples of array-like objects include:
- The special variable
arguments
(see All Parameters by Index: The Special Variable arguments), which is an important array-like object, because it is such a fundamental part of JavaScript.arguments
looks like an array:
- > function args() { return arguments }
- > var arrayLike = args('a', 'b');
- > arrayLike[0]
- 'a'
- > arrayLike.length
- 2
But none of the array methods are available:
- > arrayLike.join('-')
- TypeError: object has no method 'join'
That’s because arrayLike
is not an instance of Array
(and Array.prototype
is not in the prototype chain):
- > arrayLike instanceof Array
- false
- Browser DOM node lists, which are returned by
document.getElementsBy*()
(e.g.,getElementsByTagName()
),document.forms
, and so on:
- > var elts = document.getElementsByTagName('h3');
- > elts.length
- 3
- > elts instanceof Array
- false
- Strings, which are array-like, too:
- > 'abc'[1]
- 'b'
- > 'abc'.length
- 3
The term array-like can also be seen as a contract between generic array methods and objects. The objects have to fulfill certain requirements; otherwise, the methods won’t work on them. The requirements are:
- The elements of an array-like object must be accessible via square brackets and integer indices starting at 0. All methods need read access, and some methods additionally need write access. Note that all objects support this kind of indexing: an index in brackets is converted to a string and used as a key to look up a property value:
- > var obj = { '0': 'abc' };
- > obj[0]
- 'abc'
- An array-like object must have a
length
property whose value is the number of its elements. Some methods requirelength
to be mutable (for example,reverse()
). Values whose lengths are immutable (for example, strings) cannot be used with those methods.
Patterns for working with array-like objects
The following patterns are useful for working with array-like objects:
- Turn an array-like object into an array:
var
arr
=
Array
.
prototype
.
slice
.
call
(
arguments
);
The method slice()
(see Concatenating, Slicing, Joining (Nondestructive)) without any arguments creates a copy of an array-like receiver:
var
copy
=
[
'a'
,
'b'
].
slice
();
- To iterate over all elements of an array-like object, you can use a simple
for
loop:
function
logArgs
()
{
for
(
var
i
=
0
;
i
<
arguments
.
length
;
i
++
)
{
console
.
log
(
i
+
'. '
+
arguments
[
i
]);
}
}
But you can also borrow Array.prototype.forEach()
:
function
logArgs
()
{
Array
.
prototype
.
forEach
.
call
(
arguments
,
function
(
elem
,
i
)
{
console
.
log
(
i
+
'. '
+
elem
);
});
}
In both cases, the interaction looks as follows:
- > logArgs('hello', 'world');
- 0. hello
- 1. world
A List of All Generic Methods
The following list includes all methods that are generic, as mentioned in the ECMAScript language specification:
Array.prototype
(see Array Prototype Methods):
concat
every
filter
forEach
indexOf
join
lastIndexOf
map
pop
push
reduce
reduceRight
reverse
shift
slice
some
sort
splice
toLocaleString
toString
unshift
Date.prototype
(see Date Prototype Methods)
toJSON
Object.prototype
(see Methods of All Objects)
- (All
Object
methods are automatically generic—they have to work for all objects.)
String.prototype
(see String Prototype Methods)
charAt
charCodeAt
concat
indexOf
lastIndexOf
localeCompare
match
replace
search
slice
split
substring
toLocaleLowerCase
toLocaleUpperCase
toLowerCase
toUpperCase
trim
Pitfalls: Using an Object as a Map
Since JavaScript has no built-in data structure for maps, objects are often used as maps from strings to values. Alas, that is more error-prone than it seems. This section explains three pitfalls that are involved in this task.
Pitfall 1: Inheritance Affects Reading Properties
The operations that read properties can be partitioned into two kinds:
- Some operations consider the whole prototype chain and see inherited properties.
- Other operations access only the own (noninherited) properties of an object.
You need to choose carefully between these kinds of operations when you read the entries of an object-as-map. To see why, consider the following example:
var
proto
=
{
protoProp
:
'a'
};
var
obj
=
Object
.
create
(
proto
);
obj
.
ownProp
=
'b'
;
obj
is an object with one own property whose prototype is proto
, which also has one own property. proto
has the prototype Object.prototype
, like all objects that are created by object literals. Thus, obj
inherits properties from both proto
and Object.
prototype
.
We want obj
to be interpreted as a map with the single entry:
- ownProp: 'b'
That is, we want to ignore inherited properties and only consider own properties. Let’s see which read operations interpret obj
in this manner and which don’t. Note that for objects-as-maps, we normally want to use arbitrary property keys, stored in variables. That rules out dot notation.
Checking whether a property exists
The in
operator checks whether an object has a property with a given key, but it considers inherited properties:
- > 'ownProp' in obj // ok
- true
- > 'unknown' in obj // ok
- false
- > 'toString' in obj // wrong, inherited from Object.prototype
- true
- > 'protoProp' in obj // wrong, inherited from proto
- true
We need the check to ignore inherited properties. hasOwnProperty()
does what we want:
- > obj.hasOwnProperty('ownProp') // ok
- true
- > obj.hasOwnProperty('unknown') // ok
- false
- > obj.hasOwnProperty('toString') // ok
- false
- > obj.hasOwnProperty('protoProp') // ok
- false
Collecting property keys
What operations can we use to find all of the keys of obj
, while honoring our interpretation of it as a map? for-in
looks like it might work. But, alas, it doesn’t:
- > for (propKey in obj) console.log(propKey)
- ownProp
- protoProp
It considers inherited enumerable properties. The reason that no properties of Object.prototype
show up here is that all of them are nonenumerable.
In contrast, Object.keys()
lists only own properties:
- > Object.keys(obj)
- [ 'ownProp' ]
This method returns only enumerable own properties; ownProp
has been added via assignment and is thus enumerable by default. If you want to list all own properties, you need to use Object.getOwnPropertyNames()
.
Getting a property value
For reading the value of a property, we can only choose between the dot operator and the bracket operator. We can’t use the former, because we have arbitrary keys, stored in variables. That leaves us with the bracket operator, which considers inherited properties:
- > obj['toString']
- [Function: toString]
This is not what we want.There is no built-in operation for reading only own properties, but you can easily implement one yourself:
function
getOwnProperty
(
obj
,
propKey
)
{
// Using hasOwnProperty() in this manner is problematic
// (explained and fixed later)
return
(
obj
.
hasOwnProperty
(
propKey
)
?
obj
[
propKey
]
:
undefined
);
}
With that function, the inherited property toString
is ignored:
- > getOwnProperty(obj, 'toString')
- undefined
Pitfall 2: Overriding Affects Invoking Methods
The function getOwnProperty()
invoked the method hasOwnProperty()
on obj
. Normally, that is fine:
- > getOwnProperty({ foo: 123 }, 'foo')
- 123
However, if you add a property to obj
whose key is hasOwnProperty
, then that property overrides the method Object.prototype.hasOwnProperty()
and getOwnProperty()
ceases to work:
- > getOwnProperty({ hasOwnProperty: 123 }, 'foo')
- TypeError: Property 'hasOwnProperty' is not a function
You can fix this problem by directly referring to hasOwnProperty()
. This avoids going through obj
to find it:
function
getOwnProperty
(
obj
,
propKey
)
{
return
(
Object
.
prototype
.
hasOwnProperty
.
call
(
obj
,
propKey
)
?
obj
[
propKey
]
:
undefined
);
}
We have called hasOwnProperty()
generically (see Generic Methods: Borrowing Methods from Prototypes).
Pitfall 3: The Special Property proto
In many JavaScript engines, the property proto
(see The Special Property proto) is special: getting it retrieves the prototype of an object, and setting it changes the prototype of an object. This is why the object can’t store map data in a property whose key is 'proto'
. If you want to allow the map key 'proto'
, you must escape it before using it as a property key:
function
get
(
obj
,
key
)
{
return
obj
[
escapeKey
(
key
)];
}
function
set
(
obj
,
key
,
value
)
{
obj
[
escapeKey
(
key
)]
=
value
;
}
// Similar: checking if key exists, deleting an entry
function
escapeKey
(
key
)
{
if
(
key
.
indexOf
(
'__proto__'
)
===
0
)
{
// (1)
return
key
+
'%'
;
}
else
{
return
key
;
}
}
We also need to escape the escaped version of 'proto'
(etc.) to avoid clashes; that is, if we escape the key 'proto'
as 'proto%'
, then we also need to escape the key 'proto%'
so that it doesn’t replace a 'proto'
entry. That’s what happens in line (1).
Mark S. Miller mentions the real-world implications of this pitfall in an email:
Think this exercise is academic and doesn’t arise in real systems? As observed at a support thread, until recently, on all non-IE browsers, if you typed “proto” at the beginning of a new Google Doc, your Google Doc would hang. This was tracked down to such a buggy use of an object as a string map.
The dict Pattern: Objects Without Prototypes Are Better Maps
You create an object without a prototype like this:
var
dict
=
Object
.
create
(
null
);
Such an object is a better map (dictionary) than a normal object, which is why this pattern is sometimes called the dict pattern (dict for dictionary). Let’s first examine normal objects and then find out why prototype-less objects are better maps.
Normal objects
Usually, each object you create in JavaScript has at least Object.prototype
in its prototype chain. The prototype of Object.prototype
is null
, so that’s where most prototype chains end:
- > Object.getPrototypeOf({}) === Object.prototype
- true
- > Object.getPrototypeOf(Object.prototype)
- null
Prototype-less objects
Prototype-less objects have two advantages as maps:
- Inherited properties (pitfall #1) are not an issue anymore, simply because there are none. Therefore, you can now freely use the
in
operator to detect whether a property exists and brackets to read properties. - Soon,
proto
will be disabled. In ECMAScript 6, the special propertyproto
will be disabled ifObject.prototype
is not in the prototype chain of an object. You can expect JavaScript engines to slowly migrate to this behavior, but it is not yet very common.
The only disadvantage is that you’ll lose the services provided by Object.prototype
. For example, a dict object can’t be automatically converted to a string anymore:
- > console.log('Result: '+obj)
- TypeError: Cannot convert object to primitive value
But that is not a real disadvantage, because it isn’t safe to directly invoke methods on a dict object anyway.
Recommendation
Use the dict pattern for quick hacks and as a foundation for libraries. In (nonlibrary) production code, a library is preferable, because you can be sure to avoid all pitfalls. The next section lists a few such libraries.
Best Practices
There are many applications for using objects as maps. If all property keys are known statically (at development time), then you just need to make sure that you ignore inheritance and look only at own properties. If arbitrary keys can be used, you should turn to a library to avoid the pitfalls mentioned in this section. Here are two examples:
- StringMap.js by Google’s es-lab
- stringmap.js by Olov Lassus
Cheat Sheet: Working with Objects
This section is a quick reference with pointers to more thorough explanations.
- Object literals (see Object Literals):
var
jane
=
{
name
:
'Jane'
,
'not an identifier'
:
123
,
describe
:
function
()
{
// method
return
'Person named '
+
this
.
name
;
},
};
// Call a method:
console
.
log
(
jane
.
describe
());
// Person named Jane
- Dot operator (.) (see Dot Operator (.): Accessing Properties via Fixed Keys):
obj
.
propKey
obj
.
propKey
=
value
delete
obj
.
propKey
- Bracket operator ([]) (see Bracket Operator ([]): Accessing Properties via Computed Keys):
obj
[
'propKey'
]
obj
[
'propKey'
]
=
value
delete
obj
[
'propKey'
]
- Getting and setting the prototype (see Getting and Setting the Prototype):
Object
.
create
(
proto
,
propDescObj
?
)
Object
.
getPrototypeOf
(
obj
)
- Iteration and detection of properties (see Iteration and Detection of Properties):
Object
.
keys
(
obj
)
Object
.
getOwnPropertyNames
(
obj
)
Object
.
prototype
.
hasOwnProperty
.
call
(
obj
,
propKey
)
propKey
in
obj
- Getting and defining properties via descriptors (see Getting and Defining Properties via Descriptors):
Object
.
defineProperty
(
obj
,
propKey
,
propDesc
)
Object
.
defineProperties
(
obj
,
propDescObj
)
Object
.
getOwnPropertyDescriptor
(
obj
,
propKey
)
Object
.
create
(
proto
,
propDescObj
?
)
- Protecting objects (see Protecting Objects):
Object
.
preventExtensions
(
obj
)
Object
.
isExtensible
(
obj
)
Object
.
seal
(
obj
)
Object
.
isSealed
(
obj
)
Object
.
freeze
(
obj
)
Object
.
isFrozen
(
obj
)
- Methods of all objects (see Methods of All Objects):
Object
.
prototype
.
toString
()
Object
.
prototype
.
valueOf
()
Object
.
prototype
.
toLocaleString
()
Object
.
prototype
.
isPrototypeOf
(
obj
)
Object
.
prototype
.
hasOwnProperty
(
key
)
Object
.
prototype
.
propertyIsEnumerable
(
propKey
)
[17] Using map()
in this manner is a tip by Brandon Benvie (@benvie).