The current best practice for block bindings is to use
const
by default and only uselet
when you know a variable’s value needs to change. This ensures a basic level of immutability in code that can help prevent certain types of errors.Any attempt to read a property of
null
orundefined
results in a runtime error.you must always provide an initializer when using array destructuring with
var
,let
, orconst
.Symbols are a new type of primitive value in JavaScript and are used to create properties that can't be accessed without referencing the symbol.
When a primitive conversion is needed,
Symbol.toPrimitive
is called with a single argument, referred to ashint
in the specification. (thehint
argument is filled in by the JavaScript engine)One of the most interesting problems in JavaScript has been the availability of multiple global execution environments.
Changing the string tag for native objects is also possible. Just assign to
Symbol.toStringTag
on the object's prototype. While I recommended not changing built-in objects in this way, there's nothing in the language that forbids doing so.The
with
statement is one of the most controversial parts of JavaScript.A set is a list of values that cannot contain duplicates. (The only exception is that
-0
and+0
are considered to be the same.)A map is a collection of keys that correspond to specific values.
All object properties must be strings.
JavaScript has the
in
operator that returnstrue
if a property exists in an object without reading the value of the object. However, thein
operator also searches the prototype of an object, which makes it only safe to use when an object has anull
prototype.the spread operator (
...
) as a way to split items in an array into separate function parameters. You can also use the spread operator to work on iterable objects, such as sets, to convert them into arrays.ECMAScript 6 also includes weak sets, which only store weak object references and cannot store primitive values. A weak reference to an object does not prevent garbage collection if it is the only remaining reference.
Weak sets are created using the
WeakSet
constructor and have anadd()
method, ahas()
method, and adelete()
method.In general, if you only need to track object references, then you should use a weak set instead of a regular set.
Weak maps are to maps what weak sets are to sets: they're a way to store weak object references.
The most useful place to employ weak maps is when creating an object related to a particular DOM element in a web page.
In ECMAScript 5, it's possible to get close to having truly private data, by creating an object using a pattern such as this:
/* I often use it like this >.< */
var Person = (function() {
var privateData = {},
privateId = 0;
function Person(name) {
Object.defineProperty(this, "_id", { value: privateId++ });
privateData[this._id] = {
name: name
};
}
Person.prototype.getName = function() {
return privateData[this._id].name;
};
return Person;
}());
/*
This example wraps the definition of `Person`
in an IIFE that contains two private variables,
`privateData` and `privateId`.
The big problem with this approach is that the
data in `privateData` never disappears because
there is no way to know when an object instance
is destroyed; the `privateData` object will always
contain extra data. This problem can be solved
by using a weak map instead, as follows:
*/
let Person = (function() {
let privateData = new WeakMap();
function Person(name) {
privateData.set(this, { name: name });
}
Person.prototype.getName = function() {
return privateData.get(this).name;
};
return Person;
}());
/*
This version of the `Person` example uses a weak map
for the private data instead of an object. Because the
`Person` object instance itself can be used as a key,
there's no need to keep track of a separate `ID`. When
the `Person` constructor is called, a new entry is made
into the weak map with a key of this and a value of an
object containing private information.
*/
Anytime you're going to use only object keys, then the best choice is a weak map. That will allow you to optimize memory usage and avoid memory leaks by ensuring that extra data isn't kept around after it's no longer accessible.
Iterators are just objects with a specific interface designed for iteration.
A generator is a function that returns an iterator.
Perhaps the most interesting aspect of generator functions is that they stop execution after each
yield
statement. This ability to stop execution in the middle of a function is extremely powerful and leads to some interesting uses of generator functions.Creating an arrow function that is also a generator is not possible.
All iterators created by generators are also iterables, as generators assign the
Symbol.iterator
property by default.A
for-of
loop callsnext()
on an iterable each time the loop executes and stores thevalue
from the result object in a variable.The
for-of
statement will throw an error when used on, a non-iterable object,null
, orundefined
.For sets, the keys are the same as the values, and so
keys()
andvalues()
return the same iterator. For maps, thekeys()
iterator returns each unique key.Generator delegation also lets you make further use of generator return values.
A lot of the excitement around generators is directly related to asynchronous programming. Asynchronous programming in JavaScript is a double-edged sword: simple tasks are easy to do asynchronously, while complex tasks become an errand in code organization. Since generators allow you to effectively pause code in the middle of execution, they open up a lot of possibilities related to asynchronous processing.
Because
yield
stops execution and waits for thenext()
method to be called before starting again, you can implement asynchronous calls without managing callbacks.Interestingly, class declarations are just syntactic sugar on top of the existing custom type declarations.
-
Despite the similarities between classes and custom types, there are some important differences to keep in mind:
- Class declarations, unlike function declarations, are not hoisted. Class declarations act like let declarations and so exist in the temporal dead zone until execution reaches the declaration.
- All code inside of class declarations runs in strict mode automatically. There's no way to opt-out of strict mode inside of classes.
- All methods are non-enumerable. This is a significant change from custom types, where you need to use Object.defineProperty() to make a method non-enumerable.
- All methods lack an internal [[Construct]] method and will throw an error if you try to call them with new.
- Calling the class constructor without new throws an error.
- Attempting to overwrite the class name within a class method throws an error.
Classes and functions are similar in that they have two forms: declarations and expressions.
ECMAScript 6 continues this tradition by making classes first-class citizens as well.
Static members are not accessible from instances. You must always access static members from the class directly.
Accepting any type of expression after
extends
offers powerful possibilities, such as dynamically determining what to inherit from.ECMAScript 6 classes start out as syntactic sugar for the classical inheritance model of ECMAScript 5, but add a lot of features to reduce mistakes.
To make JavaScript arrays easier to create, ECMAScript 6 adds the
Array.of()
andArray.from()
methods.When the
Array
constructor is passed a single numeric value, the length property of the array is set to that value. If a single non-numeric value is passed, then that value becomes the one and only item in the array. If multiple values are passed (numeric or not), then those values become items in the array.To create an array with the
Array.of()
method, just pass it the values you want in your array.slice()
needs only numeric indices and alength
property to function correctly, any array-like object will work.The
Array.from()
call creates a new array based on the items inarguments
.If the mapping function is on an object, you can also optionally pass a third argument to
Array.from()
that represents thethis
value for the mapping function.The
Array.from()
method works on both array-like objects and iterables. That means the method can convert any object with aSymbol.iterator
property into an array.If you represent a number that fits in an int8 as a normal JavaScript number, you'll waste 56 bits.
Using bits more efficiently is one of the use cases typed arrays address.
-
Typed arrays allow the storage and manipulation of eight different numeric types:
- Signed 8-bit integer (int8)
- Unsigned 8-bit integer (uint8)
- Signed 16-bit integer (int16)
- Unsigned 16-bit integer (uint16)
- Signed 32-bit integer (int32)
- Unsigned 32-bit integer (uint32)
- 32-bit float (float32)
- 64-bit float (float64)
The foundation for all typed arrays is an array buffer, which is a memory location that can contain a specified number of bytes.
Creating an array buffer is akin to calling
malloc()
in C to allocate memory without specifying what the memory block contains.An array buffer always represents the exact number of bytes specified when it was created. You can change the data contained within an array buffer, but never the size of the array buffer itself.
Array buffers represent memory locations, and views are the interfaces you'll use to manipulate that memory.
Of course, reading information about memory isn't very useful on its own. You need to write data into and read data out of that memory to get any benefit.
Little-endian means the least significant byte is at byte 0, instead of in the last byte.
Each typed array is made up of a number of elements, and the element size is the number of bytes each element represents.
Unlike regular arrays, you cannot change the size of a typed array using the
length
property. Thelength
property is not writable. (You cannot assign a value to a nonexistent numeric index in a typed array like you can with regular arrays, as typed arrays ignore the operation.)The most importance difference between typed arrays and regular arrays is that typed arrays are not regular arrays. Typed arrays don't inherit from
Array
andArray.isArray()
returnsfalse
when passed a typed array.Zero is used in place of any invalid values. (Of course, strings are invalid data types in typed arrays, so the value is inserted as
0
instead. )Finally, typed arrays methods have two methods not present on regular arrays: the
set()
andsubarray()
methods.Typed arrays are not technically arrays, as they do not inherit from
Array
, but they do look and behave a lot like arrays. Typed arrays contain one of eight different numeric data types and are built uponArrayBuffer
objects that represent the underlying bits of a number or series of numbers.Typed arrays are a more efficient way of doing bitwise arithmetic because the values are not converted back and forth between formats, as is the case with the JavaScript number type.
One of the most powerful aspects of JavaScript is how easily it handles asynchronous programming.
A promise specifies some code to be executed later (as with events and callbacks) and also explicitly indicates whether the code succeeded or failed at its job. You can chain promises together based on success or failure in ways that make your code easier to understand and debug.
JavaScript engines are built on the concept of a single-threaded event loop. Single-threaded means that only one piece of code is ever executed at a time.
JavaScript engines can only execute one piece of code at a time, so they need to keep track of code that is meant to run. That code is kept in a job queue.
The event loop is a process inside the JavaScript engine that monitors code execution and manages the job queue. Keep in mind that as a queue, job execution runs from the first job in the queue to the last.
Callback hell occurs when you nest too many callbacks.
A promise is a placeholder for the result of an asynchronous operation.
Each promise goes through a short lifecycle starting in the pending state, which indicates that the asynchronous operation hasn't completed yet.
An internal
[[PromiseState]]
property is set to"pending"
,"fulfilled"
, or"rejected"
to reflect the promise's state. This property isn't exposed on promise objects, so you can't determine which state the promise is in programmatically.Any object that implements the
then()
method in this way is called a thenable. All promises are thenables, but not all thenables are promises.A fulfillment or rejection handler will still be executed even if it is added to the job queue after the promise is already settled.
When either
resolve()
orreject()
is called inside the executor, a job is added to the job queue to resolve the promise. This is called job scheduling, and if you've ever used thesetTimeout()
orsetInterval()
functions, then you're already familiar with it.The promise executor executes immediately, before anything that appears after it in the source code.
Calling
resolve()
triggers an asynchronous operation. Functions passed tothen()
andcatch()
are executed asynchronously, as these are also added to the job queue.Fulfillment and rejection handlers are always added to the end of the job queue after the executor has completed.
The
Promise.resolve()
method accepts a single argument and returns a promise in the fulfilled state.You can also create rejected promises by using the
Promise.reject()
method.When you're unsure if an object is a promise, passing the object through
Promise.resolve()
orPromise.reject()
(depending on your anticipated result) is the best way to find out because promises just pass through unchanged.One of the most controversial aspects of promises is the silent failure that occurs when a promise is rejected without a rejection handler.
To properly track potentially unhandled rejections, use the
rejectionHandled
andunhandledRejection
events to keep a list of potentially unhandled rejections. Then wait some period of time to inspect the list.Always have a rejection handler at the end of a promise chain to ensure that you can properly handle any errors that may occur.
Another important aspect of promise chains is the ability to pass data from one promise to the next.
The promises passed to
Promise.race()
are truly in a race to see which is settled first. If the first promise to settle is fulfilled, then the returned promise is fulfilled; if the first promise to settle is rejected, then the returned promise is rejected.That means both synchronous and asynchronous methods work correctly when called using
yield
, and you never have to check that the return value is a promise.The only concern is ensuring that asynchronous functions like
readFile()
return a promise that correctly identifies its state.Work is progressing on an
await
syntax that would closely mirror the promise-based example in the preceding section. The basic idea is to use a function marked withasync
instead of a generator and useawait
instead ofyield
when calling a function.The
async
keyword beforefunction
indicates that the function is meant to run in an asynchronous manner. Theawait
keyword signals that the function call toreadFile("config.json")
should return a promise, and if it doesn't, the response should be wrapped in a promise.Promises have three states: pending, fulfilled, and rejected.
You can create a proxy to use in place of another object (called the target) by calling
new Proxy()
. The proxy virtualizes the target so that the proxy and the target appear to be the same object to functionality using the proxy.The reflection API, represented by the
Reflect
object, is a collection of methods that provide the default behavior for the same low-level operations that proxies can override. There is aReflect
method for every proxy trap.Proxy traps in JavaScript
Proxy Trap | Overrides the Behavior Of | Default Behavior |
---|---|---|
get | Reading a property value | Reflect.get() |
set | Writing to a property | Reflect.set() |
has | The in operator | Reflect.has() |
deleteProperty | The delete operator | Reflect.deleteProperty() |
getPrototypeOf | Object.getPrototypeOf() | Reflect.getPrototypeOf() |
setPrototypeOf | Object.setPrototypeOf() | Reflect.setPrototypeOf() |
isExtensible | Object.isExtensible() | Reflect.isExtensible() |
preventExtensions | Object.preventExtensions() | Reflect.preventExtensions() |
getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor() | Reflect.getOwnPropertyDescriptor() |
defineProperty | Object.defineProperty() | Reflect.defineProperty |
ownKeys | Object.keys, Object.getOwnPropertyNames(), Object.getOwnPropertySymbols() | Reflect.ownKeys() |
apply | Calling a function | Reflect.apply() |
construct | Calling a function with new | Reflect.construct() |
Each trap overrides some built-in behavior of JavaScript objects, allowing you to intercept and modify the behavior.
To validate the values of properties, you'd use the
set
trap and inspect thevalue
that is passed in.An object shape is the collection of properties and methods available on the object. JavaScript engines use object shapes to optimize code, often creating classes to represent the objects.
The
has
trap is called whenever thein
operator is used.The
delete
operator removes a property from an object and returnstrue
when successful andfalse
when unsuccessful.First, the
getPrototypeOf
trap must return an object ornull
, and any other return value results in a runtime error.One of the most important features of ECMAScript 5 was the ability to define property attributes using the
Object.defineProperty()
method.Proxies let you intercept calls to
Object.defineProperty()
andObject.getOwnPropertyDescriptor()
using thedefineProperty
andgetOwnPropertyDescriptor
traps, respectively.You can also have
Object.defineProperty()
silently fail by returning true and not calling theReflect.defineProperty()
method.No matter what object is passed as the third argument to the
Object.defineProperty()
method, only the propertiesenumerable
,configurable
,value
,writable
,get
, andset
will be on the descriptor object passed to thedefineProperty
trap.The
Object.defineProperty()
andReflect.defineProperty()
methods are exactly the same except for their return values. TheObject.defineProperty()
method returns the first argument, whileReflect.defineProperty()
returnstrue
if the operation succeeded andfalse
if not.The
ownKeys
proxy trap intercepts the internal method[[OwnPropertyKeys]]
and allows you to override that behavior by returning an array of values.The
ownKeys
trap receives a single argument, the target, and must always return an array or array-like object; otherwise, an error is thrown.The
ownKeys
trap also affects thefor-in
loop, which calls the trap to determine which keys to use inside of the loop.Of all the proxy traps, only
apply
andconstruct
require the proxy target to be a function.Creating callable class constructors is something that is only possible using proxies.
A String property name
P
is an array index if and only ifToString(ToUint32(P))
is equal toP
andToUint32(P)
is not equal to 232-1.The simplest way to create a class that uses a proxy is to define the class as usual and then return a proxy from the constructor.
When the internal
[[Get]]
method is called to read a property, the operation looks for own properties first. If an own property with the given name isn't found, then the operation continues to the prototype and looks for a property there. The process continues until there are no further prototypes to check.The internal
[[Set]]
method also checks for own properties and then continues to the prototype if needed.The
has
trap is therefore only called when the search reaches the proxy object in the prototype chain. When using a proxy as a prototype, that only happens when there's no own property of the given name.Even though it takes a little bit of extra code to create a class with a proxy in its prototype chain, it can be worth the effort if you need such functionality.
Only the
get
,set
, andhas
proxy traps will ever be called on a proxy when it's used as a prototype, making the set of use cases much smaller.JavaScript's "shared everything" approach to loading code is one of the most error-prone and confusing aspects of the language.
One goal of ECMAScript 6 was to solve the scope problem and bring some order to JavaScript applications. That's where modules come in.
-
Modules are JavaScript files that are loaded in a different mode (as opposed to scripts, which are loaded in the original way JavaScript worked). This different mode is necessary because modules have very different semantics than scripts:
- Module code automatically runs in strict mode, and there's no way to opt-out of strict mode.
- Variables created in the top level of a module aren't automatically added to the shared global scope. They exist only within the top-level scope of the module.
- The value of
this
in the top level of a module isundefined
. - Modules don't allow HTML-style comments within code (a leftover feature from JavaScript's early browser days).
- Modules must export anything that should be available to code outside of the module.
- Modules may import bindings from other modules.
The real power of modules is the ability to export and import only bindings you need, rather than everything in a file.
You can use the
export
keyword to expose parts of published code to other modules.Each exported function or class also has a name; that's because exported function and class declarations require a name. You can't export anonymous functions or classes using this syntax unless you use the
default
keyword.Once you have a module with exports, you can access the functionality in another module by using the
import
keyword.The list of bindings to import looks similar to a destructured object, but it isn't one.
When importing a binding from a module, the binding acts as if it were defined using
const
.namespace import
Keep in mind, however, that no matter how many times you use a module in
import
statements, the module will only be executed once. After the code to import the module executes, the instantiated module is kept in memory and reused whenever anotherimport
statement references it.Imports without bindings are most likely to be used to create polyfills and shims.