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

二、三级标题 ch6--ch10

上级 5c13d167
......@@ -11,7 +11,7 @@ Node modules using require()
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.
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() {
// And here is how we might use the module
stats.mean([1, 3, 5, 7, 9]) // => 5
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.
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);
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.
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, 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.
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:
const sum = (x, y) => x + y;
......@@ -140,7 +140,7 @@ const stddev = d => {
// Now export only the public ones
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.
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');
// This is nice and succinct, though we lose a bit of context
// without the 'stats' prefix as a namspace for the stddev() function.
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.
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.
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
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:
export const PI = Math.PI;
......@@ -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.)
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:
import BitSet from './bitset.js';
......@@ -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.
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:
import { render as renderImage } from "./imageutils.js";
......@@ -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:
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.
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
// The average.js module simply re-exports the stats/mean.js default export
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.
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
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.
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
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.)
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
function localStringsURL(locale) {
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:
In the early days of JavaScript, modularity could only be achieved through the clever use of immediately invoked function expressions.
......
此差异已折叠。
......@@ -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.
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:
Array literals
......@@ -18,7 +18,7 @@ The Array() constructor
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:
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
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.
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:
let a = [1, 2, 3];
......@@ -58,7 +58,7 @@ Set objects (§11.1.1) are iterable, so an easy way to remove duplicate elements
let letters = [..."hello world"];
[...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:
Call it with no arguments:
......@@ -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");
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.
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
Array.of() // => []; returns empty array with no arguments
Array.of(10) // => [10]; can create arrays with a single numeric argument
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:
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
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.)
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:
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
let a = [true, false]; // This array has elements at indexes 0 and 1
a[2] // => undefined; no element at this index.
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.
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
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.
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:
[].length // => 0: the array has no elements
......@@ -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)
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:
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
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:
let letters = [..."Hello world"]; // An array of letters
......@@ -237,7 +237,7 @@ for(let i = 0; i < a.length; i++) {
if (a[i] === undefined) continue; // Skip undefined + nonexistent elements
// 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:
// Create a multidimensional array
......@@ -255,7 +255,7 @@ for(let row = 0; row < table.length; row++) {
// Use the multidimensional array to compute 5*7
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.
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
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.
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
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:
[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
// Map non-negative numbers to their square roots
[-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:
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
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.
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:
let stack = []; // stack == []
......@@ -416,7 +416,7 @@ a.unshift(1) // a == [1]
a.unshift(2) // a == [2, 1]
a = []; // a == []
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.
SLICE()
......@@ -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
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.
INDEXOF() AND LASTINDEXOF()
......@@ -534,7 +534,7 @@ The reverse() method reverses the order of the elements of an array and returns
let a = [1,2,3];
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 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
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.
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({}) // => 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:
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
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().
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:
let s = "test";
......@@ -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"
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:
Array literals are written as comma-separated lists of values within square brackets.
......
此差异已折叠。
......@@ -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.
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.
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
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-2. A Range class using a constructor
......@@ -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.
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.
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
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.
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:
let F = function() {}; // This is a function object.
......@@ -173,7 +173,7 @@ Range.prototype.includes = function(x) {
Range.prototype.toString = function() {
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.
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 (§
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.
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
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.
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
*[Symbol.iterator]() {
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.
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) {
}
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. Complex.js: a complex number class
......@@ -398,7 +398,7 @@ c.plus(d).toString() // => "{5,5}"; use instance methods
c.magnitude // => Math.hypot(2,3); use a getter function
Complex.product(c, d) // => new Complex(0, 13); a static method
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.
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) {
};
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.
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.
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
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:
// 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
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
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 {
}
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).
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 {
// 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.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:
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.
先完成此消息的编辑!
想要评论请 注册