Understanding ECMAScript 6
Table of Contents
Introduction
The JavaScript core language features are defined in a standard called ECMA-262. The language defined in this standard is called ECMAScript, of which the JavaScript in the browser and Node.js environments are a superset. While browsers and Node.js may add more capabilities through additional objects and methods, the core of the language remains as defined in ECMAScript, which is why the ongoing development of ECMA-262 is vital to the success of JavaScript as a whole.
In 2007, JavaScript was at a crossroads. The popularity of Ajax was ushering in a new age of dynamic web applications while JavaScript hadn’t changed since the third edition of ECMA-262 was published in 1999. TC-39, the committee responsible for driving the ECMAScript process, put together a large draft specification for ECMAScript 4. ECMAScript 4 was massive in scope, introducing changes both small and large to the language. Languages features included new syntax, modules, classes, classical inheritance, private object members, optional type annotations, and more.
The scope of the ECMAScript 4 changes caused a rift to form in TC-39, with some members feeling that the fourth edition was trying to accomplish too much. A group of leaders from Yahoo, Google, and Microsoft came up with an alternate proposal for the next version of ECMAScript that they initially called ECMAScript 3.1. The “3.1” was intended to show that this was an incremental change to the existing standard.
ECMAScript 3.1 introduced very few syntax changes, instead focusing on property attributes, native JSON support, and adding methods to already-existing objects. Although there was an early attempt to reconcile ECMAScript 3.1 and ECMAScript 4, this ultimately failed as the two camps had difficulty with the very different perspectives on how the language should grow.
In 2008, Brendan Eich, the creator of JavaScript, announced that TC-39 would focus its efforts on standardizing ECMAScript 3.1. They would table the major syntax and feature changes of ECMAScript 4 until after the next version of ECMAScript was standardized, and all members of the committee would work to bring the best pieces of ECMAScript 3.1 and 4 together after that point into an effort initially nicknamed ECMAScript Harmony.
ECMAScript 3.1 was eventually standardized as the fifth edition of ECMA-262, also described as ECMAScript 5. The committee never released an ECMAScript 4 standard to avoid confusion with the now-defunct effort of the same name. Work then began on ECMAScript Harmony, with ECMAScript 6 being the first standard released in this new “harmonious” spirit.
ECMAScript 6 reached feature complete status in 2014. The features vary widely from completely new objects and patterns to syntax changes to new methods on existing objects. The exciting thing about ECMAScript 6 is that all of these changes are geared towards problems that developers are actually facing. And while it will still take time for adoption and implementation to reach the point where ECMAScript 6 is the minimum that developers can expect, there’s a lot to be gained from a good understanding of what the future of JavaScript looks like.
Browser and Node.js Compatibility
Many JavaScript environments, such as web browsers and Node.js, are actively working on implementing ECMAScript 6. This book does not attempt to address to inconsistencies between implementations and instead focuses on what the specification defines as the correct behavior. As such, it’s possible that your JavaScript environment may not conform to the behavior described in this book.
Who This Book is For
This book is intended as a guide for those who are already familiar with JavaScript and ECMAScript 5. While a deep understanding of the language isn’t necessary to use this book, it is helpful in understanding the differences between ECMAScript 5 and 6. In particular, this book is aimed at intermediate-to-advanced JavaScript developers (both browser and Node.js environments) who want to learn about the future of the language.
This book is not for beginners who have never written JavaScript. You will need to have a good basic understanding of the language to make use of this book.
Overview
Chapter 1: The Basics introduces the smallest changes in the language. These are the new features that don’t necessarily introduce syntax changes, but rather are incremental changes on top of ECMAScript 5.
Chapter 2: Functions discusses the various changes to functions. This includes the arrow function form, default parameters, rest parameters, and more.
Chapter 3: Objects explains the changes to how objects are created, modified, and used. Topics include changes to object literal syntax, and new reflection methods.
Chapter 4: Classes introduces the first formal concept of classes in JavaScript. Often a point of confusion for those coming from other languages, the addition of class syntax in JavaScript makes the language more approachable to others and more concise for enthusiasts.
Chapter 5: Arrays details the changes to native arrays and the interesting new ways they can be used in JavaScript.
Chapter 6: Iterators and Generators discusses the addition of iterators and generators to the language. These features allow you to work with collections of data in powerful ways that were not possible in previous versions of JavaScript.
Chapter 7: Collections details the new collection types of Set
, WeakSet
, Map
, and WeakMap
. These types expand on the usefulness of arrays by adding semantics, de-duping, and memory management designed specifically for JavaScript.
Chapter 8: Symbols introduces the concept of symbols, a new way to define properties. Symbols are a new primitive type that can be used to obscure (but not hide) object properties and methods.
Chapter 9: Proxies discusses the new proxy object that allows you to intercept every operation performed on an object. Proxies give developers unprecedented control over objects and, as such, unlimited possibilities for defining new interaction patterns.
Chapter 10: Promises introduces promises as a new part of the language. Promises were a grassroots effort that eventually took off and gained in popularity due to extensive library support. ECMAScript 6 formalizes promises and makes them available by default.
Chapter 11: Modules details the official module format for JavaScript. The intent is that these modules can replace the numerous ad-hoc module definition formats that have appeared over the years.
Chapter 12: Template Strings discusses the new built-in templating functionality. Template strings are designed to easily create DSLs in a secure way.
Chapter 13: Reflection introduces the formalized reflection API for JavaScript. Similar to other languages, ECMAScript 6 reflection allows you to inspect objects at a granular level, even if you didn’t create the object.
Help and Support
You can file issues, suggest changes, and open pull requests against this book by visiting: https://github.com/nzakas/understandinges6
For anything else, please send a message to the mailing list: http://groups.google.com/group/zakasbooks.
The Basics
ECMAScript 6 makes a large number of changes on top of ECMAScript 5. Some of the changes are larger, such as adding new types or syntax, while others are quite small, providing incremental improvements on top of the language. This chapter covers those incremental improvements that likely won’t gain a lot of attention but provide some important functionality that may make certain types of problems easier to solve.
Better Unicode Support
Prior to ECMAScript 6, JavaScript strings were based solely on the idea of 16-bit character encodings. All string properties and methods, such as length
and charAt()
, were based around the idea that every 16-bit sequence represented a single character. ECMAScript 5 allowed JavaScript engines to decide which of two encodings to use, either UCS-2 or UTF-16 (both encoding using 16-bit code units, making all observable operations the same). While it’s true that all of the world’s characters used to fit into 16 bits at one point in time, that is no longer the case.
Keeping within 16 bits wasn’t possible for Unicode’s stated goal of providing a globally unique identifier to every character in the world. These globally unique identifiers, called code points, are simply numbers starting at 0 (you might think of these as character codes, but there is subtle difference). A character encoding is responsible for encoding a code point into code units that are internally consistent. While UCS-2 had a one-to-one mapping of code point to code unit, UTF-16 is more variable.
The first 2^16 code points are represented as single 16-bit code units in UTF-16. This is called the Basic Multilingual Plane (BMP). Everything beyond that range is considered to be in a supplementary plane, where the code points can no longer be represented in just 16-bits. UTF-16 solves this problem by introducing surrogate pairs in which a single code point is represented by two 16-bit code units. That means any single character in a string can be either one code unit (for BMP, total of 16 bits) or two (for supplementary plane characters, total of 32 bits).
ECMAScript 5 kept all operations as working on 16-bit code units, meaning that you could get unexpected results from strings containing surrogate pairs. For example:
var
text
=
"𠮷"
;
console
.
log
(
text
.
length
);
// 2
console
.
log
(
/^.$/
.
test
(
text
));
// false
console
.
log
(
text
.
charAt
(
0
));
// ""
console
.
log
(
text
.
charAt
(
1
));
// ""
console
.
log
(
text
.
charCodeAt
(
0
));
// 55362
console
.
log
(
text
.
charCodeAt
(
1
));
// 57271
In this example, a single Unicode character is represented using surrogate pairs, and as such, the JavaScript string operations treat the string as having two 16-bit characters. That means length
is 2, a regular expression trying to match a single character fails, and charAt()
is unable to return a valid character string. The charCodeAt()
method returns the appropriate 16-bit number for each code unit, but that is the closest you could get to the real value in ECMAScript 5.
ECMAScript 6 enforces encoding of strings in UTF-16. Standardizing on this character encoding means that the language can now support functionality designed to work specifically with surrogate pairs.
The codePointAt() Method
The first example of fully supporting UTF-16 is the codePointAt()
method, which can be used to retrieve the Unicode code point that maps to a given character. This method accepts the character position (not the code unit position) and returns an integer value:
var
text
=
"𠮷a"
;
console
.
log
(
text
.
codePointAt
(
0
));
// 134071
console
.
log
(
text
.
codePointAt
(
1
));
// 97
The value returned is the Unicode code point value. For BMP characters, this will be the same result as using charCodeAt()
, so the "a"
returns 97. This method is the easiest way to determine if a given character is represented by one or two code points:
function
is32Bit
(
c
)
{
return
c
.
codePointAt
(
0
)
>
0xFFFF
;
}
console
.
log
(
is32Bit
(
"𠮷"
));
// true
console
.
log
(
is32Bit
(
"a"
));
// false
The upper bound of 16-bit characters is represented in hexadecimal as FFFF
, so any code point above that number must be represented by two code units.
String.fromCodePoint()
When ECMAScript provides a way to do something, it also tends to provide a way to do the reverse. You can use codePointAt()
to retrieve the code point for a character in a string while String.fromCodePoint()
produces a single-character string for the given code point. For example:
console
.
log
(
String
.
fromCodePoint
(
134071
));
// "𠮷"
You can think of String.fromCodePoint()
as a more complete version of String.fromCharCode()
. Each method has the same result for all characters in the BMP; the only difference is with characters outside of that range.
Escaping Non-BMP Characters
ECMAScript 5 allows strings to contain 16-bit Unicode characters represented by an escape sequence. The escape sequence is the \u
followed by four hexadecimal values. For example, the escape sequence \u0061
represents the letter "a"
:
console
.
log
(
"\u0061"
);
// "a"
If you try to use an escape sequence with a number past FFFF
, the upper bound of the BMP, then you can get some surprising results:
console
.
log
(
"\u20BB7"
);
// "7"
Since Unicode escape sequences were defined as always having exactly four hexadecimal characters, ECMAScript evaluates \u20BB7
as two characters: \u20BB
and "7"
. The first character is unprintable and the second is the number 7.
ECMAScript 6 solves this problem by introducing an extended Unicode escape sequence where the hexadecimal numbers are contained within curly braces. This allows up to 8 hexadecimal characters to specify a single character:
console
.
log
(
"\u{20BB7}"
);
// "𠮷"
Using the extended escape sequence, the correct character is contained in the string.
Make sure that you use this new escape sequence only in an ECMAScript 6 environment. In all other environments, doing so causes a syntax error. You may want to check and see if the environment supports the extended escape sequence using a function such as:
|
The normalize() Method
Another interesting aspect of Unicode is that different characters may be considered equivalent for the purposes of sorting or other comparison-based operations. There are two ways to define these relationships. First, canonical equivalence means that two sequences of code points are considered interchangeable in all respects. That even means that a combination of two characters can be canonically equivalent to one character. The second relationship is compatibility, meaning that two sequences of code points having different appearances but can be used interchangeably in certain situations.
The important thing to understand is that due to these relationships, it’s possible to have two strings that represent fundamentally the same text and yet have them contain different code point sequences. For example, the character “æ” and the string “ae” may be used interchangeably even though they are different code points. These two strings would therefore be unequal in JavaScript unless they are normalized in some way.
ECMAScript 6 supports the four Unicode normalization forms through a new normalize()
method on strings. This method optionally accepts a single parameter, one of "NFC"
(default), "NFD"
, "NFKC"
, or "NFKD"
. It’s beyond the scope of this book to explain the differences between these four forms. Just keep in mind that, in order to be used, you must normalize both strings that are being compared to the same form. For example:
var
normalized
=
values
.
map
(
function
(
text
)
{
return
text
.
normalize
();
});
normalized
.
sort
(
function
(
first
,
second
)
{
if
(
first
<
second
)
{
return
-
1
;
}
else
if
(
first
===
second
)
{
return
0
;
}
else
{
return
1
;
}
});
In this code, the strings in a values
array are converted into a normalized form so that the array can be sorted appropriately. You can accomplish the sort on the original array by calling normalize()
as part of the comparator:
values
.
sort
(
function
(
first
,
second
)
{
var
firstNormalized
=
first
.
normalize
(),
secondNormalized
=
second
.
normalize
();
if
(
firstNormalized
<
secondNormalized
)
{
return
-
1
;
}
else
if
(
firstNormalized
===
secondNormalized
)
{
return
0
;
}
else
{
return
1
;
}
});
Once again, the most important thing to remember is that both values must be normalized in the same way. These examples have used the default, NFC, but you can just as easily specify one of the others:
values
.
sort
(
function
(
first
,
second
)
{
var
firstNormalized
=
first
.
normalize
(
"NFD"
),
secondNormalized
=
second
.
normalize
(
"NFD"
);
if
(
firstNormalized
<
secondNormalized
)
{
return
-
1
;
}
else
if
(
firstNormalized
===
secondNormalized
)
{
return
0
;
}
else
{
return
1
;
}
});
If you’ve never worried about Unicode normalization before, then you probably won’t have much use for this method. However, knowing that it is available will help should you ever end up working on an internationalized application.
The Regular Expression u Flag
Many common string operations are accomplished by using regular expressions. However, as noted earlier, regular expressions also work on the basis of 16-bit code units each representing a single character. That’s why the single character match in the earlier example didn’t work. To address this problem, ECMAScript 6 defines a new flag for regular expressions: u
for “Unicode”.
When a regular expression has the u
flag set, it switches modes to work on characters and not code units. That means the regular expression will no longer get confused about surrogate pairs in strings and can behave as expected. For example:
var
text
=
"𠮷"
;
console
.
log
(
text
.
length
);
// 2
console
.
log
(
/^.$/
.
test
(
text
));
// false
console
.
log
(
/^.$/u.test(text)); // true
Adding the u
flag allows the regular expression to correctly match the string by characters. Unfortunately, ECMAScript 6 does not have a way of determining how many code points are present in a string; fortunately, regular expressions can be used to figure it out:
function
codePointLength
(
text
)
{
var
result
=
text
.
match
(
/[\s\S]/gu);
return
result
?
result
.
length
:
0
;
}
console
.
log
(
codePointLength
(
"abc"
));
// 3
console
.
log
(
codePointLength
(
"𠮷bc"
));
// 3
The regular expression in this example matches both whitespace and non-whitespace characters, and is applied globally with Unicode enabled. The result
contains an array of matches when there’s at least one match, so the array length ends up being the number of code points in the string.
Although this approach works, it’s not very fast, especially when applied to long strings. Try to minimize counting code points whenever possible. Hopefully ECMAScript 7 will bring a more performant means by which to count code points. |
Since the u
flag is a syntax change, attempting to use it in non-compliant JavaScript engines means a syntax error is thrown. The safest way to determine if the u
flag is supported is with a function:
function
hasRegExpU
()
{
try
{
var
pattern
=
new
RegExp
(
"."
,
"u"
);
return
true
;
}
catch
(
ex
)
{
return
false
;
}
}
This function uses the RegExp
constructor to pass in the u
flag as an argument. This is valid syntax even in older JavaScript engines, however, the constructor will throw an error if u
isn’t supported.
If your code needs to still work in older JavaScript engines, it’s best to use the |
Other String Changes
JavaScript strings have always lagged behind similar features of other languages. It was only in ECMAScript 5 that strings finally gained a trim()
method, and ECMAScript 6 continues extending strings with new functionality.
includes(), startsWith(), endsWith()
Developers have used indexOf()
as a way to identify strings inside of other strings since JavaScript was first introduced. ECMAScript 6 adds three new methods whose purpose is to identify strings inside of other strings:
-
includes()
- returns true if the given text is found anywhere within the string or false if not. -
startsWith()
- returns true if the given text is found at the beginning of the string or false if not. -
endsWith()
- returns true if the given text is found at the end of the string or false if not.
Each of these methods accepts two arguments: the text to search for and an optional location from which to start the search. When the second argument is omitted, includes()
and startsWith()
start search from the beginning of the string while endsWith()
starts from the end. In effect, the second argument results in less of the string being searched. Here are some examples:
var
msg
=
"Hello world!"
;
console
.
log
(
msg
.
startsWith
(
"Hello"
));
// true
console
.
log
(
msg
.
endsWith
(
"!"
));
// true
console
.
log
(
msg
.
includes
(
"o"
));
// true
console
.
log
(
msg
.
startsWith
(
"o"
));
// false
console
.
log
(
msg
.
endsWith
(
"world!"
));
// true
console
.
log
(
msg
.
includes
(
"x"
));
// false
console
.
log
(
msg
.
startsWith
(
"o"
,
4
));
// true
console
.
log
(
msg
.
endsWith
(
"o"
,
8
));
// true
console
.
log
(
msg
.
includes
(
"o"
,
8
));
// false
These three methods make it much easier to identify substrings without needing to worry about identifying their exact position.
All of these methods return a boolean value. If you need to find the position of a string within another, use |
The |
repeat()
ECMAScript 6 also adds a repeat()
method to strings. This method accepts a single argument, which is the number of times to repeat the string, and returns a new string that has the original string repeated the specified number of times. For example:
console
.
log
(
"x"
.
repeat
(
3
));
// "xxx"
console
.
log
(
"hello"
.
repeat
(
2
));
// "hellohello"
console
.
log
(
"abc"
.
repeat
(
4
));
// "abcabcabcabc"
This method is really a convenience function above all else, which can be especially useful when dealing with text manipulation. One example where this functionality comes in useful is with code formatting utilities where you need to create indentation levels:
// indent using a specified number of spaces
var
indent
=
" "
.
repeat
(
size
),
indentLevel
=
0
;
// whenever you increase the indent
var
newIndent
=
indent
.
repeat
(
++
indentLevel
);
Other Regular Expression Changes
Regular expressions are an important part of working with strings in JavaScript, and like many parts of the language, haven’t really changed very much in recent versions. ECMAScript 6, however, made several improvements to regular expressions to go along with the updates to strings.
The Regular Expression y Flag
ECMAScript 6 standardized the y
flag after it had been implemented in Firefox as a proprietary extension to regular expressions. The y
(sticky) flag indicates that the next match should be made starting with the value of lastIndex
on the regular expression.
The lastIndex
property indicates the position at which to start the match of a string and is set to 0
by default, meaning matches always start at the beginning of a string. You can, however, overwrite lastIndex
to have it start from somewhere else:
var
pattern
=
/hello\d\s?/g
,
text
=
"hello1 hello2 hello3"
,
result
=
pattern
.
exec
(
text
);
console
.
log
(
result
[
0
]);
// "hello1 "
pattern
.
lastIndex
=
7
;
result
=
pattern
.
exec
(
text
);
console
.
log
(
result
[
0
]);
// "hello2 "
In this example, the regular expression matches the string "hello"
followed by a number and optionally a whitespace character. The g
flag is important as it allows the regular expression to use lastIndex
when set (without it, matches always start at 0
regardless of the lastIndex
value). The first call to exec()
results in matching “hello1” first while the second call, with a lastIndex
of 7, matches “hello2” first.
The sticky flag tells the regular expression to save the index of the next character after the last match in lastIndex
whenever an operation is performed (in the previous example, 7 is the location of next character after “hello1 “). If an operation results in no match then lastIndex
is set back to 0.
var
pattern
=
/hello\d\s?/y,
text
=
"hello1 hello2 hello3"
,
result
=
pattern
.
exec
(
text
);
console
.
log
(
result
[
0
]);
// "hello1 "
console
.
log
(
pattern
.
lastIndex
);
// 7
result
=
pattern
.
exec
(
text
);
console
.
log
(
result
[
0
]);
// "hello2 "
console
.
log
(
pattern
.
lastIndex
);
// 14
Here, the same pattern is used but with the sticky flag instead of the global flag. The value of lastIndex
changed to 7 after the first call to exec()
and to 14 after the second call. Since the sticky flag is updating lastIndex
for you, there’s no need to keep track and manually update it yourself.
Perhaps the most important thing to understand about the sticky flag is that sticky regular expressions have an implied ^
at the beginning, indicating that the pattern should match from the beginning of the input. For example, if the previous example is changed to not match the whitespace character, there are different results:
var
pattern
=
/hello\d/y,
text
=
"hello1 hello2 hello3"
,
result
=
pattern
.
exec
(
text
);
console
.
log
(
result
[
0
]);
// "hello1"
console
.
log
(
pattern
.
lastIndex
);
// 6
result
=
pattern
.
exec
(
text
);
console
.
log
(
result
);
// null
console
.
log
(
pattern
.
lastIndex
);
// 0
Without matching the whitespace character, the lastIndex
is set to 6 after the first call to exec()
. That means the regular expression will be evaluating the string as if it were this:
" hello2 hello3"
Since there is an implied ^
at the beginning of the regular expression pattern, the pattern starts by matching "h"
against the space and sees that they are not equivalent. The matching stops there and null
is returned. The lastIndex
property is reset to 0.
As with other regular expression flags, you can detect the presence of y
by using a property. The sticky
property is set to true with the sticky flag is present and false if not:
var
pattern
=
/hello\d/y;
console
.
log
(
pattern
.
sticky
);
// true
The sticky
property is read-only based on the presence of the flag and so cannot be changed in code.
The |
Similar to the u
flag, the y
flag is a syntax change, so it will cause a syntax error in older JavaScript engines. You can use the same approach to detect support:
function
hasRegExpY
()
{
try
{
var
pattern
=
new
RegExp
(
"."
,
"y"
);
return
true
;
}
catch
(
ex
)
{
return
false
;
}
}
Also similar to u
, if you need to use y
in code that runs in older JavaScript engines, be sure to use the RegExp
constructor when defining those regular expressions to avoid a syntax error.
Duplicating Regular Expressions
In ECMAScript 5, you can duplicate regular expressions by passing them into the RegExp
constructor, such as:
js
var
re1
=
/
ab
/
i
,
re2
=
new
RegExp
(
re1
);
However, if you provide the second argument to RegExp
, which specifies the flags for the regular expression, then an error is thrown:
js
var
re1
=
/
ab
/
i
,
// throws an error in ES5, okay in ES6
re2
=
new
RegExp
(
re1
,
"g"
);
If you execute this code in an ECMAScript 5 environment, you’ll get an error stating that the second argument cannot be used when the first argument is a regular expression. ECMAScript 6 changed this behavior such that the second argument is allowed and will override whichever flags are present on the first argument. For example:
js
var
re1
=
/
ab
/
i
,
// throws an error in ES5, okay in ES6
re2
=
new
RegExp
(
re1
,
"g"
);
console
.
log
(
re1
.
toString
());
// "/ab/i"
console
.
log
(
re2
.
toString
());
// "/ab/g"
console
.
log
(
re1
.
test
(
"ab"
));
// true
console
.
log
(
re2
.
test
(
"ab"
));
// true
console
.
log
(
re1
.
test
(
"AB"
));
// true
console
.
log
(
re2
.
test
(
"AB"
));
// false
In this code, re1
has the case-insensitive i
flag present while re2
has only the global g
flag. The RegExp
constructor duplicated the pattern from re1
and then substituted g
for i
. If the second argument was missing then re2
would have the same flags as re1
.
The flags
Property
In ECMAScript 5, it’s possible to get the text of the regular expression by using the source
property, but to get the flag string requires parsing the output of toString()
, such as:
function
getFlags
(
re
)
{
var
text
=
re
.
toString
();
return
text
.
substring
(
text
.
lastIndexOf
(
"/"
)
+
1
,
text
.
length
);
}
// toString() is "/ab/g"
var
re
=
/ab/g
;
console
.
log
(
getFlags
(
re
));
// "g"
ECMAScript 6 adds a flags
property to go along with source
. Both properties are prototype accessor properties with only a getter assigned (making them read-only). The addition of flags
makes it easier to inspect regular expressions for both debugging and inheritance purposes.
A late addition to ECMAScript 6, the flags
property returns the string representation of any flags applied to a regular expression.
Object.is()
When you want to compare two values, you’re probably used to using either the equals operator (==
) or the identically equals operator (===
). Many prefer to use the latter to avoid type coercion during the comparison. However, even the identically equals operator isn’t entirely accurate. For example, the values +0 and -0 are considered equal by ===
even though they are represented differently in the JavaScript engine. Also NaN === NaN
returns false
, which necessitates using isNaN()
to detect NaN
properly.
ECMAScript 6 introduces Object.is()
to make up for the remaining quirks of the identically equals operator. This method accepts two arguments and returns true
if the values are equivalent. Two values are considered equivalent when they are of the same type and have the same value. In many cases, Object.is()
works the same as ===
. The only differences are that +0 and -0 are considered not equivalent and NaN
is considered equivalent to NaN
. Here are some examples:
console
.
log
(
+
0
==
-
0
);
// true
console
.
log
(
+
0
===
-
0
);
// true
console
.
log
(
Object
.
is
(
+
0
,
-
0
));
// false
console
.
log
(
NaN
==
NaN
);
// false
console
.
log
(
NaN
===
NaN
);
// false
console
.
log
(
Object
.
is
(
NaN
,
NaN
));
// true
console
.
log
(
5
==
5
);
// true
console
.
log
(
5
==
"5"
);
// true
console
.
log
(
5
===
5
);
// true
console
.
log
(
5
===
"5"
);
// false
console
.
log
(
Object
.
is
(
5
,
5
));
// true
console
.
log
(
Object
.
is
(
5
,
"5"
));
// false
In most cases you will probably still want to use ==
or ===
for comparing values, as special cases covered by Object.is()
may not affect you directly.
Block bindings
Traditionally, one of the tricky parts of JavaScript has been the way that var
declarations work. In most C-based languages, variables are created at the spot where the declaration occurs. In JavaScript, however, this is not the case. Variables declared using var
are hoisted to the top of the function (or global scope) regardless of where the actual declaration occurs. For example:
function
getValue
(
condition
)
{
if
(
condition
)
{
var
value
=
"blue"
;
// other code
return
value
;
}
else
{
// value exists here with a value of undefined
return
null
;
}
// value exists here with a value of undefined
}
If you are unfamiliar with JavaScript, you might expect that the variable value
is only defined if condition
evaluates to true. In fact, the variable value
is declared regardless. The JavaScript engine changes the function to look like this:
function
getValue
(
condition
)
{
var
value
;
if
(
condition
)
{
value
=
"blue"
;
// other code
return
value
;
}
else
{
return
null
;
}
}
The declaration of value
is moved to the top (hoisted) while the initialization remains in the same spot. That means the variable value
is actually still accessible from within the else
clause, it just has a value of undefined
because it hasn’t been initialized.
It often takes new JavaScript developers some time to get used to declaration hoisting and this unique behavior can end up causing bugs. For this reason, ECMAScript 6 introduces block level scoping options to make the control of variable lifecycle a little more powerful.
Let declarations
The let
declaration syntax is the same as for var
. You can basically replace var
with let
to declare a variable but keep its scope to the current code block. For example:
function
getValue
(
condition
)
{
if
(
condition
)
{
let
value
=
"blue"
;
// other code
return
value
;
}
else
{
// value doesn't exist here
return
null
;
}
// value doesn't exist here
}
This function now behaves much closer to other C-based languages. The variable value
is declared using let
instead of var
. That means the declaration is not hoisted to the top, and the variable value
is destroyed once execution has flowed out of the if
block. If condition
evaluates to false, then value
is never declared or initialized.
Perhaps one of the areas where developers most want block level scoping of variables is with for
loops. It’s not uncommon to see code such as this:
for
(
var
i
=
0
;
i
<
items
.
length
;
i
++
)
{
process
(
items
[
i
]);
}
// i is still accessible here and is equal to items.length
In other languages, where block level scoping is the default, code like this works as intended. In JavaScript, the variable i
is still accessible after the loop is completed because the var
declaration was hoisted. Using let
allows you to get the intended behavior:
for
(
let
i
=
0
;
i
<
items
.
length
;
i
++
)
{
process
(
items
[
i
]);
}
// i is not accessible here
In this example, the variable i
only exists within the for
loop. Once the loop is complete, the variable is destroyed and is no longer accessible elsewhere.
Unlike var
, let
has no hoisting characteristics. A variable declared with let
cannot be accessed until after the let
statement. Attempting to do so results in a reference error:
if
(
condition
)
{
console
.
log
(
value
);
// ReferenceError!
let
value
=
"blue"
;
}
In this code, the variable value
is defined and initialized using let
, but that statement is never executed because the previous line throws an error. The same is true anytime you attempt to use a let
variable inside of the same block prior to it being defined. Even the normally safe-to-use typeof
operator isn’t safe:
if
(
condition
)
{
console
.
log
(
typeof
value
);
// ReferenceError!
let
value
=
"blue"
;
}
Here, typeof value
throws the same error as the previous example. You cannot use a let
variable before its declaration within the same block. However, you can use typeof
outside of the block:
console
.
log
(
typeof
value
);
// "undefined"
if
(
condition
)
{
let
value
=
"blue"
;
}
This example has the typeof
operator applied outside of the block in which value
is declared. That means there is no value
binding and typeof
simply returns "undefined"
.
If an identifier has already been defined in the block, then using the identifier in a let
declaration causes an error to be thrown. For example:
var
count
=
30
;
// Syntax error
let
count
=
40
;
In this example, count
is declared twice, once with var
and once with let
. Because let
will not redefine an identifier that already exists in the same scope, the declaration throws an error. No error is thrown if a let
declaration creates a new variable in a scope with the same name as a variable in the containing scope, such as:
var
count
=
30
;
// Does not throw an error
if
(
condition
)
{
let
count
=
40
;
// more code
}
Here, the let
declaration will not throw an error because it is creating a new variable called count
within the if
statement. This new variable shadows the global count
, preventing access to it from within the if
block.
The intent of let
is to replace var
long term, as the former behaves more like variable declarations in other languages. If you are writing JavaScript that will execute only in an ECMAScript 6 or higher environment, you may want to try using let
exclusively and leaving var
for other scripts that require backwards compatibility.
Since |
Constant declarations
Another new way to define variables is to use the const
declaration syntax. Variables declared using const
are considered to be constants, so the value cannot be changed once set. For this reason, every const
variable must be initialized. For example:
// Valid constant
const
MAX_ITEMS
=
30
;
// Syntax error: missing initialization
const
NAME
;
Constants are also block-level declarations, similar to let
. That means constants are destroyed once execution flows out of the block in which they were declared and declarations are hoisted to the top of the block. For example:
if
(
condition
)
{
const
MAX_ITEMS
=
5
;
// more code
}
// MAX_ITEMS isn't accessible here
In this code, the constant MAX_ITEMS
is declared within and if
statement. Once the statement finishes executing, MAX_ITEMS
is destroyed and is not accessible outside of that block.
Also similar to let
, an error is thrown whenever a const
declaration is made with an identifier for an already-defined variable in the same scope. It doesn’t matter if that variable was declared using var
(for global or function scope) or let
(for block scope). For example:
var
message
=
"Hello!"
;
let
age
=
25
;
// Each of these would cause an error given the previous declarations
const
message
=
"Goodbye!"
;
const
age
=
30
;
Several browsers implement pre-ECMAScript 6 versions of |
Destructuring Assignment
JavaScript developers spend a lot of time pulling data out of objects and arrays. It’s not uncommon to see code such as this:
var
options
=
{
repeat
:
true
,
save
:
false
};
// later
var
localRepeat
=
options
.
repeat
,
localSave
=
options
.
save
;
Frequently, object properties are stored into local variables for more succinct code and easier access. ECMAScript 6 makes this easy by introducing destructuring assignment, which systematically goes through an object or array and stores specified pieces of data into local variables.
If the right side value of a destructuring assignment evaluates to |
Object Destructuring
Object destructuring assignment syntax uses an object literal on the left side of an assignment operation. For example:
var
options
=
{
repeat
:
true
,
save
:
false
};
// later
var
{
repeat
:
localRepeat
,
save
:
localSave
}
=
options
;
console
.
log
(
localRepeat
);
// true
console
.
log
(
localSave
);
// false
In this code, the value of options.repeat
is stored in a variable called localRepeat
and the value of options.save
is stored in a variable called localSave
. These are both specified using the object literal syntax where the key is the property to find on options
and the value is the variable in which to store the property value.
If the property with the given name doesn’t exist on the object, then the local variable gets a value of |
If you want to use the property name as the local variable name, you can omit the colon and the identifier, such as:
var
options
=
{
repeat
:
true
,
save
:
false
};
// later
var
{
repeat
,
save
}
=
options
;
console
.
log
(
repeat
);
// true
console
.
log
(
save
);
// false
Here, two local variables called repeat
and save
are created. They are initialized with the value of options.repeat
and options.save
, respectively. This shorthand is helpful when there’s no need to have different variable names.
Destructuring can also handled nested objects, such as the following:
var
options
=
{
repeat
:
true
,
save
:
false
,
rules
:
{
custom
:
10
,
}
};
// later
var
{
repeat
,
save
,
rules
:
{
custom
}}
=
options
;
console
.
log
(
repeat
);
// true
console
.
log
(
save
);
// false
console
.
log
(
custom
);
// 10
In this example, the custom
property is embedded in another object. The extra set of curly braces allows you to descend into a nested an object and pull out its properties.
Array Destructuring
Similarly, you can destructure arrays using array literal syntax on the left side of an assignment operation. For example:
var
colors
=
[
"red"
,
"green"
,
"blue"
];
// later
var
[
firstColor
,
secondColor
]
=
colors
;
console
.
log
(
firstColor
);
// "red"
console
.
log
(
secondColor
);
// "green"
In this example, array destructuring pulls out the first and second values in the colors
array. Keep in mind that the array itself isn’t changed in any way.
Similar to object destructuring, you can also nest array destructuring. Just use another set of square brackets to descend into a subarray:
var
colors
=
[
"red"
,
[
"green"
,
"lightgreen"
],
"blue"
];
// later
var
[
firstColor
,
[
secondColor
]
]
=
colors
;
console
.
log
(
firstColor
);
// "red"
console
.
log
(
secondColor
);
// "green"
Here, the secondColor
variable refers to the "green"
value inside of the colors
array. That item is contained within a second array, so the extra square brackets around secondColor
in the destructuring assignment is necessary.
Mixed Destructuring
It’s possible to mix objects and arrays together in a destructuring assignment expression using a mix of object and array literals. For example:
var
options
=
{
repeat
:
true
,
save
:
false
,
colors
:
[
"red"
,
"green"
,
"blue"
]
};
var
{
repeat
,
save
,
colors
:
[
firstColor
,
secondColor
]}
=
options
;
console
.
log
(
repeat
);
// true
console
.
log
(
save
);
// false
console
.
log
(
firstColor
);
// "red"
console
.
log
(
secondColor
);
// "green"
This example extracts two property values, repeat
and save
, and then two items from the colors
array, firstColor
and secondColor
. Of course, you could also choose to retrieve the entire array:
var
options
=
{
repeat
:
true
,
save
:
false
,
colors
:
[
"red"
,
"green"
,
"blue"
]
};
var
{
repeat
,
save
,
colors
}
=
options
;
console
.
log
(
repeat
);
// true
console
.
log
(
save
);
// false
console
.
log
(
colors
);
// "red,green,blue"
console
.
log
(
colors
===
options
.
colors
);
// true
This modified example retrieves options.colors
and stores it in the colors
variable. Notice that colors
is a direct reference to options.colors
and not a copy.
Mixed destructuring is very useful for pulling values out of JSON configuration structures without navigating the entire structure.
Numbers
JavaScript numbers can be particularly complex due to the dual usage of a single type for both integers and floats. Numbers are stored in the IEEE 754 double precision floating point format, and that same format is used to represent both types of numbers. As one of the foundational data types of JavaScript (along with strings and booleans), numbers are quite important to JavaScript developers. Given the new emphasis on gaming and graphics in JavaScript, ECMAScript 6 sought to make working with numbers easier and more powerful.
Octal and Binary Literals
ECMAScript 5 sought to simplify some common numerical errors by removing the previously-included octal integer literal notation in two places: parseInt()
and strict mode. In ECMAScript 3 and earlier, octal numbers were represented with a leading 0
followed by any number of digits. For example:
// ECMAScript 3
var
number
=
071
;
// 57 in decimal
var
value1
=
parseInt
(
"71"
);
// 71
var
value2
=
parseInt
(
"071"
);
// 57
Many developers were confused by this version of octal literal numbers, and many mistakes were made as a result of misunderstanding the effects of a leading zero in various places. The most egregious was in parseInt()
, where a leading zero meant the value would be treated as an octal rather than a decimal. This led to one of Douglas Crockford’s first JSLint rules: always use the second argument of parseInt()
to specify how the string should be interpreted.
ECMAScript 5 cut down on the use of octal numbers. First, parseInt()
was changed so that it ignores leading zeros in the first argument when there is no second argument. This means a number cannot accidentally be treated as octal anymore. The second change was to eliminate octal literal notation in strict mode. Attempting to use an octal literal in strict mode results in a syntax error.
// ECMAScript 5
var
number
=
071
;
// 57 in decimal
var
value1
=
parseInt
(
"71"
);
// 71
var
value2
=
parseInt
(
"071"
);
// 71
var
value3
=
parseInt
(
"071"
,
8
);
// 57
function
getValue
()
{
"use strict"
;
return
071
;
// syntax error
}
By making these two changes, ECMAScript 5 sought to eliminate a lot of the confusion and errors associated with octal literals.
ECMAScript 6 took things a step further by reintroducing an octal literal notation, along with a binary literal notation. Both of these notations take a hint for the hexadecimal literal notation of prepending 0x
or 0X
to a value. The new octal literal format begins with 0o
or 0O
while the new binary literal format begins with 0b
or 0B
. Each literal type must be followed by one or more digits, 0-7 for octal, 0-1 for binary. Here’s an example:
// ECMAScript 6
var
value1
=
0
o71
;
// 57 in decimal
var
value2
=
0
b101
;
// 5 in decimal
Adding these two literal types allows JavaScript developers to quickly and easily include numeric values in binary, octal, decimal, and hexadecimal formats, which is very important in certain types of mathematical operations.
The parseInt()
method doesn’t handle strings that look like octal or binary literals:
console
.
log
(
parseInt
(
"0o71"
));
// 0
console
.
log
(
parseInt
(
"0b101"
));
// 0
However, the Number()
function will convert a string containing octal or binary literals correctly:
console
.
log
(
Number
(
"0o71"
));
// 57
console
.
log
(
Number
(
"0b101"
));
// 5
When using octal or binary literal in strings, be sure to understand your use case and use the most appropriate method for converting them into numeric values.
isFinite() and isNaN()
JavaScript has long had a couple of global methods for identifying certain types of numbers:
-
isFinite()
determines if a value represents a finite number (notInfinity
or-Infinity
) -
isNaN()
determines if a value isNaN
(sinceNaN
is the only value that is not equal to itself)
Although intended to work with numbers, these methods are capable of inferring a numeric value from and value that is passed in. That both methods can return incorrect results when passed a value that isn’t a number. For example:
console
.
log
(
isFinite
(
25
));
// true
console
.
log
(
isFinite
(
"25"
));
// true
console
.
log
(
isNaN
(
NaN
));
// true
console
.
log
(
isNaN
(
"NaN"
));
// true
Both isFinite()
and isNaN()
pass their arguments through Number()
to get a numeric value and then perform their comparisons on that numeric value rather than the original. This confusing outcome can lead to errors when value types are not checked before being used with one of these methods.
ECMAScript 6 adds two new methods that perform the same comparison but only for number values: Number.isFinite()
and Number.isNaN()
. These methods always return false
when passed a non-number value and return the same values as their global counterparts when passed a number value:
console
.
log
(
isFinite
(
25
));
// true
console
.
log
(
isFinite
(
"25"
));
// true
console
.
log
(
Number
.
isFinite
(
25
));
// true
console
.
log
(
Number
.
isFinite
(
"25"
));
// false
console
.
log
(
isNaN
(
NaN
));
// true
console
.
log
(
isNaN
(
"NaN"
));
// true
console
.
log
(
Number
.
isNaN
(
NaN
));
// true
console
.
log
(
Number
.
isNaN
(
"NaN"
));
// false
In this code, Number.isFinite("25")
returns false
even though isFinite("25")
returns true
; likewise Number.isNaN("NaN") returns
false even though
isNaN(“NaN”) returns
true`.
These two new methods are aimed at eliminating certain types of errors that can be caused when non-number values are used with isFinite()
and isNaN()
without dramatically changing the language.
parseInt() and parseFloat()
The global functions parseInt()
and parseFloat()
now also reside at Number.parseInt()
and Number.parseFloat()
. These functions behave exactly the same as the global functions of the same name. The only purpose in making this move is to categorize purely global functions that clearly relate to a specific data type. Since these functions both create numbers from strings, they are now on Number
along with the other functions that relate to numbers.
Working with Integers
A lot of confusion has been caused over the years related to JavaScript’s single number type that is used to represent both integers and floats. The language goes through great pains to ensure that developers don’t need to worry about the details, but problems still leak through from time to time. ECMAScript 6 seeks to address this by making it easier to identify and work with integers.
Identifying Integers
The first addition is Number.isInteger()
, which allows you to determine if a value represents an integer in JavaScript. Since integers and floats are stored differently, the JavaScript engine looks at the underlying representation of the value to make this determination. That means numbers that look like floats might actually be stored as integers and therefore return true
from Number.isInteger()
. For example:
console
.
log
(
Number
.
isInteger
(
25
));
// true
console
.
log
(
Number
.
isInteger
(
25.0
));
// true
console
.
log
(
Number
.
isInteger
(
25.1
));
// false
In this code, Number.isInteger()
returns true
for both 25
and 25.0
even though the latter looks like a float. Simply adding a decimal point to a number doesn’t automatically make it a float in JavaScript. Since 25.0
is really just 25
, it is stored as an integer. The number 25.1
, however, is stored as a float because there is a fraction value.
Safe Integers
However, all is not so simple with integers. JavaScript can only accurately represent integers between -253 and 253, and outside of this “safe” range, binary representations end up reused for multiple numeric values. For example:
console
.
log
(
Math
.
pow
(
2
,
53
));
// 9007199254740992
console
.
log
(
Math
.
pow
(
2
,
53
)
+
1
);
// 9007199254740992
This example doesn’t contain a typo, two different numbers end up represented by the same JavaScript integer. The effect becomes more prevalent the further the value is outside of the safe range.
ECMAScript 6 introduces Number.isSafeInteger()
to better identify integers that can accurately be represented in the language. There is also Number.MAX_SAFE_INTEGER
and Number.MIN_SAFE_INTEGER
that represent the upper and lower bounds of the same range, respectively. The Number.isSafeInteger()
method ensures that a value is an integer and falls within the safe range of integer values:
var
inside
=
Number
.
MAX_SAFE_INTEGER
,
outside
=
inside
+
1
;
console
.
log
(
Number
.
isInteger
(
inside
));
// true
console
.
log
(
Number
.
isSafeInteger
(
inside
));
// true
console
.
log
(
Number
.
isInteger
(
outside
));
// true
console
.
log
(
Number
.
isSafeInteger
(
outside
));
// false
The number inside
is the largest safe integer, so it returns true
for both Number.isInteger()
and Number.isSafeInteger()
. The number outside
is the first questionable integer value, so it is no longer considered safe even though it’s still an integer.
Most of the time, you only want to deal with safe integers when doing integer arithmetic or comparisons in JavaScript, so it’s a good idea to use Number.isSafeInteger()
as part of input validation.
New Math Methods
The aforementioned new emphasis on gaming and graphics in JavaScript led to the realization that many mathematical calculations could be done more efficiently by a JavaScript engine than with pure JavaScript code. Optimization strategies like asm.js, which works on a subset of JavaScript to improve performance, need more information to perform calculations in the fastest way possible. It’s important, for instance, to know whether the numbers should be treated as 32-bit integers or as 64-bit floats.
As a result, ECMAScript 6 adds several new methods to the Math
object. These new methods are important for improving the speed of common mathematical calculations, and therefore, improving the speed of applications that must perform many calculations (such as graphics programs). The new methods are listed below.
Method | Description |
---|---|
Math.acosh(x) |
Returns the inverse hyperbolic cosine of x . |
Math.asinh(x) |
Returns the inverse hyperbolic sine of x . |
Math.atanh(x) |
Returns the inverse hyperbolic tangent of x
|
Math.cbrt(x) |
Returns the cubed root of x . |
Math.clz32(x) |
Returns the number of leading zero bits in the 32-bit integer representation of x . |
Math.cosh(x) |
Returns the hyperbolic cosine of x . |
Math.expm1(x) |
Returns the result of subtracting 1 from the exponential function of x
|
Math.fround(x) |
Returns the nearest single-precision float of x . |
Math.hypot(...values) |
Returns the square root of the sum of the squares of each argument. |
Math.imul(x, y) |
Returns the result of performing true 32-bit multiplication of the two arguments. |
Math.log1p(x) |
Returns the natural logarithm of 1 + x . |
Math.log10(x) |
Returns the base 10 logarithm of x . |
Math.log2(x) |
Returns the base 2 logarithm of x . |
Math.sign(x) |
Returns -1 if the x is negative 0 if x is +0 or -0, or 1 if x is positive. |
Math.sinh(x) |
Returns the hyperbolic sine of x . |
Math.tanh(x) |
Returns the hyperbolic tangent of x . |
Math.trunc(x) |
Removes fraction digits from a float and returns an integer. |
It’s beyond the scope of this book to explain each new method and what it does in detail. However, if you are looking for a reasonably common calculation, be sure to check the new Math
methods before implementing it yourself.
Summary
ECMAScript 6 makes a lot of changes, both large and small, to JavaScript. Some of the smaller changes detailed in this chapter will likely be overlooked by many but they are just as important to the evolution of the language as the big changes.
Full Unicode support allows JavaScript to start dealing with UTF-16 characters in logical ways. The ability to transfer between code point and character via codePointAt()
and String.fromCodePoint()
is an important step for string manipulation. The addition of the regular expression u
flag makes it possible to operate on code points instead of 16-bit characters, and the normalize()
method allows for more appropriate string comparisons.
Additional methods for working with strings were added, allowing you to more easily identify substrings no matter where they are found, and more functionality was added to regular expressions. The Object.is()
method performs strict equality on any value, effectively becoming a safer version of ===
when dealing with special JavaScript values.
The let
and const
block bindings introduce lexical scoping to JavaScript. These declarations are not hoisted and only exist within the block in which they are declared. That means behavior that is more like other languages and less likely to cause unintentional errors, as variables can now be declared exactly where they are needed. It’s expected that the majority of JavaScript code going forward will use let
and const
exclusively, effectively making var
a deprecated part of the language.
ECMAScript 6 makes it easier to work with numbers through the introduction of new syntax and new methods. The binary and octal literal forms allow you to embed numbers directly into source code while keeping the most appropriate representation visible. There are Number.isFinite()
and Number.isNaN()
that are safer versions of the global methods of the same names due to their lack of type coercion. You can more easily identify integers using Number.isInteger()
and Number.isSafeInteger()
as well as perform a lot more mathematical operations thanks to new methods on Math
.
Though many of these changes are small, they will make a significant difference in the lives of JavaScript developers for years to come. Each change addresses a particular concern that can otherwise requires a lot of custom code to address. By building this functionality into the language, developers can focus on writing the code for their product rather than low-level utilities.
Functions
Functions are an important part of any programming language, and JavaScript functions hadn’t changed much since the language was first introduced. This left a backlog of problems and nuanced behavior that made it easy to make mistakes or require more code just to achieve a very common behavior.
ECMAScript 6 functions made a big leap forward, taking into account years of complaints and asks from JavaScript developers. The result is a number of incremental improvements on top of ECMAScript 5 functions that make programming in JavaScript less error-prone and more powerful than ever before.
Default Parameters
Functions in JavaScript are unique in that they allow any number of parameters to be passed regardless of the number of declared parameters in the function definition. This allows you to define functions that can handle different number of parameters, often by just filling in default values when ones are provided. In ECMAScript 5 and earlier, you would likely use the following pattern to accomplish this:
function
makeRequest
(
url
,
timeout
,
callback
)
{
timeout
=
timeout
||
2000
;
callback
=
callback
||
function
()
{};
// the rest of the function
}
In this example, both timeout
and callback
are actually optional because they are given a default value if not provided. The logical OR operator (||
) always returns the second operand when the first is falsy. Since named function parameters that are not explicitly provided are set to undefined
, the logical OR operator is frequently used to provide default values for missing parameters. There is a flaw with this approach, however, in that a valid value for timeout
might actually be 0
, but this could would replace it with 2000
because 0
is falsy.
Other ways of determining if any parameters are missing include checking arguments.length
for the number of parameters that were passed or directly inspecting each parameter to see if it is not undefined
.
ECMAScript 6 makes it easier to provide default values for parameters by providing initializations that are used when the parameter isn’t formally passed. For example:
function
makeRequest
(
url
,
timeout
=
2000
,
callback
=
function
()
{})
{
// the rest of the function
}
Here, only the first parameter is expected to be passed all the time. The other two parameters have default values, which makes the body of the function much smaller because you don’t need to add any code to check for a missing value. When makeRequest()
is called with all three parameters, then the defaults are not used. For example:
// uses default timeout and callback
makeRequest
(
"/foo"
);
// uses default callback
makeRequest
(
"/foo"
,
500
);
// doesn't use defaults
makeRequest
(
"/foo"
,
500
,
function
(
body
)
{
doSomething
(
body
);
});
Any parameters with a default value are considered to be optional parameters while those without default value are considered to be required parameters.
It’s possible to specify default values for any arguments, including those that appear before arguments without default values. For example, this is fine:
function
makeRequest
(
url
,
timeout
=
2000
,
callback
)
{
// the rest of the function
}
In this case, the default value for timeout
will only be used if there is no second argument passed in or if the second argument is explicitly passed in as undefined
. For example:
// uses default timeout
makeRequest
(
"/foo"
,
undefined
,
function
(
body
)
{
doSomething
(
body
);
});
// uses default timeout
makeRequest
(
"/foo"
);
// doesn't use default timeout
makeRequest
(
"/foo"
,
null
,
function
(
body
)
{
doSomething
(
body
);
});
In the case of default parameter values, the value of null
is considered to be valid and the default value will not be used.
Perhaps the most interesting feature of default parameter arguments is that the default value need not be a primitive value. You can, for example, execute a function to retrieve the default parameter:
function
getCallback
()
{
return
function
()
{
// some code
};
}
function
makeRequest
(
url
,
timeout
=
2000
,
callback
=
getCallback
())
{
// the rest of the function
}
Here, if the last argument isn’t provided, the function getCallback()
is called to retrieve the correct default value. This opens up a lot of interesting possibilities to dynamically inject information into functions.
Rest Parameters
Since JavaScript functions can be passed any number of parameters, it’s not always necessary to define each parameter specifically. Early on, JavaScript provided the arguments
object as a way of inspecting all function parameters that were passed without necessarily defining each one individually. While that worked fine in most cases, it can become a little cumbersome to work with. For example:
function
sum
(
first
)
{
let
result
=
first
,
i
=
1
,
len
=
arguments
.
length
;
while
(
i
<
len
)
{
result
+=
arguments
[
i
];
i
++
;
}
return
result
;
}
This function adds together all of the parameters that are passed to it so you can call sum(1)
or sum(1,2,3,4)
and it will still work. There are couple of things to notice about this function. First, it’s not at all obvious that the function is capable of handling more than one parameter. You could add in several more named parameters, but you would always fall short of indicating that this function can take any number of parameters. Second, because the first parameter is named and used directly, you have to start looking in the arguments
object at index 1 instead of starting at index 0. Remembering to use the appropriate indices with arguments
isn’t necessarily difficult, but it’s one more thing to keep track of. ECMAScript 6 introduces rest parameters to help with these issues.
Rest parameters are indicated by three dots (...
) preceding a named parameter. That named parameter then becomes an Array
containing the rest of the parameters (which is why these are called “rest” parameters). For example, sum()
can be rewritten using rest parameters like this:
function
sum
(
first
,
...
numbers
)
{
let
result
=
first
,
i
=
0
,
len
=
numbers
.
length
;
while
(
i
<
len
)
{
result
+=
numbers
[
i
];
i
++
;
}
return
result
;
}
In this version of the function, numbers
is a rest parameter that contains all parameters after the first one (unlike arguments
, which contains all parameters including the first one). That means you can iterate over numbers
from beginning to end without worry. As a bonus, you can tell by looking at the function that it is capable of handling any number of parameters.
The |
The only restriction on rest parameters is that no other named arguments can follow in the function declaration. For example, this causes syntax error:
// Syntax error: Can't have a named parameter after rest parameters
function
sum
(
first
,
...
numbers
,
last
)
{
let
result
=
first
,
i
=
0
,
len
=
numbers
.
length
;
while
(
i
<
len
)
{
result
+=
numbers
[
i
];
i
++
;
}
return
result
;
}
Here, the parameter last
follows the rest parameter numbers
and causes a syntax error.
Rest parameters were designed to replace arguments
in ECMAScript. Originally ECMAScript 4 did away with arguments
and added rest parameters to allow for an unlimited number of arguments to be passed to functions. Even though ECMAScript 4 never came into being, the idea was kept around and reintroduced in ECMAScript 6 despite arguments
not being removed from the language.
Destructured Parameters
In Chapter 1, you learned about destructuring assignment. Destructuring can also be used outside of the context of an assignment expression and perhaps the most interesting such case is with destructured parameters.
It’s common for functions that take a large number of optional parameters to use an options object as one or more parameters. For example:
function
setCookie
(
name
,
value
,
options
)
{
options
=
options
||
{};
var
secure
=
options
.
secure
,
path
=
options
.
path
,
domain
=
options
.
domain
,
expires
=
options
.
expires
;
// ...
}
setCookie
(
"type"
,
"js"
,
{
secure
:
true
,
expires
:
60000
});
There are many setCookie()
functions in JavaScript libraries that look similar to this. The name
and value
are required but everything else is not. And since there is no priority order for the other data, it makes sense to have an options object with named properties rather than extra named properties. This approach is okay, although it makes the expect input for the function a bit opaque.
Using destructured parameters, the previous function can be rewritten as follows:
function
setCookie
(
name
,
value
,
{
secure
,
path
,
domain
,
expires
})
{
// ...
}
setCookie
(
"type"
,
"js"
,
{
secure
:
true
,
expires
:
60000
});
The behavior of this function is similar to the previous example, the biggest difference is the third argument uses destructuring to pull out the necessary data. Doing so makes it clear which parameters are really expected, and the destructured parameters also act like regular parameters in that they are set to undefined
if they are not passed.
One quirk of this pattern is that the destructured parameters throw an error when the argument isn’t provided. If setCookie()
is called with just two arguments, it results in a runtime error:
// Error!
setCookie
(
"type"
,
"js"
);
This code throws an error because the third argument is missing (undefined
). To understand why this is an error, it helps to understand that destructured parameters are really just a shorthand for destructured assignment. The JavaScript engine is actually doing this:
function
setCookie
(
name
,
value
,
options
)
{
var
{
secure
,
path
,
domain
,
expires
}
=
options
;
// ...
}
Since destructuring assignment throws an error when the right side expression evaluates to null
or undefined
, the same is true when the third argument isn’t passed.
You can work around this behavior by providing a default value for the destructured parameter:
function
setCookie
(
name
,
value
,
{
secure
,
path
,
domain
,
expires
}
=
{})
{
// ...
}
This example now works exactly the same as the first example in this section. Providing the default value for the destructured parameter means that secure
, path
, domain
, and expires
will all be undefined
if the third argument to setCookie()
isn’t provided.
It’s recommended to always provide the default value for destructured parameters to avoid all errors that are unique to their usage. |
The Spread Operator
Closely related to rest parameters is the spread operator. Whereas rest parameters allow you to specify multiple independent arguments should be combined into an array, the spread operator allows you to specify an array that should be be split and have its items passed in as separate arguments to a function. Consider the Math.max()
method, which accepts any number of arguments and returns the one with the highest value. It’s basic usage is as follows:
let
value1
=
25
,
value2
=
50
;
console
.
log
(
Math
.
max
(
value1
,
value2
));
// 50
When you’re dealing with just two values, as in this example, Math.max()
is very easy to use. The two values are passed in and the higher value is returned. But what if you have been tracking values in an array, and now you want to find the highest value? The Math.max()
method doesn’t allow you to pass in an array, so in ECMAScript 5 and earlier, you’d be stuck either searching the array yourself or using apply()
:
let
values
=
[
25
,
50
,
75
,
100
]
console
.
log
(
Math
.
max
.
apply
(
Math
,
values
));
// 100
While possible, using apply()
in this manner is a bit confusing - it actually seems to obfuscate the true meaning of the code with additional syntax.
The ECMAScript 6 spread operator makes this case very simple. Instead of calling apply()
, you can pass in the array and prefix it with the same ...
pattern that is used with rest parameters. The JavaScript engine then splits up the array into individual arguments and passes them in:
let
values
=
[
25
,
50
,
75
,
100
]
// equivalent to
// console.log(Math.max(25, 50, 75, 100));
console
.
log
(
Math
.
max
(...
values
));
// 100
Now the call to Math.max()
looks a bit more conventional and avoids the complexity of specifying a this
-binding for a simple mathematical operation.
You can mix and match the spread operator with other arguments as well. Suppose you want the smallest number returned from Math.max()
to be 0 (just in case negative numbers sneak into the array). You can pass that argument separately and still use the spread operator for the other arguments:
let
values
=
[
-
25
,
-
50
,
-
75
,
-
100
]
console
.
log
(
Math
.
max
(...
values
,
0
));
// 0
In this example, the last argument passed to Math.max()
is 0
, which comes after the other arguments are passed in using the spread operator.
The spread operator for argument passing makes using arrays for function arguments much easier. You’ll likely find it to be a suitable replacement for the apply()
method in most circumstances.
The name Property
Identifying functions can be challenging in JavaScript given the various ways a function can be defined. Additionally, the prevalence of anonymous function expressions makes debugging a bit more difficult, often resulting in stack traces that are hard to read and decipher. For these reasons, ECMAScript 6 adds the name
property to all functions.
All functions in an ECMAScript 6 program will have an appropriate value for their name
property while all others will have an empty string. For example:
function
doSomething
()
{
// ...
}
var
doAnotherThing
=
function
()
{
// ...
};
console
.
log
(
doSomething
.
name
);
// "doSomething"
console
.
log
(
doSomethingElse
.
name
);
// "doSomethingElse"
console
.
log
(
doAnotherThing
.
name
);
// "doAnotherThing"
In this code, doSomething()
has a name
property equal to "doSomething"
because it’s a function declaration. The anonymous function expression doAnotherThing()
has a name
of "doAnotherThing"
due to the variable to which it is assigned.
While function declarations and function expressions as in the last example are easy to find an appropiate name for, ECMAScript 6 goes further to ensure that all functions have appropriate names:
var
doSomething
=
function
doSomethingElse
()
{
// ...
};
var
person
=
{
get
firstName
()
{
return
"Nicholas"
},
sayName
:
function
()
{
console
.
log
(
this
.
name
);
}
}
console
.
log
(
doSomething
.
name
);
// "doSomethingElse"
console
.
log
(
person
.
sayName
.
name
);
// "sayName"
console
.
log
(
person
.
firstName
.
name
);
// "get firstName"
In this example, doSomething.name
is "doSomethingElse"
because the function expression itself has a name and that name takes priority over the variable to which the function was assigned. The name
property of person.sayName()
is "sayName"
, as the value was interpreted from the object literal. Similarly, person.firstName
is actually a getter function, so its name is "get firstName"
to indicate this difference (setter functions are prefixed with "set"
as well).
There are a couple of other special cases for function names. Functions created using bind()
will have their name prefixed with "bound"
and functions created using the Function
constructor have a name of "anonymous"
:
var
doSomething
=
function
()
{
// ...
};
console
.
log
(
doSomething
.
bind
().
name
);
// "bound doSomething"
console
.
log
((
new
Function
()).
name
);
// "anonymous"
The name
of a bound function will always be the name
of the function being bound prefixed with the "bound "
, so the bound version of doSomething()
is "bound doSomething"
.
Block-Level Functions
In ECMAScript 3 and earlier, a function declaration occurring inside of a block (a block-level function) was technically a syntax error, but many browsers still supported it. Unfortunately, each browser that allowed the syntax behaved in a slightly different way, so it is considered a best practice to avoid function declarations inside of blocks (the best alternative is to use a function expression).
In an attempt to reign in this incompatible behavior, ECMAScript 5 strict mode introduced an error whenever a function declaration was used inside of a block. For example:
"use strict"
;
if
(
true
)
{
// Throws a syntax error in ES5, not so in ES6
function
doSomething
()
{
// ...
}
}
In ECMAScript 5, this code throws a syntax error. In ECMAScript 6, the doSomething()
function is considered a block-level declaration and can be accessed and called within the same block in which it was defined. For example:
"use strict"
;
if
(
true
)
{
console
.
log
(
typeof
doSomething
);
// "function"
function
doSomething
()
{
// ...
}
doSomething
();
}
console
.
log
(
typeof
doSomething
);
// "undefined"
Block level functions are hoisted to the top of the block in which they are defined, so typeof doSomething
returns "function"
even though it appears before the function declaration in the code. Once the if
block is finished executing, doSomething()
no longer exists.
Block level functions are a similar to let
function expressions in that the function definition is removed once execution flows out of the block in which it’s defined. The key difference is that block level functions are hoisted to the top of the containing block while let
function expressions are not hoisted. For example:
"use strict"
;
if
(
true
)
{
console
.
log
(
typeof
doSomething
);
// throws error
let
doSomething
=
function
()
{
// ...
}
doSomething
();
}
console
.
log
(
typeof
doSomething
);
Here, code execution stops when typeof doSomething
is executed because the let
statement hasn’t been executed yet.
Whether you want to use block level functions or let
expressions depends on whether or not you want the hoisting behavior.
ECMAScript 6 also allows block-level functions in nonstrict mode, but the behavior is slightly different. Instead of hoisting these declarations to the top of the block, they are hoisted all the way to the containing function or global environment. For example:
// ECMAScript 6 behavior
if
(
true
)
{
console
.
log
(
typeof
doSomething
);
// "function"
function
doSomething
()
{
// ...
}
doSomething
();
}
console
.
log
(
typeof
doSomething
);
// "function"
In this example, doSomething()
is hoisted into the global scope so that it still exists outside of the if
block. ECMAScript 6 standardized this behavior to remove the incompatible browser behaviors that previously existed. ECMAScript 6 runtimes will all behave in the same way.
Arrow Functions
One of the most interesting new parts of ECMAScript 6 are arrow functions. Arrow functions are, as the name suggests, functions defined with a new syntax that uses an “arrow” (=>
). However, arrow functions behave differently than traditional JavaScript functions in a number of important ways:
-
Lexical
this
binding - The value ofthis
inside of the function is determined by where the arrow function is defined not where it is used. -
Not
new
able - Arrow functions cannot be used a constructors and will throw an error when used withnew
. -
Can’t change
this
- The value ofthis
inside of the function can’t be changed, it remains the same value throughout the entire lifecycle of the function. -
No
arguments
object - You can’t access arguments through thearguments
object, you must use named arguments or other ES6 features such as rest arguments.
There are a few reasons why these differences exist. First and foremost, this
binding is a common source of error in JavaScript. It’s very easy to lose track of the this
value inside of a function and can easily result in unintended consequences. Second, by limiting arrow functions to simply executing code with a single this
value, JavaScript engines can more easily optimize these operations (as opposed to regular functions, which might be used as a constructor or otherwise modified).
Arrow functions also have a |
Syntax
The syntax for arrow functions comes in many flavors depending upon what you are trying to accomplish. All variations begin with function arguments, followed by the arrow, followed by the body of the function. Both the arguments and the body can take different forms depending on usage. For example, the following arrow function takes a single argument and simply returns it:
var
reflect
=
value
=>
value
;
// effectively equivalent to:
var
reflect
=
function
(
value
)
{
return
value
;
};
When there is only one argument for an arrow function, that one argument can be used directly without any further syntax. The arrow comes next and the expression to the right of the arrow is evaluated and returned. Even though there is no explicit return
statement, this arrow function will return the first argument that is passed in.
If you are passing in more than one argument, then you must include parentheses around those arguments. For example:
var
sum
=
(
num1
,
num2
)
=>
num1
+
num2
;
// effectively equivalent to:
var
sum
=
function
(
num1
,
num2
)
{
return
num1
+
num2
;
};
The sum()
function simply adds two arguments together and returns the result. The only difference is that the arguments are enclosed in parentheses with a comma separating them (same as traditional functions).
If there are no arguments to the function, then you must include an empty set of parentheses:
var
getName
=
()
=>
"Nicholas"
;
// effectively equivalent to:
var
getName
=
function
()
{
return
"Nicholas"
;
};
When you want to provide a more traditional function body, perhaps consisting of more than one expression, then you need to wrap the function body in braces and explicitly define a return value, such as:
var
sum
=
(
num1
,
num2
)
=>
{
return
num1
+
num2
;
};
// effectively equivalent to:
var
sum
=
function
(
num1
,
num2
)
{
return
num1
+
num2
;
};
You can more or less treat the inside of the curly braces as the same as in a traditional function with the exception that arguments
is not available.
If you want to create a function that does nothing, then you need to include curly braces:
var
doNothing
=
()
=>
{};
// effectively equivalent to:
var
doNothing
=
function
()
{};
Because curly braces are used to denote the function’s body, an arrow function that wants to return an object literal outside of a function body must wrap the literal in parentheses. For example:
var
getTempItem
=
id
=>
({
id
:
id
,
name
:
"Temp"
});
// effectively equivalent to:
var
getTempItem
=
function
(
id
)
{
return
{
id
:
id
,
name
:
"Temp"
};
};
Wrapping the object literal in parentheses signals that the braces are an object literal instead of the function body.
Lexical this Binding
One of the most common areas of error in JavaScript is the binding of this
inside of functions. Since the value of this
can change inside of a single function depending on the context in which it’s called, it’s possible to mistakenly affect one object when you meant to affect another. Consider the following example:
var
PageHandler
=
{
id
:
"123456"
,
init
:
function
()
{
document
.
addEventListener
(
"click"
,
function
(
event
)
{
this
.
doSomething
(
event
.
type
);
// error
},
false
);
},
doSomething
:
function
(
type
)
{
console
.
log
(
"Handling "
+
type
+
" for "
+
this
.
id
);
}
};
In this code, the object PageHandler
is designed to handle interactions on the page. The init()
method is called to set up the interactions and that method in turn assigns an event handler to call this.doSomething()
. However, this code doesn’t work as intended. The call to this.doSomething()
is broken because this
is a reference to the element object (in this case document
) that was the target of the event, instead of being bound to PageHandler
. If you tried to run this code, you will get an error when the event handler fires because this.doSomething()
doesn’t exist on the target document
object.
You can bind the value of this
to PageHandler
explicitly using the bind()
method on the function:
var
PageHandler
=
{
id
:
"123456"
,
init
:
function
()
{
document
.
addEventListener
(
"click"
,
(
function
(
event
)
{
this
.
doSomething
(
event
.
type
);
// no error
}).
bind
(
this
),
false
);
},
doSomething
:
function
(
type
)
{
console
.
log
(
"Handling "
+
type
+
" for "
+
this
.
id
);
}
};
Now the code works as expected, but may look a little bit strange. By calling bind(this)
, you’re actually creating a new function whose this
is bound to the current this
, which is PageHandler
. The code now works as you would expect even though you had to create an extra function to get the job done.
Arrow functions have implicit this
binding, which means that the value of this
inside of an arrow function is always the same as the value of this
in the scope in which the arrow function was defined. For example:
var
PageHandler
=
{
id
:
"123456"
,
init
:
function
()
{
document
.
addEventListener
(
"click"
,
event
=>
this
.
doSomething
(
event
.
type
),
false
);
},
doSomething
:
function
(
type
)
{
console
.
log
(
"Handling "
+
type
+
" for "
+
this
.
id
);
}
};
The event handler in this example is an arrow function that calls this.doSomething()
. The value of this
is the same as it is within init()
, so this version of the example works similarly to the one using bind()
. Even though the doSomething()
method doesn’t return a value, it is still the only statement executed necessary for the function body and so there is no need to include braces.
Arrow functions are designed to be “throwaway” functions and so cannot be used to define new types. This is evident by the missing prototype
property that regular functions have. If you try to use the new
operator with an arrow function, you’ll get an error:
var
MyType
=
()
=>
{},
object
=
new
MyType
();
// error - you can't use arrow functions with 'ne\
w
'
Also, since the this
value is statically bound to the arrow function, you cannot change the value of this
using call()
, apply()
, or bind()
.
The concise syntax for arrow functions makes them ideal for use with array processing. For example, if you want to sort an array using a custom comparator, you typically write something like this:
var
result
=
values
.
sort
(
function
(
a
,
b
)
{
return
a
-
b
;
});
That’s a lot of syntax for a very simple procedure. Compare that to the more terse arrow function version:
var
result
=
values
.
sort
((
a
,
b
)
=>
a
-
b
);
The array methods that accept callback functions such as sort()
, map()
, and reduce()
all can benefit from simpler syntax with arrow functions to change what would appear to be more complex processes into simpler code.
Generally speaking, arrow functions are designed to be used in places where anonymous functions have traditionally been used. They are not really designed to be kept around for long periods of time, hence the inability to use arrow functions as constructors. Arrow functions are best used for callbacks that are passed into other functions, as seen in the examples in this section.
Identifying Arrow Functions
Despite the different syntax, arrow functions are still functions and are identified as such:
var
comparator
=
(
a
,
b
)
=>
a
-
b
;
console
.
log
(
typeof
comparator
);
// "function"
console
.
log
(
comparator
instanceof
Function
);
// true
Both typeof
and instanceof
behave the same with arrow functions as they do with other functions.
Also like other functions, you can still use call()
, apply()
, and bind()
, although the this
-binding of the function will not be affected. Here are some examples:
var
sum
=
(
num1
,
num2
)
=>
num1
+
num2
;
console
.
log
(
sum
.
call
(
null
,
1
,
2
));
// 3
console
.
log
(
sum
.
apply
(
null
,
[
1
,
2
]));
// 3
var
boundSum
=
sum
.
bind
(
null
,
1
,
2
);
console
.
log
(
boundSum
());
// 3
In this example, the sum()
function is called using call()
and apply()
to pass arguments as you would with any function. The bind()
method is used to create boundSum()
, which has its two arguments bound to 1
and 2
so that they don’t need to be passed directly.
Arrow functions are appropriate to use anywhere you’re currently using an anonymous function expression, such as with callbacks.
Summary
Functions haven’t undergone a huge change in ECMAScript 6, but rather, a series of incremental changes that make them easier to work with.
Default function parameters allow you to easily specify what value to use when a particular argument isn’t passed. Prior to ECMAScript 6, this would require some extra code inside of the function to both check for the presence of arguments and assign a different value.
Rest parameters allow you to specify an array into which all remaining parameters should be placed. Using a real array and letting you indicate which parameters to include makes rest parameters a much more flexible solution than arguments
.
Destructured parameters use the destructuring syntax to make options objects more transparent when used as function parameters. The actual data you’re interested in can be listed out along with other named parameters.
The spread operator is a companion to rest parameters, allowing you to destructure an array into separate parameters when calling a function. Prior to ECMAScript 6, the only ways to pass individual parameters that were contained in an array were either manually specifying each parameter or using apply()
. With the spread operator, you can easily pass an array to any function without worrying about the this
binding of the function.
The addition of the name
property helps to more easily identify functions for debugging and evaluation purposes. Additionally, ECMAScript 6 formally defines the behavior of block-level functions so they are no longer a syntax error in strict mode.
The biggest change to functions in ECMAScript 6 was the addition of arrow functions. Arrow functions are designed to be used in places where anonymous function expressions have traditionally been used. Arrow functions have a more concise syntax, lexical this
binding, and no arguments
object. Additionally, arrow functions can’t change their this
binding and so can’t be used as constructors.
Objects
This chapter is a work-in-progress. As such, it may have more typos or content errors than others. |
A lot of ECMAScript 6 focused on improving the utility of objects. The focus makes sense given that nearly every value in JavaScript is represented by some type of object. Additionally, the number of objects used in an average Javascript program continues to increase, meaning that developers are writing more objects all the time. With more objects comes the necessity to use them more effectively.
ECMAScript 6 improves objects in a number of ways, from simple syntax to new ways of manipulating and interacting with objects.
Object Categories
The ECMAScript 6 specification introduced some new terminology to help distinguish between categories of objects. JavaScript has long been littered with a mix of terminology used to describe objects found in the standard as opposed to those that are added by execution environments such as the browser. ECMAScript 6 takes the time to clearly define each category of object, and it’s important to understand this terminology to have a good understanding of the language as a whole. The object categories are:
- Ordinary objects are objects that have all of the default internal behaviors for objects in JavaScript.
- Exotic objects are objects whose internal behavior is different than the default in some way.
-
Standard objects are objects defined by ECMAScript 6, such as
Array
,Date
, etc. Standard objects may be ordinary or exotic. - Built-in objects are objects that are present in a JavaScript execution environment when a script begins to execute. All standard objects are built-in objects.
These terms are used throughout the book to explain the various objects defined by ECMAScript 6.
Object Literal Extensions
One of the most popular patterns in JavaScript is the object literal. It’s the syntax upon which JSON is built and can be seen in nearly every JavaScript file on the Internet. The reason for the popularity is clear: a succinct syntax for creating objects that otherwise would take several lines of code to accomplish. ECMAScript 6 recognized the popularity of the object literal and extends the syntax in several ways to make object literals more powerful and even more succinct.
Property Initializer Shorthand
In ECMAScript 5 and earlier, object literals were simply collections of name-value pairs. That meant there could be some duplication when property values are being initialized. For example:
function
createPerson
(
name
,
age
)
{
return
{
name
:
name
,
age
:
age
};
}
The createPerson()
function creates an object whose property names are the same as the function parameter names. The result is what appears to be duplication of name
and age
even though each represents a different aspect of the process.
In ECMAScript 6, you can eliminate the duplication that exists around property names and local variables by using the property initializer shorthand. When the property name is going to be the same as the local variable name, you can simply include the name without a colon and value. For example, createPerson()
can be rewritten as follows:
function
createPerson
(
name
,
age
)
{
return
{
name
,
age
};
}
When a property in an object literal only has a name and no value, the JavaScript engine looks into the surrounding scope for a variable of the same name. If found, that value is assigned to the same name on the object literal. So in this example, the object literal property name
is assigned the value of the local variable name
.
The purpose of this extension is to make object literal initialization even more succinct than it already was. Assigning a property with the same name as a local variable is a very common pattern in JavaScript and so this extension is a welcome addition.
Method Initializer Shorthand
ECMAScript 6 also improves syntax for assigning methods to object literals. In ECMAScript 5 and earlier, you must specify a name and then the full function definition to add a method to an object. For example:
var
person
=
{
name
:
"Nicholas"
,
sayName
:
function
()
{
console
.
log
(
this
.
name
);
}
};
In ECMAScript 6, the syntax is made more succinct by eliminating the colon and the function
keyword. You can then rewrite the previous example as:
var
person
=
{
name
:
"Nicholas"
,
sayName
()
{
console
.
log
(
this
.
name
);
}
};
This shorthand syntax creates a method on the person
object just as the previous example did. There is no difference aside from saving you some keystrokes, so sayName()
is assigned an anonymous function expression and has all of the same characteristics as the function defined in the previous example.
The |
Computed Property Names
JavaScript objects have long had computed property names through the use of square brackets instead of dot notation. The square brackets allow you to specify property names using variables and string literals that may contain characters that would be a syntax error if used in an identifier. For example:
var
person
=
{},
lastName
=
"last name"
;
person
[
"first name"
]
=
"Nicholas"
;
person
[
lastName
]
=
"Zakas"
;
console
.
log
(
person
[
"first name"
]);
// "Nicholas"
console
.
log
(
person
[
lastName
]);
// "Zakas"
Both of the property names in this example have a space, making it impossible to reference those names using dot notation. However, bracket notation allows any string value to be used as a property name.
In ECMAScript 5, you could use string literals as property names in object literals, such as:
var
person
=
{
"first name"
:
"Nicholas"
};
console
.
log
(
person
[
"first name"
]);
// "Nicholas"
If you could provide the string literal inside of the object literal property definition then you were all set. If, however, the property name was contained in a variable or had to be calculated, then there was no way to define that property using an object literal.
ECMAScript 6 adds computed property names to object literal syntax by using the same square bracket notation that has been used to reference computed property names in object instances. For example:
var
lastName
=
"last name"
;
var
person
=
{
"first name"
:
"Nicholas"
,
[
lastName
]
:
"Zakas"
};
console
.
log
(
person
[
"first name"
]);
// "Nicholas"
console
.
log
(
person
[
lastName
]);
// "Zakas"
The square brackets inside of the object literal indicate that the property name is computed, so its contents are evaluated as a string. That means you can also include expressions such as:
var
suffix
=
" name"
;
var
person
=
{
[
"first"
+
suffix
]
:
"Nicholas"
,
[
"last"
+
suffix
]
:
"Zakas"
};
console
.
log
(
person
[
"first name"
]);
// "Nicholas"
console
.
log
(
person
[
"last name"
]);
// "Zakas"
Anything you would be inside of square brackets while using bracket notation on object instances will also work for computed property names inside of object literals.
Object.assign()
One of the most popular patterns for object composition is mixins, in which one object receives properties and methods from another object. Many JavaScript libraries have a mixin method similar to this:
function
mixin
(
receiver
,
supplier
)
{
Object
.
keys
(
supplier
).
forEach
(
function
(
key
)
{
receiver
[
key
]
=
supplier
[
key
];
});
return
receiver
;
}
The mixin()
function iterates over the own properties of supplier
and copies them onto receiver
. This allows the receiver
to gain new behaviors without inheritance. For example:
function
EventTarget
()
{
/*...*/
}
EventTarget
.
prototype
=
{
constructor
:
EventTarget
,
emit
:
function
()
{
/*...*/
},
on
:
function
()
{
/*...*/
}
}
var
myObject
=
{}
mixin
(
myObject
,
EventTarget
.
prototype
);
myObject
.
emit
(
"somethingChanged"
);
In this example, myObject
receives behavior from EventTarget.prototype
. This gives myObject
the ability to publish events and let others subscribe to them using emit()
and on()
, respectively.
This pattern became popular enough that ECMAScript 6 added Object.assign()
, which behaves the same way. The difference in name is to reflect the actual operation that occurs. Since the mixin()
method uses the assignment operator (=
), it cannot copy accessor properties to the receiver as accessor properties. The name Object.assign()
was chosen to reflect this distinction.
You can use Object.assign()
anywhere the mixin()
function would have been used:
function
EventTarget
()
{
/*...*/
}
EventTarget
.
prototype
=
{
constructor
:
EventTarget
,
emit
:
function
()
{
/*...*/
},
on
:
function
()
{
/*...*/
}
}
var
myObject
=
{}
Object
.
assign
(
myObject
,
EventTarget
.
prototype
);
myObject
.
emit
(
"somethingChanged"
);
The Object.assign()
method accepts any number of suppliers, and the receiver receives the properties in the order in which the suppliers are specified. That means the second supplier might overwrite a value from the first supplier on the receiver. For example:
var
receiver
=
{};
Object
.
assign
(
receiver
,
{
type
:
"js"
,
name
:
"file.js"
},
{
type
:
"css"
}
);
console
.
log
(
receiver
.
type
);
// "css"
console
.
log
(
receiver
.
name
);
// "file.js"
The value of receiver.type
is "css"
because the second supplier overwrote the value of the first.
The Object.assign()
method isn’t a big addition to ECMAScript 6, but it does formalize a common function that is found in many JavaScript libraries.
Duplicate Object Literal Properties
ECMAScript 5 strict mode introduced a check for duplicate object literal properties that would throw an error if a duplicate was found. For example:
var
person
=
{
name
:
"Nicholas"
,
name
:
"Greg"
// syntax error in ES5 strict mode
};
When running in ECMAScript 5 strict mode, this example results in a syntax error on the second name
property.
In ECMAScript 6, the duplicate property check has been removed. This change was made due to the additional complexity of spread arguments in object destructuring. Both strict and nonstrict mode code no longer check for duplicate properties and instead take the last property of the given name as the actual value.
var
person
=
{
name
:
"Nicholas"
,
name
:
"Greg"
// not an error in ES6
};
console
.
log
(
person
.
name
);
// "Greg"
In this example, the value of person.name
is "Greg"
because that was the last value assigned to the property.
proto, Object.setPrototypeOf()
TODO
super
toMethod()
TODO
Reflection Methods
TODO
Object.getOwnPropertyDescriptors()
TODO
Object.getPropertyNames()
TODO
Object.getPropertyDescriptor()
TODO
Summary
TODO
Iterators and Generators
This chapter is a work-in-progress. As such, it may have more typos or content errors than others. |
Iterators have been used in many programming languages as a way to more easily work with collections of data. In ECMAScript 6, JavaScript adds iterators as an important feature of the language. When coupled with new array methods and new types of collections (such as sets and maps), iterators become even more important for efficient processing of data.
What are Iterators?
Iterators are nothing more than objects with a certain interface. That interface consists of a method called next()
that returns a result object. The result object has two properties, value
, which is the next value, and done
, which is a boolean value that’s true
when there are no more values to return. The iterator keeps an internal pointer to a location within a collection of values and, with each call to next()
, returns the next appropriate value.
If you call next()
after the last value has been returned, the method returns done
as true
and value
contains the return value for the iterator. The return value is not considered part of the data set, but rather a final piece of related data or undefined
if no such data exists. (This concept will become clearer in the generators section later in this chapter.)
With that understanding, it’s fairly easy to create an iterator using ECMAScript 5, for example:
function
createIterator
(
items
)
{
let
i
=
0
;
return
{
next
:
function
()
{
let
done
=
(
i
>=
items
.
length
);
let
value
=
!
done
?
items
[
i
++
]
:
undefined
;
return
{
done
:
done
,
value
:
value
};
}
};
}
let
iterator
=
createIterator
([
1
,
2
,
3
]);
console
.
log
(
iterator
.
next
());
// "{ value: 1, done: false }"
console
.
log
(
iterator
.
next
());
// "{ value: 2, done: false }"
console
.
log
(
iterator
.
next
());
// "{ value: 3, done: false }"
console
.
log
(
iterator
.
next
());
// "{ value: undefined, done: true }"
// for all further calls
console
.
log
(
iterator
.
next
());
// "{ value: undefined, done: true }"
The createIterator()
function in this example returns an object with a next()
method. Each time the method is called, the next value in the items
array is returned as value
. When i
is 4, items[i++]
returns undefined
and done
is true
, which fulfills the special last case for iterators in ECMAScript 6.
ECMAScript 6 makes use of iterators in a number of places to make dealing with collections of data easier, so having a good basic understanding allows you to better understand the language as a whole.
for-of
The first place you’ll see iterators in ECMAScript 6 is with the new for-of
loop. The for-of
loop works with iterators to return each successive value. The loop itself calls next()
behind the scenes and exits when the done
property of the returned object is true
. For example:
let
iterator
=
createIterator
([
1
,
2
,
3
]);
for
(
let
i
of
iterator
)
{
console
.
log
(
i
);
}
This code outputs the following:
1
2
3
The for-of
loop in this example is calling iterator.next()
and assigning the variable i
to the value returned on the value
property. So i
is first 1, then 2, and finally 3. When done
is true
, the loop exits, so i
is never assigned the value of undefined
.
The |
Generators
You might be thinking that iterators sound interesting but they look like a bunch of work. Indeed, writing iterators so that they adhere to the correct behavior is a bit difficult, which is why ECMAScript 6 provides generators. A generator is a special kind of function that returns an iterator. Generator functions are indicated by inserting a star character (*
) after the function
keyword (it doesn’t matter if the star is directly next to function
or if there’s some whitespace between them). The yield
keyword is used inside of generators to specify the values that the iterator should return when next()
is called. So if you want to return three different values for each successive call to next()
, you can do so as follows:
For example:
// generator
function
*
createIterator
()
{
yield
1
;
yield
2
;
yield
3
;
}
// generators are called like regular functions but return an iterator
let
iterator
=
createIterator
();
for
(
let
i
of
iterator
)
{
console
.
log
(
i
);
}
This code outputs the following:
1
2
3
In this example, the createIterator()
function is a generator (as indicated by the *
before the name) and it’s called like any other function. The value returned is an object that adheres to the iterator pattern. Multiple yield
statements inside the generator indicate the progression of values that should be returned when next()
is called on the iterator. First, next()
should return 1
, then 2
, and then 3
before the iterator is finished.
Perhaps the most interesting aspect of generator functions is that they stop execution after each yield
statement, so yield 1
executes and then the function doesn’t execute anything else until the iterator’s next()
method is called. At that point, execution resumes with the next statement after yield 1
, which in this case is yield 2
. This ability to stop execution in the middle of a function is extremely powerful and lends to some interesting uses of generator functions (discussed later in this chapter).
The yield
keyword can be used with any value or expression, so you can do interesting things like use yield
inside of a for
loop:
function
*
createIterator
(
items
)
{
for
(
let
i
=
0
;
i
<
items
.
length
;
i
++
)
{
yield
items
[
i
];
}
}
let
iterator
=
createIterator
([
1
,
2
,
3
]);
for
(
let
i
of
iterator
)
{
console
.
log
(
i
);
}
In this example, an array is iterated over, yielding each item as the loop progresses. Each time yield
is encountered, the loop stops, and each time next()
is called on iterator
, the loop picks back up where it left off.
Of course, you can still call iterator.next()
directly:
function
*
createIterator
(
items
)
{
for
(
let
i
=
0
;
i
<
items
.
length
;
i
++
)
{
yield
items
[
i
]
;
}
}
let
iterator
=
createIterator
(
[
1
,
2
,
3
]
);
console
.
log
(
iterator
.
next
());
// "{ value: 1, done: false }"
console
.
log
(
iterator
.
next
());
// "{ value: 2, done: false }"
console
.
log
(
iterator
.
next
());
// "{ value: 3, done: false }"
console
.
log
(
iterator
.
next
());
// "{ value: undefined, done: true }"
// for all further calls
console
.
log
(
iterator
.
next
());
// "{ value: undefined, done: true }"
Of course, collections such as arrays naturally lend themselves to iteration, and that’s why ECMAScript 6 has a number of built-in iterators.
Built-in Iterators
Another way that ECMAScript 6 makes using iterators easier is by making iterators available on many objects by default. You don’t actually need to create your own iterators for many of the built-in types because the language has them already. You only need to create iterators when you find that the built-in ones don’t serve your purpose.
Collection Iterators
The ECMAScript 6 collection objects, arrays, maps, and sets, all have three default iterators to help you navigate data. You can retrieve an iterator for the array by calling one of these methods:
-
entries()
- returns an iterator whose values are a key-value pair. -
values()
- returns an iterator whose values are the values of the collection. -
keys()
- returns an iterator whose values are the keys contained in the collection.
The entries()
iterator actually returns a two-item array where the first item is the key and the second item is the value. For arrays, the first item is the numeric index; for sets, the first item is also the value (since values double as keys in sets). Here are some examples:
let
colors
=
[
"red"
,
"green"
,
"blue"
];
let
tracking
=
new
Set
([
1234
,
5678
,
9012
]);
let
data
=
new
Map
();
data
.
set
(
"title"
,
"Understanding ECMAScript 6"
);
data
.
set
(
"format"
,
"ebook"
);
for
(
let
entry
of
colors
.
entries
())
{
console
.
log
(
entry
);
}
for
(
let
entry
of
tracking
.
entries
())
{
console
.
log
(
entry
);
}
for
(
let
entry
of
data
.
entries
())
{
console
.
log
(
entry
);
}
This example outputs the following:
[0, "red"]
[1, "green"]
[2, "blue"]
[1234, 1234]
[5678, 5678]
[9012, 9012]
["title", "Understanding ECMAScript 6"]
["format", "ebook"]
The values()
iterator simply returns the values as they are stored in the collection. For example:
let
colors
=
[
"red"
,
"green"
,
"blue"
];
let
tracking
=
new
Set
([
1234
,
5678
,
9012
]);
let
data
=
new
Map
();
data
.
set
(
"title"
,
"Understanding ECMAScript 6"
);
data
.
set
(
"format"
,
"ebook"
);
for
(
let
value
of
colors
.
values
())
{
console
.
log
(
value
);
}
for
(
let
value
of
tracking
.
values
())
{
console
.
log
(
value
);
}
for
(
let
value
of
data
.
values
())
{
console
.
log
(
value
);
}
This example outputs the following:
"red"
"green"
"blue"
1234
5678
9012
"Understanding ECMAScript 6"
"ebook"
In this case, using values()
returns the exact data contained in the value
property returned from next()
.
The keys()
iterator returns each key present in the collection. For arrays, this is the numeric keys only (it never returns other own properties of the array); for sets, the keys are the same as the values and so keys()
and values()
return the same iterator.
let
colors
=
[
"red"
,
"green"
,
"blue"
];
let
tracking
=
new
Set
([
1234
,
5678
,
9012
]);
let
data
=
new
Map
();
data
.
set
(
"title"
,
"Understanding ECMAScript 6"
);
data
.
set
(
"format"
,
"ebook"
);
for
(
let
key
of
colors
.
keys
())
{
console
.
log
(
key
);
}
for
(
let
key
of
tracking
.
keys
())
{
console
.
log
(
key
);
}
for
(
let
key
of
data
.
keys
())
{
console
.
log
(
key
);
}
This example outputs the following:
0
1
2
1234
5678
9012
"title"
"format"
Additionally, each collection type has a default iterator that is used by for-of
whenever an iterator isn’t explicitly specified. The default iterator for array and set is values()
while the default iterator for maps is entries()
. This makes it a little bit easier to use collection objects in for-of
:
let
colors
=
[
"red"
,
"green"
,
"blue"
];
let
tracking
=
new
Set
([
1234
,
5678
,
9012
]);
let
data
=
new
Map
();
data
.
set
(
"title"
,
"Understanding ECMAScript 6"
);
data
.
set
(
"format"
,
"ebook"
);
// same as using colors.values()
for
(
let
value
of
colors
)
{
console
.
log
(
value
);
}
// same as using tracking.values()
for
(
let
num
of
tracking
)
{
console
.
log
(
num
);
}
// same as using data.entries()
for
(
let
entry
of
data
)
{
console
.
log
(
entry
);
}
This example outputs the following:
"red"
"green"
"blue"
1234
5678
9012
[
"title"
,
"Understanding ECMAScript 6"
]
[
"format"
,
"ebook"
]
String Iterators
TODO
NodeList Iterators
TODO
Advanced Functionality
TODO
Passing Arguments to Iterators
TODO
Delegating Generators
TODO
Asynchronous Task Scheduling
TODO
Summary
TODO
Symbols
This chapter is a work-in-progress. As such, it may have more typos or content errors than others. |
ECMAScript 6 symbols began as a way to create private object members, a feature JavaScript developers have long wanted. The focus was around creating properties that were not identified by string names. Any property with a string name was easy picking to access regardless of the obscurity of the name. The initial “private names” feature aimed to create non-string property names. That way, normal techniques for detecting these private names wouldn’t work.
The private names proposal eventually evolved into ECMAScript 6 symbols. While the implementation details remained the same (non-string values for property identifiers), TC-39 dropped the requirement that these properties be private. Instead, the properties would be categorized separately, being non-enumerable by default but still discoverable.
Symbols are actually a new kind of primitive value, joining strings, numbers, booleans, null
, and undefined
. They are unique among JavaScript primitives in that they do not have a literal form. The ECMAScript 6 standard uses a special notation to indicate symbols, prefixing the identifier with @@
, such as @@create
. This book uses this same convention for ease of understanding.
Despite the notation, symbols do not exactly map to strings beginning with “@@”. Don’t try to use string values where symbols are required. |
Creating Symbols
You can create a symbol by using the Symbol
function, such as:
var
firstName
=
Symbol
();
var
person
=
{};
person
[
firstName
]
=
"Nicholas"
;
console
.
log
(
person
[
firstName
]);
// "Nicholas"
In this example, the symbol firstName
is created and used to assign a new property on person
. That symbol must be used each time you want to access that same property. It’s a good idea to name the symbol variable appropriately so you can easily tell what the symbol represents.
Because symbols are primitive values, |
The Symbol
function accepts an optional argument that is the description of the symbol. The description itself cannot be used to access the property but is used for debugging purposes. For example:
var
firstName
=
Symbol
(
"first name"
);
var
person
=
{};
person
[
firstName
]
=
"Nicholas"
;
console
.
log
(
"first name"
in
person
);
// false
console
.
log
(
person
[
firstName
]);
// "Nicholas"
console
.
log
(
firstName
);
// "Symbol(first name)"
A symbol’s description is stored internally in a property called [[Description]]
. This property is read whenever the symbol’s toString()
method is called either explicitly or implicitly (as in this example). It is not otherwise possible to access [[Description]]
directly from code. It’s recommended to always provide a description to make both reading and debugging code using symbols easier.
Identifying Symbols
Since symbols are primitive values, you can use the typeof
operator to identify them. ECMAScript 6 extends typeof
to return "symbol"
when used on a symbol. For example:
var
symbol
=
Symbol
(
"test symbol"
);
console
.
log
(
typeof
symbol
);
// "symbol"
While there are other indirect ways of determining whether a variable is a symbol, typeof
is the most accurate and preferred way of doing so.
Using Symbols
You can use symbols anywhere you would use a computed property name. You’ve already seen the use of bracket notation in the previous sections, but you can use symbols in computed object literal property names as well as with Object.defineProperty()
, and Object.defineProperties()
, such as:
var
firstName
=
Symbol
(
"first name"
);
var
person
=
{
[
firstName
]
:
"Nicholas"
};
// make the property read only
Object
.
defineProperty
(
person
,
firstName
,
{
writable
:
false
});
var
lastName
=
Symbol
(
"last name"
);
Object
.
defineProperties
(
person
,
{
[
lastName
]
:
{
value
:
"Zakas"
,
writable
:
false
}
});
console
.
log
(
person
[
firstName
]);
// "Nicholas"
console
.
log
(
person
[
lastName
]);
// "Zakas"
With computer property names in object literals, symbols are very easy to work with.
Sharing Symbols
You may find that you want different parts of your code to use the same symbols. For example, suppose you have two different object types in your application that should use the same symbol property to represent a unique identifier. Keeping track of symbols across files or large codebases can be difficult and error-prone. That’s why ECMAScript 6 provides a global symbol registry that you can access at any point in time.
When you want to create a symbol to be shared, use the Symbol.for()
method instead of calling Symbol()
. The Symbol.for()
method accepts a single parameter, which is a string identifier for the symbol you want to create (this value doubles as the description). For example:
var
uid
=
Symbol
.
for
(
"uid"
);
var
object
=
{};
object
[
uid
]
=
"12345"
;
console
.
log
(
object
[
uid
]);
// "12345"
console
.
log
(
uid
);
// "Symbol(uid)"
The Symbol.for()
method first searches the global symbol registry to see if a symbol with the key "uid"
exists. If so, then it returns the already existing symbol. If no such symbol exists, then a new symbol is created and registered into the global symbol registry using the specified key. The new symbol is then returned. That means subsequent calls to Symbol.for()
using the same key will return the same symbol:
var
uid
=
Symbol
.
for
(
"uid"
);
var
object
=
{
[
uid
]
:
"12345"
};
console
.
log
(
object
[
uid
]);
// "12345"
console
.
log
(
uid
);
// "Symbol(uid)"
var
uid2
=
Symbol
.
for
(
"uid"
);
console
.
log
(
uid
===
uid2
);
// true
console
.
log
(
object
[
uid2
]);
// "12345"
console
.
log
(
uid2
);
// "Symbol(uid)"
In this example, uid
and uid2
contain the same symbol and so they can be used interchangeably. The first call to Symbol.for()
creates the symbol and second call retrieves the symbol from the global symbol registry.
Another unique aspect of shared symbols is that you can retrieve the key associated with a symbol in the global symbol registry by using Symbol.keyFor()
, for example:
var
uid
=
Symbol
.
for
(
"uid"
);
console
.
log
(
Symbol
.
keyFor
(
uid
));
// "uid"
var
uid2
=
Symbol
.
for
(
"uid"
);
console
.
log
(
Symbol
.
keyFor
(
uid2
));
// "uid"
var
uid3
=
Symbol
(
"uid"
);
console
.
log
(
Symbol
.
keyFor
(
uid3
));
// undefined
Notice that both uid
and uid2
return the key "uid"
. The symbol uid3
doesn’t exist in the global symbol registry, so it has no key associated with it and so Symbol.keyFor()
returns undefined
.
The global symbol registry is a shared environment, just like the global scope. That means you can’t make assumptions about what is or is not already present in that environment. You should use namespacing of symbol keys to reduce the likelihood of naming collisions when using third-party components. For example, jQuery might prefix all keys with |
Finding Object Symbols
As with other properties on objects, you can access symbol properties using the Object.getOwnPropertySymbols()
method. This method works exactly the same as Object.getOwnPropertyNames()
except that the returned values are symbols rather than strings. Since symbols technically aren’t property names, they are omitted from the result of Object.getOwnPropertyNames()
.
The return value of Object.getOwnPropertySymbols()
is an array of symbols that represent own properties. For example:
var
uid
=
Symbol
.
for
(
"uid"
);
var
object
=
{
[
uid
]
:
"12345"
};
var
symbols
=
Object
.
getOwnPropertySymbols
(
object
);
console
.
log
(
symbols
.
length
);
// 1
console
.
log
(
symbols
[
0
]);
// "Symbol(uid)"
console
.
log
(
object
[
symbols
[
0
]]);
// "12345"
In this code, object
has a single symbol property. The array returned from Object.getOwnPropertySymbols()
is an array containing just that symbol.
All objects start off with zero own symbol properties (although they do have some inherited symbol properties). |
Well-Known Symbols
In addition to the symbols you defined, there are some predefined symbols as well (called well-known symbols in the specification). These symbols represent common behaviors in JavaScript that were previously considered internal-only operations. Each well-known symbol is represented by a property on Symbol
, such as Symbol.create
for the @@create
symbol.
A central theme for both ECMAScript 5 and ECMAScript 6 was exposing and defining some of the “magic” parts of JavaScript - the parts that couldn’t be emulated by a developer. ECMAScript 6 follows this tradition by exposing even more of the previously internal-only logic of the language. It does so primarily through the use of symbol prototype properties that define the basic behavior of certain objects.
Overwriting a method defined with a well-known symbol changes an ordinary object to an exotic object because this changes some internal default behavior. |
@@toStringTag
Once of the most interesting problems in JavaScript has been the availability of multiple global execution environments. This occurs in web browsers when a page includes an iframe, as the page and the iframe each have their own execution environments. In most cases, this isn’t a problem, as data can be passed back and forth between the environments with little cause for concern. The problem arises when trying to identify what type of an object you’re dealing with.
The canonical example of this is passing an array from the iframe into the containing page or vice-versa. Now in a different execution environment, instanceof Array
returns false
because the array was created with a constructor from a different environment.
Developers soon found a good way to identify arrays. It was discovered that by calling the standard toString()
method on the object, a predictable string was always returned. Thus, many JavaScript libraries began including a function that works similar to this:
function
isArray
(
value
)
{
return
Object
.
prototype
.
toString
.
call
(
value
)
===
"[object Array]"
;
}
console
.
log
(
isArray
([]));
// true
This may look a bit roundabout, but in reality it was found to work quite well in all browsers. The toString()
method on arrays isn’t very useful for this purpose because it returns a string representation of the items it contains. The toString()
method on Object.prototype
, however, had this quirk where it included some internally-defined name in the result. By using this method on an object, you could retrieve what the JavaScript environment thought the data type was.
Developers quickly realized that since there was no way to change this behavior, it was possible to use the same approach to distinguish between native objects and those created by developers. The most important case of this was the ECMAScript 5 JSON
object.
Prior to ECMAScript 5, many used Douglas Crockford’s json2.js
, which created a global JSON
object. As browsers started to implement the JSON
global object, it became necessary to tell whether the global JSON
was provided by the JavaScript environment itself or through some other library. Using the same technique, many created functions like this:
function
supportsNativeJSON
()
{
return
typeof
JSON
!==
"undefined"
&&
Object
.
prototype
.
toString
.
call
(
JSON
)
===
"[object JSON]"
;
}
Here, the same characteristic that allowed developers to identify arrays across iframe boundaries also provided a way to tell if JSON
was the native one or not. A non-native JSON
object would return [object Object]
while the native version returned [object JSON]
. From that point on, this approach became the de facto standard for identifying native objects.
ECMAScript 6 explains this behavior through the @@toStringTag
symbol. This symbol represents a method on each object that defines what value should be produced when Object.prototype.toString.call()
is called on it. So the value returned for arrays is explained by having the @@toStringTag
method return "Array"
. Likewise, you can define that value for your own objects:
function
Person
(
name
)
{
this
.
name
=
name
;
}
Person
.
prototype
[
Symbol
.
toStringTag
]
=
function
()
{
return
"Person"
;
};
var
me
=
new
Person
(
"Nicholas"
);
console
.
log
(
me
.
toString
());
// "[object Person]"
console
.
log
(
Object
.
prototype
.
toString
.
call
(
me
));
// "[object Person]"
In this example, a @@toStringTag
method is defined on Person.prototype
to provide the default behavior for creating a string representation. Since Person.prototype
inherits Object.prototype.toString()
, the value returned from @@toStringTag
is also used when calling me.toString()
. However, you can still define your own toString()
that provides a different behavior without affecting the use of Object.prototype.toString.call()
:
function
Person
(
name
)
{
this
.
name
=
name
;
}
Person
.
prototype
[
Symbol
.
toStringTag
]
=
function
()
{
return
"Person"
;
};
Person
.
prototype
.
toString
=
function
()
{
return
this
.
name
;
};
var
me
=
new
Person
(
"Nicholas"
);
console
.
log
(
me
.
toString
());
// "Nicholas"
console
.
log
(
Object
.
prototype
.
toString
.
call
(
me
));
// "[object Person]"
This code defines Person.prototype.toString()
to return the value of the name
property. Since Person
instances no longer inherit Object.prototype.toString()
, calling me.toString()
exhibits a different behavior.
All objects inherit |
@@toPrimitive
JavaScript frequently attempts to convert objects into primitive values implicitly when certain operations are applied. For instance, when you compare a string to an object using double equals (==
), the object is converted into a primitive value before comparing. Exactly what value should be used was previously an internal operation that is exposed in ECMAScript 6 through the @@toPrimitive
method.
The @@toPrimitive
method is defined on the prototype of each standard type and prescribes the exact behavior. When a primitive conversion is needed, @@toPrimitive
is called with a single argument, referred to as hint
in the specification. The hint
argument is "default"
, specifying that the operation has no preference as to the type, "string"
, indicating a string should be returned, or "number"
, if a number is necessary to perform the operation. Most standard objects treat "default"
as equivalent to "number"
(except for Date
, which treats "default"
as "string"
).
TODO
@@isConcatSpreadable
TODO
@@unscopeables
TODO
Only applied with with
statement object records - does not refer to other scopes.
@@isRegExp
TODO
@@iterator
TODO
@@create
TODO
@@hasInstance
TODO
Summary
TODO
Understanding ECMAScript 6
Minimum price: $14.99
Suggested price: $19.99