Skip to content

Instantly share code, notes, and snippets.

@jonnyjava
Created November 8, 2019 23:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jonnyjava/acacff99e7fbcf0125b9298a1f062ae3 to your computer and use it in GitHub Desktop.
Save jonnyjava/acacff99e7fbcf0125b9298a1f062ae3 to your computer and use it in GitHub Desktop.

Understanding ECMAScript 6

1 Block Bindings

Variable declarations using var are treated as if they are at the top of the function (or in the global scope, if declared outside of a function) regardless of where the actual declaration occurs; this is called hoisting.

Block-level declarations declare bindings that are inaccessible outside a given block scope. Block scopes, also called lexical scopes, are created in the following places:

  • Inside a function
  • Inside a block (indicated by the { and } characters)

let declarations are not hoisted to the top of the enclosing block; use let to declare a variable but limit the variable’s scope to only the current code block. If an identifier has already been defined in a scope, using the identifier in a let declaration inside that scope causes an error to be thrown. Bindings declared using const are considered constants, meaning their values cannot be changed once set. For this reason, every const binding must be initialized on declaration. Constants, like let declarations, are block-level declarations. That means constants are no longer accessible once execution flows out of the block in which they were declared, and declarations are not hoisted. Const declaration throws an error when made with an identifier for an already defined variable in the same scope. Attempting to assign a constto a previously defined constant will throw an error, However the value a constant holds can be modified if it is an object.constprevents modification of the binding, not modification of the bound value.Const` declarations for objects don’t prevent modification of those objects.

A variable declared with either let or const cannot be accessed until after the declaration. Attempting to do so results in a reference error, even when using normally safe operations, such as the typeOf operation. The issue is that value exists in what the JavaScript community has dubbed the temporal dead zone (TDZ) Perhaps one area where developers most want block-level scoping of variables is within for loops, where the throwaway counter variable is meant to be used only inside the loop. The characteristics of var have long made creating functions inside loops problematic, because the loop variables are accessible from outside the scope of the loop. To fix this problem, developers use immediately invoked function expressions (IIFEs) inside loops to force a new copy of the variable they want to iterate over to be created. For a normal for loop, you can use const in the initializer, but the loop will throw a warning if you attempt to change the value. On the other hand, when used in a for-in or for-of loop, a const variable behaves similarly to a let variable. The for-in and for-of loops work with const because the loop initializer creates a new binding on each iteration through the loop rather than attempting to modify the value of an existing binding. Another way in which let and const are different from var is in their global scope behavior. When var is used in the global scope, it creates a new global variable, which is a property on the global object (window in browsers). That means you can accidentally overwrite an existing global using var. If you instead use let or const in the global scope, a new binding is created in the global scope but no property is added to the global object. That also means you cannot overwrite a global variable using let or const declarations; you can only shadow it. Use const by default, and only use let when you know a variable’s value needs to change. The rationale is that most variables should not change their value after initialization because unexpected value changes are a source of bugs.

2 Strings and regular expressions

ECMAScript 6 includes the following three methods

  • The includes() method returns true if the given text is found anywhere within the string. It returns false if not.
  • The startsWith() method returns true if the given text is found at the beginning of the string. It returns false if not.
  • The endsWith() method returns true if the given text is found at the end of the string. It returns false if not.

Each method accepts two arguments: the text to search for and an optional index from which to start the search. ECMAScript 6 also adds a repeat() method to strings, which accepts the number of times to repeat the string as an argument. It returns a new string containing the original string repeated the specified number of times.

Template Literals

Template literals are ECMAScript 6’s answer to the following features that JavaScript lacked in ECMAScript 5 and in earlier versions:

  • Multiline strings A formal concept of multiline strings
  • Basic string formatting The ability to substitute parts of the string for values contained in variables
  • HTML escaping The ability to transform a string so it is safe to insert into HTML

ECMAScript 6’s template literals make multiline strings easy because there’s no special syntax. Just include a newline where you want, and it appears in the result. All whitespace inside the backticks is part of the string, so be careful with indentation.

Substitutions are delimited by an opening ${ and a closing } that can have any JavaScript expression inside. The simplest substitutions lets you embed local variables directly into a resulting string. Template literals are also JavaScript expressions, which means you can place a template literal inside another template literal.

Functions

Functions in JavaScript are unique in that they allow any number of parameters to be passed regardless of the number of parameters declared in the function definition. This allows you to define functions that can handle different numbers of parameters, often by just filling in default values when parameters aren’t provided. It’s possible to specify default values for any arguments, including those that appear before arguments without default values in the function declaration. In this case, the default value will be used only if there is no second argument passed in or if the second argument is explicitly passed in as undefined.

The presence of default parameter values triggers the arguments object to remain detached from the named parameters so you can rely on arguments to always reflect the initial call state. Perhaps the most interesting feature of default parameter values is that the default value need not be a primitive value. Because the default value is evaluated only when the function is called, changes to that value can be made at any time. This behavior introduces another useful capability. You can use a previous parameter as the default for a later parameter. The ability to reference parameters from default parameter assignments works only for previous arguments, so earlier arguments don’t have access to later arguments. Function parameters have their own scope and their own TDZ that is separate from the function body scope. That means the default value of a parameter cannot access any variables declared inside the function body.

Rest Parameters

A rest parameter is indicated by three dots (...) preceding a named parameter. That named parameter becomes an Array containing the rest of the parameters passed to the function. The rest parameter contains all parameters passed after object (unlike arguments, which contains all parameters including the first one). That means you can iterate over keys from beginning to end without worry. As a bonus, you can tell by looking at the function that it’s capable of handling any number of parameters. Rest parameters have two restrictions. The first restriction is that there can be only one rest parameter, and the rest parameter must be last.

Spread operator

Whereas rest parameters allow you to specify that multiple independent arguments should be combined into an array, the spread operator allows you to specify an array that should be split and passed in as separate arguments to a function.

Deciding When to Use Block-Level Functions

Block-level functions are 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. Function expressions that use let are not hoisted. ECMAScript 6 also allows block-level functions in non-strict 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.

Arrow Functions

Arrow functions behave differently than traditional JavaScript functions in a number of important ways:

  • The values of this, super, arguments, and new.target inside the function are defined by the closest containing non-arrow function.
  • They cannot be called with new
  • No prototype
  • Can’t change this: the value of this inside the function can’t be changed. It remains the same throughout the entire life cycle of the function.
  • No arguments object: because arrow functions have no arguments binding, you must rely on named and rest parameters to access function arguments.
  • No duplicate named parameters

Arrow Function Syntax

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. If you are passing in more than one argument, you must include parentheses around those arguments. If there are no arguments to the function, you must include an empty set of parentheses in the declaration. When you want to provide a more traditional function body, perhaps consisting of more than one expression, you need to wrap the function body in curly braces and explicitly define a return value.

Curly braces denote the function’s body, which works just fine in the cases you’ve seen so far. But an arrow function that wants to return an object literal outside a function body must wrap the literal in parentheses.

You can accomplish the IIFE using arrow functions, as long as you wrap the arrow function in parentheses.

Arrow functions have no this binding, which means the value of this inside an arrow function can only be determined by looking up the scope chain. If the arrow function is contained within a non-arrow function, this will be the same as the containing function; otherwise, this is undefined. Arrow functions are designed to be “throwaway” functions, and so cannot be used to define new types; this is evident from the missing prototype property, which regular functions have. Also, because the this value is determined by the containing function in which the arrow function is defined, you cannot change the value of this using call(), apply(), or bind(). You can still use call(), apply(), and bind() on arrow functions, although the this binding of the function will not be affected. Even though arrow functions don’t have their own arguments object, it’s possible for them to access the arguments object from a containing function. That arguments object is then available no matter where the arrow function is executed later on.

##Expanded object functionality

  • Ordinary objects Have all the default internal behaviors for objects in JavaScript.
  • Exotic objects Have internal behavior that differs from the default in some way.
  • Standard objects Defined by ECMAScript 6, such as Array, Date, and so on. Standard objects can be ordinary or exotic.
  • Built-in objects Present in a JavaScript execution environment when a script begins to execute. All standard objects are built­in objects.

ECMA6 property initializer shorthand syntax.

When an object property name is the same as the local variable name, you can simply include the name without a colon and value. ECMAScript 6 also improves the syntax for assigning methods to object literals which is made more concise by eliminating the colon and the function keyword. ECMAScript 6 introduces the Object.is() method to remedy the remaining inaccuracies 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’re the same type and have the same value.

The mixin pattern became popular enough that ECMAScript 6 added the Object.assign() method, which behaves the same way, accepting a receiver and any number of suppliers and then returning the receiver. The name change from mixin() to assign() reflects the actual operation that occurs. Because the mixin() function 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. Keep in mind that Object.assign() doesn’t create accessor properties on the receiver when a supplier has accessor properties. Because Object.assign() uses the assignment operator, an accessor property on a supplier will become a data property on the receiver. ECMAScript 6 strictly defines the order in which own properties must be returned when they’re enumerated. The basic order for own property enumeration is:

  1. All numeric keys in ascending order
  2. All string keys in the order in which they were added to the object
  3. All symbol keys in the order in which they were added to the object

Object.setPrototypeOf() method, which allows you to change the prototype of any given object, accepts two arguments: the object whose prototype should change and the object that should become the first argument’s prototype. At its simplest, super is a pointer to the current object’s prototype: the Object.getPrototypeOf(this) value. you can call any method on an object’s prototype by using a super reference, as long as it’s inside a concise method. Attempting to use super outside of concise methods results in a syntax error. ECMAScript 6 formally defines a method as a function that has an internal [[HomeObject]] property containing the object to which the method belong, this becomes very important when using super references.

Destructuring for easier Data access

Object destructuring

Object destructuring syntax uses an object literal on the left side of an assignment operation Note that you must put parentheses around a destructuring assignment statement. The reason is that an opening curly brace is expected to be a block statement, and a block statement cannot appear on the left side of an assignment. A destructuring assignment expression evaluates to the right side of the expression (after the =). That means you can use a destructuring assignment expression anywhere a value is expected. When you use a destructuring assignment statement and you specify a local variable with a property name that doesn’t exist on the object, that local variable is assigned a value of undefined. ECMAScript 6 has an extended syntax that allows you to assign to a local variable with a different name, and that syntax looks like the object literal non-shorthand property initializer syntax. By using syntax similar to that of object literals, you can navigate into a nested object structure to retrieve just the information you want.

Array Destructuring

Unlike object destructuring, there is no need to wrap the expression in parentheses. You can destructure nested arrays in a manner similar to destructuring nested objects. By inserting another array pattern into the overall pattern, the destructuring will descend into a nested array. Rest items use the ... syntax to assign the remaining items in an array to a particular variable. In ECMAScript 6, you can use rest items to clone an array. You can use object and array destructuring together to create more complex expressions. By doing so, you’re able to extract just the pieces of information you want from any mixture of objects and arrays. This approach is particularly useful for pulling values out of JSON configuration structures without navigating the entire structure. Destructuring has one more particularly helpful use case and that is when passing function arguments: when a JavaScript function takes a large number of optional parameters, one common pattern is to create an options object whose properties specify the additional parameters Destructured parameters offer an alternative that makes it clearer what arguments a function expects. A destructured parameter uses an object or array destructuring pattern in place of a named parameter. One quirk of using destructured parameters is that, by default, an error is thrown when they’re not provided in a function call. If you want the destructured parameter to be optional, you can work around this behavior by providing a default value for the destructured parameter. You can specify destructured default values for destructured parameters just as you would in destructured assignment. Just add the equal sign after the parameter and specify the default value.

Symbols and symbol properties

ECMAScript 6 introduces symbols as a primitive type. Symbols are unique among JavaScript primitives in that they don’t have a literal form. You can create a symbol using the global Symbol function, The Symbol function also accepts a description of the symbol as an optional argument. You can use the typeof operator to determine whether a variable contains a symbol. You can use symbols anywhere you would use a computed property name. When you want to create a symbol to be shared, use the Symbol.for() method instead of calling the Symbol() method. The Symbol.for() method accepts a single parameter, which is a string identifier for the symbol you want to create. The Symbol.for() method first searches the global symbol registry to see whether a symbol with the given key exists. If so, the method returns the existing symbol. If no such symbol exists, a new symbol is created and registered to the global symbol registry using the specified key. The new symbol is then returned. Symbols cannot be coerced into strings or numbers to prevent them from being accidentally used as properties that would otherwise be expected to behave as symbols. The return value of Object.getOwnPropertySymbols() is an array of own property symbols

Well-Known Symbols

  • Symbol.hasInstance accepts a single argument: the value to check. It returns true if the value passed is an instance of the function.
  • Symbol.isConcatSpreadable Unlike other well-known symbols, this symbol property doesn’t appear on any standard objects by default. Instead, the symbol is available as a way to augment how concat() works on certain types of objects, effectively short-circuiting the default behavior. You can define any type to behave like arrays do in a concat()
  • Symbol.iterator
  • Symbol.match
  • Symbol.replace
  • Symbol.search
  • Symbol.species
  • Symbol.split
  • Symbol.toPrimitive is defined on the prototype of each standard type and prescribes what should happen when the object is converted into a primitive. When a primitive conversion is needed, Symbol.toPrimitive is called with a single argument, referred to as hint in the specification. The hint argument is one of three string values. If "number" is passed, Symbol.toPrimitive should return a number. If "string" is passed, a string should be returned, and if "default" is passed, the operation has no preference as to the type.
  • Symbol.toStringTag
  • Symbol.unscopables is only for backwars compatibility with with() function.

The Symbol.match, Symbol.replace, Symbol.search, and Symbol.split Properties

The four symbol properties are defined on RegExp.prototype as the default implementation that the string methods should use. Knowing this, you can create an object to use with the string methods in a way that is similar to regular expressions.

Sets and Maps

  • A set is a list of values that cannot contain duplicates
  • A map is a collection of keys that correspond to specific values.

Sets

Sets are created using new Set(), and items are added to a set by calling the add() method. You can see how many items are in a set by checking the size property. You can also initialize a set using an array, and the Set constructor will ensure that only unique values are used. You can test which values are in a set using the has() method. You can remove a single item by using the delete() method, or you can remove all items from the set by calling the clear() method. The forEach() method is passed a callback function that accepts three arguments:

  • The value from the next position in the set
  • The same value as the first argument
  • The set from which the value is read You can also use an arrow function to get the same effect without passing the second argument. Keep in mind that although sets are great for tracking values and forEach() lets you work on each item sequentially, you can’t directly access an item by index like you can with an array. If you need to do so, the best option is to convert the set to an array. Converting a set back to an array is also easy if you use the spread operator (...). The Set type could be called a strong set because of the way it stores object references. Storing an object in an instance of Set is effectively the same as storing that object inside a variable. As long as a reference to that Set instance exists, the object cannot be garbage-collected to free memory. That works fine for most programs, but sometimes it’s best for references in a set to disappear when all other references disappear. To address such issues, ECMAScript 6 also includes weak sets, which only store weak object references and cannot store primitive values. A weak reference to an object doesn’t prevent garbage collection if it’s the only remaining reference. Weak sets are created using the WeakSet constructor and have an add() method, a has() method, and a delete() method. You can also seed a weak set with values by passing an iterable to the constructor. Keep in mind that an error will be thrown if the array contains any nonobject values, because WeakSet can’t accept primitive values. weak sets share some characteristics with regular sets, but there are some key differences:
  • In a WeakSet instance, the add() method, has() method, and delete() method all throw an error when passed a nonobject.
  • Weak sets aren’t iterables and therefore cannot be used in a for-of loop.
  • Weak sets don’t expose any iterators (such as the keys() and values() methods), so there is no way to programmatically determine the contents of a weak set.
  • Weak sets don’t have a forEach() method.
  • Weak sets don’t have a size property.

Maps

The ECMAScript 6 Map type is an ordered list of key-value pairs, where the key and the value can be any type. Key equivalence is determined by calling the Object.is() method, so you can have a key of 5 and a key of "5" because they’re different types. This is quite different from using object properties as keys, because object properties always coerce values into strings. You can add items to maps by calling the set() method and passing it a key and the value to associate with the key. You can later retrieve a value by passing the key to the get() method. You can also use objects as keys, because these keys are not coerced into another form, each object is considered unique. This allows you to associate additional data with an object without modifying the object. These three methods are available on maps and sets:

  • has(key) Determines if the given key exists in the map
  • delete(key) Removes the key and its associated value from the map
  • clear() Removes all keys and values from the map Maps also have a size property that indicates how many key-value pairs it contains. Also similar to sets, you can initialize a map with data by passing an array to the Map constructor. Each item in the array must itself be an array where the first item is the key and the second is that key’s corresponding value. Therefore, the entire map is an array of these two-item arrays. The forEach() method for maps is similar to forEach() for sets and arrays in that it accepts a callback function that receives three arguments:
  • The value from the next position in the map
  • The key for that value
  • The map from which the value is read

These callback arguments more closely match the forEach() behavior in arrays, where the first argument is the value and the second is the key (corresponding to a numeric index in arrays). In weak maps, every key must be an object (an error is thrown if you try to use a nonobject key), and those object references are held weakly so they don’t interfere with garbage collection. When there are no references to a weak map key outside a weak map, the key-value pair is removed from the weak map. But only weak map keys, not weak map values, are weak references. An object stored as a weak map value will prevent garbage collection, even if all other references are removed. The ECMAScript 6 WeakMap type is an unordered list of key-value pairs, where a key must be a non-null object and a value can be of any type. The interface for WeakMap is very similar to that of Map in that set() and get() are used to add and retrieve data. There is no way to verify that a weak map is empty, because it doesn’t have a size property. Weak maps have only two additional methods available to interact with key-value pairs.

  • has() method determines if a given key exists in the map
  • delete() method removes a specific key-value pair. There is no clear() method because that would require enumerating keys, and like weak sets, that isn’t possible with weak maps.

Weak Map Uses and Limitations

When you’re deciding whether to use a weak map or a regular map, the primary decision to consider is whether you want to use only object keys. Anytime you’ll be using only object keys, a weak map is the best choice. A weak map will allow you to optimize memory usage and avoid memory leaks by ensuring that extra data isn’t retained after it’s no longer accessible. Keep in mind that weak maps give you very little visibility into their contents, so you can’t use the forEach() method, the size property, or the clear() method to manage the items. If you need some inspection capabilities, regular maps are a better choice. Just be sure to keep an eye on memory usage. Of course, if you only want to use nonobject keys, regular maps are your only choice.

Iterators and Generators

Iterators are objects with a specific interface designed for iteration. All iterator objects have a next() method that returns a result object. The result object has two properties: value, which is the next value, and done, which is a Boolean that’s true when there are no more values to return. A generator is a function that returns an iterator. Generator functions are indicated by an asterisk character (*) after the function keyword and use the new yield keyword. You can use the yield keyword only inside generators. Using yield anywhere else is a syntax error, including in functions that are inside generators. Because generators are just functions, you can add them to objects, too. Closely related to iterators, an iterable is an object with a Symbol.iterator property. The well-known Symbol.iterator symbol specifies a function that returns an iterator for the given object. Iterables are designed to be used with a new addition to ECMAScript: the for-of loop. The for-of loop first calls the Symbol.iterator method on the values array to retrieve an iterator. Developer-defined objects are not iterable by default, but you can make them iterable by creating a Symbol.iterator property containing a generator.

Built-In Iterators

Collection Iterators

ECMAScript 6 has three types of collection objects: arrays, maps, and sets. All three have the following built-in iterators to help you navigate their content:

  • entries() Returns an iterator whose values are key-value pairs
  • 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.

Each collection type also has a default iterator that is used by for-of whenever an iterator isn’t explicitly specified. The values() method is the default iterator for arrays and sets, whereas the entries() method is the default iterator for maps. Weak sets and weak maps do not have built-in iterators. Managing weak references means there’s no way to know exactly how many values are in these collections, which also means there’s no way to iterate over them.

String Iterators

The default iterator for strings works on characters rather than code units.

NodeList Iterators

With the addition of default iterators in ECMAScript 6, the DOM definition of NodeList includes a default iterator that behaves in the same manner as the array default iterator. That means you can use NodeList in a for-of loop or any other place that uses an object’s default iterator.

The Spread Operator and Nonarray Iterables

Because you can use the spread operator on any iterable, using it is the easiest way to convert an iterable into an array. You can convert strings into arrays of characters (not code units) and NodeList objects in the browser into arrays of nodes.

Introducing Javascript classes

In ECMAScript 5 and earlier, JavaScript had no classes. The closest equivalent to a class was creating a constructor and then assigning methods to the constructor’s prototype, an approach typically called creating a custom type. Class declarations begin with the class keyword followed by the name of the class. Instead of defining a function as the constructor, class declarations allow you to define the constructor directly inside the class using the special constructor method name. Own properties, properties that occur on the instance rather than the prototype, can only be created inside a class constructor or method. Class declarations are just syntactic sugar on top of the existing custom type declarations. Despite the similarities between classes and custom types, you need to keep some important differences in mind:

  • Class declarations, unlike function declarations, are not hoisted.
  • All code inside class declarations runs in strict mode automatically.
  • All methods are nonenumerable.
  • All methods lack an internal [[Construct]] method
  • Calling the class constructor without new throws an error.
  • Attempting to overwrite the class name within a class method throws an error.

Class expressions are designed to be used in variable declarations or passed into functions as arguments. Classes have an expression form that doesn’t require an identifier after class. These class expressions are designed to be used in variable declarations or passed into functions as arguments. Unlike function declarations and function expressions, class declarations and class expressions are not hoisted. An use of class expressions is creating singletons by immediately invoking the class constructor. To do so, you must use new with a class expression and include parentheses at the end. Although you should create own properties inside class constructors, classes allow you to define accessor properties on the prototype. To create a getter, use the keyword get followed by a space, followed by an identifier; to create a setter, do the same using the keyword set. Class methods and accessor properties can also have computed names. Instead of using an identifier, use square brackets around an expression, which is the same syntax you use for object literal computed names. ECMAScript 6 classes simplify the creation of static members by using the formal static annotation before the method or accessor property name. Static members are not accessible from instances. You must always access static members from the class directly. Classes make inheritance easier to implement by using the familiar extends keyword to specify the function from which the class should inherit. The prototypes are automatically adjusted, and you can access the base class constructor by calling the super() method. Classes that inherit from other classes are referred to as derived classes. Derived classes require you to use super() if you specify a constructor; if you don’t, an error will occur. If you choose not to use a constructor, super() is automatically called for you with all arguments upon creating a new instance of the class. The methods on derived classes always shadow methods of the same name on the base class, of course, you can always decide to call the base class version of the method by using super(). If a base class has static members, those static members are also available on the derived class. Inheritance works like that in other languages, but this is a new concept in JavaScript. You can use extends with any expression as long as the expression resolves to a function with [[Construct]] and a prototype. One goal of ECMAScript 6 classes is to allow inheritance from all builtins. To accomplish this, the inheritance model of classes is slightly different than the classical inheritance model found in ECMAScript 5 and earlier, in two significant ways. In ECMAScript 5 classical inheritance, the value of this is first created by the derived type and then the base type constructor is called. That means this starts out as an instance of our custom type and then is decorated with additional properties from the built-in type. Conversely, in ECMAScript 6 class-based inheritance, the value of this is first created by the bas and then modified by the derived class constructor. The result is that this starts with all the built-in functionality of the base and correctly receives all functionality related to it. A convenient aspect of inheriting from built-ins is that any method that returns an instance of the built-in will automatically return a derived class instance instead. Using Symbol.species, any derived class can determine what type of value should be returned when a method returns an instance. You can also use new.target in class constructors to determine how the class is being invoked. This is important because it gives each constructor the ability to alter its behavior based on how it’s being called. For instance, you can create an abstract base class (one that can’t be instantiated directly) by using new.target and throw new Error("This class cannot be instantiated directly.") when the base class is instanciated.

Improved Array capabilities

When the Array constructor is passed a single numeric value, the array’s length property is set to that value. If a single nonnumeric value is passed, that value becomes the one and only item in the array. If multiple values are passed (numeric or not), those values become items in the array. ECMAScript 6 introduces Array.of() to solve this problem. The Array.of() method always creates an array containing its arguments regardless of the number of arguments or the argument types. Given either an iterable or an array-like object as the first argument, the Array.from() method returns an array.

New Methods on all Arrays

find() returns the value, whereas findIndex() returns the index at which the value was found. Both find() and findIndex() are useful to find an array element that matches a condition rather than a value. If you only want to find a value, indexOf() and lastIndexOf() are better choices. The fill() method fills one or more array elements with a specific value. When passed a value, fill() overwrites all the values in an array with that value. If you want to change only some of the elements rather than all of them, you can optionally include a start index and an exclusive end index. copyWithin() always copies values up to the end of the array, but you can provide an optional third argument to limit how many elements will be overwritten. That third argument is an exclusive end index at which copying of values stops.

Typed Arrays

Typed arrays are special-purpose arrays designed to work with numeric types. The foundation for all typed arrays is an array buffer, which is a memory location that can contain a specified number of bytes. 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. Array buffers represent memory locations, and views are the interfaces you’ll use to manipulate that memory. A view operates on an array buffer or a subset of an array buffer’s bytes, reading and writing data in one of the numeric data types. The DataView type is a generic view on an array buffer that allows you to operate on all eight numeric data types. To use a DataView, you first create an instance of ArrayBuffer and use it to create a new DataView. For each of JavaScript’s eight numeric data types, the DataView prototype has a method to write data and a method to read data from an array buffer. The method names all begin with either set or get and are followed by the data type abbreviation. Instead of using a generic DataView object to operate on an array buffer, you can use objects that enforce specific data types. Eight type-specific views correspond to the eight numeric data types. Typed array constructors accept multiple types of arguments, so you can create typed arrays in a few ways. First, you can create a new typed array by passing the same arguments DataView takes (an array buffer, an optional byte offset, and an optional byte length). The second way to create a typed array is to pass a single number to the constructor. That number represents the number of elements (not bytes) to allocate to the array. The constructor will create a new buffer with the correct number of bytes to represent that number of array elements, and you can access the number of elements in the array by using the length property. The third way to create a typed array is to pass an object as the only argument to the constructor. In a typed array each element is copied into a new element on the new typed array. For example, if you pass an int8 to the Int16Array constructor, the int8 values would be copied into an int16 array. The new typed array has a different array buffer than the one that was passed in. An iterable The object’s iterator is called to retrieve the items to insert into the typed array. The constructor will throw an error if any elements are invalid for the view type. An array The elements of the array are copied into a new typed array. The constructor will throw an error if any elements are invalid for the type. An array-like object The object behaves the same as an array. Although typed arrays do have many of the same methods as regular arrays, they also lack several array methods. The following methods are not available on typed arrays:

  • concat()
  • pop()
  • push()
  • shift()
  • splice()
  • unshift()

Except for the concat() method, the methods in this list can change the size of an array. Typed arrays can’t change size, which is why these methods aren’t available for typed arrays. Typed array methods have two methods not present on regular arrays: the set() and subarray() methods. The set() method accepts an array (either typed or regular) and an optional offset at which to insert the data; if you pass nothing, the offset defaults to zero. The subarray() method accepts an optional start and end index and returns a new typed array. You can also omit both arguments to create a clone of the typed array

Promises and asynchronous Programming

JavaScript engines are built on the concept of a single-threaded event loop. Single-threaded means that only one piece of code is executed at a time. JavaScript engines can execute only 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. Whenever a piece of code is ready to be executed, it is added to the job queue. When the JavaScript engine is finished executing code, the event loop executes the next job in the 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.

A promise is a placeholder for the result of an asynchronous operation. Instead of subscribing to an event or passing a callback to a function, the function can return a promise. Each promise goes through a short life cycle starting in the pending state, which indicates that the asynchronous operation hasn’t completed yet. A pending promise is considered unsettled. Once the asynchronous operation completes, the promise is considered settled and enters one of two possible states:

  • Fulfilled The promise’s asynchronous operation has completed successfully.
  • Rejected The promise’s asynchronous operation didn’t complete successfully due to either an error or some other cause.

The then() method is present on all promises and takes two arguments. The first argument is a function to call when the promise is fulfilled. Any additional data related to the asynchronous operation is passed to this fulfillment function. The second argument is a function to call when the promise is rejected. Similar to the fulfillment function, the rejection function is passed any additional data related to the rejection.

Promises also have a catch() method that behaves the same as then() when only a rejection handler is passed. The then() and catch() methods are intended to be used in combination to properly handle the result of asynchronous operations. Just remember that if you don’t attach a rejection handler to a promise, all failures will happen silently. Always attach a rejection handler, even if the handler just logs the failure. New promises are created using the Promise constructor. This constructor accepts a single argument: a function called the executor, which contains the code to initialize the promise. The executor is passed two functions named resolve() and reject() as arguments. The resolve() function is called when the executor has finished successfully to signal that the promise is ready to be resolved, whereas the reject() function indicates that the executor has failed. The Promise constructor is the best way to create unsettled promises due to the dynamic nature of what the promise executor does. But if you want a promise to represent just a single known value, it doesn’t make sense to schedule a job that simply passes a value to the resolve() function. Instead, you can use either of two methods that create settled promises given a specific value. the promise will never be in the rejected state.

The Promise.resolve() method accepts a single argument and returns a promise in the fulfilled state. The Promise.reject() method works like Promise.resolve() except the created promise is in the rejected state. Any additional rejection handlers would be called but not fulfillment handlers. Both Promise.resolve() and Promise.reject() also accept non-promise thenables as arguments. When passed a non-promise thenable, these methods create a new promise that is called after the then() function. A non-promise thenable is created when an object has a then() method that accepts a resolve and a reject argument. If an error is thrown inside an executor, the promise’s rejection handler is called. An implicit try-catch is inside every executor so that the error is caught and then passed to the rejection handler. An error thrown in the executor is only reported when a rejection handler is present. Otherwise, the error is suppressed. Each call to then() or catch() actually creates and returns another promise. This second promise is resolved only when the first has been fulfilled or rejected. Promise chaining allows you to catch errors that may occur in a fulfillment or rejection handler from a previous promise. Another important aspect of promise chains is the ability to pass data from one promise to the next. Returning thenables from fulfillment or rejection handlers doesn’t change when the promise executors are executed. The first defined promise will run its executor first, then the second promise executor will run, and so on. Returning thenables simply allows you to define additional responses to the promise results. You defer the execution of fulfillment handlers by creating a new promise within a fulfillment handler. Sometimes you’ll want to monitor the progress of multiple promises to determine the next action. ECMAScript 6 provides two methods that monitor multiple promises: Promise.all() and Promise.race(). The Promise.all() method accepts a single argument, which is an iterable (such as an array) of promises to monitor, and returns a promise that is resolved only when every promise in the iterable is resolved. The returned promise is fulfilled when every promise in the iterable is fulfilled. If any promise passed to Promise.all() is rejected, the returned promise is immediately rejected without waiting for the other promises to complete. The rejection handler always receives a single value rather than an array, and the value is the rejection value from the promise that was rejected. The Promise.race() also accepts an iterable of promises to monitor and returns a promise, but the returned promise is settled as soon as the first promise is settled. Instead of waiting for all promises to be fulfilled, Promise.race() returns an appropriate promise as soon as any promise in the array is fulfilled. Just like other built-in types, you can use a promise as the base for a derived class. This allows you to define your own variation of promises to extend what built-in promises can do. Both MyPromise.resolve() and MyPromise.reject() will return an instance of MyPromise regardless of the value passed because those methods use the Symbol.species property to determine the type of promise to return. If a built-in promise is passed to either method, the promise will be resolved or rejected, and the method will return a new MyPromise so you can assign fulfillment and rejection handlers.

Proxies and the reflection API

Calling new Proxy() creates a proxy to use in place of another object (called the target). The proxy virtualizes the target so the proxy and the target appear to be functionally the same. Proxies allow you to intercept low-level object operations on the target that are otherwise internal to the JavaScript engine. These low-level operations are intercepted using a trap, which is a function that responds to a specific operation. 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 a Reflect method for every proxy trap.

When you use the Proxy constructor to make a proxy, you’ll pass it two arguments: the target and a handler. A handler is an object that defines one or more traps. The proxy uses the default behavior for all operations except when traps are defined for that operation. Normally, a proxy can’t be unbound from its target once the proxy has been created. You can create revocable proxies using the Proxy.revocable() method, which takes the same arguments as the Proxy constructor: a target object and the proxy handler. The return value is an object with the following properties:

  • proxy The proxy object that can be revoked
  • revoke The function to call to revoke the proxy.

When the revoke() function is called, no further operations can be performed through the proxy. Any attempt to interact with the proxy object in a way that would trigger a proxy trap throws an error.

Encapsulating code with Modules

Variables created in the top level of a module aren’t automatically added to the shared global scope. The variables exist only within the top-level scope of the module, and the module must export any elements, like variables or functions, that should be available to code outside the module. You can use the export keyword to expose parts of published code to other modules. When you have a module with exports, you can access the functionality in another module by using the import keyword.

The two parts of an import statement are the identifiers you’re importing and the module from which those identifiers should be imported. The keyword from indicates the module from which to import the given binding. The module is specified by a string representing the path to the module (called the module specifier). When you’re importing a binding from a module, the binding acts as though it was defined using const. As a result, you can’t define another variable with the same name (including importing another binding of the same name), use the identifier before the import statement, or change binding’s value. You can import and use bindings from that module in a number of ways. For instance, you can just import one identifier. If you want to import multiple bindings from the example module, you can explicitly list them. A special case allows you to import the entire module as a single object. All exports are then available on that object as properties. This import format is called a namespace import because the example object doesn’t exist inside the example.js file and is instead created to be used as a namespace object for all the exported members of example.js. No matter how many times you use a module in import statements, the module will execute only once. After the code to import the module executes, the instantiated module is kept in memory and reused whenever another import statement references. You can change the name of an export during the export and during the import. You can use the as keyword to specify the name that the function should be known as outside of the module. If the module importing the function wants to use a different name, it can also use the keyword as. The default value for a module is a single variable, function, or class as specified by the default keyword, and you can only set one default export per module. For modules that export a default and one or more non-default bindings, you can import all exported bindings using one statement. The comma separates the default local name from the non-defaults, which are also surrounded by curly braces. Keep in mind that the default must come before the non-defaults in the import statement. As with exporting defaults, you can import defaults with the renaming syntax. Some modules may not export anything; instead, they might only modify objects in the global scope. The shared definitions of built-in objects, such as Array and Object, are accessible inside a module, and changes to those objects will be reflected in other modules. This code can be used as a module and as a script. Because it doesn’t export anything, you can use a simplified import to execute the module code without importing any bindings.

Minor Changes in ECMAscript 6

ECMAScript 6 added the Number.isInteger() method. ECMAScript 6 introduced the Number.isSafeInteger() method to better identify integers that the language can accurately represent. It also added the Number.MAX_SAFE_INTEGER and Number.MIN_SAFE_INTEGER properties to represent the upper and lower bounds of the integer range, respectively.

Understanding ECMAscript 7 (2016)

The only change to JavaScript syntax introduced in ECMAScript 2016 is the exponentiation operator, which is a mathematical operation that applies an exponent to a base. The exponentiation operator is two asterisks (**): the left operand is the base, and the right operand is the exponent. The left side of an exponentiation operation cannot be a unary expression other than ++ or --.

The Array.prototype.includes() method accepts two arguments: the value to search for and an optional index from which to start the search. When the second argument is provided, includes() starts the match from that index. (The default starting index is 0.) The return value is true if the value is found inside the array and false if not.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment