提交 a4785d75 编写于 作者: G gdut-yy

二、三级标题 ch6--ch10

上级 5c13d167
...@@ -11,7 +11,7 @@ Node modules using require() ...@@ -11,7 +11,7 @@ Node modules using require()
ES6 modules using export, import, and import() ES6 modules using export, import, and import()
10.1 Modules with Classes, Objects, and Closures ## 10.1 Modules with Classes, Objects, and Closures
Though it may be obvious, it is worth pointing out that one of the important features of classes is that they act as modules for their methods. Think back to Example 9-8. That example defined a number of different classes, all of which had a method named has(). But you would have no problem writing a program that used multiple set classes from that example: there is no danger that the implementation of has() from SingletonSet will overwrite the has() method of BitSet, for example. Though it may be obvious, it is worth pointing out that one of the important features of classes is that they act as modules for their methods. Think back to Example 9-8. That example defined a number of different classes, all of which had a method named has(). But you would have no problem writing a program that used multiple set classes from that example: there is no danger that the implementation of has() from SingletonSet will overwrite the has() method of BitSet, for example.
The reason that the methods of one class are independent of the methods of other, unrelated classes is that the methods of each class are defined as properties of independent prototype objects. The reason that classes are modular is that objects are modular: defining a property in a JavaScript object is a lot like declaring a variable, but adding properties to objects does not affect the global namespace of a program, nor does it affect the properties of other objects. JavaScript defines quite a few mathematical functions and constants, but instead of defining them all globally, they are grouped as properties of a single global Math object. This same technique could have been used in Example 9-8. Instead of defining global classes with names like SingletonSet and BitSet, that example could have been written to define only a single global Sets object, with properties referencing the various classes. Users of this Sets library could then refer to the classes with names like Sets.Singleton and Sets.Bit. The reason that the methods of one class are independent of the methods of other, unrelated classes is that the methods of each class are defined as properties of independent prototype objects. The reason that classes are modular is that objects are modular: defining a property in a JavaScript object is a lot like declaring a variable, but adding properties to objects does not affect the global namespace of a program, nor does it affect the properties of other objects. JavaScript defines quite a few mathematical functions and constants, but instead of defining them all globally, they are grouped as properties of a single global Math object. This same technique could have been used in Example 9-8. Instead of defining global classes with names like SingletonSet and BitSet, that example could have been written to define only a single global Sets object, with properties referencing the various classes. Users of this Sets library could then refer to the classes with names like Sets.Singleton and Sets.Bit.
...@@ -62,7 +62,7 @@ const stats = (function() { ...@@ -62,7 +62,7 @@ const stats = (function() {
// And here is how we might use the module // And here is how we might use the module
stats.mean([1, 3, 5, 7, 9]) // => 5 stats.mean([1, 3, 5, 7, 9]) // => 5
stats.stddev([1, 3, 5, 7, 9]) // => Math.sqrt(10) stats.stddev([1, 3, 5, 7, 9]) // => Math.sqrt(10)
10.1.1 Automating Closure-Based Modularity ### 10.1.1 Automating Closure-Based Modularity
Note that it is a fairly mechanical process to transform a file of JavaScript code into this kind of module by inserting some text at the beginning and end of the file. All that is needed is some convention for the file of JavaScript code to indicate which values are to be exported and which are not. Note that it is a fairly mechanical process to transform a file of JavaScript code into this kind of module by inserting some text at the beginning and end of the file. All that is needed is some convention for the file of JavaScript code to indicate which values are to be exported and which are not.
Imagine a tool that takes a set of files, wraps the content of each of those files within an immediately invoked function expression, keeps track of the return value of each function, and concatenates everything into one big file. The result might look something like this: Imagine a tool that takes a set of files, wraps the content of each of those files within an immediately invoked function expression, keeps track of the return value of each function, and concatenates everything into one big file. The result might look something like this:
...@@ -104,14 +104,14 @@ s.insert(30); ...@@ -104,14 +104,14 @@ s.insert(30);
let average = stats.mean([...s]); // average is 20 let average = stats.mean([...s]); // average is 20
This code is a rough sketch of how code-bundling tools (such as webpack and Parcel) for web browsers work, and it’s also a simple introduction to the require() function like the one used in Node programs. This code is a rough sketch of how code-bundling tools (such as webpack and Parcel) for web browsers work, and it’s also a simple introduction to the require() function like the one used in Node programs.
10.2 Modules in Node ## 10.2 Modules in Node
In Node programming, it is normal to split programs into as many files as seems natural. These files of JavaScript code are assumed to all live on a fast filesystem. Unlike web browsers, which have to read files of JavaScript over a relatively slow network connection, there is no need or benefit to bundling a Node program into a single JavaScript file. In Node programming, it is normal to split programs into as many files as seems natural. These files of JavaScript code are assumed to all live on a fast filesystem. Unlike web browsers, which have to read files of JavaScript over a relatively slow network connection, there is no need or benefit to bundling a Node program into a single JavaScript file.
In Node, each file is an independent module with a private namespace. Constants, variables, functions, and classes defined in one file are private to that file unless the file exports them. And values exported by one module are only visible in another module if that module explicitly imports them. In Node, each file is an independent module with a private namespace. Constants, variables, functions, and classes defined in one file are private to that file unless the file exports them. And values exported by one module are only visible in another module if that module explicitly imports them.
Node modules import other modules with the require() function and export their public API by setting properties of the Exports object or by replacing the module.exportsobject entirely. Node modules import other modules with the require() function and export their public API by setting properties of the Exports object or by replacing the module.exportsobject entirely.
10.2.1 Node Exports ### 10.2.1 Node Exports
Node defines a global exports object that is always defined. If you are writing a Node module that exports multiple values, you can simply assign them to the properties of this object: Node defines a global exports object that is always defined. If you are writing a Node module that exports multiple values, you can simply assign them to the properties of this object:
const sum = (x, y) => x + y; const sum = (x, y) => x + y;
...@@ -140,7 +140,7 @@ const stddev = d => { ...@@ -140,7 +140,7 @@ const stddev = d => {
// Now export only the public ones // Now export only the public ones
module.exports = { mean, stddev }; module.exports = { mean, stddev };
10.2.2 Node Imports ### 10.2.2 Node Imports
A Node module imports another module by calling the require() function. The argument to this function is the name of the module to be imported, and the return value is whatever value (typically a function, class, or object) that module exports. A Node module imports another module by calling the require() function. The argument to this function is the name of the module to be imported, and the return value is whatever value (typically a function, class, or object) that module exports.
If you want to import a system module built in to Node or a module that you have installed on your system via a package manager, then you simply use the unqualified name of the module, without any “/” characters that would turn it into a filesystem path: If you want to import a system module built in to Node or a module that you have installed on your system via a package manager, then you simply use the unqualified name of the module, without any “/” characters that would turn it into a filesystem path:
...@@ -174,12 +174,12 @@ const { stddev } = require('./stats.js'); ...@@ -174,12 +174,12 @@ const { stddev } = require('./stats.js');
// This is nice and succinct, though we lose a bit of context // This is nice and succinct, though we lose a bit of context
// without the 'stats' prefix as a namspace for the stddev() function. // without the 'stats' prefix as a namspace for the stddev() function.
let sd = stddev(data); let sd = stddev(data);
10.2.3 Node-Style Modules on the Web ### 10.2.3 Node-Style Modules on the Web
Modules with an Exports object and a require() function are built in to Node. But if you’re willing to process your code with a bundling tool like webpack, then it is also possible to use this style of modules for code that is intended to run in web browsers. Until recently, this was a very common thing to do, and you may see lots of web-based code that still does it. Modules with an Exports object and a require() function are built in to Node. But if you’re willing to process your code with a bundling tool like webpack, then it is also possible to use this style of modules for code that is intended to run in web browsers. Until recently, this was a very common thing to do, and you may see lots of web-based code that still does it.
Now that JavaScript has its own standard module syntax, however, developers who use bundlers are more likely to use the official JavaScript modules with import and export statements. Now that JavaScript has its own standard module syntax, however, developers who use bundlers are more likely to use the official JavaScript modules with import and export statements.
10.3 Modules in ES6 ## 10.3 Modules in ES6
ES6 adds import and export keywords to JavaScript and finally supports real modularity as a core language feature. ES6 modularity is conceptually the same as Node modularity: each file is its own module, and constants, variables, functions, and classes defined within a file are private to that module unless they are explicitly exported. Values that are exported from one module are available for use in modules that explicitly import them. ES6 modules differ from Node modules in the syntax used for exporting and importing and also in the way that modules are defined in web browsers. The sections that follow explain these things in detail. ES6 adds import and export keywords to JavaScript and finally supports real modularity as a core language feature. ES6 modularity is conceptually the same as Node modularity: each file is its own module, and constants, variables, functions, and classes defined within a file are private to that module unless they are explicitly exported. Values that are exported from one module are available for use in modules that explicitly import them. ES6 modules differ from Node modules in the syntax used for exporting and importing and also in the way that modules are defined in web browsers. The sections that follow explain these things in detail.
First, though, note that ES6 modules are also different from regular JavaScript “scripts” in some important ways. The most obvious difference is the modularity itself: in regular scripts, top-level declarations of variables, functions, and classes go into a single global context shared by all scripts. With modules, each file has its own private context and can use the import and export statements, which is the whole point, after all. But there are other differences between modules and scripts as well. Code inside an ES6 module (like code inside any ES6 class definition) is automatically in strict mode (see §5.6.3). This means that, when you start using ES6 modules, you’ll never have to write "use strict" again. And it means that code in modules cannot use the with statement or the arguments object or undeclared variables. ES6 modules are even slightly stricter than strict mode: in strict mode, in functions invoked as functions, this is undefined. In modules, this is undefined even in top-level code. (By contrast, scripts in web browsers and Node set this to the global object.) First, though, note that ES6 modules are also different from regular JavaScript “scripts” in some important ways. The most obvious difference is the modularity itself: in regular scripts, top-level declarations of variables, functions, and classes go into a single global context shared by all scripts. With modules, each file has its own private context and can use the import and export statements, which is the whole point, after all. But there are other differences between modules and scripts as well. Code inside an ES6 module (like code inside any ES6 class definition) is automatically in strict mode (see §5.6.3). This means that, when you start using ES6 modules, you’ll never have to write "use strict" again. And it means that code in modules cannot use the with statement or the arguments object or undeclared variables. ES6 modules are even slightly stricter than strict mode: in strict mode, in functions invoked as functions, this is undefined. In modules, this is undefined even in top-level code. (By contrast, scripts in web browsers and Node set this to the global object.)
...@@ -189,7 +189,7 @@ ES6 modules have been in use on the web for years with the help of code bundlers ...@@ -189,7 +189,7 @@ ES6 modules have been in use on the web for years with the help of code bundlers
And meanwhile, having pioneered JavaScript modularity, Node finds itself in the awkward position of having to support two not entirely compatible module systems. Node 13 supports ES6 modules, but for now, the vast majority of Node programs still use Node modules. And meanwhile, having pioneered JavaScript modularity, Node finds itself in the awkward position of having to support two not entirely compatible module systems. Node 13 supports ES6 modules, but for now, the vast majority of Node programs still use Node modules.
10.3.1 ES6 Exports ### 10.3.1 ES6 Exports
To export a constant, variable, function, or class from an ES6 module, simply add the keyword export before the declaration: To export a constant, variable, function, or class from an ES6 module, simply add the keyword export before the declaration:
export const PI = Math.PI; export const PI = Math.PI;
...@@ -218,7 +218,7 @@ It is legal, but somewhat uncommon, for modules to have a set of regular exports ...@@ -218,7 +218,7 @@ It is legal, but somewhat uncommon, for modules to have a set of regular exports
Finally, note that the export keyword can only appear at the top level of your JavaScript code. You may not export a value from within a class, function, loop, or conditional. (This is an important feature of the ES6 module system and enables static analysis: a modules export will be the same on every run, and the symbols exported can be determined before the module is actually run.) Finally, note that the export keyword can only appear at the top level of your JavaScript code. You may not export a value from within a class, function, loop, or conditional. (This is an important feature of the ES6 module system and enables static analysis: a modules export will be the same on every run, and the symbols exported can be determined before the module is actually run.)
10.3.2 ES6 Imports ### 10.3.2 ES6 Imports
You import values that have been exported by other modules with the import keyword. The simplest form of import is used for modules that define a default export: You import values that have been exported by other modules with the import keyword. The simplest form of import is used for modules that define a default export:
import BitSet from './bitset.js'; import BitSet from './bitset.js';
...@@ -248,7 +248,7 @@ A module like this runs the first time it is imported. (And subsequent imports d ...@@ -248,7 +248,7 @@ A module like this runs the first time it is imported. (And subsequent imports d
Note that you can use this import-nothing import syntax even with modules that do have exports. If a module defines useful behavior independent of the values it exports, and if your program does not need any of those exported values, you can still import the module . just for that default behavior. Note that you can use this import-nothing import syntax even with modules that do have exports. If a module defines useful behavior independent of the values it exports, and if your program does not need any of those exported values, you can still import the module . just for that default behavior.
10.3.3 Imports and Exports with Renaming ### 10.3.3 Imports and Exports with Renaming
If two modules export two different values using the same name and you want to import both of those values, you will have to rename one or both of the values when you import it. Similarly, if you want to import a value whose name is already in use in your module, you will need to rename the imported value. You can use the as keyword with named imports to rename them as you import them: If two modules export two different values using the same name and you want to import both of those values, you will have to rename one or both of the values when you import it. Similarly, if you want to import a value whose name is already in use in your module, you will need to rename the imported value. You can use the as keyword with named imports to rename them as you import them:
import { render as renderImage } from "./imageutils.js"; import { render as renderImage } from "./imageutils.js";
...@@ -271,7 +271,7 @@ export { ...@@ -271,7 +271,7 @@ export {
Keep in mind that, although the curly braces look something like object literals, they are not, and the export keyword expects a single identifier before the as, not an expression. This means, unfortunately, that you cannot use export renaming like this: Keep in mind that, although the curly braces look something like object literals, they are not, and the export keyword expects a single identifier before the as, not an expression. This means, unfortunately, that you cannot use export renaming like this:
export { Math.sin as sin, Math.cos as cos }; // SyntaxError export { Math.sin as sin, Math.cos as cos }; // SyntaxError
10.3.4 Re-Exports ### 10.3.4 Re-Exports
Throughout this chapter, we’ve discussed a hypothetical “./stats.js” module that exports mean() and stddev() functions. If we were writing such a module and we thought that many users of the module would want only one function or the other, then we might want to define mean() in a “./stats/mean.js” module and define stddev() in “./stats/stddev.js”. That way, programs only need to import exactly the functions they need and are not bloated by importing code they do not need. Throughout this chapter, we’ve discussed a hypothetical “./stats.js” module that exports mean() and stddev() functions. If we were writing such a module and we thought that many users of the module would want only one function or the other, then we might want to define mean() in a “./stats/mean.js” module and define stddev() in “./stats/stddev.js”. That way, programs only need to import exactly the functions they need and are not bloated by importing code they do not need.
Even if we had defined these statistical functions in individual modules, however, we might expect that there would be plenty of programs that want both functions and would appreciate a convenient “./stats.js” module from which they could import both on one line. Even if we had defined these statistical functions in individual modules, however, we might expect that there would be plenty of programs that want both functions and would appreciate a convenient “./stats.js” module from which they could import both on one line.
...@@ -306,7 +306,7 @@ And finally, to re-export the default export of another module as the default ex ...@@ -306,7 +306,7 @@ And finally, to re-export the default export of another module as the default ex
// The average.js module simply re-exports the stats/mean.js default export // The average.js module simply re-exports the stats/mean.js default export
export { default } from "./stats/mean.js" export { default } from "./stats/mean.js"
10.3.5 JavaScript Modules on the Web ### 10.3.5 JavaScript Modules on the Web
The preceding sections have described ES6 modules and their import and export declarations in a somewhat abstract manner. In this section and the next, we’ll be discussing how they actually work in web browsers, and if you are not already an experienced web developer, you may find the rest of this chapter easier to understand after you have read Chapter 15. The preceding sections have described ES6 modules and their import and export declarations in a somewhat abstract manner. In this section and the next, we’ll be discussing how they actually work in web browsers, and if you are not already an experienced web developer, you may find the rest of this chapter easier to understand after you have read Chapter 15.
As of early 2020, production code using ES6 modules is still generally bundled with a tool like webpack. There are trade-offs to doing this,1 but on the whole, code bundling tends to give better performance. That may well change in the future as network speeds grow and browser vendors continue to optimize their ES6 module implementations. As of early 2020, production code using ES6 modules is still generally bundled with a tool like webpack. There are trade-offs to doing this,1 but on the whole, code bundling tends to give better performance. That may well change in the future as network speeds grow and browser vendors continue to optimize their ES6 module implementations.
...@@ -328,7 +328,7 @@ Another important difference between regular scripts and module scripts has to d ...@@ -328,7 +328,7 @@ Another important difference between regular scripts and module scripts has to d
Some programmers like to use the filename extension .mjs to distinguish their modular JavaScript files from their regular, non-modular JavaScript files with the traditional .js extension. For the purposes of web browsers and <script> tags, the file extension is actually irrelevant. (The MIME type is relevant, however, so if you use .mjs files, you may need to configure your web server to serve them with the same MIME type as .js files.) Node’s support for ES6 does use the filename extension as a hint to distinguish which module system is used by each file it loads. So if you are writing ES6 modules and want them to be usable with Node, then it may be helpful to adopt the .mjs naming convention. Some programmers like to use the filename extension .mjs to distinguish their modular JavaScript files from their regular, non-modular JavaScript files with the traditional .js extension. For the purposes of web browsers and <script> tags, the file extension is actually irrelevant. (The MIME type is relevant, however, so if you use .mjs files, you may need to configure your web server to serve them with the same MIME type as .js files.) Node’s support for ES6 does use the filename extension as a hint to distinguish which module system is used by each file it loads. So if you are writing ES6 modules and want them to be usable with Node, then it may be helpful to adopt the .mjs naming convention.
10.3.6 Dynamic Imports with import() ### 10.3.6 Dynamic Imports with import()
We’ve seen that the ES6 import and export directives are completely static and enable JavaScript interpreters and other JavaScript tools to determine the relationships between modules with simple text analysis while the modules are being loaded without having to actually execute any of the code in the modules. With statically imported modules, you are guaranteed that the values you import into a module will be ready for use before any of the code in your module begins to run. We’ve seen that the ES6 import and export directives are completely static and enable JavaScript interpreters and other JavaScript tools to determine the relationships between modules with simple text analysis while the modules are being loaded without having to actually execute any of the code in the modules. With statically imported modules, you are guaranteed that the values you import into a module will be ready for use before any of the code in your module begins to run.
On the web, code has to be transferred over a network instead of being read from the filesystem. And once transfered, that code is often executed on mobile devices with relatively slow CPUs. This is not the kind of environment where static module imports—which require an entire program to be loaded before any of it runs—make a lot of sense. On the web, code has to be transferred over a network instead of being read from the filesystem. And once transfered, that code is often executed on mobile devices with relatively slow CPUs. This is not the kind of environment where static module imports—which require an entire program to be loaded before any of it runs—make a lot of sense.
...@@ -360,7 +360,7 @@ Dynamic import() looks like a function invocation, but it actually is not. Inste ...@@ -360,7 +360,7 @@ Dynamic import() looks like a function invocation, but it actually is not. Inste
Finally, note that dynamic import() is not just for web browsers. Code-packaging tools like webpack can also make good use of it. The most straightforward way to use a code bundler is to tell it the main entry point for your program and let it find all the static import directives and assemble everything into one large file. By strategically using dynamic import() calls, however, you can break that one monolithic bundle up into a set of smaller bundles that can be loaded on demand. Finally, note that dynamic import() is not just for web browsers. Code-packaging tools like webpack can also make good use of it. The most straightforward way to use a code bundler is to tell it the main entry point for your program and let it find all the static import directives and assemble everything into one large file. By strategically using dynamic import() calls, however, you can break that one monolithic bundle up into a set of smaller bundles that can be loaded on demand.
10.3.7 import.meta.url ### 10.3.7 import.meta.url
There is one final feature of the ES6 module system to discuss. Within an ES6 module (but not within a regular <script> or a Node module loaded with require()), the special syntax import.meta refers to an object that contains metadata about the currently executing module. The url property of this object is the URL from which the module was loaded. (In Node, this will be a file:// URL.) There is one final feature of the ES6 module system to discuss. Within an ES6 module (but not within a regular <script> or a Node module loaded with require()), the special syntax import.meta refers to an object that contains metadata about the currently executing module. The url property of this object is the URL from which the module was loaded. (In Node, this will be a file:// URL.)
The primary use case of import.meta.url is to be able to refer to images, data files, or other resources that are stored in the same directory as (or relative to) the module. The URL() constructor makes it easy to resolve a relative URL against an absolute URL like import.meta.url. Suppose, for example, that you have written a module that includes strings that need to be localized and that the localization files are stored in an l10n/ directory, which is in the same directory as the module itself. Your module could load its strings using a URL created with a function, like this: The primary use case of import.meta.url is to be able to refer to images, data files, or other resources that are stored in the same directory as (or relative to) the module. The URL() constructor makes it easy to resolve a relative URL against an absolute URL like import.meta.url. Suppose, for example, that you have written a module that includes strings that need to be localized and that the localization files are stored in an l10n/ directory, which is in the same directory as the module itself. Your module could load its strings using a URL created with a function, like this:
...@@ -368,7 +368,7 @@ The primary use case of import.meta.url is to be able to refer to images, data f ...@@ -368,7 +368,7 @@ The primary use case of import.meta.url is to be able to refer to images, data f
function localStringsURL(locale) { function localStringsURL(locale) {
return new URL(`l10n/${locale}.json`, import.meta.url); return new URL(`l10n/${locale}.json`, import.meta.url);
} }
10.4 Summary ## 10.4 Summary
The goal of modularity is to allow programmers to hide the implementation details of their code so that chunks of code from various sources can be assembled into large programs without worrying that one chunk will overwrite functions or variables of another. This chapter has explained three different JavaScript module systems: The goal of modularity is to allow programmers to hide the implementation details of their code so that chunks of code from various sources can be assembled into large programs without worrying that one chunk will overwrite functions or variables of another. This chapter has explained three different JavaScript module systems:
In the early days of JavaScript, modularity could only be achieved through the clever use of immediately invoked function expressions. In the early days of JavaScript, modularity could only be achieved through the clever use of immediately invoked function expressions.
......
# Chapter 6. Objects # Chapter 6. Objects
Objects are JavaScript’s most fundamental datatype, and you have already seen them many times in the chapters that precede this one. Because objects are so important to the JavaScript language, it is important that you understand how they work in detail, and this chapter provides that detail. It begins with a formal overview of objects, then dives into practical sections about creating objects and querying, setting, deleting, testing, and enumerating the properties of objects. These property-focused sections are followed by sections that explain how to extend, serialize, and define important methods on objects. Finally, the chapter concludes with a long section about new object literal syntax in ES6 and more recent versions of the language. Objects are JavaScript’s most fundamental datatype, and you have already seen them many times in the chapters that precede this one. Because objects are so important to the JavaScript language, it is important that you understand how they work in detail, and this chapter provides that detail. It begins with a formal overview of objects, then dives into practical sections about creating objects and querying, setting, deleting, testing, and enumerating the properties of objects. These property-focused sections are followed by sections that explain how to extend, serialize, and define important methods on objects. Finally, the chapter concludes with a long section about new object literal syntax in ES6 and more recent versions of the language.
6.1 Introduction to Objects ## 6.1 Introduction to Objects
An object is a composite value: it aggregates multiple values (primitive values or other objects) and allows you to store and retrieve those values by name. An object is an unordered collection of properties, each of which has a name and a value. Property names are usually strings (although, as we’ll see in §6.10.3, property names can also be Symbols), so we can say that objects map strings to values. This string-to-value mapping goes by various names—you are probably already familiar with the fundamental data structure under the name “hash,” “hashtable,” “dictionary,” or “associative array.” An object is more than a simple string-to-value map, however. In addition to maintaining its own set of properties, a JavaScript object also inherits the properties of another object, known as its “prototype.” The methods of an object are typically inherited properties, and this “prototypal inheritance” is a key feature of JavaScript. An object is a composite value: it aggregates multiple values (primitive values or other objects) and allows you to store and retrieve those values by name. An object is an unordered collection of properties, each of which has a name and a value. Property names are usually strings (although, as we’ll see in §6.10.3, property names can also be Symbols), so we can say that objects map strings to values. This string-to-value mapping goes by various names—you are probably already familiar with the fundamental data structure under the name “hash,” “hashtable,” “dictionary,” or “associative array.” An object is more than a simple string-to-value map, however. In addition to maintaining its own set of properties, a JavaScript object also inherits the properties of another object, known as its “prototype.” The methods of an object are typically inherited properties, and this “prototypal inheritance” is a key feature of JavaScript.
JavaScript objects are dynamic—properties can usually be added and deleted—but they can be used to simulate the static objects and “structs” of statically typed languages. They can also be used (by ignoring the value part of the string-to-value mapping) to represent sets of strings. JavaScript objects are dynamic—properties can usually be added and deleted—but they can be used to simulate the static objects and “structs” of statically typed languages. They can also be used (by ignoring the value part of the string-to-value mapping) to represent sets of strings.
...@@ -26,10 +26,10 @@ The configurable attribute specifies whether the property can be deleted and whe ...@@ -26,10 +26,10 @@ The configurable attribute specifies whether the property can be deleted and whe
Many of JavaScript’s built-in objects have properties that are read-only, non-enumerable, or non-configurable. By default, however, all properties of the objects you create are writable, enumerable, and configurable. §14.1 explains techniques for specifying non-default property attribute values for your objects. Many of JavaScript’s built-in objects have properties that are read-only, non-enumerable, or non-configurable. By default, however, all properties of the objects you create are writable, enumerable, and configurable. §14.1 explains techniques for specifying non-default property attribute values for your objects.
6.2 Creating Objects ## 6.2 Creating Objects
Objects can be created with object literals, with the new keyword, and with the Object.create() function. The subsections below describe each technique. Objects can be created with object literals, with the new keyword, and with the Object.create() function. The subsections below describe each technique.
6.2.1 Object Literals ### 6.2.1 Object Literals
The easiest way to create an object is to include an object literal in your JavaScript code. In its simplest form, an object literal is a comma-separated list of colon-separated name:value pairs, enclosed within curly braces. A property name is a JavaScript identifier or a string literal (the empty string is allowed). A property value is any JavaScript expression; the value of the expression (it may be a primitive value or an object value) becomes the value of the property. Here are some examples: The easiest way to create an object is to include an object literal in your JavaScript code. In its simplest form, an object literal is a comma-separated list of colon-separated name:value pairs, enclosed within curly braces. A property name is a JavaScript identifier or a string literal (the empty string is allowed). A property value is any JavaScript expression; the value of the expression (it may be a primitive value or an object value) becomes the value of the property. Here are some examples:
let empty = {}; // An object with no properties let empty = {}; // An object with no properties
...@@ -50,7 +50,7 @@ An object literal is an expression that creates and initializes a new and distin ...@@ -50,7 +50,7 @@ An object literal is an expression that creates and initializes a new and distin
The object literals shown here use simple syntax that has been legal since the earliest versions of JavaScript. Recent versions of the language have introduced a number of new object literal features, which are covered in §6.10. The object literals shown here use simple syntax that has been legal since the earliest versions of JavaScript. Recent versions of the language have introduced a number of new object literal features, which are covered in §6.10.
6.2.2 Creating Objects with new ### 6.2.2 Creating Objects with new
The new operator creates and initializes a new object. The new keyword must be followed by a function invocation. A function used in this way is called a constructor and serves to initialize a newly created object. JavaScript includes constructors for its built-in types. For example: The new operator creates and initializes a new object. The new keyword must be followed by a function invocation. A function used in this way is called a constructor and serves to initialize a newly created object. JavaScript includes constructors for its built-in types. For example:
let o = new Object(); // Create an empty object: same as {}. let o = new Object(); // Create an empty object: same as {}.
...@@ -59,7 +59,7 @@ let d = new Date(); // Create a Date object representing the current time ...@@ -59,7 +59,7 @@ let d = new Date(); // Create a Date object representing the current time
let r = new Map(); // Create a Map object for key/value mapping let r = new Map(); // Create a Map object for key/value mapping
In addition to these built-in constructors, it is common to define your own constructor functions to initialize newly created objects. Doing so is covered in Chapter 9. In addition to these built-in constructors, it is common to define your own constructor functions to initialize newly created objects. Doing so is covered in Chapter 9.
6.2.3 Prototypes ### 6.2.3 Prototypes
Before we can cover the third object creation technique, we must pause for a moment to explain prototypes. Almost every JavaScript object has a second JavaScript object associated with it. This second object is known as a prototype, and the first object inherits properties from the prototype. Before we can cover the third object creation technique, we must pause for a moment to explain prototypes. Almost every JavaScript object has a second JavaScript object associated with it. This second object is known as a prototype, and the first object inherits properties from the prototype.
All objects created by object literals have the same prototype object, and we can refer to this prototype object in JavaScript code as Object.prototype. Objects created using the new keyword and a constructor invocation use the value of the prototype property of the constructor function as their prototype. So the object created by new Object() inherits from Object.prototype, just as the object created by {} does. Similarly, the object created by new Array() uses Array.prototype as its prototype, and the object created by new Date() uses Date.prototype as its prototype. This can be confusing when first learning JavaScript. Remember: almost all objects have a prototype, but only a relatively small number of objects have a prototype property. It is these objects with prototype properties that define the prototypes for all the other objects. All objects created by object literals have the same prototype object, and we can refer to this prototype object in JavaScript code as Object.prototype. Objects created using the new keyword and a constructor invocation use the value of the prototype property of the constructor function as their prototype. So the object created by new Object() inherits from Object.prototype, just as the object created by {} does. Similarly, the object created by new Array() uses Array.prototype as its prototype, and the object created by new Date() uses Date.prototype as its prototype. This can be confusing when first learning JavaScript. Remember: almost all objects have a prototype, but only a relatively small number of objects have a prototype property. It is these objects with prototype properties that define the prototypes for all the other objects.
...@@ -68,7 +68,7 @@ Object.prototype is one of the rare objects that has no prototype: it does not i ...@@ -68,7 +68,7 @@ Object.prototype is one of the rare objects that has no prototype: it does not i
An explanation of how property inheritance works is in §6.3.2. Chapter 9 explains the connection between prototypes and constructors in more detail: it shows how to define new “classes” of objects by writing a constructor function and setting its prototype property to the prototype object to be used by the “instances” created with that constructor. And we’ll learn how to query (and even change) the prototype of an object in §14.3. An explanation of how property inheritance works is in §6.3.2. Chapter 9 explains the connection between prototypes and constructors in more detail: it shows how to define new “classes” of objects by writing a constructor function and setting its prototype property to the prototype object to be used by the “instances” created with that constructor. And we’ll learn how to query (and even change) the prototype of an object in §14.3.
6.2.4 Object.create() ### 6.2.4 Object.create()
Object.create() creates a new object, using its first argument as the prototype of that object: Object.create() creates a new object, using its first argument as the prototype of that object:
let o1 = Object.create({x: 1, y: 2}); // o1 inherits properties x and y. let o1 = Object.create({x: 1, y: 2}); // o1 inherits properties x and y.
...@@ -87,7 +87,7 @@ let o = { x: "don't change this value" }; ...@@ -87,7 +87,7 @@ let o = { x: "don't change this value" };
library.function(Object.create(o)); // Guard against accidental modifications library.function(Object.create(o)); // Guard against accidental modifications
To understand why this works, you need to know how properties are queried and set in JavaScript. These are the topics of the next section. To understand why this works, you need to know how properties are queried and set in JavaScript. These are the topics of the next section.
6.3 Querying and Setting Properties ## 6.3 Querying and Setting Properties
To obtain the value of a property, use the dot (.) or square bracket ([]) operators described in §4.4. The lefthand side should be an expression whose value is an object. If using the dot operator, the righthand side must be a simple identifier that names the property. If using square brackets, the value within the brackets must be an expression that evaluates to a string that contains the desired property name: To obtain the value of a property, use the dot (.) or square bracket ([]) operators described in §4.4. The lefthand side should be an expression whose value is an object. If using the dot operator, the righthand side must be a simple identifier that names the property. If using square brackets, the value within the brackets must be an expression that evaluates to a string that contains the desired property name:
let author = book.author; // Get the "author" property of the book. let author = book.author; // Get the "author" property of the book.
...@@ -99,7 +99,7 @@ book.edition = 7; // Create an "edition" property of book. ...@@ -99,7 +99,7 @@ book.edition = 7; // Create an "edition" property of book.
book["main title"] = "ECMAScript"; // Change the "main title" property. book["main title"] = "ECMAScript"; // Change the "main title" property.
When using square bracket notation, we’ve said that the expression inside the square brackets must evaluate to a string. A more precise statement is that the expression must evaluate to a string or a value that can be converted to a string or to a Symbol (§6.10.3). In Chapter 7, for example, we’ll see that it is common to use numbers inside the square brackets. When using square bracket notation, we’ve said that the expression inside the square brackets must evaluate to a string. A more precise statement is that the expression must evaluate to a string or a value that can be converted to a string or to a Symbol (§6.10.3). In Chapter 7, for example, we’ll see that it is common to use numbers inside the square brackets.
6.3.1 Objects As Associative Arrays ### 6.3.1 Objects As Associative Arrays
As explained in the preceding section, the following two JavaScript expressions have the same value: As explained in the preceding section, the following two JavaScript expressions have the same value:
object.property object.property
...@@ -138,7 +138,7 @@ function computeValue(portfolio) { ...@@ -138,7 +138,7 @@ function computeValue(portfolio) {
} }
JavaScript objects are commonly used as associative arrays as shown here, and it is important to understand how this works. In ES6 and later, however, the Map class described in §11.1.2 is often a better choice than using a plain object. JavaScript objects are commonly used as associative arrays as shown here, and it is important to understand how this works. In ES6 and later, however, the Map class described in §11.1.2 is often a better choice than using a plain object.
6.3.2 Inheritance ### 6.3.2 Inheritance
JavaScript objects have a set of “own properties,” and they also inherit a set of properties from their prototype object. To understand this, we must consider property access in more detail. The examples in this section use the Object.create() function to create objects with specified prototypes. We’ll see in Chapter 9, however, that every time you create an instance of a class with new, you are creating an object that inherits properties from a prototype object. JavaScript objects have a set of “own properties,” and they also inherit a set of properties from their prototype object. To understand this, we must consider property access in more detail. The examples in this section use the Object.create() function to create objects with specified prototypes. We’ll see in Chapter 9, however, that every time you create an instance of a class with new, you are creating an object that inherits properties from a prototype object.
Suppose you query the property x in the object o. If o does not have an own property with that name, the prototype object of o1 is queried for the property x. If the prototype object does not have an own property by that name, but has a prototype itself, the query is performed on the prototype of the prototype. This continues until the property x is found or until an object with a null prototype is searched. As you can see, the prototype attribute of an object creates a chain or linked list from which properties are inherited: Suppose you query the property x in the object o. If o does not have an own property with that name, the prototype object of o1 is queried for the property x. If the prototype object does not have an own property by that name, but has a prototype itself, the query is performed on the prototype of the prototype. This continues until the property x is found or until an object with a null prototype is searched. As you can see, the prototype attribute of an object creates a chain or linked list from which properties are inherited:
...@@ -162,7 +162,7 @@ c.r = 2; // c overrides its inherited property ...@@ -162,7 +162,7 @@ c.r = 2; // c overrides its inherited property
unitcircle.r // => 1: the prototype is not affected unitcircle.r // => 1: the prototype is not affected
There is one exception to the rule that a property assignment either fails or creates or sets a property in the original object. If o inherits the property x, and that property is an accessor property with a setter method (see §6.10.6), then that setter method is called rather than creating a new property x in o. Note, however, that the setter method is called on the object o, not on the prototype object that defines the property, so if the setter method defines any properties, it will do so on o, and it will again leave the prototype chain unmodified. There is one exception to the rule that a property assignment either fails or creates or sets a property in the original object. If o inherits the property x, and that property is an accessor property with a setter method (see §6.10.6), then that setter method is called rather than creating a new property x in o. Note, however, that the setter method is called on the object o, not on the prototype object that defines the property, so if the setter method defines any properties, it will do so on o, and it will again leave the prototype chain unmodified.
6.3.3 Property Access Errors ### 6.3.3 Property Access Errors
Property access expressions do not always return or set a value. This section explains the things that can go wrong when you query or set a property. Property access expressions do not always return or set a value. This section explains the things that can go wrong when you query or set a property.
It is not an error to query a property that does not exist. If the property x is not found as an own property or an inherited property of o, the property access expression o.x evaluates to undefined. Recall that our book object has a “sub-title” property, but not a “subtitle” property: It is not an error to query a property that does not exist. If the property x is not found as an own property or an inherited property of o, the property access expression o.x evaluates to undefined. Recall that our book object has a “sub-title” property, but not a “subtitle” property:
...@@ -198,7 +198,7 @@ o has an inherited property p that is read-only: it is not possible to hide an i ...@@ -198,7 +198,7 @@ o has an inherited property p that is read-only: it is not possible to hide an i
o does not have an own property p; o does not inherit a property p with a setter method, and o’s extensible attribute (see §14.2) is false. Since p does not already exist in o, and if there is no setter method to call, then p must be added to o. But if o is not extensible, then no new properties can be defined on it. o does not have an own property p; o does not inherit a property p with a setter method, and o’s extensible attribute (see §14.2) is false. Since p does not already exist in o, and if there is no setter method to call, then p must be added to o. But if o is not extensible, then no new properties can be defined on it.
6.4 Deleting Properties ## 6.4 Deleting Properties
The delete operator (§4.13.4) removes a property from an object. Its single operand should be a property access expression. Surprisingly, delete does not operate on the value of the property but on the property itself: The delete operator (§4.13.4) removes a property from an object. Its single operand should be a property access expression. Surprisingly, delete does not operate on the value of the property but on the property itself:
delete book.author; // The book object now has no author property. delete book.author; // The book object now has no author property.
...@@ -228,7 +228,7 @@ In strict mode, however, delete raises a SyntaxError if its operand is an unqual ...@@ -228,7 +228,7 @@ In strict mode, however, delete raises a SyntaxError if its operand is an unqual
delete x; // SyntaxError in strict mode delete x; // SyntaxError in strict mode
delete globalThis.x; // This works delete globalThis.x; // This works
6.5 Testing Properties ## 6.5 Testing Properties
JavaScript objects can be thought of as sets of properties, and it is often useful to be able to test for membership in the set—to check whether an object has a property with a given name. You can do this with the in operator, with the hasOwnProperty() and propertyIsEnumerable() methods, or simply by querying the property. The examples shown here all use strings as property names, but they also work with Symbols (§6.10.3). JavaScript objects can be thought of as sets of properties, and it is often useful to be able to test for membership in the set—to check whether an object has a property with a given name. You can do this with the in operator, with the hasOwnProperty() and propertyIsEnumerable() methods, or simply by querying the property. The examples shown here all use strings as property names, but they also work with Symbols (§6.10.3).
The in operator expects a property name on its left side and an object on its right. It returns true if the object has an own property or an inherited property by that name: The in operator expects a property name on its left side and an object on its right. It returns true if the object has an own property or an inherited property by that name:
...@@ -264,7 +264,7 @@ o.y !== undefined // => false: property doesn't even exist ...@@ -264,7 +264,7 @@ o.y !== undefined // => false: property doesn't even exist
"y" in o // => false: the property doesn't exist "y" in o // => false: the property doesn't exist
delete o.x; // Delete the property x delete o.x; // Delete the property x
"x" in o // => false: it doesn't exist anymore "x" in o // => false: it doesn't exist anymore
6.6 Enumerating Properties ## 6.6 Enumerating Properties
Instead of testing for the existence of individual properties, we sometimes want to iterate through or obtain a list of all the properties of an object. There are a few different ways to do this. Instead of testing for the existence of individual properties, we sometimes want to iterate through or obtain a list of all the properties of an object. There are a few different ways to do this.
The for/in loop was covered in §5.4.5. It runs the body of the loop once for each enumerable property (own or inherited) of the specified object, assigning the name of the property to the loop variable. Built-in methods that objects inherit are not enumerable, but the properties that your code adds to objects are enumerable by default. For example: The for/in loop was covered in §5.4.5. It runs the body of the loop once for each enumerable property (own or inherited) of the specified object, assigning the name of the property to the loop variable. Built-in methods that objects inherit are not enumerable, but the properties that your code adds to objects are enumerable by default. For example:
...@@ -295,7 +295,7 @@ Reflect.ownKeys() returns all own property names, both enumerable and non-enumer ...@@ -295,7 +295,7 @@ Reflect.ownKeys() returns all own property names, both enumerable and non-enumer
There are examples of the use of Object.keys() with a for/of loop in §6.7. There are examples of the use of Object.keys() with a for/of loop in §6.7.
6.6.1 Property Enumeration Order ### 6.6.1 Property Enumeration Order
ES6 formally defines the order in which the own properties of an object are enumerated. Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols(), Reflect.ownKeys(), and related methods such as JSON.stringify() all list properties in the following order, subject to their own additional constraints about whether they list non-enumerable properties or properties whose names are strings or Symbols: ES6 formally defines the order in which the own properties of an object are enumerated. Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols(), Reflect.ownKeys(), and related methods such as JSON.stringify() all list properties in the following order, subject to their own additional constraints about whether they list non-enumerable properties or properties whose names are strings or Symbols:
String properties whose names are non-negative integers are listed first, in numeric order from smallest to largest. This rule means that arrays and array-like objects will have their properties enumerated in order. String properties whose names are non-negative integers are listed first, in numeric order from smallest to largest. This rule means that arrays and array-like objects will have their properties enumerated in order.
...@@ -306,7 +306,7 @@ Finally, the properties whose names are Symbol objects are listed in the order i ...@@ -306,7 +306,7 @@ Finally, the properties whose names are Symbol objects are listed in the order i
The enumeration order for the for/in loop is not as tightly specified as it is for these enumeration functions, but implementations typically enumerate own properties in the order just described, then travel up the prototype chain enumerating properties in the same order for each prototype object. Note, however, that a property will not be enumerated if a property by that same name has already been enumerated, or even if a non-enumerable property by the same name has already been considered. The enumeration order for the for/in loop is not as tightly specified as it is for these enumeration functions, but implementations typically enumerate own properties in the order just described, then travel up the prototype chain enumerating properties in the same order for each prototype object. Note, however, that a property will not be enumerated if a property by that same name has already been enumerated, or even if a non-enumerable property by the same name has already been considered.
6.7 Extending Objects ## 6.7 Extending Objects
A common operation in JavaScript programs is needing to copy the properties of one object to another object. It is easy to do that with code like this: A common operation in JavaScript programs is needing to copy the properties of one object to another object. It is easy to do that with code like this:
let target = {x: 1}, source = {y: 2, z: 3}; let target = {x: 1}, source = {y: 2, z: 3};
...@@ -347,7 +347,7 @@ Object.assign({x: 1}, {x: 2, y: 2}, {y: 3, z: 4}) // => {x: 2, y: 3, z: 4} ...@@ -347,7 +347,7 @@ Object.assign({x: 1}, {x: 2, y: 2}, {y: 3, z: 4}) // => {x: 2, y: 3, z: 4}
merge({x: 1}, {x: 2, y: 2}, {y: 3, z: 4}) // => {x: 1, y: 2, z: 4} merge({x: 1}, {x: 2, y: 2}, {y: 3, z: 4}) // => {x: 1, y: 2, z: 4}
It is straightforward to write other property manipulation utilities like this merge() function. A restrict() function could delete properties of an object if they do not appear in another template object, for example. Or a subtract() function could remove all of the properties of one object from another object. It is straightforward to write other property manipulation utilities like this merge() function. A restrict() function could delete properties of an object if they do not appear in another template object, for example. Or a subtract() function could remove all of the properties of one object from another object.
6.8 Serializing Objects ## 6.8 Serializing Objects
Object serialization is the process of converting an object’s state to a string from which it can later be restored. The functions JSON.stringify() and JSON.parse() serialize and restore JavaScript objects. These functions use the JSON data interchange format. JSON stands for “JavaScript Object Notation,” and its syntax is very similar to that of JavaScript object and array literals: Object serialization is the process of converting an object’s state to a string from which it can later be restored. The functions JSON.stringify() and JSON.parse() serialize and restore JavaScript objects. These functions use the JSON data interchange format. JSON stands for “JavaScript Object Notation,” and its syntax is very similar to that of JavaScript object and array literals:
let o = {x: 1, y: {z: [false, null, ""]}}; // Define a test object let o = {x: 1, y: {z: [false, null, ""]}}; // Define a test object
...@@ -355,10 +355,10 @@ let s = JSON.stringify(o); // s == '{"x":1,"y":{"z":[false,null,""]}}' ...@@ -355,10 +355,10 @@ let s = JSON.stringify(o); // s == '{"x":1,"y":{"z":[false,null,""]}}'
let p = JSON.parse(s); // p == {x: 1, y: {z: [false, null, ""]}} let p = JSON.parse(s); // p == {x: 1, y: {z: [false, null, ""]}}
JSON syntax is a subset of JavaScript syntax, and it cannot represent all JavaScript values. Objects, arrays, strings, finite numbers, true, false, and null are supported and can be serialized and restored. NaN, Infinity, and -Infinity are serialized to null. Date objects are serialized to ISO-formatted date strings (see the Date.toJSON() function), but JSON.parse() leaves these in string form and does not restore the original Date object. Function, RegExp, and Error objects and the undefined value cannot be serialized or restored. JSON.stringify() serializes only the enumerable own properties of an object. If a property value cannot be serialized, that property is simply omitted from the stringified output. Both JSON.stringify() and JSON.parse() accept optional second arguments that can be used to customize the serialization and/or restoration process by specifying a list of properties to be serialized, for example, or by converting certain values during the serialization or stringification process. Complete documentation for these functions is in §11.6. JSON syntax is a subset of JavaScript syntax, and it cannot represent all JavaScript values. Objects, arrays, strings, finite numbers, true, false, and null are supported and can be serialized and restored. NaN, Infinity, and -Infinity are serialized to null. Date objects are serialized to ISO-formatted date strings (see the Date.toJSON() function), but JSON.parse() leaves these in string form and does not restore the original Date object. Function, RegExp, and Error objects and the undefined value cannot be serialized or restored. JSON.stringify() serializes only the enumerable own properties of an object. If a property value cannot be serialized, that property is simply omitted from the stringified output. Both JSON.stringify() and JSON.parse() accept optional second arguments that can be used to customize the serialization and/or restoration process by specifying a list of properties to be serialized, for example, or by converting certain values during the serialization or stringification process. Complete documentation for these functions is in §11.6.
6.9 Object Methods ## 6.9 Object Methods
As discussed earlier, all JavaScript objects (except those explicitly created without a prototype) inherit properties from Object.prototype. These inherited properties are primarily methods, and because they are universally available, they are of particular interest to JavaScript programmers. We’ve already seen the hasOwnProperty() and propertyIsEnumerable() methods, for example. (And we’ve also already covered quite a few static functions defined on the Object constructor, such as Object.create() and Object.keys().) This section explains a handful of universal object methods that are defined on Object.prototype, but which are intended to be replaced by other, more specialized implementations. In the sections that follow, we show examples of defining these methods on a single object. In Chapter 9, you’ll learn how to define these methods more generally for an entire class of objects. As discussed earlier, all JavaScript objects (except those explicitly created without a prototype) inherit properties from Object.prototype. These inherited properties are primarily methods, and because they are universally available, they are of particular interest to JavaScript programmers. We’ve already seen the hasOwnProperty() and propertyIsEnumerable() methods, for example. (And we’ve also already covered quite a few static functions defined on the Object constructor, such as Object.create() and Object.keys().) This section explains a handful of universal object methods that are defined on Object.prototype, but which are intended to be replaced by other, more specialized implementations. In the sections that follow, we show examples of defining these methods on a single object. In Chapter 9, you’ll learn how to define these methods more generally for an entire class of objects.
6.9.1 The toString() Method ### 6.9.1 The toString() Method
The toString() method takes no arguments; it returns a string that somehow represents the value of the object on which it is invoked. JavaScript invokes this method of an object whenever it needs to convert the object to a string. This occurs, for example, when you use the + operator to concatenate a string with an object or when you pass an object to a method that expects a string. The toString() method takes no arguments; it returns a string that somehow represents the value of the object on which it is invoked. JavaScript invokes this method of an object whenever it needs to convert the object to a string. This occurs, for example, when you use the + operator to concatenate a string with an object or when you pass an object to a method that expects a string.
The default toString() method is not very informative (though it is useful for determining the class of an object, as we will see in §14.4.3). For example, the following line of code simply evaluates to the string “[object Object]”: The default toString() method is not very informative (though it is useful for determining the class of an object, as we will see in §14.4.3). For example, the following line of code simply evaluates to the string “[object Object]”:
...@@ -372,7 +372,7 @@ let point = { ...@@ -372,7 +372,7 @@ let point = {
toString: function() { return `(${this.x}, ${this.y})`; } toString: function() { return `(${this.x}, ${this.y})`; }
}; };
String(point) // => "(1, 2)": toString() is used for string conversions String(point) // => "(1, 2)": toString() is used for string conversions
6.9.2 The toLocaleString() Method ### 6.9.2 The toLocaleString() Method
In addition to the basic toString() method, objects all have a toLocaleString(). The purpose of this method is to return a localized string representation of the object. The default toLocaleString() method defined by Object doesn’t do any localization itself: it simply calls toString() and returns that value. The Date and Number classes define customized versions of toLocaleString() that attempt to format numbers, dates, and times according to local conventions. Array defines a toLocaleString() method that works like toString() except that it formats array elements by calling their toLocaleString() methods instead of their toString() methods. You might do the same thing with a point object like this: In addition to the basic toString() method, objects all have a toLocaleString(). The purpose of this method is to return a localized string representation of the object. The default toLocaleString() method defined by Object doesn’t do any localization itself: it simply calls toString() and returns that value. The Date and Number classes define customized versions of toLocaleString() that attempt to format numbers, dates, and times according to local conventions. Array defines a toLocaleString() method that works like toString() except that it formats array elements by calling their toLocaleString() methods instead of their toString() methods. You might do the same thing with a point object like this:
let point = { let point = {
...@@ -387,7 +387,7 @@ point.toString() // => "(1000, 2000)" ...@@ -387,7 +387,7 @@ point.toString() // => "(1000, 2000)"
point.toLocaleString() // => "(1,000, 2,000)": note thousands separators point.toLocaleString() // => "(1,000, 2,000)": note thousands separators
The internationalization classes documented in §11.7 can be useful when implementing a toLocaleString() method. The internationalization classes documented in §11.7 can be useful when implementing a toLocaleString() method.
6.9.3 The valueOf() Method ### 6.9.3 The valueOf() Method
The valueOf() method is much like the toString() method, but it is called when JavaScript needs to convert an object to some primitive type other than a string—typically, a number. JavaScript calls this method automatically if an object is used in a context where a primitive value is required. The default valueOf() method does nothing interesting, but some of the built-in classes define their own valueOf() method. The Date class defines valueOf() to convert dates to numbers, and this allows Date objects to be chronologically compared with < and >. You could do something similar with a point object, defining a valueOf() method that returns the distance from the origin to the point: The valueOf() method is much like the toString() method, but it is called when JavaScript needs to convert an object to some primitive type other than a string—typically, a number. JavaScript calls this method automatically if an object is used in a context where a primitive value is required. The default valueOf() method does nothing interesting, but some of the built-in classes define their own valueOf() method. The Date class defines valueOf() to convert dates to numbers, and this allows Date objects to be chronologically compared with < and >. You could do something similar with a point object, defining a valueOf() method that returns the distance from the origin to the point:
let point = { let point = {
...@@ -399,7 +399,7 @@ Number(point) // => 5: valueOf() is used for conversions to numbers ...@@ -399,7 +399,7 @@ Number(point) // => 5: valueOf() is used for conversions to numbers
point > 4 // => true point > 4 // => true
point > 5 // => false point > 5 // => false
point < 6 // => true point < 6 // => true
6.9.4 The toJSON() Method ### 6.9.4 The toJSON() Method
Object.prototype does not actually define a toJSON() method, but the JSON.stringify() method (see §6.8) looks for a toJSON() method on any object it is asked to serialize. If this method exists on the object to be serialized, it is invoked, and the return value is serialized, instead of the original object. The Date class (§11.4) defines a toJSON() method that returns a serializable string representation of the date. We could do the same for our Point object like this: Object.prototype does not actually define a toJSON() method, but the JSON.stringify() method (see §6.8) looks for a toJSON() method on any object it is asked to serialize. If this method exists on the object to be serialized, it is invoked, and the return value is serialized, instead of the original object. The Date class (§11.4) defines a toJSON() method that returns a serializable string representation of the date. We could do the same for our Point object like this:
let point = { let point = {
...@@ -409,10 +409,10 @@ let point = { ...@@ -409,10 +409,10 @@ let point = {
toJSON: function() { return this.toString(); } toJSON: function() { return this.toString(); }
}; };
JSON.stringify([point]) // => '["(1, 2)"]' JSON.stringify([point]) // => '["(1, 2)"]'
6.10 Extended Object Literal Syntax ## 6.10 Extended Object Literal Syntax
Recent versions of JavaScript have extended the syntax for object literals in a number of useful ways. The following subsections explain these extensions. Recent versions of JavaScript have extended the syntax for object literals in a number of useful ways. The following subsections explain these extensions.
6.10.1 Shorthand Properties ### 6.10.1 Shorthand Properties
Suppose you have values stored in variables x and y and want to create an object with properties named x and y that hold those values. With basic object literal syntax, you’d end up repeating each identifier twice: Suppose you have values stored in variables x and y and want to create an object with properties named x and y that hold those values. With basic object literal syntax, you’d end up repeating each identifier twice:
let x = 1, y = 2; let x = 1, y = 2;
...@@ -425,7 +425,7 @@ In ES6 and later, you can drop the colon and one copy of the identifier and end ...@@ -425,7 +425,7 @@ In ES6 and later, you can drop the colon and one copy of the identifier and end
let x = 1, y = 2; let x = 1, y = 2;
let o = { x, y }; let o = { x, y };
o.x + o.y // => 3 o.x + o.y // => 3
6.10.2 Computed Property Names ### 6.10.2 Computed Property Names
Sometimes you need to create an object with a specific property, but the name of that property is not a compile-time constant that you can type literally in your source code. Instead, the property name you need is stored in a variable or is the return value of a function that you invoke. You can’t use a basic object literal for this kind of property. Instead, you have to create an object and then add the desired properties as an extra step: Sometimes you need to create an object with a specific property, but the name of that property is not a compile-time constant that you can type literally in your source code. Instead, the property name you need is stored in a variable or is the return value of a function that you invoke. You can’t use a basic object literal for this kind of property. Instead, you have to create an object and then add the desired properties as an extra step:
const PROPERTY_NAME = "p1"; const PROPERTY_NAME = "p1";
...@@ -449,7 +449,7 @@ With this new syntax, the square brackets delimit an arbitrary JavaScript expres ...@@ -449,7 +449,7 @@ With this new syntax, the square brackets delimit an arbitrary JavaScript expres
One situation where you might want to use computed properties is when you have a library of JavaScript code that expects to be passed objects with a particular set of properties, and the names of those properties are defined as constants in that library. If you are writing code to create the objects that will be passed to that library, you could hardcode the property names, but you’d risk bugs if you type the property name wrong anywhere, and you’d risk version mismatch issues if a new version of the library changes the required property names. Instead, you might find that it makes your code more robust to use computed property syntax with the property name constants defined by the library. One situation where you might want to use computed properties is when you have a library of JavaScript code that expects to be passed objects with a particular set of properties, and the names of those properties are defined as constants in that library. If you are writing code to create the objects that will be passed to that library, you could hardcode the property names, but you’d risk bugs if you type the property name wrong anywhere, and you’d risk version mismatch issues if a new version of the library changes the required property names. Instead, you might find that it makes your code more robust to use computed property syntax with the property name constants defined by the library.
6.10.3 Symbols as Property Names ### 6.10.3 Symbols as Property Names
The computed property syntax enables one other very important object literal feature. In ES6 and later, property names can be strings or symbols. If you assign a symbol to a variable or constant, then you can use that symbol as a property name using the computed property syntax: The computed property syntax enables one other very important object literal feature. In ES6 and later, property names can be strings or symbols. If you assign a symbol to a variable or constant, then you can use that symbol as a property name using the computed property syntax:
const extension = Symbol("my extension symbol"); const extension = Symbol("my extension symbol");
...@@ -461,7 +461,7 @@ As explained in §3.6, Symbols are opaque values. You can’t do anything with t ...@@ -461,7 +461,7 @@ As explained in §3.6, Symbols are opaque values. You can’t do anything with t
The point of Symbols is not security, but to define a safe extension mechanism for JavaScript objects. If you get an object from third-party code that you do not control and need to add some of your own properties to that object but want to be sure that your properties will not conflict with any properties that may already exist on the object, you can safely use Symbols as your property names. If you do this, you can also be confident that the third-party code will not accidentally alter your symbolically named properties. (That third-party code could, of course, use Object.getOwnPropertySymbols() to discover the Symbols you’re using and could then alter or delete your properties. This is why Symbols are not a security mechanism.) The point of Symbols is not security, but to define a safe extension mechanism for JavaScript objects. If you get an object from third-party code that you do not control and need to add some of your own properties to that object but want to be sure that your properties will not conflict with any properties that may already exist on the object, you can safely use Symbols as your property names. If you do this, you can also be confident that the third-party code will not accidentally alter your symbolically named properties. (That third-party code could, of course, use Object.getOwnPropertySymbols() to discover the Symbols you’re using and could then alter or delete your properties. This is why Symbols are not a security mechanism.)
6.10.4 Spread Operator ### 6.10.4 Spread Operator
In ES2018 and later, you can copy the properties of an existing object into a new object using the “spread operator” ... inside an object literal: In ES2018 and later, you can copy the properties of an existing object into a new object using the “spread operator” ... inside an object literal:
let position = { x: 0, y: 0 }; let position = { x: 0, y: 0 };
...@@ -484,7 +484,7 @@ let p = { ...o }; ...@@ -484,7 +484,7 @@ let p = { ...o };
p.x // => undefined p.x // => undefined
Finally, it is worth noting that, although the spread operator is just three little dots in your code, it can represent a substantial amount of work to the JavaScript interpreter. If an object has n properties, the process of spreading those properties into another object is likely to be an O(n) operation. This means that if you find yourself using ... within a loop or recursive function as a way to accumulate data into one large object, you may be writing an inefficient O(n2) algorithm that will not scale well as n gets larger. Finally, it is worth noting that, although the spread operator is just three little dots in your code, it can represent a substantial amount of work to the JavaScript interpreter. If an object has n properties, the process of spreading those properties into another object is likely to be an O(n) operation. This means that if you find yourself using ... within a loop or recursive function as a way to accumulate data into one large object, you may be writing an inefficient O(n2) algorithm that will not scale well as n gets larger.
6.10.5 Shorthand Methods ### 6.10.5 Shorthand Methods
When a function is defined as a property of an object, we call that function a method (we’ll have a lot more to say about methods in Chapters 8 and 9). Prior to ES6, you would define a method in an object literal using a function definition expression just as you would define any other property of an object: When a function is defined as a property of an object, we call that function a method (we’ll have a lot more to say about methods in Chapters 8 and 9). Prior to ES6, you would define a method in an object literal using a function definition expression just as you would define any other property of an object:
let square = { let square = {
...@@ -515,7 +515,7 @@ weirdMethods[METHOD_NAME](1) // => 3 ...@@ -515,7 +515,7 @@ weirdMethods[METHOD_NAME](1) // => 3
weirdMethods[symbol](1) // => 4 weirdMethods[symbol](1) // => 4
Using a Symbol as a method name is not as strange as it seems. In order to make an object iterable (so it can be used with a for/of loop), you must define a method with the symbolic name Symbol.iterator, and there are examples of doing exactly that in Chapter 12. Using a Symbol as a method name is not as strange as it seems. In order to make an object iterable (so it can be used with a for/of loop), you must define a method with the symbolic name Symbol.iterator, and there are examples of doing exactly that in Chapter 12.
6.10.6 Property Getters and Setters ### 6.10.6 Property Getters and Setters
All of the object properties we’ve discussed so far in this chapter have been data properties with a name and an ordinary value. JavaScript also supports accessor properties, which do not have a single value but instead have one or two accessor methods: a getter and/or a setter. All of the object properties we’ve discussed so far in this chapter have been data properties with a name and an ordinary value. JavaScript also supports accessor properties, which do not have a single value but instead have one or two accessor methods: a getter and/or a setter.
When a program queries the value of an accessor property, JavaScript invokes the getter method (passing no arguments). The return value of this method becomes the value of the property access expression. When a program sets the value of an accessor property, JavaScript invokes the setter method, passing the value of the righthand side of the assignment. This method is responsible for “setting,” in some sense, the property value. The return value of the setter method is ignored. When a program queries the value of an accessor property, JavaScript invokes the getter method (passing no arguments). The return value of this method becomes the value of the property access expression. When a program sets the value of an accessor property, JavaScript invokes the setter method, passing the value of the righthand side of the assignment. This method is responsible for “setting,” in some sense, the property value. The return value of the setter method is ignored.
...@@ -594,7 +594,7 @@ const random = { ...@@ -594,7 +594,7 @@ const random = {
get uint16() { return Math.floor(Math.random()*65536); }, get uint16() { return Math.floor(Math.random()*65536); },
get int16() { return Math.floor(Math.random()*65536)-32768; } get int16() { return Math.floor(Math.random()*65536)-32768; }
}; };
6.11 Summary ## 6.11 Summary
This chapter has documented JavaScript objects in great detail, covering topics that include: This chapter has documented JavaScript objects in great detail, covering topics that include:
Basic object terminology, including the meaning of terms like enumerable and own property. Basic object terminology, including the meaning of terms like enumerable and own property.
......
...@@ -7,7 +7,7 @@ Arrays inherit properties from Array.prototype, which defines a rich set of arra ...@@ -7,7 +7,7 @@ Arrays inherit properties from Array.prototype, which defines a rich set of arra
ES6 introduces a set of new array classes known collectively as “typed arrays.” Unlike regular JavaScript arrays, typed arrays have a fixed length and a fixed numeric element type. They offer high performance and byte-level access to binary data and are covered in §11.2. ES6 introduces a set of new array classes known collectively as “typed arrays.” Unlike regular JavaScript arrays, typed arrays have a fixed length and a fixed numeric element type. They offer high performance and byte-level access to binary data and are covered in §11.2.
7.1 Creating Arrays ## 7.1 Creating Arrays
There are several ways to create arrays. The subsections that follow explain how to create arrays with: There are several ways to create arrays. The subsections that follow explain how to create arrays with:
Array literals Array literals
...@@ -18,7 +18,7 @@ The Array() constructor ...@@ -18,7 +18,7 @@ The Array() constructor
The Array.of() and Array.from() factory methods The Array.of() and Array.from() factory methods
7.1.1 Array Literals ### 7.1.1 Array Literals
By far the simplest way to create an array is with an array literal, which is simply a comma-separated list of array elements within square brackets. For example: By far the simplest way to create an array is with an array literal, which is simply a comma-separated list of array elements within square brackets. For example:
let empty = []; // An array with no elements let empty = []; // An array with no elements
...@@ -37,7 +37,7 @@ let count = [1,,3]; // Elements at indexes 0 and 2. No element at index 1 ...@@ -37,7 +37,7 @@ let count = [1,,3]; // Elements at indexes 0 and 2. No element at index 1
let undefs = [,,]; // An array with no elements but a length of 2 let undefs = [,,]; // An array with no elements but a length of 2
Array literal syntax allows an optional trailing comma, so [,,] has a length of 2, not 3. Array literal syntax allows an optional trailing comma, so [,,] has a length of 2, not 3.
7.1.2 The Spread Operator ### 7.1.2 The Spread Operator
In ES6 and later, you can use the “spread operator,” ..., to include the elements of one array within an array literal: In ES6 and later, you can use the “spread operator,” ..., to include the elements of one array within an array literal:
let a = [1, 2, 3]; let a = [1, 2, 3];
...@@ -58,7 +58,7 @@ Set objects (§11.1.1) are iterable, so an easy way to remove duplicate elements ...@@ -58,7 +58,7 @@ Set objects (§11.1.1) are iterable, so an easy way to remove duplicate elements
let letters = [..."hello world"]; let letters = [..."hello world"];
[...new Set(letters)] // => ["h","e","l","o"," ","w","r","d"] [...new Set(letters)] // => ["h","e","l","o"," ","w","r","d"]
7.1.3 The Array() Constructor ### 7.1.3 The Array() Constructor
Another way to create an array is with the Array() constructor. You can invoke this constructor in three distinct ways: Another way to create an array is with the Array() constructor. You can invoke this constructor in three distinct ways:
Call it with no arguments: Call it with no arguments:
...@@ -76,7 +76,7 @@ Explicitly specify two or more array elements or a single non-numeric element fo ...@@ -76,7 +76,7 @@ Explicitly specify two or more array elements or a single non-numeric element fo
let a = new Array(5, 4, 3, 2, 1, "testing, testing"); let a = new Array(5, 4, 3, 2, 1, "testing, testing");
In this form, the constructor arguments become the elements of the new array. Using an array literal is almost always simpler than this usage of the Array() constructor. In this form, the constructor arguments become the elements of the new array. Using an array literal is almost always simpler than this usage of the Array() constructor.
7.1.4 Array.of() ### 7.1.4 Array.of()
When the Array() constructor function is invoked with one numeric argument, it uses that argument as an array length. But when invoked with more than one numeric argument, it treats those arguments as elements for the array to be created. This means that the Array() constructor cannot be used to create an array with a single numeric element. When the Array() constructor function is invoked with one numeric argument, it uses that argument as an array length. But when invoked with more than one numeric argument, it treats those arguments as elements for the array to be created. This means that the Array() constructor cannot be used to create an array with a single numeric element.
In ES6, the Array.of() function addresses this problem: it is a factory method that creates and returns a new array, using its argument values (regardless of how many of them there are) as the array elements: In ES6, the Array.of() function addresses this problem: it is a factory method that creates and returns a new array, using its argument values (regardless of how many of them there are) as the array elements:
...@@ -84,7 +84,7 @@ In ES6, the Array.of() function addresses this problem: it is a factory method t ...@@ -84,7 +84,7 @@ In ES6, the Array.of() function addresses this problem: it is a factory method t
Array.of() // => []; returns empty array with no arguments Array.of() // => []; returns empty array with no arguments
Array.of(10) // => [10]; can create arrays with a single numeric argument Array.of(10) // => [10]; can create arrays with a single numeric argument
Array.of(1,2,3) // => [1, 2, 3] Array.of(1,2,3) // => [1, 2, 3]
7.1.5 Array.from() ### 7.1.5 Array.from()
Array.from is another array factory method introduced in ES6. It expects an iterable or array-like object as its first argument and returns a new array that contains the elements of that object. With an iterable argument, Array.from(iterable) works like the spread operator [...iterable] does. It is also a simple way to make a copy of an array: Array.from is another array factory method introduced in ES6. It expects an iterable or array-like object as its first argument and returns a new array that contains the elements of that object. With an iterable argument, Array.from(iterable) works like the spread operator [...iterable] does. It is also a simple way to make a copy of an array:
let copy = Array.from(original); let copy = Array.from(original);
...@@ -93,7 +93,7 @@ Array.from() is also important because it defines a way to make a true-array cop ...@@ -93,7 +93,7 @@ Array.from() is also important because it defines a way to make a true-array cop
let truearray = Array.from(arraylike); let truearray = Array.from(arraylike);
Array.from() also accepts an optional second argument. If you pass a function as the second argument, then as the new array is being built, each element from the source object will be passed to the function you specify, and the return value of the function will be stored in the array instead of the original value. (This is very much like the array map() method that will be introduced later in the chapter, but it is more efficient to perform the mapping while the array is being built than it is to build the array and then map it to another new array.) Array.from() also accepts an optional second argument. If you pass a function as the second argument, then as the new array is being built, each element from the source object will be passed to the function you specify, and the return value of the function will be stored in the array instead of the original value. (This is very much like the array map() method that will be introduced later in the chapter, but it is more efficient to perform the mapping while the array is being built than it is to build the array and then map it to another new array.)
7.2 Reading and Writing Array Elements ## 7.2 Reading and Writing Array Elements
You access an element of an array using the [] operator. A reference to the array should appear to the left of the brackets. An arbitrary expression that has a non-negative integer value should be inside the brackets. You can use this syntax to both read and write the value of an element of an array. Thus, the following are all legal JavaScript statements: You access an element of an array using the [] operator. A reference to the array should appear to the left of the brackets. An arbitrary expression that has a non-negative integer value should be inside the brackets. You can use this syntax to both read and write the value of an element of an array. Thus, the following are all legal JavaScript statements:
let a = ["world"]; // Start with a one-element array let a = ["world"]; // Start with a one-element array
...@@ -123,7 +123,7 @@ The fact that array indexes are simply a special type of object property name me ...@@ -123,7 +123,7 @@ The fact that array indexes are simply a special type of object property name me
let a = [true, false]; // This array has elements at indexes 0 and 1 let a = [true, false]; // This array has elements at indexes 0 and 1
a[2] // => undefined; no element at this index. a[2] // => undefined; no element at this index.
a[-1] // => undefined; no property with this name. a[-1] // => undefined; no property with this name.
7.3 Sparse Arrays ## 7.3 Sparse Arrays
A sparse array is one in which the elements do not have contiguous indexes starting at 0. Normally, the length property of an array specifies the number of elements in the array. If the array is sparse, the value of the length property is greater than the number of elements. Sparse arrays can be created with the Array() constructor or simply by assigning to an array index larger than the current array length. A sparse array is one in which the elements do not have contiguous indexes starting at 0. Normally, the length property of an array specifies the number of elements in the array. If the array is sparse, the value of the length property is greater than the number of elements. Sparse arrays can be created with the Array() constructor or simply by assigning to an array index larger than the current array length.
let a = new Array(5); // No elements, but a.length is 5. let a = new Array(5); // No elements, but a.length is 5.
...@@ -141,7 +141,7 @@ let a2 = [undefined]; // This array has one undefined element ...@@ -141,7 +141,7 @@ let a2 = [undefined]; // This array has one undefined element
0 in a2 // => true: a2 has the undefined value at index 0 0 in a2 // => true: a2 has the undefined value at index 0
Understanding sparse arrays is an important part of understanding the true nature of JavaScript arrays. In practice, however, most JavaScript arrays you will work with will not be sparse. And, if you do have to work with a sparse array, your code will probably treat it just as it would treat a nonsparse array with undefined elements. Understanding sparse arrays is an important part of understanding the true nature of JavaScript arrays. In practice, however, most JavaScript arrays you will work with will not be sparse. And, if you do have to work with a sparse array, your code will probably treat it just as it would treat a nonsparse array with undefined elements.
7.4 Array Length ## 7.4 Array Length
Every array has a length property, and it is this property that makes arrays different from regular JavaScript objects. For arrays that are dense (i.e., not sparse), the length property specifies the number of elements in the array. Its value is one more than the highest index in the array: Every array has a length property, and it is this property that makes arrays different from regular JavaScript objects. For arrays that are dense (i.e., not sparse), the length property specifies the number of elements in the array. Its value is one more than the highest index in the array:
[].length // => 0: the array has no elements [].length // => 0: the array has no elements
...@@ -156,7 +156,7 @@ a.length = 0; // Delete all elements. a is []. ...@@ -156,7 +156,7 @@ a.length = 0; // Delete all elements. a is [].
a.length = 5; // Length is 5, but no elements, like new Array(5) a.length = 5; // Length is 5, but no elements, like new Array(5)
You can also set the length property of an array to a value larger than its current value. Doing this does not actually add any new elements to the array; it simply creates a sparse area at the end of the array. You can also set the length property of an array to a value larger than its current value. Doing this does not actually add any new elements to the array; it simply creates a sparse area at the end of the array.
7.5 Adding and Deleting Array Elements ## 7.5 Adding and Deleting Array Elements
We’ve already seen the simplest way to add elements to an array: just assign values to new indexes: We’ve already seen the simplest way to add elements to an array: just assign values to new indexes:
let a = []; // Start with an empty array. let a = []; // Start with an empty array.
...@@ -181,7 +181,7 @@ As we saw above, you can also remove elements from the end of an array simply by ...@@ -181,7 +181,7 @@ As we saw above, you can also remove elements from the end of an array simply by
Finally, splice() is the general-purpose method for inserting, deleting, or replacing array elements. It alters the length property and shifts array elements to higher or lower indexes as needed. See §7.8 for details. Finally, splice() is the general-purpose method for inserting, deleting, or replacing array elements. It alters the length property and shifts array elements to higher or lower indexes as needed. See §7.8 for details.
7.6 Iterating Arrays ## 7.6 Iterating Arrays
As of ES6, the easiest way to loop through each of the elements of an array (or any iterable object) is with the for/of loop, which was covered in detail in §5.4.4: As of ES6, the easiest way to loop through each of the elements of an array (or any iterable object) is with the for/of loop, which was covered in detail in §5.4.4:
let letters = [..."Hello world"]; // An array of letters let letters = [..."Hello world"]; // An array of letters
...@@ -237,7 +237,7 @@ for(let i = 0; i < a.length; i++) { ...@@ -237,7 +237,7 @@ for(let i = 0; i < a.length; i++) {
if (a[i] === undefined) continue; // Skip undefined + nonexistent elements if (a[i] === undefined) continue; // Skip undefined + nonexistent elements
// loop body here // loop body here
} }
7.7 Multidimensional Arrays ## 7.7 Multidimensional Arrays
JavaScript does not support true multidimensional arrays, but you can approximate them with arrays of arrays. To access a value in an array of arrays, simply use the [] operator twice. For example, suppose the variable matrix is an array of arrays of numbers. Every element in matrix[x] is an array of numbers. To access a particular number within this array, you would write matrix[x][y]. Here is a concrete example that uses a two-dimensional array as a multiplication table: JavaScript does not support true multidimensional arrays, but you can approximate them with arrays of arrays. To access a value in an array of arrays, simply use the [] operator twice. For example, suppose the variable matrix is an array of arrays of numbers. Every element in matrix[x] is an array of numbers. To access a particular number within this array, you would write matrix[x][y]. Here is a concrete example that uses a two-dimensional array as a multiplication table:
// Create a multidimensional array // Create a multidimensional array
...@@ -255,7 +255,7 @@ for(let row = 0; row < table.length; row++) { ...@@ -255,7 +255,7 @@ for(let row = 0; row < table.length; row++) {
// Use the multidimensional array to compute 5*7 // Use the multidimensional array to compute 5*7
table[5][7] // => 35 table[5][7] // => 35
7.8 Array Methods ## 7.8 Array Methods
The preceding sections have focused on basic JavaScript syntax for working with arrays. In general, though, it is the methods defined by the Array class that are the most powerful. The next sections document these methods. While reading about these methods, keep in mind that some of them modify the array they are called on and some of them leave the array unchanged. A number of the methods return an array: sometimes, this is a new array, and the original is unchanged. Other times, a method will modify the array in place and will also return a reference to the modified array. The preceding sections have focused on basic JavaScript syntax for working with arrays. In general, though, it is the methods defined by the Array class that are the most powerful. The next sections document these methods. While reading about these methods, keep in mind that some of them modify the array they are called on and some of them leave the array unchanged. A number of the methods return an array: sometimes, this is a new array, and the original is unchanged. Other times, a method will modify the array in place and will also return a reference to the modified array.
Each of the subsections that follows covers a group of related array methods: Each of the subsections that follows covers a group of related array methods:
...@@ -270,7 +270,7 @@ Searching and sorting methods are for locating elements within an array and for ...@@ -270,7 +270,7 @@ Searching and sorting methods are for locating elements within an array and for
The following subsections also cover the static methods of the Array class and a few miscellaneous methods for concatenating arrays and converting arrays to strings. The following subsections also cover the static methods of the Array class and a few miscellaneous methods for concatenating arrays and converting arrays to strings.
7.8.1 Array Iterator Methods ### 7.8.1 Array Iterator Methods
The methods described in this section iterate over arrays by passing array elements, in order, to a function you supply, and they provide convenient ways to iterate, map, filter, test, and reduce arrays. The methods described in this section iterate over arrays by passing array elements, in order, to a function you supply, and they provide convenient ways to iterate, map, filter, test, and reduce arrays.
Before we explain the methods in detail, however, it is worth making some generalizations about them. First, all of these methods accept a function as their first argument and invoke that function once for each element (or some elements) of the array. If the array is sparse, the function you pass is not invoked for nonexistent elements. In most cases, the function you supply is invoked with three arguments: the value of the array element, the index of the array element, and the array itself. Often, you only need the first of these argument values and can ignore the second and third values. Before we explain the methods in detail, however, it is worth making some generalizations about them. First, all of these methods accept a function as their first argument and invoke that function once for each element (or some elements) of the array. If the array is sparse, the function you pass is not invoked for nonexistent elements. In most cases, the function you supply is invoked with three arguments: the value of the array element, the index of the array element, and the array itself. Often, you only need the first of these argument values and can ignore the second and third values.
...@@ -356,7 +356,7 @@ Note that neither reduce() nor reduceRight() accepts an optional argument that s ...@@ -356,7 +356,7 @@ Note that neither reduce() nor reduceRight() accepts an optional argument that s
The examples shown so far have been numeric for simplicity, but reduce() and reduceRight() are not intended solely for mathematical computations. Any function that can combine two values (such as two objects) into one value of the same type can be used as a reduction function. On the other hand, algorithms expressed using array reductions can quickly become complex and hard to understand, and you may find that it is easier to read, write, and reason about your code if you use regular looping constructs to process your arrays. The examples shown so far have been numeric for simplicity, but reduce() and reduceRight() are not intended solely for mathematical computations. Any function that can combine two values (such as two objects) into one value of the same type can be used as a reduction function. On the other hand, algorithms expressed using array reductions can quickly become complex and hard to understand, and you may find that it is easier to read, write, and reason about your code if you use regular looping constructs to process your arrays.
7.8.2 Flattening arrays with flat() and flatMap() ### 7.8.2 Flattening arrays with flat() and flatMap()
In ES2019, the flat() method creates and returns a new array that contains the same elements as the array it is called on, except that any elements that are themselves arrays are “flattened” into the returned array. For example: In ES2019, the flat() method creates and returns a new array that contains the same elements as the array it is called on, except that any elements that are themselves arrays are “flattened” into the returned array. For example:
[1, [2, 3]].flat() // => [1, 2, 3] [1, [2, 3]].flat() // => [1, 2, 3]
...@@ -377,7 +377,7 @@ You can think of flatMap() as a generalization of map() that allows each element ...@@ -377,7 +377,7 @@ You can think of flatMap() as a generalization of map() that allows each element
// Map non-negative numbers to their square roots // Map non-negative numbers to their square roots
[-2, -1, 1, 2].flatMap(x => x < 0 ? [] : Math.sqrt(x)) // => [1, 2**0.5] [-2, -1, 1, 2].flatMap(x => x < 0 ? [] : Math.sqrt(x)) // => [1, 2**0.5]
7.8.3 Adding arrays with concat() ### 7.8.3 Adding arrays with concat()
The concat() method creates and returns a new array that contains the elements of the original array on which concat() was invoked, followed by each of the arguments to concat(). If any of these arguments is itself an array, then it is the array elements that are concatenated, not the array itself. Note, however, that concat() does not recursively flatten arrays of arrays. concat() does not modify the array on which it is invoked: The concat() method creates and returns a new array that contains the elements of the original array on which concat() was invoked, followed by each of the arguments to concat(). If any of these arguments is itself an array, then it is the array elements that are concatenated, not the array itself. Note, however, that concat() does not recursively flatten arrays of arrays. concat() does not modify the array on which it is invoked:
let a = [1,2,3]; let a = [1,2,3];
...@@ -387,7 +387,7 @@ a.concat(4, [5,[6,7]]) // => [1,2,3,4,5,[6,7]]; but not nested arrays ...@@ -387,7 +387,7 @@ a.concat(4, [5,[6,7]]) // => [1,2,3,4,5,[6,7]]; but not nested arrays
a // => [1,2,3]; the original array is unmodified a // => [1,2,3]; the original array is unmodified
Note that concat() makes a new copy of the array it is called on. In many cases, this is the right thing to do, but it is an expensive operation. If you find yourself writing code like a = a.concat(x), then you should think about modifying your array in place with push() or splice() instead of creating a new one. Note that concat() makes a new copy of the array it is called on. In many cases, this is the right thing to do, but it is an expensive operation. If you find yourself writing code like a = a.concat(x), then you should think about modifying your array in place with push() or splice() instead of creating a new one.
7.8.4 Stacks and Queues with push(), pop(), shift(), and unshift() ### 7.8.4 Stacks and Queues with push(), pop(), shift(), and unshift()
The push() and pop() methods allow you to work with arrays as if they were stacks. The push() method appends one or more new elements to the end of an array and returns the new length of the array. Unlike concat(), push() does not flatten array arguments. The pop() method does the reverse: it deletes the last element of an array, decrements the array length, and returns the value that it removed. Note that both methods modify the array in place. The combination of push() and pop() allows you to use a JavaScript array to implement a first-in, last-out stack. For example: The push() and pop() methods allow you to work with arrays as if they were stacks. The push() method appends one or more new elements to the end of an array and returns the new length of the array. Unlike concat(), push() does not flatten array arguments. The pop() method does the reverse: it deletes the last element of an array, decrements the array length, and returns the value that it removed. Note that both methods modify the array in place. The combination of push() and pop() allows you to use a JavaScript array to implement a first-in, last-out stack. For example:
let stack = []; // stack == [] let stack = []; // stack == []
...@@ -416,7 +416,7 @@ a.unshift(1) // a == [1] ...@@ -416,7 +416,7 @@ a.unshift(1) // a == [1]
a.unshift(2) // a == [2, 1] a.unshift(2) // a == [2, 1]
a = []; // a == [] a = []; // a == []
a.unshift(1,2) // a == [1, 2] a.unshift(1,2) // a == [1, 2]
7.8.5 Subarrays with slice(), splice(), fill(), and copyWithin() ### 7.8.5 Subarrays with slice(), splice(), fill(), and copyWithin()
Arrays define a number of methods that work on contiguous regions, or subarrays or “slices” of an array. The following sections describe methods for extracting, replacing, filling, and copying slices. Arrays define a number of methods that work on contiguous regions, or subarrays or “slices” of an array. The following sections describe methods for extracting, replacing, filling, and copying slices.
SLICE() SLICE()
...@@ -461,7 +461,7 @@ a.copyWithin(2, 3, 5) // => [1,1,3,4,4]: copy last 2 elements to index 2 ...@@ -461,7 +461,7 @@ a.copyWithin(2, 3, 5) // => [1,1,3,4,4]: copy last 2 elements to index 2
a.copyWithin(0, -2) // => [4,4,3,4,4]: negative offsets work, too a.copyWithin(0, -2) // => [4,4,3,4,4]: negative offsets work, too
copyWithin() is intended as a high-performance method that is particularly useful with typed arrays (see §11.2). It is modeled after the memmove() function from the C standard library. Note that the copy will work correctly even if there is overlap between the source and destination regions. copyWithin() is intended as a high-performance method that is particularly useful with typed arrays (see §11.2). It is modeled after the memmove() function from the C standard library. Note that the copy will work correctly even if there is overlap between the source and destination regions.
7.8.6 Array Searching and Sorting Methods ### 7.8.6 Array Searching and Sorting Methods
Arrays implement indexOf(), lastIndexOf(), and includes() methods that are similar to the same-named methods of strings. There are also sort() and reverse() methods for reordering the elements of an array. These methods are described in the subsections that follow. Arrays implement indexOf(), lastIndexOf(), and includes() methods that are similar to the same-named methods of strings. There are also sort() and reverse() methods for reordering the elements of an array. These methods are described in the subsections that follow.
INDEXOF() AND LASTINDEXOF() INDEXOF() AND LASTINDEXOF()
...@@ -534,7 +534,7 @@ The reverse() method reverses the order of the elements of an array and returns ...@@ -534,7 +534,7 @@ The reverse() method reverses the order of the elements of an array and returns
let a = [1,2,3]; let a = [1,2,3];
a.reverse(); // a == [3,2,1] a.reverse(); // a == [3,2,1]
7.8.7 Array to String Conversions ### 7.8.7 Array to String Conversions
The Array class defines three methods that can convert arrays to strings, which is generally something you might do when creating log and error messages. (If you want to save the contents of an array in textual form for later reuse, serialize the array with JSON.stringify() [§6.8] instead of using the methods described here.) The Array class defines three methods that can convert arrays to strings, which is generally something you might do when creating log and error messages. (If you want to save the contents of an array in textual form for later reuse, serialize the array with JSON.stringify() [§6.8] instead of using the methods described here.)
The join() method converts all the elements of an array to strings and concatenates them, returning the resulting string. You can specify an optional string that separates the elements in the resulting string. If no separator string is specified, a comma is used: The join() method converts all the elements of an array to strings and concatenates them, returning the resulting string. You can specify an optional string that separates the elements in the resulting string. If no separator string is specified, a comma is used:
...@@ -556,14 +556,14 @@ Note that the output does not include square brackets or any other sort of delim ...@@ -556,14 +556,14 @@ Note that the output does not include square brackets or any other sort of delim
toLocaleString() is the localized version of toString(). It converts each array element to a string by calling the toLocaleString() method of the element, and then it concatenates the resulting strings using a locale-specific (and implementation-defined) separator string. toLocaleString() is the localized version of toString(). It converts each array element to a string by calling the toLocaleString() method of the element, and then it concatenates the resulting strings using a locale-specific (and implementation-defined) separator string.
7.8.8 Static Array Functions ### 7.8.8 Static Array Functions
In addition to the array methods we’ve already documented, the Array class also defines three static functions that you can invoke through the Array constructor rather than on arrays. Array.of() and Array.from() are factory methods for creating new arrays. They were documented in §7.1.4 and §7.1.5. In addition to the array methods we’ve already documented, the Array class also defines three static functions that you can invoke through the Array constructor rather than on arrays. Array.of() and Array.from() are factory methods for creating new arrays. They were documented in §7.1.4 and §7.1.5.
The one other static array function is Array.isArray(), which is useful for determining whether an unknown value is an array or not: The one other static array function is Array.isArray(), which is useful for determining whether an unknown value is an array or not:
Array.isArray([]) // => true Array.isArray([]) // => true
Array.isArray({}) // => false Array.isArray({}) // => false
7.9 Array-Like Objects ## 7.9 Array-Like Objects
As we’ve seen, JavaScript arrays have some special features that other objects do not have: As we’ve seen, JavaScript arrays have some special features that other objects do not have:
The length property is automatically updated as new elements are added to the list. The length property is automatically updated as new elements are added to the list.
...@@ -625,7 +625,7 @@ Array.prototype.slice.call(a, 0) // => ["a","b","c"]: true array copy ...@@ -625,7 +625,7 @@ Array.prototype.slice.call(a, 0) // => ["a","b","c"]: true array copy
Array.from(a) // => ["a","b","c"]: easier array copy Array.from(a) // => ["a","b","c"]: easier array copy
The second-to-last line of this code invokes the Array slice() method on an array-like object in order to copy the elements of that object into a true array object. This is an idiomatic trick that exists in much legacy code, but is now much easier to do with Array.from(). The second-to-last line of this code invokes the Array slice() method on an array-like object in order to copy the elements of that object into a true array object. This is an idiomatic trick that exists in much legacy code, but is now much easier to do with Array.from().
7.10 Strings as Arrays ## 7.10 Strings as Arrays
JavaScript strings behave like read-only arrays of UTF-16 Unicode characters. Instead of accessing individual characters with the charAt() method, you can use square brackets: JavaScript strings behave like read-only arrays of UTF-16 Unicode characters. Instead of accessing individual characters with the charAt() method, you can use square brackets:
let s = "test"; let s = "test";
...@@ -638,7 +638,7 @@ The primary benefit of indexable strings is simply that we can replace calls to ...@@ -638,7 +638,7 @@ The primary benefit of indexable strings is simply that we can replace calls to
Array.prototype.join.call("JavaScript", " ") // => "J a v a S c r i p t" Array.prototype.join.call("JavaScript", " ") // => "J a v a S c r i p t"
Keep in mind that strings are immutable values, so when they are treated as arrays, they are read-only arrays. Array methods like push(), sort(), reverse(), and splice() modify an array in place and do not work on strings. Attempting to modify a string using an array method does not, however, cause an error: it simply fails silently. Keep in mind that strings are immutable values, so when they are treated as arrays, they are read-only arrays. Array methods like push(), sort(), reverse(), and splice() modify an array in place and do not work on strings. Attempting to modify a string using an array method does not, however, cause an error: it simply fails silently.
7.11 Summary ## 7.11 Summary
This chapter has covered JavaScript arrays in depth, including esoteric details about sparse arrays and array-like objects. The main points to take from this chapter are: This chapter has covered JavaScript arrays in depth, including esoteric details about sparse arrays and array-like objects. The main points to take from this chapter are:
Array literals are written as comma-separated lists of values within square brackets. Array literals are written as comma-separated lists of values within square brackets.
......
...@@ -9,14 +9,14 @@ In JavaScript, functions are objects, and they can be manipulated by programs. J ...@@ -9,14 +9,14 @@ In JavaScript, functions are objects, and they can be manipulated by programs. J
JavaScript function definitions can be nested within other functions, and they have access to any variables that are in scope where they are defined. This means that JavaScript functions are closures, and it enables important and powerful programming techniques. JavaScript function definitions can be nested within other functions, and they have access to any variables that are in scope where they are defined. This means that JavaScript functions are closures, and it enables important and powerful programming techniques.
8.1 Defining Functions ## 8.1 Defining Functions
The most straightforward way to define a JavaScript function is with the function keyword, which can be used as a declaration or as an expression. ES6 defines an important new way to define functions without the function keyword: “arrow functions” have a particularly compact syntax and are useful when passing one function as an argument to another function. The subsections that follow cover these three ways of defining functions. Note that some details of function definition syntax involving function parameters are deferred to §8.3. The most straightforward way to define a JavaScript function is with the function keyword, which can be used as a declaration or as an expression. ES6 defines an important new way to define functions without the function keyword: “arrow functions” have a particularly compact syntax and are useful when passing one function as an argument to another function. The subsections that follow cover these three ways of defining functions. Note that some details of function definition syntax involving function parameters are deferred to §8.3.
In object literals and class definitions, there is a convenient shorthand syntax for defining methods. This shorthand syntax was covered in §6.10.5 and is equivalent to using a function definition expression and assigning it to an object property using the basic name:value object literal syntax. In another special case, you can use keywords get and set in object literals to define special property getter and setter methods. This function definition syntax was covered in §6.10.6. In object literals and class definitions, there is a convenient shorthand syntax for defining methods. This shorthand syntax was covered in §6.10.5 and is equivalent to using a function definition expression and assigning it to an object property using the basic name:value object literal syntax. In another special case, you can use keywords get and set in object literals to define special property getter and setter methods. This function definition syntax was covered in §6.10.6.
Note that functions can also be defined with the Function() constructor, which is the subject of §8.7.7. Also, JavaScript defines some specialized kinds of functions. function* defines generator functions (see Chapter 12) and async function defines asynchronous functions (see Chapter 13). Note that functions can also be defined with the Function() constructor, which is the subject of §8.7.7. Also, JavaScript defines some specialized kinds of functions. function* defines generator functions (see Chapter 12) and async function defines asynchronous functions (see Chapter 13).
8.1.1 Function Declarations ### 8.1.1 Function Declarations
Function declarations consist of the function keyword, followed by these components: Function declarations consist of the function keyword, followed by these components:
An identifier that names the function. The name is a required part of function declarations: it is used as the name of a variable, and the newly defined function object is assigned to the variable. An identifier that names the function. The name is a required part of function declarations: it is used as the name of a variable, and the newly defined function object is assigned to the variable.
...@@ -55,7 +55,7 @@ The printprops() function is different: its job is to output the names and value ...@@ -55,7 +55,7 @@ The printprops() function is different: its job is to output the names and value
Prior to ES6, function declarations were only allowed at the top level within a JavaScript file or within another function. While some implementations bent the rule, it was not technically legal to define functions inside the body of loops, conditionals, or other blocks. In the strict mode of ES6, however, function declarations are allowed within blocks. A function defined within a block only exists within that block, however, and is not visible outside the block. Prior to ES6, function declarations were only allowed at the top level within a JavaScript file or within another function. While some implementations bent the rule, it was not technically legal to define functions inside the body of loops, conditionals, or other blocks. In the strict mode of ES6, however, function declarations are allowed within blocks. A function defined within a block only exists within that block, however, and is not visible outside the block.
8.1.2 Function Expressions ### 8.1.2 Function Expressions
Function expressions look a lot like function declarations, but they appear within the context of a larger expression or statement, and the name is optional. Here are some example function expressions: Function expressions look a lot like function declarations, but they appear within the context of a larger expression or statement, and the name is optional. Here are some example function expressions:
// This function expression defines a function that squares its argument. // This function expression defines a function that squares its argument.
...@@ -76,7 +76,7 @@ A name is allowed for functions, like the factorial function, that need to refer ...@@ -76,7 +76,7 @@ A name is allowed for functions, like the factorial function, that need to refer
There is an important difference between defining a function f() with a function declaration and assigning a function to the variable f after creating it as an expression. When you use the declaration form, the function objects are created before the code that contains them starts to run, and the definitions are hoisted so that you can call these functions from code that appears above the definition statement. This is not true for functions defined as expressions, however: these functions do not exist until the expression that defines them are actually evaluated. Furthermore, in order to invoke a function, you must be able to refer to it, and you can’t refer to a function defined as an expression until it is assigned to a variable, so functions defined with expressions cannot be invoked before they are defined. There is an important difference between defining a function f() with a function declaration and assigning a function to the variable f after creating it as an expression. When you use the declaration form, the function objects are created before the code that contains them starts to run, and the definitions are hoisted so that you can call these functions from code that appears above the definition statement. This is not true for functions defined as expressions, however: these functions do not exist until the expression that defines them are actually evaluated. Furthermore, in order to invoke a function, you must be able to refer to it, and you can’t refer to a function defined as an expression until it is assigned to a variable, so functions defined with expressions cannot be invoked before they are defined.
8.1.3 Arrow Functions ### 8.1.3 Arrow Functions
In ES6, you can define functions using a particularly compact syntax known as “arrow functions.” This syntax is reminiscent of mathematical notation and uses an => “arrow” to separate the function parameters from the function body. The function keyword is not used, and, since arrow functions are expressions instead of statements, there is no need for a function name, either. The general form of an arrow function is a comma-separated list of parameters in parentheses, followed by the => arrow, followed by the function body in curly braces: In ES6, you can define functions using a particularly compact syntax known as “arrow functions.” This syntax is reminiscent of mathematical notation and uses an => “arrow” to separate the function parameters from the function body. The function keyword is not used, and, since arrow functions are expressions instead of statements, there is no need for a function name, either. The general form of an arrow function is a comma-separated list of parameters in parentheses, followed by the => arrow, followed by the function body in curly braces:
const sum = (x, y) => { return x + y; }; const sum = (x, y) => { return x + y; };
...@@ -107,7 +107,7 @@ let filtered = [1,null,2,3].filter(x => x !== null); // filtered == [1,2,3] ...@@ -107,7 +107,7 @@ let filtered = [1,null,2,3].filter(x => x !== null); // filtered == [1,2,3]
let squares = [1,2,3,4].map(x => x*x); // squares == [1,4,9,16] let squares = [1,2,3,4].map(x => x*x); // squares == [1,4,9,16]
Arrow functions differ from functions defined in other ways in one critical way: they inherit the value of the this keyword from the environment in which they are defined rather than defining their own invocation context as functions defined in other ways do. This is an important and very useful feature of arrow functions, and we’ll return to it again later in this chapter. Arrow functions also differ from other functions in that they do not have a prototype property, which means that they cannot be used as constructor functions for new classes (see §9.2). Arrow functions differ from functions defined in other ways in one critical way: they inherit the value of the this keyword from the environment in which they are defined rather than defining their own invocation context as functions defined in other ways do. This is an important and very useful feature of arrow functions, and we’ll return to it again later in this chapter. Arrow functions also differ from other functions in that they do not have a prototype property, which means that they cannot be used as constructor functions for new classes (see §9.2).
8.1.4 Nested Functions ### 8.1.4 Nested Functions
In JavaScript, functions may be nested within other functions. For example: In JavaScript, functions may be nested within other functions. For example:
function hypotenuse(a, b) { function hypotenuse(a, b) {
...@@ -116,7 +116,7 @@ function hypotenuse(a, b) { ...@@ -116,7 +116,7 @@ function hypotenuse(a, b) {
} }
The interesting thing about nested functions is their variable scoping rules: they can access the parameters and variables of the function (or functions) they are nested within. In the code shown here, for example, the inner function square() can read and write the parameters a and b defined by the outer function hypotenuse(). These scope rules for nested functions are very important, and we will consider them again in §8.6. The interesting thing about nested functions is their variable scoping rules: they can access the parameters and variables of the function (or functions) they are nested within. In the code shown here, for example, the inner function square() can read and write the parameters a and b defined by the outer function hypotenuse(). These scope rules for nested functions are very important, and we will consider them again in §8.6.
8.2 Invoking Functions ## 8.2 Invoking Functions
The JavaScript code that makes up the body of a function is not executed when the function is defined, but rather when it is invoked. JavaScript functions can be invoked in five ways: The JavaScript code that makes up the body of a function is not executed when the function is defined, but rather when it is invoked. JavaScript functions can be invoked in five ways:
As functions As functions
...@@ -129,7 +129,7 @@ Indirectly through their call() and apply() methods ...@@ -129,7 +129,7 @@ Indirectly through their call() and apply() methods
Implicitly, via JavaScript language features that do not appear like normal function invocations Implicitly, via JavaScript language features that do not appear like normal function invocations
8.2.1 Function Invocation ### 8.2.1 Function Invocation
Functions are invoked as functions or as methods with an invocation expression (§4.5). An invocation expression consists of a function expression that evaluates to a function object followed by an open parenthesis, a comma-separated list of zero or more argument expressions, and a close parenthesis. If the function expression is a property-access expression—if the function is the property of an object or an element of an array—then it is a method invocation expression. That case will be explained in the following example. The following code includes a number of regular function invocation expressions: Functions are invoked as functions or as methods with an invocation expression (§4.5). An invocation expression consists of a function expression that evaluates to a function object followed by an open parenthesis, a comma-separated list of zero or more argument expressions, and a close parenthesis. If the function expression is a property-access expression—if the function is the property of an object or an element of an array—then it is a method invocation expression. That case will be explained in the following example. The following code includes a number of regular function invocation expressions:
printprops({x: 1}); printprops({x: 1});
...@@ -154,7 +154,7 @@ const strict = (function() { return !this; }()); ...@@ -154,7 +154,7 @@ const strict = (function() { return !this; }());
RECURSIVE FUNCTIONS AND THE STACK RECURSIVE FUNCTIONS AND THE STACK
A recursive function is one, like the factorial() function at the start of this chapter, that calls itself. Some algorithms, such as those involving tree-based data structures, can be implemented particularly elegantly with recursive functions. When writing a recursive function, however, it is important to think about memory constraints. When a function A calls function B, and then function B calls function C, the JavaScript interpreter needs to keep track of the execution contexts for all three functions. When function C completes, the interpreter needs to know where to resume executing function B, and when function B completes, it needs to know where to resume executing function A. You can imagine these execution contexts as a stack. When a function calls another function, a new execution context is pushed onto the stack. When that function returns, its execution context object is popped off the stack. If a function calls itself recursively 100 times, the stack will have 100 objects pushed onto it, and then have those 100 objects popped off. This call stack takes memory. On modern hardware, it is typically fine to write recursive functions that call themselves hundreds of times. But if a function calls itself ten thousand times, it is likely to fail with an error such as “Maximum call-stack size exceeded.” A recursive function is one, like the factorial() function at the start of this chapter, that calls itself. Some algorithms, such as those involving tree-based data structures, can be implemented particularly elegantly with recursive functions. When writing a recursive function, however, it is important to think about memory constraints. When a function A calls function B, and then function B calls function C, the JavaScript interpreter needs to keep track of the execution contexts for all three functions. When function C completes, the interpreter needs to know where to resume executing function B, and when function B completes, it needs to know where to resume executing function A. You can imagine these execution contexts as a stack. When a function calls another function, a new execution context is pushed onto the stack. When that function returns, its execution context object is popped off the stack. If a function calls itself recursively 100 times, the stack will have 100 objects pushed onto it, and then have those 100 objects popped off. This call stack takes memory. On modern hardware, it is typically fine to write recursive functions that call themselves hundreds of times. But if a function calls itself ten thousand times, it is likely to fail with an error such as “Maximum call-stack size exceeded.”
8.2.2 Method Invocation ### 8.2.2 Method Invocation
A method is nothing more than a JavaScript function that is stored in a property of an object. If you have a function f and an object o, you can define a method named m of o with the following line: A method is nothing more than a JavaScript function that is stored in a property of an object. If you have a function f and an object o, you can define a method named m of o with the following line:
o.m = f; o.m = f;
...@@ -233,7 +233,7 @@ const f = (function() { ...@@ -233,7 +233,7 @@ const f = (function() {
}).bind(this); }).bind(this);
We’ll talk more about bind() in §8.7.5. We’ll talk more about bind() in §8.7.5.
8.2.3 Constructor Invocation ### 8.2.3 Constructor Invocation
If a function or method invocation is preceded by the keyword new, then it is a constructor invocation. (Constructor invocations were introduced in §4.6 and §6.2.2, and constructors will be covered in more detail in Chapter 9.) Constructor invocations differ from regular function and method invocations in their handling of arguments, invocation context, and return value. If a function or method invocation is preceded by the keyword new, then it is a constructor invocation. (Constructor invocations were introduced in §4.6 and §6.2.2, and constructors will be covered in more detail in Chapter 9.) Constructor invocations differ from regular function and method invocations in their handling of arguments, invocation context, and return value.
If a constructor invocation includes an argument list in parentheses, those argument expressions are evaluated and passed to the function in the same way they would be for function and method invocations. It is not common practice, but you can omit a pair of empty parentheses in a constructor invocation. The following two lines, for example, are equivalent: If a constructor invocation includes an argument list in parentheses, those argument expressions are evaluated and passed to the function in the same way they would be for function and method invocations. It is not common practice, but you can omit a pair of empty parentheses in a constructor invocation. The following two lines, for example, are equivalent:
...@@ -244,10 +244,10 @@ A constructor invocation creates a new, empty object that inherits from the obje ...@@ -244,10 +244,10 @@ A constructor invocation creates a new, empty object that inherits from the obje
Constructor functions do not normally use the return keyword. They typically initialize the new object and then return implicitly when they reach the end of their body. In this case, the new object is the value of the constructor invocation expression. If, however, a constructor explicitly uses the return statement to return an object, then that object becomes the value of the invocation expression. If the constructor uses return with no value, or if it returns a primitive value, that return value is ignored and the new object is used as the value of the invocation. Constructor functions do not normally use the return keyword. They typically initialize the new object and then return implicitly when they reach the end of their body. In this case, the new object is the value of the constructor invocation expression. If, however, a constructor explicitly uses the return statement to return an object, then that object becomes the value of the invocation expression. If the constructor uses return with no value, or if it returns a primitive value, that return value is ignored and the new object is used as the value of the invocation.
8.2.4 Indirect Invocation ### 8.2.4 Indirect Invocation
JavaScript functions are objects, and like all JavaScript objects, they have methods. Two of these methods, call() and apply(), invoke the function indirectly. Both methods allow you to explicitly specify the this value for the invocation, which means you can invoke any function as a method of any object, even if it is not actually a method of that object. Both methods also allow you to specify the arguments for the invocation. The call() method uses its own argument list as arguments to the function, and the apply() method expects an array of values to be used as arguments. The call() and apply() methods are described in detail in §8.7.4. JavaScript functions are objects, and like all JavaScript objects, they have methods. Two of these methods, call() and apply(), invoke the function indirectly. Both methods allow you to explicitly specify the this value for the invocation, which means you can invoke any function as a method of any object, even if it is not actually a method of that object. Both methods also allow you to specify the arguments for the invocation. The call() method uses its own argument list as arguments to the function, and the apply() method expects an array of values to be used as arguments. The call() and apply() methods are described in detail in §8.7.4.
8.2.5 Implicit Function Invocation ### 8.2.5 Implicit Function Invocation
There are various JavaScript language features that do not look like function invocations but that cause functions to be invoked. Be extra careful when writing functions that may be implicitly invoked, because bugs, side effects, and performance issues in these functions are harder to diagnose and fix than in regular functions for the simple reason that it may not be obvious from a simple inspection of your code when they are being called. There are various JavaScript language features that do not look like function invocations but that cause functions to be invoked. Be extra careful when writing functions that may be implicitly invoked, because bugs, side effects, and performance issues in these functions are harder to diagnose and fix than in regular functions for the simple reason that it may not be obvious from a simple inspection of your code when they are being called.
The language features that can cause implicit function invocation include: The language features that can cause implicit function invocation include:
...@@ -262,10 +262,10 @@ A tagged template literal is a function invocation in disguise. §14.5 demonstra ...@@ -262,10 +262,10 @@ A tagged template literal is a function invocation in disguise. §14.5 demonstra
Proxy objects (described in §14.7) have their behavior completely controlled by functions. Just about any operation on one of these objects will cause a function to be invoked. Proxy objects (described in §14.7) have their behavior completely controlled by functions. Just about any operation on one of these objects will cause a function to be invoked.
8.3 Function Arguments and Parameters ## 8.3 Function Arguments and Parameters
JavaScript function definitions do not specify an expected type for the function parameters, and function invocations do not do any type checking on the argument values you pass. In fact, JavaScript function invocations do not even check the number of arguments being passed. The subsections that follow describe what happens when a function is invoked with fewer arguments than declared parameters or with more arguments than declared parameters. They also demonstrate how you can explicitly test the type of function arguments if you need to ensure that a function is not invoked with inappropriate arguments. JavaScript function definitions do not specify an expected type for the function parameters, and function invocations do not do any type checking on the argument values you pass. In fact, JavaScript function invocations do not even check the number of arguments being passed. The subsections that follow describe what happens when a function is invoked with fewer arguments than declared parameters or with more arguments than declared parameters. They also demonstrate how you can explicitly test the type of function arguments if you need to ensure that a function is not invoked with inappropriate arguments.
8.3.1 Optional Parameters and Defaults ### 8.3.1 Optional Parameters and Defaults
When a function is invoked with fewer arguments than declared parameters, the additional parameters are set to their default value, which is normally undefined. It is often useful to write functions so that some arguments are optional. Following is an example: When a function is invoked with fewer arguments than declared parameters, the additional parameters are set to their default value, which is normally undefined. It is often useful to write functions so that some arguments are optional. Following is an example:
// Append the names of the enumerable properties of object o to the // Append the names of the enumerable properties of object o to the
...@@ -303,7 +303,7 @@ const rectangle = (width, height=width*2) => ({width, height}); ...@@ -303,7 +303,7 @@ const rectangle = (width, height=width*2) => ({width, height});
rectangle(1) // => { width: 1, height: 2 } rectangle(1) // => { width: 1, height: 2 }
This code demonstrates that parameter defaults work with arrow functions. The same is true for method shorthand functions and all other forms of function definitions. This code demonstrates that parameter defaults work with arrow functions. The same is true for method shorthand functions and all other forms of function definitions.
8.3.2 Rest Parameters and Variable-Length Argument Lists ### 8.3.2 Rest Parameters and Variable-Length Argument Lists
Parameter defaults enable us to write functions that can be invoked with fewer arguments than parameters. Rest parameters enable the opposite case: they allow us to write functions that can be invoked with arbitrarily more arguments than parameters. Here is an example function that expects one or more numeric arguments and returns the largest one: Parameter defaults enable us to write functions that can be invoked with fewer arguments than parameters. Rest parameters enable the opposite case: they allow us to write functions that can be invoked with arbitrarily more arguments than parameters. Here is an example function that expects one or more numeric arguments and returns the largest one:
function max(first=-Infinity, ...rest) { function max(first=-Infinity, ...rest) {
...@@ -325,7 +325,7 @@ Functions like the previous example that can accept any number of arguments are ...@@ -325,7 +325,7 @@ Functions like the previous example that can accept any number of arguments are
Don’t confuse the ... that defines a rest parameter in a function definition with the ... spread operator, described in §8.3.4, which can be used in function invocations. Don’t confuse the ... that defines a rest parameter in a function definition with the ... spread operator, described in §8.3.4, which can be used in function invocations.
8.3.3 The Arguments Object ### 8.3.3 The Arguments Object
Rest parameters were introduced into JavaScript in ES6. Before that version of the language, varargs functions were written using the Arguments object: within the body of any function, the identifier arguments refers to the Arguments object for that invocation. The Arguments object is an array-like object (see §7.9) that allows the argument values passed to the function to be retrieved by number, rather than by name. Here is the max() function from earlier, rewritten to use the Arguments object instead of a rest parameter: Rest parameters were introduced into JavaScript in ES6. Before that version of the language, varargs functions were written using the Arguments object: within the body of any function, the identifier arguments refers to the Arguments object for that invocation. The Arguments object is an array-like object (see §7.9) that allows the argument values passed to the function to be retrieved by number, rather than by name. Here is the max() function from earlier, rewritten to use the Arguments object instead of a rest parameter:
function max(x) { function max(x) {
...@@ -341,7 +341,7 @@ function max(x) { ...@@ -341,7 +341,7 @@ function max(x) {
max(1, 10, 100, 2, 3, 1000, 4, 5, 6) // => 1000 max(1, 10, 100, 2, 3, 1000, 4, 5, 6) // => 1000
The Arguments object dates back to the earliest days of JavaScript and carries with it some strange historical baggage that makes it inefficient and hard to optimize, especially outside of strict mode. You may still encounter code that uses the Arguments object, but you should avoid using it in any new code you write. When refactoring old code, if you encounter a function that uses arguments, you can often replace it with a ...args rest parameter. Part of the unfortunate legacy of the Arguments object is that, in strict mode, arguments is treated as a reserved word, and you cannot declare a function parameter or a local variable with that name. The Arguments object dates back to the earliest days of JavaScript and carries with it some strange historical baggage that makes it inefficient and hard to optimize, especially outside of strict mode. You may still encounter code that uses the Arguments object, but you should avoid using it in any new code you write. When refactoring old code, if you encounter a function that uses arguments, you can often replace it with a ...args rest parameter. Part of the unfortunate legacy of the Arguments object is that, in strict mode, arguments is treated as a reserved word, and you cannot declare a function parameter or a local variable with that name.
8.3.4 The Spread Operator for Function Calls ### 8.3.4 The Spread Operator for Function Calls
The spread operator ... is used to unpack, or “spread out,” the elements of an array (or any other iterable object, such as strings) in a context where individual values are expected. We’ve seen the spread operator used with array literals in §7.1.2. The operator can be used, in the same way, in function invocations: The spread operator ... is used to unpack, or “spread out,” the elements of an array (or any other iterable object, such as strings) in a context where individual values are expected. We’ve seen the spread operator used with array literals in §7.1.2. The operator can be used, in the same way, in function invocations:
let numbers = [5, 2, 10, -1, 9, 100, 1]; let numbers = [5, 2, 10, -1, 9, 100, 1];
...@@ -375,7 +375,7 @@ function benchmark(n) { ...@@ -375,7 +375,7 @@ function benchmark(n) {
// Now invoke the timed version of that test function // Now invoke the timed version of that test function
timed(benchmark)(1000000) // => 500000500000; this is the sum of the numbers timed(benchmark)(1000000) // => 500000500000; this is the sum of the numbers
8.3.5 Destructuring Function Arguments into Parameters ### 8.3.5 Destructuring Function Arguments into Parameters
When you invoke a function with a list of argument values, those values end up being assigned to the parameters declared in the function definition. This initial phase of function invocation is a lot like variable assignment. So it should not be surprising that we can use the techniques of destructuring assignment (see §3.10.3) with functions. When you invoke a function with a list of argument values, those values end up being assigned to the parameters declared in the function definition. This initial phase of function invocation is a lot like variable assignment. So it should not be surprising that we can use the techniques of destructuring assignment (see §3.10.3) with functions.
If you define a function that has parameter names within square brackets, you are telling the function to expect an array value to be passed for each pair of square brackets. As part of the invocation process, the array arguments will be unpacked into the individually named parameters. As an example, suppose we are representing 2D vectors as arrays of two numbers, where the first element is the X coordinate and the second element is the Y coordinate. With this simple data structure, we could write the following function to add two vectors: If you define a function that has parameter names within square brackets, you are telling the function to expect an array value to be passed for each pair of square brackets. As part of the invocation process, the array arguments will be unpacked into the individually named parameters. As an example, suppose we are representing 2D vectors as arrays of two numbers, where the first element is the X coordinate and the second element is the Y coordinate. With this simple data structure, we could write the following function to add two vectors:
...@@ -449,7 +449,7 @@ function drawCircle({x, y, radius, color: [r, g, b]}) { ...@@ -449,7 +449,7 @@ function drawCircle({x, y, radius, color: [r, g, b]}) {
} }
If function argument destructuring is any more complicated than this, I find that the code becomes harder to read, rather than simpler. Sometimes, it is clearer to be explicit about your object property access and array indexing. If function argument destructuring is any more complicated than this, I find that the code becomes harder to read, rather than simpler. Sometimes, it is clearer to be explicit about your object property access and array indexing.
8.3.6 Argument Types ### 8.3.6 Argument Types
JavaScript method parameters have no declared types, and no type checking is performed on the values you pass to a function. You can help make your code self-documenting by choosing descriptive names for function arguments and by documenting them carefully in the comments for each function. (Alternatively, see §17.8 for a language extension that allows you to layer type checking on top of regular JavaScript.) JavaScript method parameters have no declared types, and no type checking is performed on the values you pass to a function. You can help make your code self-documenting by choosing descriptive names for function arguments and by documenting them carefully in the comments for each function. (Alternatively, see §17.8 for a language extension that allows you to layer type checking on top of regular JavaScript.)
As described in §3.9, JavaScript performs liberal type conversion as needed. So if you write a function that expects a string argument and then call that function with a value of some other type, the value you passed will simply be converted to a string when the function tries to use it as a string. All primitive types can be converted to strings, and all objects have toString() methods (if not necessarily useful ones), so an error never occurs in this case. As described in §3.9, JavaScript performs liberal type conversion as needed. So if you write a function that expects a string argument and then call that function with a value of some other type, the value you passed will simply be converted to a string when the function tries to use it as a string. All primitive types can be converted to strings, and all objects have toString() methods (if not necessarily useful ones), so an error never occurs in this case.
...@@ -471,7 +471,7 @@ function sum(a) { ...@@ -471,7 +471,7 @@ function sum(a) {
sum([1,2,3]) // => 6 sum([1,2,3]) // => 6
sum(1, 2, 3); // !TypeError: 1 is not iterable sum(1, 2, 3); // !TypeError: 1 is not iterable
sum([1,2,"3"]); // !TypeError: element 2 is not a number sum([1,2,"3"]); // !TypeError: element 2 is not a number
8.4 Functions as Values ## 8.4 Functions as Values
The most important features of functions are that they can be defined and invoked. Function definition and invocation are syntactic features of JavaScript and of most other programming languages. In JavaScript, however, functions are not only syntax but also values, which means they can be assigned to variables, stored in the properties of objects or the elements of arrays, passed as arguments to functions, and so on.3 The most important features of functions are that they can be defined and invoked. Function definition and invocation are syntactic features of JavaScript and of most other programming languages. In JavaScript, however, functions are not only syntax but also values, which means they can be assigned to variables, stored in the properties of objects or the elements of arrays, passed as arguments to functions, and so on.3
To understand how functions can be JavaScript data as well as JavaScript syntax, consider this function definition: To understand how functions can be JavaScript data as well as JavaScript syntax, consider this function definition:
...@@ -534,7 +534,7 @@ function operate2(operation, operand1, operand2) { ...@@ -534,7 +534,7 @@ function operate2(operation, operand1, operand2) {
operate2("add", "hello", operate2("add", " ", "world")) // => "hello world" operate2("add", "hello", operate2("add", " ", "world")) // => "hello world"
operate2("pow", 10, 2) // => 100 operate2("pow", 10, 2) // => 100
8.4.1 Defining Your Own Function Properties ### 8.4.1 Defining Your Own Function Properties
Functions are not primitive values in JavaScript, but a specialized kind of object, which means that functions can have properties. When a function needs a “static” variable whose value persists across invocations, it is often convenient to use a property of the function itself. For example, suppose you want to write a function that returns a unique integer whenever it is invoked. The function must never return the same value twice. In order to manage this, the function needs to keep track of the values it has already returned, and this information must persist across function invocations. You could store this information in a global variable, but that is unnecessary, because the information is used only by the function itself. It is better to store the information in a property of the Function object. Here is an example that returns a unique integer whenever it is called: Functions are not primitive values in JavaScript, but a specialized kind of object, which means that functions can have properties. When a function needs a “static” variable whose value persists across invocations, it is often convenient to use a property of the function itself. For example, suppose you want to write a function that returns a unique integer whenever it is invoked. The function must never return the same value twice. In order to manage this, the function needs to keep track of the values it has already returned, and this information must persist across function invocations. You could store this information in a global variable, but that is unnecessary, because the information is used only by the function itself. It is better to store the information in a property of the Function object. Here is an example that returns a unique integer whenever it is called:
// Initialize the counter property of the function object. // Initialize the counter property of the function object.
...@@ -565,7 +565,7 @@ function factorial(n) { ...@@ -565,7 +565,7 @@ function factorial(n) {
factorial[1] = 1; // Initialize the cache to hold this base case. factorial[1] = 1; // Initialize the cache to hold this base case.
factorial(6) // => 720 factorial(6) // => 720
factorial[5] // => 120; the call above caches this value factorial[5] // => 120; the call above caches this value
8.5 Functions as Namespaces ## 8.5 Functions as Namespaces
Variables declared within a function are not visible outside of the function. For this reason, it is sometimes useful to define a function simply to act as a temporary namespace in which you can define variables without cluttering the global namespace. Variables declared within a function are not visible outside of the function. For this reason, it is sometimes useful to define a function simply to act as a temporary namespace in which you can define variables without cluttering the global namespace.
Suppose, for example, you have a chunk of JavaScript code that you want to use in a number of different JavaScript programs (or, for client-side JavaScript, on a number of different web pages). Assume that this code, like most code, defines variables to store the intermediate results of its computation. The problem is that since this chunk of code will be used in many different programs, you don’t know whether the variables it creates will conflict with variables created by the programs that use it. The solution is to put the chunk of code into a function and then invoke the function. This way, variables that would have been global become local to the function: Suppose, for example, you have a chunk of JavaScript code that you want to use in a number of different JavaScript programs (or, for client-side JavaScript, on a number of different web pages). Assume that this code, like most code, defines variables to store the intermediate results of its computation. The problem is that since this chunk of code will be used in many different programs, you don’t know whether the variables it creates will conflict with variables created by the programs that use it. The solution is to put the chunk of code into a function and then invoke the function. This way, variables that would have been global become local to the function:
...@@ -585,7 +585,7 @@ This technique of defining and invoking a function in a single expression is use ...@@ -585,7 +585,7 @@ This technique of defining and invoking a function in a single expression is use
This use of functions as namespaces becomes really useful when we define one or more functions inside the namespace function using variables within that namesapce, but then pass them back out as the return value of the namespace function. Functions like this are known as closures, and they’re the topic of the next section. This use of functions as namespaces becomes really useful when we define one or more functions inside the namespace function using variables within that namesapce, but then pass them back out as the return value of the namespace function. Functions like this are known as closures, and they’re the topic of the next section.
8.6 Closures ## 8.6 Closures
Like most modern programming languages, JavaScript uses lexical scoping. This means that functions are executed using the variable scope that was in effect when they were defined, not the variable scope that is in effect when they are invoked. In order to implement lexical scoping, the internal state of a JavaScript function object must include not only the code of the function but also a reference to the scope in which the function definition appears. This combination of a function object and a scope (a set of variable bindings) in which the function’s variables are resolved is called a closure in the computer science literature. Like most modern programming languages, JavaScript uses lexical scoping. This means that functions are executed using the variable scope that was in effect when they were defined, not the variable scope that is in effect when they are invoked. In order to implement lexical scoping, the internal state of a JavaScript function object must include not only the code of the function but also a reference to the scope in which the function definition appears. This combination of a function object and a scope (a set of variable bindings) in which the function’s variables are resolved is called a closure in the computer science literature.
Technically, all JavaScript functions are closures, but because most functions are invoked from the same scope that they were defined in, it normally doesn’t really matter that there is a closure involved. Closures become interesting when they are invoked from a different scope than the one they were defined in. This happens most commonly when a nested function object is returned from the function within which it was defined. There are a number of powerful programming techniques that involve this kind of nested function closures, and their use has become relatively common in JavaScript programming. Closures may seem confusing when you first encounter them, but it is important that you understand them well enough to use them comfortably. Technically, all JavaScript functions are closures, but because most functions are invoked from the same scope that they were defined in, it normally doesn’t really matter that there is a closure involved. Closures become interesting when they are invoked from a different scope than the one they were defined in. This happens most commonly when a nested function object is returned from the function within which it was defined. There are a number of powerful programming techniques that involve this kind of nested function closures, and their use has become relatively common in JavaScript programming. Closures may seem confusing when you first encounter them, but it is important that you understand them well enough to use them comfortably.
...@@ -734,19 +734,19 @@ This code creates 10 closures and stores them in an array. The closures are all ...@@ -734,19 +734,19 @@ This code creates 10 closures and stores them in an array. The closures are all
Another thing to remember when writing closures is that this is a JavaScript keyword, not a variable. As discussed earlier, arrow functions inherit the this value of the function that contains them, but functions defined with the function keyword do not. So if you’re writing a closure that needs to use the this value of its containing function, you should use an arrow function, or call bind(), on the closure before returning it, or assign the outer this value to a variable that your closure will inherit: Another thing to remember when writing closures is that this is a JavaScript keyword, not a variable. As discussed earlier, arrow functions inherit the this value of the function that contains them, but functions defined with the function keyword do not. So if you’re writing a closure that needs to use the this value of its containing function, you should use an arrow function, or call bind(), on the closure before returning it, or assign the outer this value to a variable that your closure will inherit:
const self = this; // Make the this value available to nested functions const self = this; // Make the this value available to nested functions
8.7 Function Properties, Methods, and Constructor ## 8.7 Function Properties, Methods, and Constructor
We’ve seen that functions are values in JavaScript programs. The typeof operator returns the string “function” when applied to a function, but functions are really a specialized kind of JavaScript object. Since functions are objects, they can have properties and methods, just like any other object. There is even a Function() constructor to create new function objects. The subsections that follow document the length, name, and prototype properties; the call(), apply(), bind(), and toString() methods; and the Function() constructor. We’ve seen that functions are values in JavaScript programs. The typeof operator returns the string “function” when applied to a function, but functions are really a specialized kind of JavaScript object. Since functions are objects, they can have properties and methods, just like any other object. There is even a Function() constructor to create new function objects. The subsections that follow document the length, name, and prototype properties; the call(), apply(), bind(), and toString() methods; and the Function() constructor.
8.7.1 The length Property ### 8.7.1 The length Property
The read-only length property of a function specifies the arity of the function—the number of parameters it declares in its parameter list, which is usually the number of arguments that the function expects. If a function has a rest parameter, that parameter is not counted for the purposes of this length property. The read-only length property of a function specifies the arity of the function—the number of parameters it declares in its parameter list, which is usually the number of arguments that the function expects. If a function has a rest parameter, that parameter is not counted for the purposes of this length property.
8.7.2 The name Property ### 8.7.2 The name Property
The read-only name property of a function specifies the name that was used when the function was defined, if it was defined with a name, or the name of the variable or property that an unnamed function expression was assigned to when it was first created. This property is primarily useful when writing debugging or error messages. The read-only name property of a function specifies the name that was used when the function was defined, if it was defined with a name, or the name of the variable or property that an unnamed function expression was assigned to when it was first created. This property is primarily useful when writing debugging or error messages.
8.7.3 The prototype Property ### 8.7.3 The prototype Property
All functions, except arrow functions, have a prototype property that refers to an object known as the prototype object. Every function has a different prototype object. When a function is used as a constructor, the newly created object inherits properties from the prototype object. Prototypes and the prototype property were discussed in §6.2.3 and will be covered again in Chapter 9. All functions, except arrow functions, have a prototype property that refers to an object known as the prototype object. Every function has a different prototype object. When a function is used as a constructor, the newly created object inherits properties from the prototype object. Prototypes and the prototype property were discussed in §6.2.3 and will be covered again in Chapter 9.
8.7.4 The call() and apply() Methods ### 8.7.4 The call() and apply() Methods
call() and apply() allow you to indirectly invoke (§8.2.4) a function as if it were a method of some other object. The first argument to both call() and apply() is the object on which the function is to be invoked; this argument is the invocation context and becomes the value of the this keyword within the body of the function. To invoke the function f() as a method of the object o (passing no arguments), you could use either call() or apply(): call() and apply() allow you to indirectly invoke (§8.2.4) a function as if it were a method of some other object. The first argument to both call() and apply() is the object on which the function is to be invoked; this argument is the invocation context and becomes the value of the this keyword within the body of the function. To invoke the function f() as a method of the object o (passing no arguments), you could use either call() or apply():
f.call(o); f.call(o);
...@@ -780,7 +780,7 @@ function trace(o, m) { ...@@ -780,7 +780,7 @@ function trace(o, m) {
return result; // Return result. return result; // Return result.
}; };
} }
8.7.5 The bind() Method ### 8.7.5 The bind() Method
The primary purpose of bind() is to bind a function to an object. When you invoke the bind() method on a function f and pass an object o, the method returns a new function. Invoking the new function (as a function) invokes the original function f as a method of o. Any arguments you pass to the new function are passed to the original function. For example: The primary purpose of bind() is to bind a function to an object. When you invoke the bind() method on a function f and pass an object o, the method returns a new function. Invoking the new function (as a function) invokes the original function f as a method of o. Any arguments you pass to the new function are passed to the original function. For example:
function f(y) { return this.x + y; } // This function needs to be bound function f(y) { return this.x + y; } // This function needs to be bound
...@@ -802,10 +802,10 @@ let g = f.bind({x: 1}, 2); // Bind this and y ...@@ -802,10 +802,10 @@ let g = f.bind({x: 1}, 2); // Bind this and y
g(3) // => 6: this.x is bound to 1, y is bound to 2 and z is 3 g(3) // => 6: this.x is bound to 1, y is bound to 2 and z is 3
The name property of the function returned by bind() is the name property of the function that bind() was called on, prefixed with the word “bound”. The name property of the function returned by bind() is the name property of the function that bind() was called on, prefixed with the word “bound”.
8.7.6 The toString() Method ### 8.7.6 The toString() Method
Like all JavaScript objects, functions have a toString() method. The ECMAScript spec requires this method to return a string that follows the syntax of the function declaration statement. In practice, most (but not all) implementations of this toString() method return the complete source code for the function. Built-in functions typically return a string that includes something like “[native code]” as the function body. Like all JavaScript objects, functions have a toString() method. The ECMAScript spec requires this method to return a string that follows the syntax of the function declaration statement. In practice, most (but not all) implementations of this toString() method return the complete source code for the function. Built-in functions typically return a string that includes something like “[native code]” as the function body.
8.7.7 The Function() Constructor ### 8.7.7 The Function() Constructor
Because functions are objects, there is a Function() constructor that can be used to create new functions: Because functions are objects, there is a Function() constructor that can be used to create new functions:
const f = new Function("x", "y", "return x*y;"); const f = new Function("x", "y", "return x*y;");
...@@ -834,10 +834,10 @@ function constructFunction() { ...@@ -834,10 +834,10 @@ function constructFunction() {
constructFunction()() // => "global" constructFunction()() // => "global"
The Function() constructor is best thought of as a globally scoped version of eval() (see §4.12.2) that defines new variables and functions in its own private scope. You will probably never need to use this constructor in your code. The Function() constructor is best thought of as a globally scoped version of eval() (see §4.12.2) that defines new variables and functions in its own private scope. You will probably never need to use this constructor in your code.
8.8 Functional Programming ## 8.8 Functional Programming
JavaScript is not a functional programming language like Lisp or Haskell, but the fact that JavaScript can manipulate functions as objects means that we can use functional programming techniques in JavaScript. Array methods such as map() and reduce() lend themselves particularly well to a functional programming style. The sections that follow demonstrate techniques for functional programming in JavaScript. They are intended as a mind-expanding exploration of the power of JavaScript’s functions, not as a prescription for good programming style. JavaScript is not a functional programming language like Lisp or Haskell, but the fact that JavaScript can manipulate functions as objects means that we can use functional programming techniques in JavaScript. Array methods such as map() and reduce() lend themselves particularly well to a functional programming style. The sections that follow demonstrate techniques for functional programming in JavaScript. They are intended as a mind-expanding exploration of the power of JavaScript’s functions, not as a prescription for good programming style.
8.8.1 Processing Arrays with Functions ### 8.8.1 Processing Arrays with Functions
Suppose we have an array of numbers and we want to compute the mean and standard deviation of those values. We might do that in nonfunctional style like this: Suppose we have an array of numbers and we want to compute the mean and standard deviation of those values. We might do that in nonfunctional style like this:
let data = [1,1,3,5,5]; // This is our array of numbers let data = [1,1,3,5,5]; // This is our array of numbers
...@@ -881,7 +881,7 @@ let mean = reduce(data, sum)/data.length; ...@@ -881,7 +881,7 @@ let mean = reduce(data, sum)/data.length;
let deviations = map(data, x => x-mean); let deviations = map(data, x => x-mean);
let stddev = Math.sqrt(reduce(map(deviations, square), sum)/(data.length-1)); let stddev = Math.sqrt(reduce(map(deviations, square), sum)/(data.length-1));
stddev // => 2 stddev // => 2
8.8.2 Higher-Order Functions ### 8.8.2 Higher-Order Functions
A higher-order function is a function that operates on functions, taking one or more functions as arguments and returning a new function. Here is an example: A higher-order function is a function that operates on functions, taking one or more functions as arguments and returning a new function. Here is an example:
// This higher-order function returns a new function that passes its // This higher-order function returns a new function that passes its
...@@ -927,7 +927,7 @@ const square = x => x*x; ...@@ -927,7 +927,7 @@ const square = x => x*x;
compose(square, sum)(2,3) // => 25; the square of the sum compose(square, sum)(2,3) // => 25; the square of the sum
The partial() and memoize() functions defined in the sections that follow are two more important higher-order functions. The partial() and memoize() functions defined in the sections that follow are two more important higher-order functions.
8.8.3 Partial Application of Functions ### 8.8.3 Partial Application of Functions
The bind() method of a function f (see §8.7.5) returns a new function that invokes f in a specified context and with a specified set of arguments. We say that it binds the function to an object and partially applies the arguments. The bind() method partially applies arguments on the left—that is, the arguments you pass to bind() are placed at the start of the argument list that is passed to the original function. But it is also possible to partially apply arguments on the right: The bind() method of a function f (see §8.7.5) returns a new function that invokes f in a specified context and with a specified set of arguments. We say that it binds the function to an object and partially applies the arguments. The bind() method partially applies arguments on the left—that is, the arguments you pass to bind() are placed at the start of the argument list that is passed to the original function. But it is also possible to partially apply arguments on the right:
// The arguments to this function are passed on the left // The arguments to this function are passed on the left
...@@ -999,7 +999,7 @@ let stddev = sqrt(product(reduce(map(data, ...@@ -999,7 +999,7 @@ let stddev = sqrt(product(reduce(map(data,
[mean, stddev] // => [3, 2] [mean, stddev] // => [3, 2]
Notice that this code to compute mean and standard deviation is entirely function invocations; there are no operators involved, and the number of parentheses has grown so large that this JavaScript is beginning to look like Lisp code. Again, this is not a style that I advocate for JavaScript programming, but it is an interesting exercise to see how deeply functional JavaScript code can be. Notice that this code to compute mean and standard deviation is entirely function invocations; there are no operators involved, and the number of parentheses has grown so large that this JavaScript is beginning to look like Lisp code. Again, this is not a style that I advocate for JavaScript programming, but it is an interesting exercise to see how deeply functional JavaScript code can be.
8.8.4 Memoization ### 8.8.4 Memoization
In §8.4.1, we defined a factorial function that cached its previously computed results. In functional programming, this kind of caching is called memoization. The code that follows shows a higher-order function, memoize(), that accepts a function as its argument and returns a memoized version of the function: In §8.4.1, we defined a factorial function that cached its previously computed results. In functional programming, this kind of caching is called memoization. The code that follows shows a higher-order function, memoize(), that accepts a function as its argument and returns a memoized version of the function:
// Return a memoized version of f. // Return a memoized version of f.
...@@ -1042,7 +1042,7 @@ const factorial = memoize(function(n) { ...@@ -1042,7 +1042,7 @@ const factorial = memoize(function(n) {
return (n <= 1) ? 1 : n * factorial(n-1); return (n <= 1) ? 1 : n * factorial(n-1);
}); });
factorial(5) // => 120: also caches values for 4, 3, 2 and 1. factorial(5) // => 120: also caches values for 4, 3, 2 and 1.
8.9 Summary ## 8.9 Summary
Some key points to remember about this chapter are as follows: Some key points to remember about this chapter are as follows:
You can define functions with the function keyword and with the ES6 => arrow syntax. You can define functions with the function keyword and with the ES6 => arrow syntax.
......
...@@ -9,7 +9,7 @@ JavaScript has always allowed the definition of classes. ES6 introduced a brand- ...@@ -9,7 +9,7 @@ JavaScript has always allowed the definition of classes. ES6 introduced a brand-
If you’re familiar with strongly typed object-oriented programming languages like Java or C++, you’ll notice that JavaScript classes are quite different from classes in those languages. There are some syntactic similarities, and you can emulate many features of “classical” classes in JavaScript, but it is best to understand up front that JavaScript’s classes and prototype-based inheritance mechanism are substantially different from the classes and class-based inheritance mechanism of Java and similar languages. If you’re familiar with strongly typed object-oriented programming languages like Java or C++, you’ll notice that JavaScript classes are quite different from classes in those languages. There are some syntactic similarities, and you can emulate many features of “classical” classes in JavaScript, but it is best to understand up front that JavaScript’s classes and prototype-based inheritance mechanism are substantially different from the classes and class-based inheritance mechanism of Java and similar languages.
9.1 Classes and Prototypes ## 9.1 Classes and Prototypes
In JavaScript, a class is a set of objects that inherit properties from the same prototype object. The prototype object, therefore, is the central feature of a class. Chapter 6 covered the Object.create() function that returns a newly created object that inherits from a specified prototype object. If we define a prototype object and then use Object.create() to create objects that inherit from it, we have defined a JavaScript class. Usually, the instances of a class require further initialization, and it is common to define a function that creates and initializes the new object. Example 9-1 demonstrates this: it defines a prototype object for a class that represents a range of values and also defines a factory function that creates and initializes a new instance of the class. In JavaScript, a class is a set of objects that inherit properties from the same prototype object. The prototype object, therefore, is the central feature of a class. Chapter 6 covered the Object.create() function that returns a newly created object that inherits from a specified prototype object. If we define a prototype object and then use Object.create() to create objects that inherit from it, we have defined a JavaScript class. Usually, the instances of a class require further initialization, and it is common to define a function that creates and initializes the new object. Example 9-1 demonstrates this: it defines a prototype object for a class that represents a range of values and also defines a factory function that creates and initializes a new instance of the class.
Example 9-1. A simple JavaScript class Example 9-1. A simple JavaScript class
...@@ -65,7 +65,7 @@ One of the methods in the prototype has the computed name (§6.10.2) Symbol.iter ...@@ -65,7 +65,7 @@ One of the methods in the prototype has the computed name (§6.10.2) Symbol.iter
The shared, inherited methods defined in range.methods all use the from and to properties that were initialized in the range() factory function. In order to refer to them, they use the this keyword to refer to the object through which they were invoked. This use of this is a fundamental characteristic of the methods of any class. The shared, inherited methods defined in range.methods all use the from and to properties that were initialized in the range() factory function. In order to refer to them, they use the this keyword to refer to the object through which they were invoked. This use of this is a fundamental characteristic of the methods of any class.
9.2 Classes and Constructors ## 9.2 Classes and Constructors
Example 9-1 demonstrates a simple way to define a JavaScript class. It is not the idiomatic way to do so, however, because it did not define a constructor. A constructor is a function designed for the initialization of newly created objects. Constructors are invoked using the new keyword as described in §8.2.3. Constructor invocations using new automatically create the new object, so the constructor itself only needs to initialize the state of that new object. The critical feature of constructor invocations is that the prototype property of the constructor is used as the prototype of the new object. §6.2.3 introduced prototypes and emphasized that while almost all objects have a prototype, only a few objects have a prototype property. Finally, we can clarify this: it is function objects that have a prototype property. This means that all objects created with the same constructor function inherit from the same object and are therefore members of the same class. Example 9-2 shows how we could alter the Range class of Example 9-1 to use a constructor function instead of a factory function. Example 9-2 demonstrates the idiomatic way to create a class in versions of JavaScript that do not support the ES6 class keyword. Even though class is well supported now, there is still lots of older JavaScript code around that defines classes like this, and you should be familiar with the idiom so that you can read old code and so that you understand what is going on “under the hood” when you use the class keyword. Example 9-1 demonstrates a simple way to define a JavaScript class. It is not the idiomatic way to do so, however, because it did not define a constructor. A constructor is a function designed for the initialization of newly created objects. Constructors are invoked using the new keyword as described in §8.2.3. Constructor invocations using new automatically create the new object, so the constructor itself only needs to initialize the state of that new object. The critical feature of constructor invocations is that the prototype property of the constructor is used as the prototype of the new object. §6.2.3 introduced prototypes and emphasized that while almost all objects have a prototype, only a few objects have a prototype property. Finally, we can clarify this: it is function objects that have a prototype property. This means that all objects created with the same constructor function inherit from the same object and are therefore members of the same class. Example 9-2 shows how we could alter the Range class of Example 9-1 to use a constructor function instead of a factory function. Example 9-2 demonstrates the idiomatic way to create a class in versions of JavaScript that do not support the ES6 class keyword. Even though class is well supported now, there is still lots of older JavaScript code around that defines classes like this, and you should be familiar with the idiom so that you can read old code and so that you understand what is going on “under the hood” when you use the class keyword.
Example 9-2. A Range class using a constructor Example 9-2. A Range class using a constructor
...@@ -123,7 +123,7 @@ Importantly, note that neither of the two range examples uses arrow functions wh ...@@ -123,7 +123,7 @@ Importantly, note that neither of the two range examples uses arrow functions wh
Fortunately, the new ES6 class syntax doesn’t allow the option of defining methods with arrow functions, so this is not a mistake that you can accidentally make when using that syntax. We will cover the ES6 class keyword soon, but first, there are more details to cover about constructors. Fortunately, the new ES6 class syntax doesn’t allow the option of defining methods with arrow functions, so this is not a mistake that you can accidentally make when using that syntax. We will cover the ES6 class keyword soon, but first, there are more details to cover about constructors.
9.2.1 Constructors, Class Identity, and instanceof ### 9.2.1 Constructors, Class Identity, and instanceof
As we’ve seen, the prototype object is fundamental to the identity of a class: two objects are instances of the same class if and only if they inherit from the same prototype object. The constructor function that initializes the state of a new object is not fundamental: two constructor functions may have prototype properties that point to the same prototype object. Then, both constructors can be used to create instances of the same class. As we’ve seen, the prototype object is fundamental to the identity of a class: two objects are instances of the same class if and only if they inherit from the same prototype object. The constructor function that initializes the state of a new object is not fundamental: two constructor functions may have prototype properties that point to the same prototype object. Then, both constructors can be used to create instances of the same class.
Even though constructors are not as fundamental as prototypes, the constructor serves as the public face of a class. Most obviously, the name of the constructor function is usually adopted as the name of the class. We say, for example, that the Range() constructor creates Range objects. More fundamentally, however, constructors are used as the righthand operand of the instanceof operator when testing objects for membership in a class. If we have an object r and want to know if it is a Range object, we can write: Even though constructors are not as fundamental as prototypes, the constructor serves as the public face of a class. Most obviously, the name of the constructor function is usually adopted as the name of the class. We say, for example, that the Range() constructor creates Range objects. More fundamentally, however, constructors are used as the righthand operand of the instanceof operator when testing objects for membership in a class. If we have an object r and want to know if it is a Range object, we can write:
...@@ -141,7 +141,7 @@ Even though instanceof cannot actually verify the use of a constructor, it still ...@@ -141,7 +141,7 @@ Even though instanceof cannot actually verify the use of a constructor, it still
If you want to test the prototype chain of an object for a specific prototype and do not want to use the constructor function as an intermediary, you can use the isPrototypeOf() method. In Example 9-1, for example, we defined a class without a constructor function, so there is no way to use instanceof with that class. Instead, however, we could test whether an object r was a member of that constructor-less class with this code: If you want to test the prototype chain of an object for a specific prototype and do not want to use the constructor function as an intermediary, you can use the isPrototypeOf() method. In Example 9-1, for example, we defined a class without a constructor function, so there is no way to use instanceof with that class. Instead, however, we could test whether an object r was a member of that constructor-less class with this code:
range.methods.isPrototypeOf(r); // range.methods is the prototype object. range.methods.isPrototypeOf(r); // range.methods is the prototype object.
9.2.2 The constructor Property ### 9.2.2 The constructor Property
In Example 9-2, we set Range.prototype to a new object that contained the methods for our class. Although it was convenient to express those methods as properties of a single object literal, it was not actually necessary to create a new object. Any regular JavaScript function (excluding arrow functions, generator functions, and async functions) can be used as a constructor, and constructor invocations need a prototype property. Therefore, every regular JavaScript function1 automatically has a prototype property. The value of this property is an object that has a single, non-enumerable constructor property. The value of the constructor property is the function object: In Example 9-2, we set Range.prototype to a new object that contained the methods for our class. Although it was convenient to express those methods as properties of a single object literal, it was not actually necessary to create a new object. Any regular JavaScript function (excluding arrow functions, generator functions, and async functions) can be used as a constructor, and constructor invocations need a prototype property. Therefore, every regular JavaScript function1 automatically has a prototype property. The value of this property is an object that has a single, non-enumerable constructor property. The value of the constructor property is the function object:
let F = function() {}; // This is a function object. let F = function() {}; // This is a function object.
...@@ -173,7 +173,7 @@ Range.prototype.includes = function(x) { ...@@ -173,7 +173,7 @@ Range.prototype.includes = function(x) {
Range.prototype.toString = function() { Range.prototype.toString = function() {
return "(" + this.from + "..." + this.to + ")"; return "(" + this.from + "..." + this.to + ")";
}; };
9.3 Classes with the class Keyword ## 9.3 Classes with the class Keyword
Classes have been part of JavaScript since the very first version of the language, but in ES6, they finally got their own syntax with the introduction of the class keyword. Example 9-3 shows what our Range class looks like when written with this new syntax. Classes have been part of JavaScript since the very first version of the language, but in ES6, they finally got their own syntax with the introduction of the class keyword. Example 9-3 shows what our Range class looks like when written with this new syntax.
Example 9-3. The Range class rewritten using class Example 9-3. The Range class rewritten using class
...@@ -249,7 +249,7 @@ All code within the body of a class declaration is implicitly in strict mode (§ ...@@ -249,7 +249,7 @@ All code within the body of a class declaration is implicitly in strict mode (§
Unlike function declarations, class declarations are not “hoisted.” Recall from §8.1.1 that function definitions behave as if they had been moved to the top of the enclosing file or enclosing function, meaning that you can invoke a function in code that comes before the actual definition of the function. Although class declarations are like function declarations in some ways, they do not share this hoisting behavior: you cannot instantiate a class before you declare it. Unlike function declarations, class declarations are not “hoisted.” Recall from §8.1.1 that function definitions behave as if they had been moved to the top of the enclosing file or enclosing function, meaning that you can invoke a function in code that comes before the actual definition of the function. Although class declarations are like function declarations in some ways, they do not share this hoisting behavior: you cannot instantiate a class before you declare it.
9.3.1 Static Methods ### 9.3.1 Static Methods
You can define a static method within a class body by prefixing the method declaration with the static keyword. Static methods are defined as properties of the constructor function rather than properties of the prototype object. You can define a static method within a class body by prefixing the method declaration with the static keyword. Static methods are defined as properties of the constructor function rather than properties of the prototype object.
For example, suppose we added the following code to Example 9-3: For example, suppose we added the following code to Example 9-3:
...@@ -269,7 +269,7 @@ You’ll sometimes see static methods called class methods because they are invo ...@@ -269,7 +269,7 @@ You’ll sometimes see static methods called class methods because they are invo
We’ll see examples of static methods in Example 9-4. We’ll see examples of static methods in Example 9-4.
9.3.2 Getters, Setters, and other Method Forms ### 9.3.2 Getters, Setters, and other Method Forms
Within a class body, you can define getter and setter methods (§6.10.6) just as you can in object literals. The only difference is that in class bodies, you don’t put a comma after the getter or setter. Example 9-4 includes a practical example of a getter method in a class. Within a class body, you can define getter and setter methods (§6.10.6) just as you can in object literals. The only difference is that in class bodies, you don’t put a comma after the getter or setter. Example 9-4 includes a practical example of a getter method in a class.
In general, all of the shorthand method definition syntaxes allowed in object literals are also allowed in class bodies. This includes generator methods (marked with *) and methods whose names are the value of an expression in square brackets. In fact, you’ve already seen (in Example 9-3) a generator method with a computed name that makes the Range class iterable: In general, all of the shorthand method definition syntaxes allowed in object literals are also allowed in class bodies. This includes generator methods (marked with *) and methods whose names are the value of an expression in square brackets. In fact, you’ve already seen (in Example 9-3) a generator method with a computed name that makes the Range class iterable:
...@@ -277,7 +277,7 @@ In general, all of the shorthand method definition syntaxes allowed in object li ...@@ -277,7 +277,7 @@ In general, all of the shorthand method definition syntaxes allowed in object li
*[Symbol.iterator]() { *[Symbol.iterator]() {
for(let x = Math.ceil(this.from); x <= this.to; x++) yield x; for(let x = Math.ceil(this.from); x <= this.to; x++) yield x;
} }
9.3.3 Public, Private, and Static Fields ### 9.3.3 Public, Private, and Static Fields
In the discussion here of classes defined with the class keyword, we have only described the definition of methods within the class body. The ES6 standard only allows the creation of methods (including getters, setters, and generators) and static methods; it does not include syntax for defining fields. If you want to define a field (which is just an object-oriented synonym for “property”) on a class instance, you must do that in the constructor function or in one of the methods. And if you want to define a static field for a class, you must do that outside the class body, after the class has been defined. Example 9-4 includes examples of both kinds of fields. In the discussion here of classes defined with the class keyword, we have only described the definition of methods within the class body. The ES6 standard only allows the creation of methods (including getters, setters, and generators) and static methods; it does not include syntax for defining fields. If you want to define a field (which is just an object-oriented synonym for “property”) on a class instance, you must do that in the constructor function or in one of the methods. And if you want to define a static field for a class, you must do that outside the class body, after the class has been defined. Example 9-4 includes examples of both kinds of fields.
Standardization is underway, however, for extended class syntax that allows the definition of instance and static fields, in both public and private forms. The code shown in the rest of this section is not yet standard JavaScript as of early 2020 but is already supported in Chrome and partially supported (public instance fields only) in Firefox. The syntax for public instance fields is in common use by JavaScript programmers using the React framework and the Babel transpiler. Standardization is underway, however, for extended class syntax that allows the definition of instance and static fields, in both public and private forms. The code shown in the rest of this section is not yet standard JavaScript as of early 2020 but is already supported in Chrome and partially supported (public instance fields only) in Firefox. The syntax for public instance fields is in common use by JavaScript programmers using the React framework and the Babel transpiler.
...@@ -322,7 +322,7 @@ static parse(s) { ...@@ -322,7 +322,7 @@ static parse(s) {
} }
If we wanted this static field to be accessible only within the class, we could make it private using a name like #pattern. If we wanted this static field to be accessible only within the class, we could make it private using a name like #pattern.
9.3.4 Example: A Complex Number Class ### 9.3.4 Example: A Complex Number Class
Example 9-4 defines a class to represent complex numbers. The class is a relatively simple one, but it includes instance methods (including getters), static methods, instance fields, and static fields. It includes some commented-out code demonstrating how we might use the not-yet-standard syntax for defining instance fields and static fields within the class body. Example 9-4 defines a class to represent complex numbers. The class is a relatively simple one, but it includes instance methods (including getters), static methods, instance fields, and static fields. It includes some commented-out code demonstrating how we might use the not-yet-standard syntax for defining instance fields and static fields within the class body.
Example 9-4. Complex.js: a complex number class Example 9-4. Complex.js: a complex number class
...@@ -398,7 +398,7 @@ c.plus(d).toString() // => "{5,5}"; use instance methods ...@@ -398,7 +398,7 @@ c.plus(d).toString() // => "{5,5}"; use instance methods
c.magnitude // => Math.hypot(2,3); use a getter function c.magnitude // => Math.hypot(2,3); use a getter function
Complex.product(c, d) // => new Complex(0, 13); a static method Complex.product(c, d) // => new Complex(0, 13); a static method
Complex.ZERO.toString() // => "{0,0}"; a static property Complex.ZERO.toString() // => "{0,0}"; a static property
9.4 Adding Methods to Existing Classes ## 9.4 Adding Methods to Existing Classes
JavaScript’s prototype-based inheritance mechanism is dynamic: an object inherits properties from its prototype, even if the properties of the prototype change after the object is created. This means that we can augment JavaScript classes simply by adding new methods to their prototype objects. JavaScript’s prototype-based inheritance mechanism is dynamic: an object inherits properties from its prototype, even if the properties of the prototype change after the object is created. This means that we can augment JavaScript classes simply by adding new methods to their prototype objects.
Here, for example, is code that adds a method for computing the complex conjugate to the Complex class of Example 9-4: Here, for example, is code that adds a method for computing the complex conjugate to the Complex class of Example 9-4:
...@@ -426,12 +426,12 @@ Number.prototype.times = function(f, context) { ...@@ -426,12 +426,12 @@ Number.prototype.times = function(f, context) {
}; };
Adding methods to the prototypes of built-in types like this is generally considered to be a bad idea because it will cause confusion and compatibility problems in the future if a new version of JavaScript defines a method with the same name. It is even possible to add methods to Object.prototype, making them available for all objects. But this is never a good thing to do because properties added to Object.prototype are visible to for/in loops (though you can avoid this by using Object.defineProperty() [§14.1] to make the new property non-enumerable). Adding methods to the prototypes of built-in types like this is generally considered to be a bad idea because it will cause confusion and compatibility problems in the future if a new version of JavaScript defines a method with the same name. It is even possible to add methods to Object.prototype, making them available for all objects. But this is never a good thing to do because properties added to Object.prototype are visible to for/in loops (though you can avoid this by using Object.defineProperty() [§14.1] to make the new property non-enumerable).
9.5 Subclasses ## 9.5 Subclasses
In object-oriented programming, a class B can extend or subclass another class A. We say that A is the superclass and B is the subclass. Instances of B inherit the methods of A. The class B can define its own methods, some of which may override methods of the same name defined by class A. If a method of B overrides a method of A, the overriding method in B often needs to invoke the overridden method in A. Similarly, the subclass constructor B() must typically invoke the superclass constructor A() in order to ensure that instances are completely initialized. In object-oriented programming, a class B can extend or subclass another class A. We say that A is the superclass and B is the subclass. Instances of B inherit the methods of A. The class B can define its own methods, some of which may override methods of the same name defined by class A. If a method of B overrides a method of A, the overriding method in B often needs to invoke the overridden method in A. Similarly, the subclass constructor B() must typically invoke the superclass constructor A() in order to ensure that instances are completely initialized.
This section starts by showing how to define subclasses the old, pre-ES6 way, and then quickly moves on to demonstrate subclassing using the class and extends keywords and superclass constructor method invocation with the super keyword. Next is a subsection about avoiding subclasses and relying on object composition instead of inheritance. The section ends with an extended example that defines a hierarchy of Set classes and demonstrates how abstract classes can be used to separate interface from implementation. This section starts by showing how to define subclasses the old, pre-ES6 way, and then quickly moves on to demonstrate subclassing using the class and extends keywords and superclass constructor method invocation with the super keyword. Next is a subsection about avoiding subclasses and relying on object composition instead of inheritance. The section ends with an extended example that defines a hierarchy of Set classes and demonstrates how abstract classes can be used to separate interface from implementation.
9.5.1 Subclasses and Prototypes ### 9.5.1 Subclasses and Prototypes
Suppose we wanted to define a Span subclass of the Range class from Example 9-2. This subclass will work just like a Range, but instead of initializing it with a start and an end, we’ll instead specify a start and a distance, or span. An instance of this Span class is also an instance of the Range superclass. A span instance inherits a customized toString() method from Span.prototype, but in order to be a subclass of Range, it must also inherit methods (such as includes()) from Range.prototype. Suppose we wanted to define a Span subclass of the Range class from Example 9-2. This subclass will work just like a Range, but instead of initializing it with a start and an end, we’ll instead specify a start and a distance, or span. An instance of this Span class is also an instance of the Range superclass. A span instance inherits a customized toString() method from Span.prototype, but in order to be a subclass of Range, it must also inherit methods (such as includes()) from Range.prototype.
Example 9-5. Span.js: a simple subclass of Range Example 9-5. Span.js: a simple subclass of Range
...@@ -467,7 +467,7 @@ You may notice that our Span() constructor sets the same from and to properties ...@@ -467,7 +467,7 @@ You may notice that our Span() constructor sets the same from and to properties
Fortunately, ES6 solves these problems with the super keyword as part of the class syntax. The next section demonstrates how it works. Fortunately, ES6 solves these problems with the super keyword as part of the class syntax. The next section demonstrates how it works.
9.5.2 Subclasses with extends and super ### 9.5.2 Subclasses with extends and super
In ES6 and later, you can create a superclass simply by adding an extends clause to a class declaration, and you can do this even for built-in classes: In ES6 and later, you can create a superclass simply by adding an extends clause to a class declaration, and you can do this even for built-in classes:
// A trivial Array subclass that adds getters for the first and last elements. // A trivial Array subclass that adds getters for the first and last elements.
...@@ -555,7 +555,7 @@ In constructors, you are required to invoke the superclass constructor before yo ...@@ -555,7 +555,7 @@ In constructors, you are required to invoke the superclass constructor before yo
Finally, before we leave the TypedMap example behind, it is worth noting that this class is an ideal candidate for the use of private fields. As the class is written now, a user could change the keyType or valueType properties to subvert the type checking. Once private fields are supported, we could change these properties to #keyType and #valueType so that they could not be altered from the outside. Finally, before we leave the TypedMap example behind, it is worth noting that this class is an ideal candidate for the use of private fields. As the class is written now, a user could change the keyType or valueType properties to subvert the type checking. Once private fields are supported, we could change these properties to #keyType and #valueType so that they could not be altered from the outside.
9.5.3 Delegation Instead of Inheritance ### 9.5.3 Delegation Instead of Inheritance
The extends keyword makes it easy to create subclasses. But that does not mean that you should create lots of subclasses. If you want to write a class that shares the behavior of some other class, you can try to inherit that behavior by creating a subclass. But it is often easier and more flexible to get that desired behavior into your class by having your class create an instance of the other class and simply delegating to that instance as needed. You create a new class not by subclassing, but instead by wrapping or “composing” other classes. This delegation approach is often called “composition,” and it is an oft-quoted maxim of object-oriented programming that one should “favor composition over inheritance.”2 The extends keyword makes it easy to create subclasses. But that does not mean that you should create lots of subclasses. If you want to write a class that shares the behavior of some other class, you can try to inherit that behavior by creating a subclass. But it is often easier and more flexible to get that desired behavior into your class by having your class create an instance of the other class and simply delegating to that instance as needed. You create a new class not by subclassing, but instead by wrapping or “composing” other classes. This delegation approach is often called “composition,” and it is an oft-quoted maxim of object-oriented programming that one should “favor composition over inheritance.”2
Suppose, for example, we wanted a Histogram class that behaves something like JavaScript’s Set class, except that instead of just keeping track of whether a value has been added to set or not, it instead maintains a count of the number of times the value has been added. Because the API for this Histogram class is similar to Set, we might consider subclassing Set and adding a count() method. On the other hand, once we start thinking about how we might implement this count() method, we might realize that the Histogram class is more like a Map than a Set because it needs to maintain a mapping between values and the number of times they have been added. So instead of subclassing Set, we can create a class that defines a Set-like API but implements those methods by delegating to an internal Map object. Example 9-7 shows how we could do this. Suppose, for example, we wanted a Histogram class that behaves something like JavaScript’s Set class, except that instead of just keeping track of whether a value has been added to set or not, it instead maintains a count of the number of times the value has been added. Because the API for this Histogram class is similar to Set, we might consider subclassing Set and adding a count() method. On the other hand, once we start thinking about how we might implement this count() method, we might realize that the Histogram class is more like a Map than a Set because it needs to maintain a mapping between values and the number of times they have been added. So instead of subclassing Set, we can create a class that defines a Set-like API but implements those methods by delegating to an internal Map object. Example 9-7 shows how we could do this.
...@@ -606,7 +606,7 @@ class Histogram { ...@@ -606,7 +606,7 @@ class Histogram {
} }
All the Histogram() constructor does in Example 9-7 is create a Map object. And most of the methods are one-liners that just delegate to a method of the map, making the implementation quite simple. Because we used delegation rather than inheritance, a Histogram object is not an instance of Set or Map. But Histogram implements a number of commonly used Set methods, and in an untyped language like JavaScript, that is often good enough: a formal inheritance relationship is sometimes nice, but often optional. All the Histogram() constructor does in Example 9-7 is create a Map object. And most of the methods are one-liners that just delegate to a method of the map, making the implementation quite simple. Because we used delegation rather than inheritance, a Histogram object is not an instance of Set or Map. But Histogram implements a number of commonly used Set methods, and in an untyped language like JavaScript, that is often good enough: a formal inheritance relationship is sometimes nice, but often optional.
9.5.4 Class Hierarchies and Abstract Classes ### 9.5.4 Class Hierarchies and Abstract Classes
Example 9-6 demonstrated how we can subclass Map. Example 9-7 demonstrated how we can instead delegate to a Map object without actually subclassing anything. Using JavaScript classes to encapsulate data and modularize your code is often a great technique, and you may find yourself using the class keyword frequently. But you may find that you prefer composition to inheritance and that you rarely need to use extends (except when you’re using a library or framework that requires you to extend its base classes). Example 9-6 demonstrated how we can subclass Map. Example 9-7 demonstrated how we can instead delegate to a Map object without actually subclassing anything. Using JavaScript classes to encapsulate data and modularize your code is often a great technique, and you may find yourself using the class keyword frequently. But you may find that you prefer composition to inheritance and that you rarely need to use extends (except when you’re using a library or framework that requires you to extend its base classes).
There are some circumstances when multiple levels of subclassing are appropriate, however, and we’ll end this chapter with an extended example that demonstrates a hierarchy of classes representing different kinds of sets. (The set classes defined in Example 9-8 are similar to, but not completely compatible with, JavaScript’s built-in Set class.) There are some circumstances when multiple levels of subclassing are appropriate, however, and we’ll end this chapter with an extended example that demonstrates a hierarchy of classes representing different kinds of sets. (The set classes defined in Example 9-8 are similar to, but not completely compatible with, JavaScript’s built-in Set class.)
...@@ -819,7 +819,7 @@ class BitSet extends AbstractWritableSet { ...@@ -819,7 +819,7 @@ class BitSet extends AbstractWritableSet {
// Some pre-computed values used by the has(), insert() and remove() methods // Some pre-computed values used by the has(), insert() and remove() methods
BitSet.bits = new Uint8Array([1, 2, 4, 8, 16, 32, 64, 128]); BitSet.bits = new Uint8Array([1, 2, 4, 8, 16, 32, 64, 128]);
BitSet.masks = new Uint8Array([~1, ~2, ~4, ~8, ~16, ~32, ~64, ~128]); BitSet.masks = new Uint8Array([~1, ~2, ~4, ~8, ~16, ~32, ~64, ~128]);
9.6 Summary ## 9.6 Summary
This chapter has explained the key features of JavaScript classes: This chapter has explained the key features of JavaScript classes:
Objects that are members of the same class inherit properties from the same prototype object. The prototype object is the key feature of JavaScript classes, and it is possible to define classes with nothing more than the Object.create() method. Objects that are members of the same class inherit properties from the same prototype object. The prototype object is the key feature of JavaScript classes, and it is possible to define classes with nothing more than the Object.create() method.
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册