JavaScript: The Definitive Guide
SIXTH EDITION
JavaScript: The Definitive Guide
David Flanagan
Beijing • Cambridge • Farnham • Köln • Sebastopol • Tokyo
JavaScript: The Definitive Guide, Sixth Edition by David Flanagan Copyright © 2011 David Flanagan. All rights reserved. Printed in the United States of America. Published by O’Reilly Media, Inc., 1005 Gravenstein Highway North, Sebastopol, CA 95472. O’Reilly books may be purchased for educational, business, or sales promotional use. Online editions are also available for most titles (http://my.safaribooksonline.com). For more information, contact our corporate/institutional sales department: (800) 998-9938 or
[email protected].
Editor: Mike Loukides Production Editor: Teresa Elsey Proofreader: Teresa Elsey
Indexer: Ellen Troutman Zaig Cover Designer: Karen Montgomery Interior Designer: David Futato Illustrator: Robert Romano
Printing History: August 1996: January 1997: June 1998: January 2002: August 2006: March 2011:
Beta Edition. Second Edition. Third Edition. Fourth Edition. Fifth Edition. Sixth Edition.
Nutshell Handbook, the Nutshell Handbook logo, and the O’Reilly logo are registered trademarks of O’Reilly Media, Inc. JavaScript: The Definitive Guide, the image of a Javan rhinoceros, and related trade dress are trademarks of O’Reilly Media, Inc. Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations appear in this book, and O’Reilly Media, Inc., was aware of a trademark claim, the designations have been printed in caps or initial caps. While every precaution has been taken in the preparation of this book, the publisher and authors assume no responsibility for errors or omissions, or for damages resulting from the use of the information contained herein.
ISBN: 978-0-596-80552-4 [LSI] 1302719886
This book is dedicated to all who teach peace and resist violence.
Table of Contents
Preface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xiii 1. Introduction to JavaScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.1 Core JavaScript 1.2 Client-Side JavaScript
4 8
Part I. Core JavaScript 2. Lexical Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 2.1 2.2 2.3 2.4 2.5
Character Set Comments Literals Identifiers and Reserved Words Optional Semicolons
21 23 23 23 25
3. Types, Values, and Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10
Numbers Text Boolean Values null and undefined The Global Object Wrapper Objects Immutable Primitive Values and Mutable Object References Type Conversions Variable Declaration Variable Scope
31 36 40 41 42 43 44 45 52 53
4. Expressions and Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 4.1 Primary Expressions 4.2 Object and Array Initializers 4.3 Function Definition Expressions
57 58 59 vii
4.4 4.5 4.6 4.7 4.8 4.9 4.10 4.11 4.12 4.13
Property Access Expressions Invocation Expressions Object Creation Expressions Operator Overview Arithmetic Expressions Relational Expressions Logical Expressions Assignment Expressions Evaluation Expressions Miscellaneous Operators
60 61 61 62 66 71 75 77 79 82
5. Statements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 5.1 5.2 5.3 5.4 5.5 5.6 5.7 5.8
Expression Statements Compound and Empty Statements Declaration Statements Conditionals Loops Jumps Miscellaneous Statements Summary of JavaScript Statements
88 88 89 92 97 102 108 112
6. Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115 6.1 6.2 6.3 6.4 6.5 6.6 6.7 6.8 6.9 6.10
Creating Objects Querying and Setting Properties Deleting Properties Testing Properties Enumerating Properties Property Getters and Setters Property Attributes Object Attributes Serializing Objects Object Methods
116 120 124 125 126 128 131 135 138 138
7. Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141 7.1 7.2 7.3 7.4 7.5 7.6 7.7 7.8 7.9 7.10
Creating Arrays Reading and Writing Array Elements Sparse Arrays Array Length Adding and Deleting Array Elements Iterating Arrays Multidimensional Arrays Array Methods ECMAScript 5 Array Methods Array Type
viii | Table of Contents
141 142 144 144 145 146 148 148 153 157
7.11 Array-Like Objects 7.12 Strings As Arrays
158 160
8. Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163 8.1 8.2 8.3 8.4 8.5 8.6 8.7 8.8
Defining Functions Invoking Functions Function Arguments and Parameters Functions As Values Functions As Namespaces Closures Function Properties, Methods, and Constructor Functional Programming
164 166 171 176 178 180 186 191
9. Classes and Modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199 9.1 9.2 9.3 9.4 9.5 9.6 9.7 9.8 9.9
Classes and Prototypes Classes and Constructors Java-Style Classes in JavaScript Augmenting Classes Classes and Types Object-Oriented Techniques in JavaScript Subclasses Classes in ECMAScript 5 Modules
200 201 205 208 209 215 228 238 246
10. Pattern Matching with Regular Expressions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251 10.1 Defining Regular Expressions 10.2 String Methods for Pattern Matching 10.3 The RegExp Object
251 259 261
11. JavaScript Subsets and Extensions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265 11.1 11.2 11.3 11.4 11.5 11.6 11.7
JavaScript Subsets Constants and Scoped Variables Destructuring Assignment Iteration Shorthand Functions Multiple Catch Clauses E4X: ECMAScript for XML
266 269 271 274 282 283 283
12. Server-Side JavaScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 289 12.1 Scripting Java with Rhino 12.2 Asynchronous I/O with Node
289 296
Table of Contents | ix
Part II. Client-Side JavaScript 13. JavaScript in Web Browsers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 307 13.1 13.2 13.3 13.4 13.5 13.6 13.7
Client-Side JavaScript Embedding JavaScript in HTML Execution of JavaScript Programs Compatibility and Interoperability Accessibility Security Client-Side Frameworks
307 311 317 325 332 332 338
14. The Window Object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 341 14.1 14.2 14.3 14.4 14.5 14.6 14.7 14.8
Timers Browser Location and Navigation Browsing History Browser and Screen Information Dialog Boxes Error Handling Document Elements As Window Properties Multiple Windows and Frames
341 343 345 346 348 351 351 353
15. Scripting Documents . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 361 15.1 15.2 15.3 15.4 15.5 15.6 15.7 15.8 15.9 15.10
Overview of the DOM Selecting Document Elements Document Structure and Traversal Attributes Element Content Creating, Inserting, and Deleting Nodes Example: Generating a Table of Contents Document and Element Geometry and Scrolling HTML Forms Other Document Features
361 364 371 375 378 382 387 389 396 405
16. Scripting CSS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 413 16.1 16.2 16.3 16.4 16.5 16.6
Overview of CSS Important CSS Properties Scripting Inline Styles Querying Computed Styles Scripting CSS Classes Scripting Stylesheets
414 419 431 435 437 440
17. Handling Events . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 445 17.1 Types of Events x | Table of Contents
447
17.2 17.3 17.4 17.5 17.6 17.7 17.8 17.9
Registering Event Handlers Event Handler Invocation Document Load Events Mouse Events Mousewheel Events Drag and Drop Events Text Events Keyboard Events
456 460 465 467 471 474 481 484
18. Scripted HTTP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 491 18.1 Using XMLHttpRequest 18.2 HTTP by : JSONP 18.3 Comet with Server-Sent Events
494 513 515
19. The jQuery Library . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 523 19.1 19.2 19.3 19.4 19.5 19.6 19.7 19.8 19.9 19.10
jQuery Basics jQuery Getters and Setters Altering Document Structure Handling Events with jQuery Animated Effects Ajax with jQuery Utility Functions jQuery Selectors and Selection Methods Extending jQuery with Plug-ins The jQuery UI Library
524 531 537 540 551 558 571 574 582 585
20. Client-Side Storage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 587 20.1 20.2 20.3 20.4
localStorage and sessionStorage Cookies IE userData Persistence Application Storage and Offline Webapps
589 593 599 601
21. Scripted Media and Graphics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 613 21.1 21.2 21.3 21.4
Scripting Images Scripting Audio and Video SVG: Scalable Vector Graphics Graphics in a
613 615 622 630
22. HTML5 APIs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 667 22.1 22.2 22.3 22.4
Geolocation History Management Cross-Origin Messaging Web Workers
668 671 676 680
Table of Contents | xi
22.5 22.6 22.7 22.8 22.9
Typed Arrays and ArrayBuffers Blobs The Filesystem API Client-Side Databases Web Sockets
687 691 700 705 712
Part III. Core JavaScript Reference Core JavaScript Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 719
Part IV. Client-Side JavaScript Reference Client-Side JavaScript Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 859 Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1019
xii | Table of Contents
Preface
This book covers the JavaScript language and the JavaScript APIs implemented by web browsers. I wrote it for readers with at least some prior programming experience who want to learn JavaScript and also for programmers who already use JavaScript but want to take their understanding to a new level and really master the language and the web platform. My goal with this book is to document the JavaScript language and platform comprehensively and definitively. As a result, this is a large and detailed book. My hope, however, is that it will reward careful study, and that the time you spend reading it will be easily recouped in the form of higher programming productivity. This book is divided into four parts. Part I covers the JavaScript language itself. Part II covers client-side JavaScript: the JavaScript APIs defined by HTML5 and related standards and implemented by web browsers. Part III is the reference section for the core language, and Part IV is the reference for client-side JavaScript. Chapter 1 includes an outline of the chapters in Parts I and II (see §1.1). This sixth edition of the book covers both ECMAScript 5 (the latest version of the core language) and HTML5 (the latest version of the web platform). You’ll find ECMAScript 5 material throughout Part I. The new material on HTML5 is mostly in the chapters at the end of Part II, but there is also some in other chapters as well. Completely new chapters in this edition include Chapter 11, JavaScript Subsets and Extensions; Chapter 12, Server-Side JavaScript; Chapter 19, The jQuery Library; and Chapter 22, HTML5 APIs. Readers of previous editions may notice that I have completely rewritten many of the chapters in this book for the sixth edition. The core of Part I—the chapters covering objects, arrays, functions, and classes—is all new and brings the book in line with current programming styles and best practices. Similarly, key chapters of Part II, such as those covering documents and events, have been completely rewritten to bring them up-to-date.
xiii
A Note About Piracy If you are reading a digital version of this book that you (or your employer) did not pay for (or borrow from someone who did) then you probably have an illegally pirated copy. Writing the sixth edition of this book was a full-time job, and it took more than a year. The only way I get paid for that time is when readers actually buy the book. And the only way I can afford to work on a seventh edition is if I get paid for the sixth. I do not condone piracy, but if you have a pirated copy, go ahead and read a couple of chapters. I think that you’ll find that this is a valuable source of information about JavaScript, better organized and of higher quality than what you can find freely (and legally) available on the Web. If you agree that this is a valuable source of information, then please pay for that value by purchasing a legal copy (either digital or print) of the book. On the other hand, if you find that this book is no more valuable than the free information on the web, then please discard your pirated copy and use those free information sources.
Conventions Used in This Book I use the following typographical conventions in this book: Italic Is used for emphasis and to indicate the first use of a term. Italic is also used for email addresses, URLs and file names. Constant width
Is used in all JavaScript code and CSS and HTML listings, and generally for anything that you would type literally when programming. Constant width italic
Is used for the names of function parameters, and generally as a placeholder to indicate an item that should be replaced with an actual value in your program.
Example Code The examples in this book are available online. You can find them linked from the book’s catalog page at the publisher’s website: http://oreilly.com/catalog/9780596805531/ This book is here to help you get your job done. In general, you may use the code in this book in your programs and documentation. You do not need to contact O’Reilly for permission unless you’re reproducing a significant portion of the code. For example, writing a program that uses several chunks of code from this book does not require permission. Selling or distributing a CD-ROM of examples from O’Reilly books does require permission. Answering a question by citing this book and quoting example
xiv | Preface
code does not require permission. Incorporating a significant amount of example code from this book into your product’s documentation does require permission. If you use the code from this book, I appreciate, but do not require, attribution. An attribution usually includes the title, author, publisher, and ISBN. For example: “JavaScript: The Definitive Guide, by David Flanagan (O’Reilly). Copyright 2011 David Flanagan, 978-0-596-80552-4.” For more details on the O’Reilly code reuse policy, see http://oreilly.com/pub/a/oreilly/ ask_tim/2001/codepolicy.html. If you feel your use of the examples falls outside of the permission given above, feel free to contact O’Reilly at
[email protected].
Errata and How to Contact Us The publisher maintains a public list of errors found in this book. You can view the list, and submit the errors you find, by visiting the book’s web page: http://oreilly.com/catalog/9780596805531 To comment or ask technical questions about this book, send email to:
[email protected] For more information about our books, conferences, Resource Centers, and the O’Reilly Network, see our website at: http://www.oreilly.com Find us on Facebook: http://facebook.com/oreilly Follow us on Twitter: http://twitter.com/oreillymedia Watch us on YouTube: http://www.youtube.com/oreillymedia
Acknowledgments Many people have helped me with the creation of this book. I’d like to thank my editor, Mike Loukides, for trying to keep me on schedule and for his insightful comments. Thanks also to my technical reviewers: Zachary Kessin, who reviewed many of the chapters in Part I, and Raffaele Cecco, who reviewed Chapter 19 and the material in Chapter 21. The production team at O’Reilly has done their usual fine job: Dan Fauxsmith managed the production process, Teresa Elsey was the production editor, Rob Romano drew the figures, and Ellen Troutman Zaig created the index. In this era of effortless electronic communication, it is impossible to keep track of all those who influence and inform us. I’d like to thank everyone who has answered my questions on the es5, w3c, and whatwg mailing lists, and everyone who has shared their insightful ideas about JavaScript programming online. I’m sorry I can’t list you all by
Preface | xv
name, but it is a pleasure to work within such a vibrant community of JavaScript programmers. Editors, reviewers, and contributors to previous editions of this book have included: Andrew Schulman, Angelo Sirigos, Aristotle Pagaltzis, Brendan Eich, Christian Heilmann, Dan Shafer, Dave C. Mitchell, Deb Cameron, Douglas Crockford, Dr. Tankred Hirschmann, Dylan Schiemann, Frank Willison, Geoff Stearns, Herman Venter, Jay Hodges, Jeff Yates, Joseph Kesselman, Ken Cooper, Larry Sullivan, Lynn Rollins, Neil Berkman, Nick Thompson, Norris Boyd, Paula Ferguson, Peter-Paul Koch, Philippe Le Hegaret, Richard Yaker, Sanders Kleinfeld, Scott Furman, Scott Issacs, Shon Katzenberger, Terry Allen, Todd Ditchendorf, Vidur Apparao, and Waldemar Horwat. This edition of the book is substantially rewritten and kept me away from my family for many late nights. My love to them and my thanks for putting up with my absences. — David Flanagan (davidflanagan.com), March 2011
xvi | Preface
CHAPTER 1
Introduction to JavaScript
JavaScript is the programming language of the Web. The overwhelming majority of modern websites use JavaScript, and all modern web browsers—on desktops, game consoles, tablets, and smart phones—include JavaScript interpreters, making JavaScript the most ubiquitous programming language in history. JavaScript is part of the triad of technologies that all Web developers must learn: HTML to specify the content of web pages, CSS to specify the presentation of web pages, and JavaScript to specify the behavior of web pages. This book will help you master the language. If you are already familiar with other programming languages, it may help you to know that JavaScript is a high-level, dynamic, untyped interpreted programming language that is well-suited to object-oriented and functional programming styles. JavaScript derives its syntax from Java, its first-class functions from Scheme, and its prototypebased inheritance from Self. But you do not need to know any of those languages, or be familiar with those terms, to use this book and learn JavaScript. The name “JavaScript” is actually somewhat misleading. Except for a superficial syntactic resemblance, JavaScript is completely different from the Java programming language. And JavaScript has long since outgrown its scripting-language roots to become a robust and efficient general-purpose language. The latest version of the language (see the sidebar) defines new features for serious large-scale software development.
1
JavaScript: Names and Versions JavaScript was created at Netscape in the early days of the Web, and technically, “JavaScript” is a trademark licensed from Sun Microsystems (now Oracle) used to describe Netscape’s (now Mozilla’s) implementation of the language. Netscape submitted the language for standardization to ECMA—the European Computer Manufacturer’s Association—and because of trademark issues, the standardized version of the language was stuck with the awkward name “ECMAScript.” For the same trademark reasons, Microsoft’s version of the language is formally known as “JScript.” In practice, just about everyone calls the language JavaScript. This book uses the name “ECMAScript” only to refer to the language standard. For the last decade, all web browsers have implemented version 3 of the ECMAScript standard and there has really been no need to think about version numbers: the language standard was stable and browser implementations of the language were, for the most part, interoperable. Recently, an important new version of the language has been defined as ECMAScript version 5 and, at the time of this writing, browsers are beginning to implement it. This book covers all the new features of ECMAScript 5 as well as all the long-standing features of ECMAScript 3. You’ll sometimes see these language versions abbreviated as ES3 and ES5, just as you’ll sometimes see the name JavaScript abbreviated as JS. When we’re speaking of the language itself, the only version numbers that are relevant are ECMAScript versions 3 or 5. (Version 4 of ECMAScript was under development for years, but proved to be too ambitious and was never released.) Sometimes, however, you’ll also see a JavaScript version number, such as JavaScript 1.5 or JavaScript 1.8. These are Mozilla’s version numbers: version 1.5 is basically ECMAScript 3, and later versions include nonstandard language extensions (see Chapter 11). Finally, there are also version numbers attached to particular JavaScript interpreters or “engines.” Google calls its JavaScript interpreter V8, for example, and at the time of this writing the current version of the V8 engine is 3.0.
To be useful, every language must have a platform or standard library or API of functions for performing things like basic input and output. The core JavaScript language defines a minimal API for working with text, arrays, dates, and regular expressions but does not include any input or output functionality. Input and output (as well as more sophisticated features, such as networking, storage, and graphics) are the responsibility of the “host environment” within which JavaScript is embedded. Usually that host environment is a web browser (though we’ll see two uses of JavaScript without a web browser in Chapter 12). Part I of this book covers the language itself and its minimal built-in API. Part II explains how JavaScript is used in web browsers and covers the sprawling browser-based APIs loosely known as “client-side JavaScript.” Part III is the reference section for the core API. You can read about the JavaScript array manipulation API by looking up “Array” in this part of the book, for example. Part IV is the reference section for client-side JavaScript. You might look up “Canvas” 2 | Chapter 1: Introduction to JavaScript
in this part of the book to read about the graphics API defined by the HTML5 element, for example. This book covers low-level fundamentals first, and then builds on those to more advanced and higher-level abstractions. The chapters are intended to be read more or less in order. But learning a new programming language is never a linear process, and describing a language is not linear either: each language feature is related to other features and this book is full of cross-references—sometimes backward and sometimes forward to material you have not yet read. This chapter makes a quick first pass through the core language and the client-side API, introducing key features that will make it easier to understand the in-depth treatment in the chapters that follow.
Exploring JavaScript When learning a new programming language, it’s important to try the examples in the book, and then modify them and try them again to test your understanding of the language. To do that, you need a JavaScript interpreter. Fortunately, every web browser includes a JavaScript interpreter, and if you’re reading this book, you probably already have more than one web browser installed on your computer. We’ll see later on in this chapter that you can embed JavaScript code within tags in HTML files, and when the browser loads the file, it will execute the code. Fortunately, however, you don’t have to do that every time you want to try out simple snippets of JavaScript code. Spurred on by the powerful and innovative Firebug extension for Firefox (pictured in Figure 1-1 and available for download from http://getfirebug .com/), today’s web browsers all include web developer tools that are indispensable for debugging, experimenting, and learning. You can usually find these tools in the Tools menu of the browser under names like “Developer Tools” or “Web Console.” (Firefox 4 includes a built-in “Web Console,” but at the time of this writing, the Firebug extension is better.) Often, you can call up a console with a keystroke like F12 or CtrlShift-J. These console tools often appear as panes at the top or bottom of the browser window, but some allow you to open them as separate windows (as pictured in Figure 1-1), which is often quite convenient. A typical “developer tools” pane or window includes multiple tabs that allow you to inspect things like HTML document structure, CSS styles, network requests, and so on. One of the tabs is a “JavaScript console” that allows you to type in lines of JavaScript code and try them out. This is a particularly easy way to play around with JavaScript, and I recommend that you use it as you read this book. There is a simple console API that is portably implemented by modern browsers. You can use the function console.log() to display text on the console. This is often surprisingly helpful while debugging, and some of the examples in this book (even in the core language section) use console.log() to perform simple output. A similar but more intrusive way to display output or debugging messages is by passing a string of text to the alert() function, which displays it in a modal dialog box.
Introduction to JavaScript | 3
Figure 1-1. The Firebug debugging console for Firefox
1.1 Core JavaScript This section is a tour of the JavaScript language, and also a tour of Part I of this book. After this introductory chapter, we dive into JavaScript at the lowest level: Chapter 2, Lexical Structure, explains things like JavaScript comments, semicolons, and the Unicode character set. Chapter 3, Types, Values, and Variables, starts to get more interesting: it explains JavaScript variables and the values you can assign to those variables. Here’s some sample code to illustrate the highlights of those two chapters: // Anything following double slashes is an English-language comment. // Read the comments carefully: they explain the JavaScript code. // variable is a symbolic name for a value. // Variables are declared with the var keyword: var x; // Declare a variable named x. // Values can be assigned to variables with an = sign x = 0; // Now the variable x has the value 0 x // => 0: A variable evaluates to its value. // JavaScript supports several types of values x = 1; // Numbers. x = 0.01; // Just one Number type for integers and reals. x = "hello world"; // Strings of text in quotation marks. x = 'JavaScript'; // Single quote marks also delimit strings. x = true; // Boolean values. x = false; // The other Boolean value.
4 | Chapter 1: Introduction to JavaScript
x = null; x = undefined;
// Null is a special value that means "no value". // Undefined is like null.
Two other very important types that JavaScript programs can manipulate are objects and arrays. These are the subject of Chapter 6, Objects, and Chapter 7, Arrays, but they are so important that you’ll see them many times before you reach those chapters. // JavaScript's most important data type is the object. // An object is a collection of name/value pairs, or a string to value map. var book = { // Objects are enclosed in curly braces. topic: "JavaScript", // The property "topic" has value "JavaScript". fat: true // The property "fat" has value true. }; // The curly brace marks the end of the object. // Access the properties of an object with . or []: book.topic // => "JavaScript" book["fat"] // => true: another way to access property values. book.author = "Flanagan"; // Create new properties by assignment. book.contents = {}; // {} is an empty object with no properties. // JavaScript also supports arrays (numerically indexed lists) of values: var primes = [2, 3, 5, 7]; // An array of 4 values, delimited with [ and ]. primes[0] // => 2: the first element (index 0) of the array. primes.length // => 4: how many elements in the array. primes[primes.length-1] // => 7: the last element of the array. primes[4] = 9; // Add a new element by assignment. primes[4] = 11; // Or alter an existing element by assignment. var empty = []; // [] is an empty array with no elements. empty.length // => 0 // Arrays and objects can hold other arrays and objects: var points = [ // An array with 2 elements. {x:0, y:0}, // Each element is an object. {x:1, y:1} ]; var data = { // An object with 2 properties trial1: [[1,2], [3,4]], // The value of each property is an array. trial2: [[2,3], [4,5]] // The elements of the arrays are arrays. };
The syntax illustrated above for listing array elements within square braces or mapping object property names to property values inside curly braces is known as an initializer expression, and it is just one of the topics of Chapter 4, Expressions and Operators. An expression is a phrase of JavaScript that can be evaluated to produce a value. The use of . and [] to refer to the value of an object property or array element is an expression, for example. You may have noticed in the code above that when an expression stands alone on a line, the comment that follows it begins with an arrow (=>) and the value of the expression. This is a convention that you’ll see throughout this book. One of the most common ways to form expressions in JavaScript is to use operators like these: // Operators act on values (the operands) to produce a new value. // Arithmetic operators are the most common: 3 + 2 // => 5: addition
1.1 Core JavaScript | 5
3 - 2 3 * 2 3 / 2 points[1].x - points[0].x "3" + "2"
// // // // //
=> => => => =>
1: subtraction 6: multiplication 1.5: division 1: more complicated operands work, too "32": + adds numbers, concatenates strings
// JavaScript defines some shorthand arithmetic operators var count = 0; // Define a variable count++; // Increment the variable count--; // Decrement the variable count += 2; // Add 2: same as count = count + 2; count *= 3; // Multiply by 3: same as count = count * 3; count // => 6: variable names are expressions, too. // Equality and relational operators test whether two values are equal, // unequal, less than, greater than, and so on. They evaluate to true or false. var x = 2, y = 3; // These = signs are assignment, not equality tests x == y // => false: equality x != y // => true: inequality x < y // => true: less-than x true: less-than or equal x > y // => false: greater-than x >= y // => false: greater-than or equal "two" == "three" // => false: the two strings are different "two" > "three" // => true: "tw" is alphabetically greater than "th" false == (x > y) // => true: false is equal to false // Logical operators combine or invert boolean values (x == 2) && (y == 3) // => true: both comparisons are true. && is AND (x > 3) || (y < 3) // => false: neither comparison is true. || is OR !(x == y) // => true: ! inverts a boolean value
If the phrases of JavaScript are expressions, then the full sentences are statements, which are the topic of Chapter 5, Statements. In the code above, the lines that end with semicolons are statements. (In the code below, you’ll see multiline statements that do not end with semicolons.) There is actually a lot of overlap between statements and expressions. Roughly, an expression is something that computes a value but doesn’t do anything: it doesn’t alter the program state in any way. Statements, on the other hand, don’t have a value (or don’t have a value that we care about), but they do alter the state. You’ve seen variable declarations and assignment statements above. The other broad category of statement is control structures, such as conditionals and loops. Examples are below, after we cover functions. A function is a named and parametrized block of JavaScript code that you define once, and can then invoke over and over again. Functions aren’t covered formally until Chapter 8, Functions, but like objects and arrays, you’ll see them many times before you get to that chapter. Here are some simple examples: // Functions are parameterized blocks of JavaScript code that we can invoke. function plus1(x) { // Define a function named "plus1" with parameter "x" return x+1; // Return a value one larger than the value passed in } // Functions are enclosed in curly braces
6 | Chapter 1: Introduction to JavaScript
plus1(y)
// => 4: y is 3, so this invocation returns 3+1
var square = function(x) { // Functions are values and can be assigned to vars return x*x; // Compute the function's value }; // Semicolon marks the end of the assignment. square(plus1(y))
// => 16: invoke two functions in one expression
When we combine functions with objects, we get methods: // When functions are assigned to the properties of an object, we call // them "methods". All JavaScript objects have methods: var a = []; // Create an empty array a.push(1,2,3); // The push() method adds elements to an array a.reverse(); // Another method: reverse the order of elements // We can define our own methods, too. The "this" keyword refers to the object // on which the method is defined: in this case, the points array from above. points.dist = function() { // Define a method to compute distance between points var p1 = this[0]; // First element of array we're invoked on var p2 = this[1]; // Second element of the "this" object var a = p2.x-p1.x; // Difference in X coordinates var b = p2.y-p1.y; // Difference in Y coordinates return Math.sqrt(a*a + // The Pythagorean theorem b*b); // Math.sqrt() computes the square root }; points.dist() // => 1.414: distance between our 2 points
Now, as promised, here are some functions whose bodies demonstrate common JavaScript control structure statements: // JavaScript statements include conditionals and loops using the syntax // of C, C++, Java, and other languages. function abs(x) { // A function to compute the absolute value if (x >= 0) { // The if statement... return x; // executes this code if the comparison is true. } // This is the end of the if clause. else { // The optional else clause executes its code if return -x; // the comparison is false. } // Curly braces optional when 1 statement per clause. } // Note return statements nested inside if/else. function factorial(n) { var product = 1; while(n > 1) { product *= n; n--; } return product; } factorial(4)
// // // // // // //
function factorial2(n) { var i, product = 1; for(i=2; i 24: 1*4*3*2 Another version using a different loop Start with 1 Automatically increment i from 2 up to n Do this each time. {} not needed for 1-line loops Return the factorial
1.1 Core JavaScript | 7
} factorial2(5)
// => 120: 1*2*3*4*5
JavaScript is an object-oriented language, but it is quite different than most. Chapter 9, Classes and Modules, covers object-oriented programming in JavaScript in detail, with lots of examples, and is one of the longest chapters in the book. Here is a very simple example that demonstrates how to define a JavaScript class to represent 2D geometric points. Objects that are instances of this class have a single method named r() that computes the distance of the point from the origin: // Define a constructor function to initialize a new Point object function Point(x,y) { // By convention, constructors start with capitals this.x = x; // this keyword is the new object being initialized this.y = y; // Store function arguments as object properties } // No return is necessary // Use a constructor function with the keyword "new" to create instances var p = new Point(1, 1); // The geometric point (1,1) // Define methods for Point objects by assigning them to the prototype // object associated with the constructor function. Point.prototype.r = function() { return Math.sqrt( // Return the square root of x² + y² this.x * this.x + // This is the Point object on which the method... this.y * this.y // ...is invoked. ); }; // Now the Point object p (and all future Point objects) inherits the method r() p.r() // => 1.414...
Chapter 9 is really the climax of Part I, and the chapters that follow wrap up some loose ends and bring our exploration of the core language to a close. Chapter 10, Pattern Matching with Regular Expressions, explains the regular expression grammar and demonstrates how to use these “regexps” for textual pattern matching. Chapter 11, JavaScript Subsets and Extensions, covers subsets and extensions of core JavaScript. Finally, before we plunge into client-side JavaScript in web browsers, Chapter 12, Server-Side JavaScript, introduces two ways to use JavaScript outside of web browsers.
1.2 Client-Side JavaScript Client-side JavaScript does not exhibit the nonlinear cross-reference problem nearly to the extent that the core language does, and it is possible to learn how to use JavaScript in web browsers in a fairly linear sequence. But you’re probably reading this book to learn client-side JavaScript, and Part II is a long way off, so this section is a quick sketch of basic client-side programming techniques, followed by an in-depth example. Chapter 13, JavaScript in Web Browsers, is the first chapter of Part II and it explains in detail how to put JavaScript to work in web browsers. The most important thing you’ll
8 | Chapter 1: Introduction to JavaScript
learn in that chapter is that JavaScript code can be embedded within HTML files using the tag:
This is a paragraph of HTML
// And this is some client-side JavaScript code // literally embedded within the HTML file
Here is more HTML.
Chapter 14, The Window Object, explains techniques for scripting the web browser and covers some important global functions of client-side JavaScript. For example: function moveon() { // Display a modal dialog to ask the user a question var answer = confirm("Ready to move on?"); // If they clicked the "OK" button, make the browser load a new page if (answer) window.location = "http://google.com"; } // Run the function defined above 1 minute (60,000 milliseconds) from now. setTimeout(moveon, 60000);
Note that the client-side example code shown in this section comes in longer snippets than the core language examples earlier in the chapter. These examples are not designed to be typed into a Firebug (or similar) console window. Instead you can embed them in an HTML file and try them out by loading them in your web browser. The code above, for instance, works as a stand-alone HTML file. Chapter 15, Scripting Documents, gets down to the real business of client-side JavaScript, scripting HTML document content. It shows you how to select particular HTML elements from within a document, how to set HTML attributes of those elements, how to alter the content of those elements, and how to add new elements to the document. This function demonstrates a number of these basic document searching and modification techniques: // Display a message in a special debugging output section of the document. // If the document does not contain such a section, create one. function debug(msg) { // Find the debugging section of the document, looking at HTML id attributes var log = document.getElementById("debuglog"); // If no element with the id "debuglog" exists, create one. if (!log) { log = document.createElement("div"); // Create a new element log.id = "debuglog"; // Set the HTML id attribute on it
1.2 Client-Side JavaScript | 9
}
log.innerHTML = "Debug Log"; // Define initial content document.body.appendChild(log); // Add it at end of document // Now wrap the message in its own and append it to the log var pre = document.createElement("pre"); // Create a tag var text = document.createTextNode(msg); // Wrap msg in a text node pre.appendChild(text); // Add text to the log.appendChild(pre); // Add to the log }
Chapter 15 shows how JavaScript can script the HTML elements that define web content. Chapter 16, Scripting CSS, shows how you can use JavaScript with the CSS styles that define the presentation of that content. This is often done with the style or class attribute of HTML elements: function hide(e, reflow) { // Hide the if (reflow) { e.style.display = "none" } else { e.style.visibility = "hidden"; } }
element e by scripting its style // If 2nd argument is true // hide element and use its space // Otherwise // make e invisible, but leave its space
function highlight(e) { // Highlight e by setting a CSS class // Simply define or append to the HTML class attribute. // This assumes that a CSS stylesheet already defines the "hilite" class if (!e.className) e.className = "hilite"; else e.className += " hilite"; }
JavaScript allows us to script the HTML content and CSS presentation of documents in web browsers, but it also allows us to define behavior for those documents with event handlers. An event handler is a JavaScript function that we register with the browser and the browser invokes when some specified type of event occurs. The event of interest might be a mouse click or a key press (or on a smart phone, it might be a two-finger gesture of some sort). Or an event handler might be triggered when the browser finishes loading a document, when the user resizes the browser window, or when the user enters data into an HTML form element. Chapter 17, Handling Events, explains how you can define and register event handlers and how the browser invokes them when events occur. The simplest way to define event handlers is with HTML attributes that begin with “on”. The “onclick” handler is a particularly useful one when you’re writing simple test programs. Suppose that you had typed in the debug() and hide() functions from above and saved them in files named debug.js and hide.js. You could write a simple HTML test file using elements with onclick event handler attributes: Hello Hide1
10 | Chapter 1: Introduction to JavaScript
Hide2 World
Here is some more client-side JavaScript code that uses events. It registers an event handler for the very important “load” event, and it also demonstrates a more sophisticated way of registering event handler functions for “click” events: // The "load" event occurs when a document is fully loaded. Usually we // need to wait for this event before we start running our JavaScript code. window.onload = function() { // Run this function when the document loads // Find all tags in the document var images = document.getElementsByTagName("img"); // Loop through them, adding an event handler for "click" events to each // so that clicking on the image hides it. for(var i = 0; i < images.length; i++) { var image = images[i]; if (image.addEventListener) // Another way to register a handler image.addEventListener("click", hide, false); else // For compatibility with IE8 and before image.attachEvent("onclick", hide); }
};
// This is the event handler function registered above function hide(event) { event.target.style.visibility = "hidden"; }
Chapters 15, 16, and 17 explain how you can use JavaScript to script the content (HTML), presentation (CSS), and behavior (event handling) of web pages. The APIs described in those chapters are somewhat complex and, until recently, riddled with browser incompatibilities. For these reasons, many or most client-side JavaScript programmers choose to use a client-side library or framework to simplify their basic programming tasks. The most popular such library is jQuery, the subject of Chapter 19, The jQuery Library. jQuery defines a clever and easy-to-use API for scripting document content, presentation, and behavior. It has been thoroughly tested and works in all major browsers, including old ones like IE6. jQuery code is easy to identify because it makes frequent use of a function named $(). Here is what the debug() function used previously looks like when rewritten to use jQuery: function debug(msg) { var log = $("#debuglog"); // Find the element to display msg if (log.length == 0) { // If it doesn't exist yet, create log = $("Debug Log"); log.appendTo(document.body); // and insert it at the end of the } log.append($("").text(msg)); // Wrap msg in and append to }
in. it... body. log.
The four chapters of Part II described so far have all really been about web pages. Four more chapters shift gears to focus on web applications. These chapters are not about using web browsers to display documents with scriptable content, presentation, and
1.2 Client-Side JavaScript | 11
behavior. Instead, they’re about using web browsers as application platforms, and they describe the APIs that modern browsers provide to support sophisticated client-side web apps. Chapter 18, Scripted HTTP, explains how to make scripted HTTP requests with JavaScript—a kind of networking API. Chapter 20, Client-Side Storage, describes mechanisms for storing data—and even entire applications—on the client side for use in future browsing sessions. Chapter 21, Scripted Media and Graphics, covers a clientside API for drawing arbitrary graphics in an HTML tag. And, finally, Chapter 22, HTML5 APIs, covers an assortment of new web app APIs specified by or affiliated with HTML5. Networking, storage, graphics: these are OS-type services being provided by the web browser, defining a new cross-platform application environment. If you are targeting browsers that support these new APIs, it is an exciting time to be a client-side JavaScript programmer. There are no code samples from these final four chapters here, but the extended example below uses some of these new APIs.
1.2.1 Example: A JavaScript Loan Calculator This chapter ends with an extended example that puts many of these techniques together and shows what real-world client-side JavaScript (plus HTML and CSS) programs look like. Example 1-1 lists the code for the simple loan payment calculator application pictured in Figure 1-2.
Figure 1-2. A loan calculator web application
It is worth reading through Example 1-1 carefully. You shouldn’t expect to understand everything, but the code is heavily commented and you should be able to at least get 12 | Chapter 1: Introduction to JavaScript
the big-picture view of how it works. The example demonstrates a number of core JavaScript language features, and also demonstrates important client-side JavaScript techniques: • • • • • •
How to find elements in a document. How to get user input from form input elements. How to set the HTML content of document elements. How to store data in the browser. How to make scripted HTTP requests. How to draw graphics with the element.
Example 1-1. A loan calculator in JavaScript JavaScript Loan Calculator /* This is a CSS style sheet: it adds style to the program output */ .output { font-weight: bold; } /* Calculated values in bold */ #payment { text-decoration: underline; } /* For element with id="payment" */ #graph { border: solid black 1px; } /* Chart has a simple border */ th, td { vertical-align: top; } /* Don't center table cells */ elements that allow the user to enter data and elements in which the program can display its results. These elements have ids like "interest" and "years". These ids are used in the JavaScript code that follows the table. Note that some of the input elements define "onchange" or "onclick" event handlers. These specify strings of JavaScript code to be executed when the user enters data or clicks. -->
Enter Loan Data: | | Loan Balance, Cumulative Equity, and Interest Payments |
Amount of the loan ($): | | |
Annual interest (%): | |
Repayment period (years): | |
Zipcode (to find lenders): | |
Approximate Payments: | Calculate |
Monthly payment: | $ |
Total payment: | $ |
1.2 Client-Side JavaScript | 13
Total interest: | $ |
Sponsors: | Apply for your loan with one of these fine lenders: |
"use strict"; // Use ECMAScript 5 strict mode in browsers that support it /* * This script defines the calculate() function called by the event handlers * in HTML above. The function reads values from elements, calculates * loan payment information, displays the results in elements. It also * saves the user's data, displays links to lenders, and draws a chart. */ function calculate() { // Look up the input and output elements in the document var amount = document.getElementById("amount"); var apr = document.getElementById("apr"); var years = document.getElementById("years"); var zipcode = document.getElementById("zipcode"); var payment = document.getElementById("payment"); var total = document.getElementById("total"); var totalinterest = document.getElementById("totalinterest"); // Get the user's input from the input elements. Assume it is all valid. // Convert interest from a percentage to a decimal, and convert from // an annual rate to a monthly rate. Convert payment period in years // to the number of monthly payments. var principal = parseFloat(amount.value); var interest = parseFloat(apr.value) / 100 / 12; var payments = parseFloat(years.value) * 12; // Now compute the monthly payment figure. var x = Math.pow(1 + interest, payments); // Math.pow() computes powers var monthly = (principal*x*interest)/(x-1); // If the result is a finite number, the user's input was good and // we have meaningful results to display if (isFinite(monthly)) { // Fill in the output fields, rounding to 2 decimal places payment.innerHTML = monthly.toFixed(2); total.innerHTML = (monthly * payments).toFixed(2); totalinterest.innerHTML = ((monthly*payments)-principal).toFixed(2); // Save the user's input so we can restore it the next time they visit save(amount.value, apr.value, years.value, zipcode.value); // Advertise: find and display local lenders, but ignore network errors try { // Catch any errors that occur within these curly braces getLenders(amount.value, apr.value, years.value, zipcode.value); }
14 | Chapter 1: Introduction to JavaScript
catch(e) { /* And ignore those errors */ } // Finally, chart loan balance, and interest and equity payments chart(principal, interest, monthly, payments);
} else { // Result was Not-a-Number or infinite, which means the input was // incomplete or invalid. Clear any previously displayed output. payment.innerHTML = ""; // Erase the content of these elements total.innerHTML = "" totalinterest.innerHTML = ""; chart(); // With no arguments, clears the chart } } // Save the user's input as properties of the localStorage object. Those // properties will still be there when the user visits in the future // This storage feature will not work in some browsers (Firefox, e.g.) if you // run the example from a local file:// URL. It does work over HTTP, however. function save(amount, apr, years, zipcode) { if (window.localStorage) { // Only do this if the browser supports it localStorage.loan_amount = amount; localStorage.loan_apr = apr; localStorage.loan_years = years; localStorage.loan_zipcode = zipcode; } } // Automatically attempt to restore input fields when the document first loads. window.onload = function() { // If the browser supports localStorage and we have some stored data if (window.localStorage && localStorage.loan_amount) { document.getElementById("amount").value = localStorage.loan_amount; document.getElementById("apr").value = localStorage.loan_apr; document.getElementById("years").value = localStorage.loan_years; document.getElementById("zipcode").value = localStorage.loan_zipcode; } }; // Pass the user's input to a server-side script which can (in theory) return // a list of links to local lenders interested in making loans. This example // does not actually include a working implementation of such a lender-finding // service. But if the service existed, this function would work with it. function getLenders(amount, apr, years, zipcode) { // If the browser does not support the XMLHttpRequest object, do nothing if (!window.XMLHttpRequest) return; // Find the element to display the list of lenders in var ad = document.getElementById("lenders"); if (!ad) return; // Quit if no spot for output
1.2 Client-Side JavaScript | 15
// Encode the user's input as query parameters in a URL var url = "getLenders.php" + // Service url plus "?amt=" + encodeURIComponent(amount) + // user data in query string "&apr=" + encodeURIComponent(apr) + "&yrs=" + encodeURIComponent(years) + "&zip=" + encodeURIComponent(zipcode); // Fetch the contents of that URL using the XMLHttpRequest object var req = new XMLHttpRequest(); // Begin a new request req.open("GET", url); // An HTTP GET request for the url req.send(null); // Send the request with no body // Before returning, register an event handler function that will be called // at some later time when the HTTP server's response arrives. This kind of // asynchronous programming is very common in client-side JavaScript. req.onreadystatechange = function() { if (req.readyState == 4 && req.status == 200) { // If we get here, we got a complete valid HTTP response var response = req.responseText; // HTTP response as a string var lenders = JSON.parse(response); // Parse it to a JS array // Convert the array of lender objects to a string of HTML var list = ""; for(var i = 0; i < lenders.length; i++) { list += "
" + lenders[i].name + ""; }
}
}
}
// Display the HTML in the element from above. ad.innerHTML = "";
// Chart monthly loan balance, interest and equity in an HTML element. // If called with no arguments then just erase any previously drawn chart. function chart(principal, interest, monthly, payments) { var graph = document.getElementById("graph"); // Get the tag graph.width = graph.width; // Magic to clear and reset the canvas element // If we're called with no arguments, or if this browser does not support // graphics in a element, then just return now. if (arguments.length == 0 || !graph.getContext) return; // Get the "context" object for the that defines the drawing API var g = graph.getContext("2d"); // All drawing is done with this object var width = graph.width, height = graph.height; // Get canvas size // These functions convert payment numbers and dollar amounts to pixels function paymentToX(n) { return n * width/payments; } function amountToY(a) { return height-(a * height/(monthly*payments*1.05));} // Payments are a straight line from (0,0) to (payments, monthly*payments) g.moveTo(paymentToX(0), amountToY(0)); // Start at lower left g.lineTo(paymentToX(payments), // Draw to upper right amountToY(monthly*payments));
16 | Chapter 1: Introduction to JavaScript
g.lineTo(paymentToX(payments), amountToY(0)); g.closePath(); g.fillStyle = "#f88"; g.fill(); g.font = "bold 12px sans-serif"; g.fillText("Total Interest Payments", 20,20);
// // // // // //
Down to lower right And back to start Light red Fill the triangle Define a font Draw text in legend
// Cumulative equity is non-linear and trickier to chart var equity = 0; g.beginPath(); // Begin a new shape g.moveTo(paymentToX(0), amountToY(0)); // starting at lower-left for(var p = 1; p "hello": the original string has not changed
Primitives are also compared by value: two values are the same only if they have the same value. This sounds circular for numbers, booleans, null, and undefined: there is no other way that they could be compared. Again, however, it is not so obvious for strings. If two distinct string values are compared, JavaScript treats them as equal if, and only if, they have the same length and if the character at each index is the same. Objects are different than primitives. First, they are mutable—their values can change: var o = { x:1 }; o.x = 2; o.y = 3;
// Start with an object // Mutate it by changing the value of a property // Mutate it again by adding a new property
var a = [1,2,3] a[0] = 0; a[3] = 4;
// Arrays are also mutable // Change the value of an array element // Add a new array element
44 | Chapter 3: Types, Values, and Variables
var o o === var a a ===
= {x:1}, p = {x:1}; p = [], b = []; b
// // // //
Two objects with the same properties => false: distinct objects are never equal Two distinct, empty arrays => false: distinct arrays are never equal
Objects are sometimes called reference types to distinguish them from JavaScript’s primitive types. Using this terminology, object values are references, and we say that objects are compared by reference: two object values are the same if and only if they refer to the same underlying object. var a = []; var b = a; b[0] = 1; a[0] a === b
// // // // //
The variable a refers to an empty array. Now b refers to the same array. Mutate the array referred to by variable b. => 1: the change is also visible through variable a. => true: a and b refer to the same object, so they are equal.
As you can see from the code above, assigning an object (or array) to a variable simply assigns the reference: it does not create a new copy of the object. If you want to make a new copy of an object or array, you must explicitly copy the properties of the object or the elements of the array. This example demonstrates using a for loop (§5.5.3): var a = ['a','b','c']; // var b = []; // for(var i = 0; i < a.length; i++) { // b[i] = a[i]; // }
An array we want to copy A distinct array we'll copy into For each index of a[] Copy an element of a into b
Similarly, if we want to compare two distinct objects or arrays, we must compare their properties or elements. This code defines a function to compare two arrays: function equalArrays(a,b) { if (a.length != b.length) return false; for(var i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; return true; }
// // // //
Different-size arrays not equal Loop through all elements If any differ, arrays not equal Otherwise they are equal
3.8 Type Conversions JavaScript is very flexible about the types of values it requires. We’ve seen this for booleans: when JavaScript expects a boolean value, you may supply a value of any type, and JavaScript will convert it as needed. Some values (“truthy” values) convert to true and others (“falsy” values) convert to false. The same is true for other types: if JavaScript wants a string, it will convert whatever value you give it to a string. If JavaScript wants a number, it will try to convert the value you give it to a number (or to NaN if it cannot perform a meaningful conversion). Some examples: 10 + " objects" "7" * "4"
// => "10 objects". Number 10 converts to a string // => 28: both strings convert to numbers
3.8 Type Conversions | 45
Core JavaScript
Objects are not compared by value: two objects are not equal even if they have the same properties and values. And two arrays are not equal even if they have the same elements in the same order:
var n = 1 - "x"; n + " objects"
// => NaN: string "x" can't convert to a number // => "NaN objects": NaN converts to string "NaN"
Table 3-2 summarizes how values convert from one type to another in JavaScript. Bold entries in the table highlight conversions that you may find surprising. Empty cells indicate that no conversion is necessary and none is performed. Table 3-2. JavaScript type conversions Value
Converted to: String
Number
Boolean
Object
undefined
"undefined"
NaN
false
throws TypeError
null
"null"
0
false
throws TypeError
true
"true"
1
new Boolean(true)
false
"false"
0
new Boolean(false)
"" (empty string)
0
false
new String("")
"1.2" (nonempty, numeric)
1.2
true
new String("1.2")
"one" (nonempty, non-numeric)
NaN
true
new String("one")
0
"0"
false
new Number(0)
-0
"0"
false
new Number(-0)
NaN
"NaN"
false
new Number(NaN)
Infinity
"Infinity"
true
new Number(Infinity)
-Infinity
"-Infinity"
true
new Number(-Infinity)
1 (finite, non-zero)
"1"
true
new Number(1)
{} (any object)
see §3.8.3
see §3.8.3
true
[] (empty array)
""
0
true
[9] (1 numeric elt)
"9"
9
true
['a'] (any other array)
use join() method
NaN
true
function(){} (any function)
see §3.8.3
NaN
true
The primitive-to-primitive conversions shown in the table are relatively straightforward. Conversion to boolean was already discussed in §3.3. Conversion to strings is well-defined for all primitive values. Conversion to numbers is just a little trickier. Strings that can be parsed as numbers convert to those numbers. Leading and trailing spaces are allowed, but any leading or trailing nonspace characters that are not part of a numeric literal cause the string-to-number conversion to produce NaN. Some numeric conversions may seem surprising: true converts to 1, and false and the empty string "" convert to 0. Primitive-to-object conversions are straightforward: primitive values convert to their wrapper object (§3.6) as if by calling the String(), Number(), or Boolean() constructor.
46 | Chapter 3: Types, Values, and Variables
Object-to-primitive conversion is somewhat more complicated, and it is the subject of §3.8.3.
3.8.1 Conversions and Equality Because JavaScript can convert values flexibly, its == equality operator is also flexible with its notion of equality. All of the following comparisons are true, for example: null == undefined "0" == 0 0 == false "0" == false
// // // //
These two values are treated as equal. String converts to a number before comparing. Boolean converts to number before comparing. Both operands convert to numbers before comparing.
§4.9.1 explains exactly what conversions are performed by the == operator in order to determine whether two values should be considered equal, and it also describes the strict equality operator === that does not perform conversions when testing for equality. Keep in mind that convertibility of one value to another does not imply equality of those two values. If undefined is used where a boolean value is expected, for example, it will convert to false. But this does not mean that undefined == false. JavaScript operators and statements expect values of various types, and perform conversions to those types. The if statement converts undefined to false, but the == operator never attempts to convert its operands to booleans.
3.8.2 Explicit Conversions Although JavaScript performs many type conversions automatically, you may sometimes need to perform an explicit conversion, or you may prefer to make the conversions explicit to keep your code clearer. The simplest way to perform an explicit type conversion is to use the Boolean(), Number(), String(), or Object() functions. We’ve already seen these functions as constructors for wrapper objects (in §3.6). When invoked without the new operator, however, they work as conversion functions and perform the conversions summarized in Table 3-2: Number("3") String(false) Boolean([]) Object(3)
// // // //
=> => => =>
3 "false" Or use false.toString() true new Number(3)
Note that any value other than null or undefined has a toString() method and the result of this method is usually the same as that returned by the String() function. Also note that Table 3-2 shows a TypeError if you attempt to convert null or undefined to an object. The Object() function does not throw an exception in this case: instead it simply returns a newly created empty object.
3.8 Type Conversions | 47
Core JavaScript
The exceptions are null and undefined: any attempt to use these values where an object is expected raises a TypeError exception rather than performing a conversion.
Certain JavaScript operators perform implicit type conversions, and are sometimes used for the purposes of type conversion. If one operand of the + operator is a string, it converts the other one to a string. The unary + operator converts its operand to a number. And the unary ! operator converts its operand to a boolean and negates it. These facts lead to the following type conversion idioms that you may see in some code: x + "" +x !!x
// Same as String(x) // Same as Number(x). You may also see x-0 // Same as Boolean(x). Note double !
Formatting and parsing numbers are common tasks in computer programs and JavaScript has specialized functions and methods that provide more precise control over number-to-string and string-to-number conversions. The toString() method defined by the Number class accepts an optional argument that specifies a radix, or base, for the conversion. If you do not specify the argument, the conversion is done in base 10. However, you can also convert numbers in other bases (between 2 and 36). For example: var n = 17; binary_string = n.toString(2); octal_string = "0" + n.toString(8); hex_string = "0x" + n.toString(16);
// Evaluates to "10001" // Evaluates to "021" // Evaluates to "0x11"
When working with financial or scientific data, you may want to convert numbers to strings in ways that give you control over the number of decimal places or the number of significant digits in the output, or you may want to control whether exponential notation is used. The Number class defines three methods for these kinds of numberto-string conversions. toFixed() converts a number to a string with a specified number of digits after the decimal point. It never uses exponential notation. toExponential() converts a number to a string using exponential notation, with one digit before the decimal point and a specified number of digits after the decimal point (which means that the number of significant digits is one larger than the value you specify). toPreci sion() converts a number to a string with the number of significant digits you specify. It uses exponential notation if the number of significant digits is not large enough to display the entire integer portion of the number. Note that all three methods round the trailing digits or pad with zeros as appropriate. Consider the following examples: var n = 123456.789; n.toFixed(0); n.toFixed(2); n.toFixed(5); n.toExponential(1); n.toExponential(3); n.toPrecision(4); n.toPrecision(7); n.toPrecision(10);
// // // // // // // //
"123457" "123456.79" "123456.78900" "1.2e+5" "1.235e+5" "1.235e+5" "123456.8" "123456.7890"
If you pass a string to the Number() conversion function, it attempts to parse that string as an integer or floating-point literal. That function only works for base-10 integers, and does not allow trailing characters that are not part of the literal. The parseInt()
48 | Chapter 3: Types, Values, and Variables
parseInt("3 blind mice") parseFloat(" 3.14 meters") parseInt("-12.34") parseInt("0xFF") parseInt("0xff") parseInt("-0XFF") parseFloat(".1") parseInt("0.1") parseInt(".1") parseFloat("$72.47");
// // // // // // // // // //
=> => => => => => => => => =>
3 3.14 -12 255 255 -255 0.1 0 NaN: integers can't start with "." NaN: numbers can't start with "$"
parseInt() accepts an optional second argument specifying the radix (base) of the
number to be parsed. Legal values are between 2 and 36. For example: parseInt("11", 2); parseInt("ff", 16); parseInt("zz", 36); parseInt("077", 8); parseInt("077", 10);
// // // // //
=> => => => =>
3 (1*2 + 1) 255 (15*16 + 15) 1295 (35*36 + 35) 63 (7*8 + 7) 77 (7*10 + 7)
3.8.3 Object to Primitive Conversions Object-to-boolean conversions are trivial: all objects (including arrays and functions) convert to true. This is so even for wrapper objects: new Boolean(false) is an object rather than a primitive value, and so it converts to true. Object-to-string and object-to-number conversions are performed by invoking a method of the object to be converted. This is complicated by the fact that JavaScript objects have two different methods that perform conversions, and it is also complicated by some special cases described below. Note that the string and number conversion rules described here apply only to native objects. Host objects (defined by web browsers, for example) can convert to numbers and strings according to their own algorithms. All objects inherit two conversion methods. The first is called toString(), and its job is to return a string representation of the object. The default toString() method does not return a very interesting value (though we’ll find it useful in Example 6-4): ({x:1, y:2}).toString()
// => "[object Object]"
2. In ECMAScript 3, parseInt() may parse a string that begins with “0” (but not “0x” or “0X”) as an octal number or as a decimal number. Because the behavior is unspecified, you should never use parseInt() to parse numbers with leading zeros, unless you explicitly specify the radix to be used! In ECMAScript 5, parseInt() only parses octal numbers if you explicitly pass 8 as the second argument.
3.8 Type Conversions | 49
Core JavaScript
and parseFloat() functions (these are global functions, not methods of any class) are more flexible. parseInt() parses only integers, while parseFloat() parses both integers and floating-point numbers. If a string begins with “0x” or “0X”, parseInt() interprets it as a hexadecimal number.2 Both parseInt() and parseFloat() skip leading whitespace, parse as many numeric characters as they can, and ignore anything that follows. If the first nonspace character is not part of a valid numeric literal, they return NaN:
Many classes define more specific versions of the toString() method. The toString() method of the Array class, for example, converts each array element to a string and joins the resulting strings together with commas in between. The toString() method of the Function class returns an implementation-defined representation of a function. In practice, implementations usually convert user-defined functions to strings of JavaScript source code. The Date class defines a toString() method that returns a humanreadable (and JavaScript-parsable) date and time string. The RegExp class defines a toString() method that converts RegExp objects to a string that looks like a RegExp literal: [1,2,3].toString() // => (function(x) { f(x); }).toString() // => /\d+/g.toString() // => new Date(2010,0,1).toString() // => "Fri
"1,2,3" "function(x) {\n f(x);\n}" "/\\d+/g" Jan 01 2010 00:00:00 GMT-0800 (PST)"
The other object conversion function is called valueOf(). The job of this method is less well-defined: it is supposed to convert an object to a primitive value that represents the object, if any such primitive value exists. Objects are compound values, and most objects cannot really be represented by a single primitive value, so the default valueOf() method simply returns the object itself rather than returning a primitive. Wrapper classes define valueOf() methods that return the wrapped primitive value. Arrays, functions, and regular expressions simply inherit the default method. Calling valueOf() for instances of these types simply returns the object itself. The Date class defines a valueOf() method that returns the date in its internal representation: the number of milliseconds since January 1, 1970: var d = new Date(2010, 0, 1); d.valueOf()
// January 1st, 2010, (Pacific time) // => 1262332800000
With the toString() and valueOf() methods explained, we can now cover object-tostring and object-to-number conversions. Do note, however, that there are some special cases in which JavaScript performs a different object-to-primitive conversion. These special cases are covered at the end of this section. To convert an object to a string, JavaScript takes these steps: • If the object has a toString() method, JavaScript calls it. If it returns a primitive value, JavaScript converts that value to a string (if it is not already a string) and returns the result of that conversion. Note that primitive-to-string conversions are all well-defined in Table 3-2. • If the object has no toString() method, or if that method does not return a primitive value, then JavaScript looks for a valueOf() method. If the method exists, JavaScript calls it. If the return value is a primitive, JavaScript converts that value to a string (if it is not already) and returns the converted value. • Otherwise, JavaScript cannot obtain a primitive value from either toString() or valueOf(), so it throws a TypeError.
50 | Chapter 3: Types, Values, and Variables
• If the object has a valueOf() method that returns a primitive value, JavaScript converts (if necessary) that primitive value to a number and returns the result. • Otherwise, if the object has a toString() method that returns a primitive value, JavaScript converts and returns the value. • Otherwise, JavaScript throws a TypeError. The details of this object-to-number conversion explain why an empty array converts to the number 0 and why an array with a single element may also convert to a number. Arrays inherit the default valueOf() method that returns an object rather than a primitive value, so array-to-number conversion relies on the toString() method. Empty arrays convert to the empty string. And the empty string converts to the number 0. An array with a single element converts to the same string that that one element does. If an array contains a single number, that number is converted to a string, and then back to a number. The + operator in JavaScript performs numeric addition and string concatenation. If either of its operands is an object, JavaScript converts the object using a special objectto-primitive conversion rather than the object-to-number conversion used by the other arithmetic operators. The == equality operator is similar. If asked to compare an object with a primitive value, it converts the object using the object-to-primitive conversion. The object-to-primitive conversion used by + and == includes a special case for Date objects. The Date class is the only predefined core JavaScript type that defines meaningful conversions to both strings and numbers. The object-to-primitive conversion is basically an object-to-number conversion (valueof() first) for all objects that are not dates, and an object-to-string conversion (toString() first) for Date objects. The conversion is not exactly the same as those explained above, however: the primitive value returned by valueOf() or toString() is used directly without being forced to a number or string. The < operator and the other relational operators perform object-to-primitive conversions like == does, but without the special case for Date objects: any object is converted by trying valueOf() first and then toString(). Whatever primitive value is obtained is used directly, without being further converted to a number or string. +, ==, != and the relational operators are the only ones that perform this special kind of
string-to-primitive conversions. Other operators convert more explicitly to a specified type and do not have any special case for Date objects. The - operator, for example, converts its operands to numbers. The following code demonstrates the behavior of +, -, ==, and > with Date objects: var now = new Date(); typeof (now + 1) typeof (now - 1)
// Create a Date object // => "string": + converts dates to strings // => "number": - uses object-to-number conversion
3.8 Type Conversions | 51
Core JavaScript
To convert an object to a number, JavaScript does the same thing, but it tries the valueOf() method first:
now == now.toString() now > (now -1)
// => true: implicit and explicit string conversions // => true: > converts a Date to a number
3.9 Variable Declaration Before you use a variable in a JavaScript program, you should declare it. Variables are declared with the var keyword, like this: var i; var sum;
You can also declare multiple variables with the same var keyword: var i, sum;
And you can combine variable declaration with variable initialization: var message = "hello"; var i = 0, j = 0, k = 0;
If you don’t specify an initial value for a variable with the var statement, the variable is declared, but its value is undefined until your code stores a value into it. Note that the var statement can also appear as part of the for and for/in loops (introduced in Chapter 5), allowing you to succinctly declare the loop variable as part of the loop syntax itself. For example: for(var i = 0; i < 10; i++) console.log(i); for(var i = 0, j=10; i < 10; i++,j--) console.log(i*j); for(var p in o) console.log(p);
If you’re used to statically typed languages such as C or Java, you will have noticed that there is no type associated with JavaScript’s variable declarations. A JavaScript variable can hold a value of any type. For example, it is perfectly legal in JavaScript to assign a number to a variable and then later assign a string to that variable: var i = 10; i = "ten";
3.9.1 Repeated and Omitted Declarations It is legal and harmless to declare a variable more than once with the var statement. If the repeated declaration has an initializer, it acts as if it were simply an assignment statement. If you attempt to read the value of an undeclared variable, JavaScript generates an error. In ECMAScript 5 strict mode (§5.7.3), it is also an error to assign a value to an undeclared variable. Historically, however, and in non-strict mode, if you assign a value to an undeclared variable, JavaScript actually creates that variable as a property of the global object, and it works much like (but not exactly the same as, see §3.10.2) a properly declared global variable. This means that you can get away with leaving your global variables undeclared. This is a bad habit and a source of bugs, however, and you should always declare your variables with var. 52 | Chapter 3: Types, Values, and Variables
The scope of a variable is the region of your program source code in which it is defined. A global variable has global scope; it is defined everywhere in your JavaScript code. On the other hand, variables declared within a function are defined only within the body of the function. They are local variables and have local scope. Function parameters also count as local variables and are defined only within the body of the function. Within the body of a function, a local variable takes precedence over a global variable with the same name. If you declare a local variable or function parameter with the same name as a global variable, you effectively hide the global variable: var scope = "global"; function checkscope() { var scope = "local"; return scope; } checkscope()
// Declare a global variable // Declare a local variable with the same name // Return the local value, not the global one // => "local"
Although you can get away with not using the var statement when you write code in the global scope, you must always use var to declare local variables. Consider what happens if you don’t: scope = "global"; function checkscope2() { scope = "local"; myscope = "local"; return [scope, myscope]; } checkscope2() scope myscope
// Declare a global variable, even without var. // Oops! We just changed the global variable. // This implicitly declares a new global variable. // Return two values. // => ["local", "local"]: has side effects! // => "local": global variable has changed. // => "local": global namespace cluttered up.
Function definitions can be nested. Each function has its own local scope, so it is possible to have several nested layers of local scope. For example: var scope = "global scope"; function checkscope() { var scope = "local scope"; function nested() { var scope = "nested scope"; return scope; } return nested(); } checkscope()
// A global variable // A local variable // A nested scope of local variables // Return the value in scope here
// => "nested scope"
3.10.1 Function Scope and Hoisting In some C-like programming languages, each block of code within curly braces has its own scope, and variables are not visible outside of the block in which they are declared. This is called block scope, and JavaScript does not have it. Instead, JavaScript uses
3.10 Variable Scope | 53
Core JavaScript
3.10 Variable Scope
function scope: variables are visible within the function in which they are defined and within any functions that are nested within that function. In the following code, the variables i, j, and k are declared in different spots, but all have the same scope—all three are defined throughout the body of the function: function test(o) { var i = 0; // i is defined throughout function if (typeof o == "object") { var j = 0; // j is defined everywhere, not just block for(var k=0; k < 10; k++) { // k is defined everywhere, not just loop console.log(k); // print numbers 0 through 9 } console.log(k); // k is still defined: prints 10 } console.log(j); // j is defined, but may not be initialized }
JavaScript’s function scope means that all variables declared within a function are visible throughout the body of the function. Curiously, this means that variables are even visible before they are declared. This feature of JavaScript is informally known as hoisting: JavaScript code behaves as if all variable declarations in a function (but not any associated assignments) are “hoisted” to the top of the function. Consider the following code: var scope = "global"; function f() { console.log(scope); // Prints "undefined", not "global" var scope = "local"; // Variable initialized here, but defined everywhere console.log(scope); // Prints "local" }
You might think that the first line of the function would print “global”, because the var statement declaring the local variable has not yet been executed. Because of the rules of function scope, however, this is not what happens. The local variable is defined throughout the body of the function, which means the global variable by the same name is hidden throughout the function. Although the local variable is defined throughout, it is not actually initialized until the var statement is executed. Thus, the function above is equivalent to the following, in which the variable declaration is “hoisted” to the top and the variable initialization is left where it is: function f() { var scope; console.log(scope); scope = "local"; console.log(scope); }
// // // //
Local variable is declared at the top of the function It exists here, but still has "undefined" value Now we initialize it and give it a value And here it has the value we expect
In programming languages with block scope, it is generally good programming practice to declare variables as close as possible to where they are used and with the narrowest possible scope. Since JavaScript does not have block scope, some programmers make a point of declaring all their variables at the top of the function, rather than trying to
54 | Chapter 3: Types, Values, and Variables
3.10.2 Variables As Properties When you declare a global JavaScript variable, what you are actually doing is defining a property of the global object (§3.5). If you use var to declare the variable, the property that is created is nonconfigurable (see §6.7), which means that it cannot be deleted with the delete operator. We’ve already noted that if you’re not using strict mode and you assign a value to an undeclared variable, JavaScript automatically creates a global variable for you. Variables created in this way are regular, configurable properties of the global object and they can be deleted: var truevar = 1; fakevar = 2; this.fakevar2 = 3; delete truevar delete fakevar delete this.fakevar2
// // // // // //
A properly declared global variable, nondeletable. Creates a deletable property of the global object. This does the same thing. => false: variable not deleted => true: variable deleted => true: variable deleted
JavaScript global variables are properties of the global object, and this is mandated by the ECMAScript specification. There is no such requirement for local variables, but you can imagine local variables as the properties of an object associated with each function invocation. The ECMAScript 3 specification referred to this object as the “call object,” and the ECMAScript 5 specification calls it a “declarative environment record.” JavaScript allows us to refer to the global object with the this keyword, but it does not give us any way to refer to the object in which local variables are stored. The precise nature of these objects that hold local variables is an implementation detail that need not concern us. The notion that these local variable objects exist, however, is an important one, and it is developed further in the next section.
3.10.3 The Scope Chain JavaScript is a lexically scoped language: the scope of a variable can be thought of as the set of source code lines for which the variable is defined. Global variables are defined throughout the program. Local variables are defined throughout the function in which they are declared, and also within any functions nested within that function. If we think of local variables as properties of some kind of implementation-defined object, then there is another way to think about variable scope. Every chunk of JavaScript code (global code or functions) has a scope chain associated with it. This scope chain is a list or chain of objects that defines the variables that are “in scope” for that code. When JavaScript needs to look up the value of a variable x (a process called variable resolution), it starts by looking at the first object in the chain. If that object has a property named x, the value of that property is used. If the first object does not have a property named x, JavaScript continues the search with the next object in the chain. If the second object does not have a property named x, the search moves on to the next
3.10 Variable Scope | 55
Core JavaScript
declare them closer to the point at which they are used. This technique makes their source code accurately reflect the true scope of the variables.
object, and so on. If x is not a property of any of the objects in the scope chain, then x is not in scope for that code, and a ReferenceError occurs. In top-level JavaScript code (i.e., code not contained within any function definitions), the scope chain consists of a single object, the global object. In a non-nested function, the scope chain consists of two objects. The first is the object that defines the function’s parameters and local variables, and the second is the global object. In a nested function, the scope chain has three or more objects. It is important to understand how this chain of objects is created. When a function is defined, it stores the scope chain then in effect. When that function is invoked, it creates a new object to store its local variables, and adds that new object to the stored scope chain to create a new, longer, chain that represents the scope for that function invocation. This becomes more interesting for nested functions because each time the outer function is called, the inner function is defined again. Since the scope chain differs on each invocation of the outer function, the inner function will be subtly different each time it is defined—the code of the inner function will be identical on each invocation of the outer function, but the scope chain associated with that code will be different. This notion of a scope chain is helpful for understanding the with statement (§5.7.1) and is crucial for understanding closures (§8.6).
56 | Chapter 3: Types, Values, and Variables
CHAPTER 4
Expressions and Operators
An expression is a phrase of JavaScript that a JavaScript interpreter can evaluate to produce a value. A constant embedded literally in your program is a very simple kind of expression. A variable name is also a simple expression that evaluates to whatever value has been assigned to that variable. Complex expressions are built from simpler expressions. An array access expression, for example, consists of one expression that evaluates to an array followed by an open square bracket, an expression that evaluates to an integer, and a close square bracket. This new, more complex expression evaluates to the value stored at the specified index of the specified array. Similarly, a function invocation expression consists of one expression that evaluates to a function object and zero or more additional expressions that are used as the arguments to the function. The most common way to build a complex expression out of simpler expressions is with an operator. An operator combines the values of its operands (usually two of them) in some way and evaluates to a new value. The multiplication operator * is a simple example. The expression x * y evaluates to the product of the values of the expressions x and y. For simplicity, we sometimes say that an operator returns a value rather than “evaluates to” a value. This chapter documents all of JavaScript’s operators, and it also explains expressions (such as array indexing and function invocation) that do not use operators. If you already know another programming language that uses C-style syntax, you’ll find that the syntax of most of JavaScript’s expressions and operators is already familiar to you.
4.1 Primary Expressions The simplest expressions, known as primary expressions, are those that stand alone— they do not include any simpler expressions. Primary expressions in JavaScript are constant or literal values, certain language keywords, and variable references.
57
Literals are constant values that are embedded directly in your program. They look like these: 1.23 "hello" /pattern/
// A number literal // A string literal // A regular expression literal
JavaScript syntax for number literals was covered in §3.1. String literals were documented in §3.2. The regular expression literal syntax was introduced in §3.2.4 and will be documented in detail in Chapter 10. Some of JavaScript’s reserved words are primary expressions: true false null this
// // // //
Evalutes to the boolean true value Evaluates to the boolean false value Evaluates to the null value Evaluates to the "current" object
We learned about true, false, and null in §3.3 and §3.4. Unlike the other keywords, this is not a constant—it evaluates to different values in different places in the program. The this keyword is used in object-oriented programming. Within the body of a method, this evaluates to the object on which the method was invoked. See §4.5, Chapter 8 (especially §8.2.2), and Chapter 9 for more on this. Finally, the third type of primary expression is the bare variable reference: i sum undefined
// Evaluates to the value of the variable i. // Evaluates to the value of the variable sum. // undefined is a global variable, not a keyword like null.
When any identifier appears by itself in a program, JavaScript assumes it is a variable and looks up its value. If no variable with that name exists, the expression evaluates to the undefined value. In the strict mode of ECMAScript 5, however, an attempt to evaluate a nonexistent variable throws a ReferenceError instead.
4.2 Object and Array Initializers Object and array initializers are expressions whose value is a newly created object or array. These initializer expressions are sometimes called “object literals” and “array literals.” Unlike true literals, however, they are not primary expressions, because they include a number of subexpressions that specify property and element values. Array initializers have a slightly simpler syntax, and we’ll begin with those. An array initializer is a comma-separated list of expressions contained within square brackets. The value of an array initializer is a newly created array. The elements of this new array are initialized to the values of the comma-separated expressions: [] [1+2,3+4]
// An empty array: no expressions inside brackets means no elements // A 2-element array. First element is 3, second is 7
The element expressions in an array initializer can themselves be array initializers, which means that these expressions can create nested arrays:
58 | Chapter 4: Expressions and Operators
The element expressions in an array initializer are evaluated each time the array initializer is evaluated. This means that the value of an array initializer expression may be different each time it is evaluated. Undefined elements can be included in an array literal by simply omitting a value between commas. For example, the following array contains five elements, including three undefined elements: var sparseArray = [1,,,,5];
A single trailing comma is allowed after the last expression in an array initializer and does not create an undefined element. Object initializer expressions are like array initializer expressions, but the square brackets are replaced by curly brackets, and each subexpression is prefixed with a property name and a colon: var p = { x:2.3, y:-1.2 }; // An object with 2 properties var q = {}; // An empty object with no properties q.x = 2.3; q.y = -1.2; // Now q has the same properties as p
Object literals can be nested. For example: var rectangle = { upperLeft: { x: 2, y: 2 }, lowerRight: { x: 4, y: 5 } };
The expressions in an object initializer are evaluated each time the object initializer is evaluated, and they need not have constant values: they can be arbitrary JavaScript expressions. Also, the property names in object literals may be strings rather than identifiers (this is useful to specify property names that are reserved words or are otherwise not legal identifiers): var side = 1; var square = { "upperLeft": { x: p.x, y: p.y }, 'lowerRight': { x: p.x + side, y: p.y + side}};
We’ll see object and array initializers again in Chapters 6 and 7.
4.3 Function Definition Expressions A function definition expression defines a JavaScript function, and the value of such an expression is the newly defined function. In a sense, a function definition expression is a “function literal” in the same way that an object initializer is an “object literal.” A function definition expression typically consists of the keyword function followed by a comma-separated list of zero or more identifiers (the parameter names) in parentheses and a block of JavaScript code (the function body) in curly braces. For example: // This function returns the square of the value passed to it. var square = function(x) { return x * x; }
4.3 Function Definition Expressions | 59
Core JavaScript
var matrix = [[1,2,3], [4,5,6], [7,8,9]];
A function definition expression can also include a name for the function. Functions can also be defined using a function statement rather than a function expression. Complete details on function definition are in Chapter 8.
4.4 Property Access Expressions A property access expression evaluates to the value of an object property or an array element. JavaScript defines two syntaxes for property access: expression . identifier expression [ expression ]
The first style of property access is an expression followed by a period and an identifier. The expression specifies the object, and the identifier specifies the name of the desired property. The second style of property access follows the first expression (the object or array) with another expression in square brackets. This second expression specifies the name of the desired property of the index of the desired array element. Here are some concrete examples: var o = {x:1,y:{z:3}}; var a = [o,4,[5,6]]; o.x o.y.z o["x"] a[1] a[2]["1"] a[0].x
// // // // // // // //
An An => => => => => =>
example object example array that contains the object 1: property x of expression o 3: property z of expression o.y 1: property x of object o 4: element at index 1 of expression a 6: element at index 1 of expression a[2] 1: property x of expression a[0]
With either type of property access expression, the expression before the . or [ is first evaluated. If the value is null or undefined, the expression throws a TypeError, since these are the two JavaScript values that cannot have properties. If the value is not an object (or array), it is converted to one (see §3.6). If the object expression is followed by a dot and an identifier, the value of the property named by that identifier is looked up and becomes the overall value of the expression. If the object expression is followed by another expression in square brackets, that second expression is evaluated and converted to a string. The overall value of the expression is then the value of the property named by that string. In either case, if the named property does not exist, then the value of the property access expression is undefined. The .identifier syntax is the simpler of the two property access options, but notice that it can only be used when the property you want to access has a name that is a legal identifier, and when you know then name when you write the program. If the property name is a reserved word or includes spaces or punctuation characters, or when it is a number (for arrays), you must use the square bracket notation. Square brackets are also used when the property name is not static but is itself the result of a computation (see §6.2.1 for an example). Objects and their properties are covered in detail in Chapter 6, and arrays and their elements are covered in Chapter 7.
60 | Chapter 4: Expressions and Operators
An invocation expression is JavaScript’s syntax for calling (or executing) a function or method. It starts with a function expression that identifies the function to be called. The function expression is followed by an open parenthesis, a comma-separated list of zero or more argument expressions, and a close parenthesis. Some examples: f(0) // f is the function expression; 0 is the argument expression. Math.max(x,y,z) // Math.max is the function; x, y and z are the arguments. a.sort() // a.sort is the function; there are no arguments.
When an invocation expression is evaluated, the function expression is evaluated first, and then the argument expressions are evaluated to produce a list of argument values. If the value of the function expression is not a callable object, a TypeError is thrown. (All functions are callable. Host objects may also be callable even if they are not functions. This distinction is explored in §8.7.7.) Next, the argument values are assigned, in order, to the parameter names specified when the function was defined, and then the body of the function is executed. If the function uses a return statement to return a value, then that value becomes the value of the invocation expression. Otherwise, the value of the invocation expression is undefined. Complete details on function invocation, including an explanation of what happens when the number of argument expressions does not match the number of parameters in the function definition, are in Chapter 8. Every invocation expression includes a pair of parentheses and an expression before the open parenthesis. If that expression is a property access expression, then the invocation is known as a method invocation. In method invocations, the object or array that is the subject of the property access becomes the value of the this parameter while the body of the function is being executed. This enables an object-oriented programming paradigm in which functions (known by their OO name, “methods”) operate on the object of which they are part. See Chapter 9 for details. Invocation expressions that are not method invocations normally use the global object as the value of the this keyword. In ECMAScript 5, however, functions that are defined in strict mode are invoked with undefined as their this value rather than the global object. See §5.7.3 for more on strict mode.
4.6 Object Creation Expressions An object creation expression creates a new object and invokes a function (called a constructor) to initialize the properties of that object. Object creation expressions are like invocation expressions except that they are prefixed with the keyword new: new Object() new Point(2,3)
4.6 Object Creation Expressions | 61
Core JavaScript
4.5 Invocation Expressions
If no arguments are passed to the constructor function in an object creation expression, the empty pair of parentheses can be omitted: new Object new Date
When an object creation expression is evaluated, JavaScript first creates a new empty object, just like the one created by the object initializer {}. Next, it invokes the specified function with the specified arguments, passing the new object as the value of the this keyword. The function can then use this to initialize the properties of the newly created object. Functions written for use as constructors do not return a value, and the value of the object creation expression is the newly created and initialized object. If a constructor does return an object value, that value becomes the value of the object creation expression and the newly created object is discarded. Constructors are explained in more detail in Chapter 9.
4.7 Operator Overview Operators are used for JavaScript’s arithmetic expressions, comparison expressions, logical expressions, assignment expressions, and more. Table 4-1 summarizes the operators and serves as a convenient reference. Note that most operators are represented by punctuation characters such as + and =. Some, however, are represented by keywords such as delete and instanceof. Keyword operators are regular operators, just like those expressed with punctuation; they simply have a less succinct syntax. Table 4-1 is organized by operator precedence. The operators listed first have higher precedence than those listed last. Operators separated by a horizontal line have different precedence levels. The column labeled A gives the operator associativity, which can be L (left-to-right) or R (right-to-left), and the column N specifies the number of operands. The column labeled Types lists the expected types of the operands and (after the → symbol) the result type for the operator. The subsections that follow the table explain the concepts of precedence, associativity, and operand type. The operators themselves are individually documented following that discussion. Table 4-1. JavaScript operators Operator
Operation
A
N
Types
++
Pre- or post-increment
R
1
lval→num
--
Pre- or post-decrement
R
1
lval→num
-
Negate number
R
1
num→num
+
Convert to number
R
1
num→num
~
Invert bits
R
1
int→int
!
Invert boolean value
R
1
bool→bool
62 | Chapter 4: Expressions and Operators
Operation
A
N
Types
delete
Remove a property
R
1
lval→bool
typeof
Determine type of operand
R
1
any→str
void
Return undefined value
R
1
any→undef
*, /, %
Multiply, divide, remainder
L
2
num,num→num
+, -
Add, subtract
L
2
num,num→num
+
Concatenate strings
L
2
str,str→str
>
Shift right with sign extension
L
2
int,int→int
>>>
Shift right with zero extension
L
2
int,int→int
=
Compare in numeric order
L
2
num,num→bool
=
Compare in alphabetic order
L
2
str,str→bool
instanceof
Test object class
L
2
obj,func→bool
in
Test whether property exists
L
2
str,obj→bool
==
Test for equality
L
2
any,any→bool
!=
Test for inequality
L
2
any,any→bool
===
Test for strict equality
L
2
any,any→bool
!==
Test for strict inequality
L
2
any,any→bool
&
Compute bitwise AND
L
2
int,int→int
^
Compute bitwise XOR
L
2
int,int→int
|
Compute bitwise OR
L
2
int,int→int
&&
Compute logical AND
L
2
any,any→any
||
Compute logical OR
L
2
any,any→any
?:
Choose 2nd or 3rd operand
R
3
bool,any,any→any
=
Assign to a variable or property
R
2
lval,any→any
*=, /=, %=, +=,
Operate and assign
R
2
lval,any→any
Discard 1st operand, return second
L
2
any,any→any
Core JavaScript
Operator
-=, &=, ^=, |=, =, >>>= ,
4.7.1 Number of Operands Operators can be categorized based on the number of operands they expect (their arity). Most JavaScript operators, like the * multiplication operator, are binary operators that combine two expressions into a single, more complex expression. That is, they expect two operands. JavaScript also supports a number of unary operators, which convert a single expression into a single, more complex expression. The − operator in
4.7 Operator Overview | 63
the expression −x is a unary operator that performs the operation of negation on the operand x. Finally, JavaScript supports one ternary operator, the conditional operator ?:, which combines three expressions into a single expression.
4.7.2 Operand and Result Type Some operators work on values of any type, but most expect their operands to be of a specific type, and most operators return (or evaluate to) a value of a specific type. The Types column in Table 4-1 specifies operand types (before the arrow) and result type (after the arrow) for the operators. JavaScript operators usually convert the type (see §3.8) of their operands as needed. The multiplication operator * expects numeric operands, but the expression "3" * "5" is legal because JavaScript can convert the operands to numbers. The value of this expression is the number 15, not the string “15”, of course. Remember also that every JavaScript value is either “truthy” or “falsy,” so operators that expect boolean operands will work with an operand of any type. Some operators behave differently depending on the type of the operands used with them. Most notably, the + operator adds numeric operands but concatenates string operands. Similarly, the comparison operators such as < perform comparison in numerical or alphabetical order depending on the type of the operands. The descriptions of individual operators explain their type-dependencies and specify what type conversions they perform.
4.7.3 Lvalues Notice that the assignment operators and a few of the other operators listed in Table 4-1 expect an operand of type lval. lvalue is a historical term that means “an expression that can legally appear on the left side of an assignment expression.” In JavaScript, variables, properties of objects, and elements of arrays are lvalues. The ECMAScript specification allows built-in functions to return lvalues but does not define any functions that behave that way.
4.7.4 Operator Side Effects Evaluating a simple expression like 2 * 3 never affects the state of your program, and any future computation your program performs will be unaffected by that evaluation. Some expressions, however, have side effects, and their evaluation may affect the result of future evaluations. The assignment operators are the most obvious example: if you assign a value to a variable or property, that changes the value of any expression that uses that variable or property. The ++ and -- increment and decrement operators are similar, since they perform an implicit assignment. The delete operator also has side effects: deleting a property is like (but not the same as) assigning undefined to the property.
64 | Chapter 4: Expressions and Operators
4.7.5 Operator Precedence The operators listed in Table 4-1 are arranged in order from high precedence to low precedence, with horizontal lines separating groups of operators at the same precedence level. Operator precedence controls the order in which operations are performed. Operators with higher precedence (nearer the top of the table) are performed before those with lower precedence (nearer to the bottom). Consider the following expression: w = x + y*z;
The multiplication operator * has a higher precedence than the addition operator +, so the multiplication is performed before the addition. Furthermore, the assignment operator = has the lowest precedence, so the assignment is performed after all the operations on the right side are completed. Operator precedence can be overridden with the explicit use of parentheses. To force the addition in the previous example to be performed first, write: w = (x + y)*z;
Note that property access and invocation expressions have higher precedence than any of the operators listed in Table 4-1. Consider this expression: typeof my.functions[x](y)
Although typeof is one of the highest-priority operators, the typeof operation is performed on the result of the two property accesses and the function invocation. In practice, if you are at all unsure about the precedence of your operators, the simplest thing to do is to use parentheses to make the evaluation order explicit. The rules that are important to know are these: multiplication and division are performed before addition and subtraction, and assignment has very low precedence and is almost always performed last.
4.7.6 Operator Associativity In Table 4-1, the column labeled A specifies the associativity of the operator. A value of L specifies left-to-right associativity, and a value of R specifies right-to-left associativity. The associativity of an operator specifies the order in which operations of the same precedence are performed. Left-to-right associativity means that operations are performed from left to right. For example, the subtraction operator has left-to-right associativity, so: w = x - y - z;
4.7 Operator Overview | 65
Core JavaScript
No other JavaScript operators have side effects, but function invocation and object creation expressions will have side effects if any of the operators used in the function or constructor body have side effects.
Download from Wow! eBook
is the same as: w = ((x - y) - z);
On the other hand, the following expressions: x = ~-y; w = x = y = z; q = a?b:c?d:e?f:g;
are equivalent to: x = ~(-y); w = (x = (y = z)); q = a?b:(c?d:(e?f:g));
because the unary, assignment, and ternary conditional operators have right-to-left associativity.
4.7.7 Order of Evaluation Operator precedence and associativity specify the order in which operations are performed in a complex expression, but they do not specify the order in which the subexpressions are evaluated. JavaScript always evaluates expressions in strictly leftto-right order. In the expression w=x+y*z, for example, the subexpression w is evaluated first, followed by x, y, and z. Then the values of y and z are multiplied, added to the value of x, and assigned to the variable or property specified by expression w. Adding parentheses to the expressions can change the relative order of the multiplication, addition, and assignment, but not the left-to-right order of evaluation. Order of evaluation only makes a difference if any of the expressions being evaluated has side effects that affect the value of another expression. If expression x increments a variable that is used by expression z, then the fact that x is evaluated before z is important.
4.8 Arithmetic Expressions This section covers the operators that perform arithmetic or other numerical manipulations on their operands. The multiplication, division, and subtraction operators are straightforward and are covered first. The addition operator gets a subsection of its own because it can also perform string concatenation and has some unusual type conversion rules. The unary operators and the bitwise operators are also covered in subsections of their own. The basic arithmetic operators are * (multiplication), / (division), % (modulo: remainder after division), + (addition), and - (subtraction). As noted, we’ll discuss the + operator in a section of its own. The other basic four operators simply evaluate their operands, convert the values to numbers if necessary, and then compute the product, quotient, remainder, or difference between the values. Non-numeric operands that cannot convert to numbers convert to the NaN value. If either operand is (or converts to) NaN, the result of the operation is also NaN. 66 | Chapter 4: Expressions and Operators
The % operator computes the first operand modulo the second operand. In other words, it returns the remainder after whole-number division of the first operand by the second operand. The sign of the result is the same as the sign of the first operand. For example, 5 % 2 evaluates to 1 and -5 % 2 evaluates to -1. While the modulo operator is typically used with integer operands, it also works for floating-point values. For example, 6.5 % 2.1 evaluates to 0.2.
4.8.1 The + Operator The binary + operator adds numeric operands or concatenates string operands: 1 + 2 "hello" + " " + "there" "1" + "2"
// => 3 // => "hello there" // => "12"
When the values of both operands are numbers, or are both strings, then it is obvious what the + operator does. In any other case, however, type conversion is necessary, and the operation to be performed depends on the conversion performed. The conversions rules for + give priority to string concatenation: if either of the operands is a string or an object that converts to a string, the other operand is converted to a string and concatenation is performed. Addition is performed only if neither operand is string-like. Technically, the + operator behaves like this: • If either of its operand values is an object, it converts it to a primitive using the object-to-primitive algorithm described in §3.8.3: Date objects are converted by their toString() method, and all other objects are converted via valueOf(), if that method returns a primitive value. Most objects do not have a useful valueOf() method, however, so they are converted via toString() as well. • After object-to-primitive conversion, if either operand is a string, the other is converted to a string and concatenation is performed. • Otherwise, both operands are converted to numbers (or to NaN) and addition is performed. Here are some examples: 1 + 2 "1" + "2" "1" + 2 1 + {} true + true
// // // // //
=> => => => =>
3: addition "12": concatenation "12": concatenation after number-to-string "1[object Object]": concatenation after object-to-string 2: addition after boolean-to-number
4.8 Arithmetic Expressions | 67
Core JavaScript
The / operator divides its first operand by its second. If you are used to programming languages that distinguish between integer and floating-point numbers, you might expect to get an integer result when you divide one integer by another. In JavaScript, however, all numbers are floating-point, so all division operations have floating-point results: 5/2 evaluates to 2.5, not 2. Division by zero yields positive or negative infinity, while 0/0 evaluates to NaN: neither of these cases raises an error.
2 + null // => 2: addition after null converts to 0 2 + undefined // => NaN: addition after undefined converts to NaN
Finally, it is important to note that when the + operator is used with strings and numbers, it may not be associative. That is, the result may depend on the order in which operations are performed. For example: 1 + 2 + " blind mice"; 1 + (2 + " blind mice");
// => "3 blind mice" // => "12 blind mice"
The first line has no parentheses, and the + operator has left-to-right associativity, so the two numbers are added first, and their sum is concatenated with the string. In the second line, parentheses alter this order of operations: the number 2 is concatenated with the string to produce a new string. Then the number 1 is concatenated with the new string to produce the final result.
4.8.2 Unary Arithmetic Operators Unary operators modify the value of a single operand to produce a new value. In JavaScript, the unary operators all have high precedence and are all right-associative. The arithmetic unary operators described in this section (+, -, ++, and --) all convert their single operand to a number, if necessary. Note that the punctuation characters + and - are used as both unary and binary operators. The unary arithmetic operators are the following: Unary plus (+) The unary plus operator converts its operand to a number (or to NaN) and returns that converted value. When used with an operand that is already a number, it doesn’t do anything. Unary minus (-) When - is used as a unary operator, it converts its operand to a number, if necessary, and then changes the sign of the result. Increment (++) The ++ operator increments (i.e., adds 1 to) its single operand, which must be an lvalue (a variable, an element of an array, or a property of an object). The operator converts its operand to a number, adds 1 to that number, and assigns the incremented value back into the variable, element, or property. The return value of the ++ operator depends on its position relative to the operand. When used before the operand, where it is known as the pre-increment operator, it increments the operand and evaluates to the incremented value of that operand. When used after the operand, where it is known as the post-increment operator, it increments its operand but evaluates to the unincremented value of that operand. Consider the difference between these two lines of code: var i = 1, j = ++i; var i = 1, j = i++;
// i and j are both 2 // i is 2, j is 1
68 | Chapter 4: Expressions and Operators
4.8.3 Bitwise Operators The bitwise operators perform low-level manipulation of the bits in the binary representation of numbers. Although they do not perform traditional arithmetic operations, they are categorized as arithmetic operators here because they operate on numeric operands and return a numeric value. These operators are not commonly used in JavaScript programming, and if you are not familiar with the binary representation of decimal integers, you can probably skip this section. Four of these operators perform Boolean algebra on the individual bits of the operands, behaving as if each bit in each operand were a boolean value (1=true, 0=false). The other three bitwise operators are used to shift bits left and right. The bitwise operators expect integer operands and behave as if those values were represented as 32-bit integers rather than 64-bit floating-point values. These operators convert their operands to numbers, if necessary, and then coerce the numeric values to 32-bit integers by dropping any fractional part and any bits beyond the 32nd. The shift operators require a right-side operand between 0 and 31. After converting this operand to an unsigned 32-bit integer, they drop any bits beyond the 5th, which yields a number in the appropriate range. Surprisingly, NaN, Infinity, and -Infinity all convert to 0 when used as operands of these bitwise operators. Bitwise AND (&) The & operator performs a Boolean AND operation on each bit of its integer arguments. A bit is set in the result only if the corresponding bit is set in both operands. For example, 0x1234 & 0x00FF evaluates to 0x0034.
4.8 Arithmetic Expressions | 69
Core JavaScript
Note that the expression ++x is not always the same as x=x+1. The ++ operator never performs string concatenation: it always converts its operand to a number and increments it. If x is the string “1”, ++x is the number 2, but x+1 is the string “11”. Also note that, because of JavaScript’s automatic semicolon insertion, you cannot insert a line break between the post-increment operator and the operand that precedes it. If you do so, JavaScript will treat the operand as a complete statement by itself and insert a semicolon before it. This operator, in both its pre- and post-increment forms, is most commonly used to increment a counter that controls a for loop (§5.5.3). Decrement (--) The -- operator expects an lvalue operand. It converts the value of the operand to a number, subtracts 1, and assigns the decremented value back to the operand. Like the ++ operator, the return value of -- depends on its position relative to the operand. When used before the operand, it decrements and returns the decremented value. When used after the operand, it decrements the operand but returns the undecremented value. When used after its operand, no line break is allowed between the operand and the operator.
Bitwise OR (|) The | operator performs a Boolean OR operation on each bit of its integer arguments. A bit is set in the result if the corresponding bit is set in one or both of the operands. For example, 0x1234 | 0x00FF evaluates to 0x12FF. Bitwise XOR (^) The ^ operator performs a Boolean exclusive OR operation on each bit of its integer arguments. Exclusive OR means that either operand one is true or operand two is true, but not both. A bit is set in this operation’s result if a corresponding bit is set in one (but not both) of the two operands. For example, 0xFF00 ^ 0xF0F0 evaluates to 0x0FF0. Bitwise NOT (~) The ~ operator is a unary operator that appears before its single integer operand. It operates by reversing all bits in the operand. Because of the way signed integers are represented in JavaScript, applying the ~ operator to a value is equivalent to changing its sign and subtracting 1. For example ~0x0F evaluates to 0xFFFFFFF0, or −16. Shift left (> 1 evaluates to 3, and −7 >> 1 evaluates to −4. Shift right with zero fill (>>>) The >>> operator is just like the >> operator, except that the bits shifted in on the left are always zero, regardless of the sign of the first operand. For example, −1 >> 4 evaluates to −1, but −1 >>> 4 evaluates to 0x0FFFFFFF.
70 | Chapter 4: Expressions and Operators
This section describes JavaScript’s relational operators. These operators test for a relationship (such as “equals,” “less than,” or “property of”) between two values and return true or false depending on whether that relationship exists. Relational expressions always evaluate to a boolean value, and that value is often used to control the flow of program execution in if, while, and for statements (see Chapter 5). The subsections that follow document the equality and inequality operators, the comparison operators, and JavaScript’s other two relational operators, in and instanceof.
4.9.1 Equality and Inequality Operators The == and === operators check whether two values are the same, using two different definitions of sameness. Both operators accept operands of any type, and both return true if their operands are the same and false if they are different. The === operator is known as the strict equality operator (or sometimes the identity operator), and it checks whether its two operands are “identical” using a strict definition of sameness. The == operator is known as the equality operator; it checks whether its two operands are “equal” using a more relaxed definition of sameness that allows type conversions. JavaScript supports =, ==, and === operators. Be sure you understand the differences between these assignment, equality, and strict equality operators, and be careful to use the correct one when coding! Although it is tempting to read all three operators “equals,” it may help to reduce confusion if you read “gets or is assigned” for =, “is equal to” for ==, and “is strictly equal to” for ===. The != and !== operators test for the exact opposite of the == and === operators. The != inequality operator returns false if two values are equal to each other according to == and returns true otherwise. The !== operator returns false if two values are strictly equal to each other and returns true otherwise. As you’ll see in §4.10, the ! operator computes the Boolean NOT operation. This makes it easy to remember that != and !== stand for “not equal to” and “not strictly equal to.” As mentioned in §3.7, JavaScript objects are compared by reference, not by value. An object is equal to itself, but not to any other object. If two distinct objects have the same number of properties, with the same names and values, they are still not equal. Two arrays that have the same elements in the same order are not equal to each other. The strict equality operator === evaluates its operands, and then compares the two values as follows, performing no type conversion: • If the two values have different types, they are not equal. • If both values are null or both values are undefined, they are equal. • If both values are the boolean value true or both are the boolean value false, they are equal.
4.9 Relational Expressions | 71
Core JavaScript
4.9 Relational Expressions
• If one or both values is NaN, they are not equal. The NaN value is never equal to any other value, including itself! To check whether a value x is NaN, use x !== x. NaN is the only value of x for which this expression will be true. • If both values are numbers and have the same value, they are equal. If one value is 0 and the other is -0, they are also equal. • If both values are strings and contain exactly the same 16-bit values (see the sidebar in §3.2) in the same positions, they are equal. If the strings differ in length or content, they are not equal. Two strings may have the same meaning and the same visual appearance, but still be encoded using different sequences of 16-bit values. JavaScript performs no Unicode normalization, and a pair of strings like this are not considered equal to the === or to the == operators. See String.localeCompare() in Part III for another way to compare strings. • If both values refer to the same object, array, or function, they are equal. If they refer to different objects they are not equal, even if both objects have identical properties. The equality operator == is like the strict equality operator, but it is less strict. If the values of the two operands are not the same type, it attempts some type conversions and tries the comparison again: • If the two values have the same type, test them for strict equality as described above. If they are strictly equal, they are equal. If they are not strictly equal, they are not equal. • If the two values do not have the same type, the == operator may still consider them equal. Use the following rules and type conversions to check for equality: — If one value is null and the other is undefined, they are equal. — If one value is a number and the other is a string, convert the string to a number and try the comparison again, using the converted value. — If either value is true, convert it to 1 and try the comparison again. If either value is false, convert it to 0 and try the comparison again. — If one value is an object and the other is a number or string, convert the object to a primitive using the algorithm described in §3.8.3 and try the comparison again. An object is converted to a primitive value by either its toString() method or its valueOf() method. The built-in classes of core JavaScript attempt valueOf() conversion before toString() conversion, except for the Date class, which performs toString() conversion. Objects that are not part of core JavaScript may convert themselves to primitive values in an implementation-defined way. — Any other combinations of values are not equal. As an example of testing for equality, consider the comparison: "1" == true
72 | Chapter 4: Expressions and Operators
4.9.2 Comparison Operators The comparison operators test the relative order (numerical or alphabetics) of their two operands: Less than () The > operator evaluates to true if its first operand is greater than its second operand; otherwise it evaluates to false. Less than or equal (= operator evaluates to true if its first operand is greater than or equal to its second operand; otherwise it evaluates to false. The operands of these comparison operators may be of any type. Comparison can be performed only on numbers and strings, however, so operands that are not numbers or strings are converted. Comparison and conversion occur as follows: • If either operand evaluates to an object, that object is converted to a primitive value as described at the end of §3.8.3: if its valueOf() method returns a primitive value, that value is used. Otherwise, the return value of its toString() method is used. • If, after any required object-to-primitive conversion, both operands are strings, the two strings are compared, using alphabetical order, where “alphabetical order” is defined by the numerical order of the 16-bit Unicode values that make up the strings. • If, after object-to-primitive conversion, at least one operand is not a string, both operands are converted to numbers and compared numerically. 0 and -0 are considered equal. Infinity is larger than any number other than itself, and -Infinity is smaller than any number other than itself. If either operand is (or converts to) NaN, then the comparison operator always returns false. Remember that JavaScript strings are sequences of 16-bit integer values, and that string comparison is just a numerical comparison of the values in the two strings. The numerical encoding order defined by Unicode may not match the traditional collation order used in any particular language or locale. Note in particular that string comparison is case-sensitive, and all capital ASCII letters are “less than” all lowercase ASCII
4.9 Relational Expressions | 73
Core JavaScript
This expression evaluates to true, indicating that these very different-looking values are in fact equal. The boolean value true is first converted to the number 1, and the comparison is done again. Next, the string "1" is converted to the number 1. Since both values are now the same, the comparison returns true.
letters. This rule can cause confusing results if you do not expect it. For example, according to the < operator, the string “Zoo” comes before the string “aardvark”. For a more robust string-comparison algorithm, see the String.localeCompare() method, which also takes locale-specific definitions of alphabetical order into account. For case-insensitive comparisons, you must first convert the strings to all lowercase or all uppercase using String.toLowerCase() or String.toUpperCase(). Both the + operator and the comparison operators behave differently for numeric and string operands. + favors strings: it performs concatenation if either operand is a string. The comparison operators favor numbers and only perform string comparison if both operands are strings: 1 + 2 "1" + "2" "1" + 2 11 < 3 "11" < "3" "11" < 3 "one" < 3
// // // // // // //
Addition. Result is 3. Concatenation. Result is "12". Concatenation. 2 is converted to "2". Result is "12". Numeric comparison. Result is false. String comparison. Result is true. Numeric comparison. "11" converted to 11. Result is false. Numeric comparison. "one" converted to NaN. Result is false.
Finally, note that the = (greater than or equal) operators do not rely on the equality or strict equality operators for determining whether two values are “equal.” Instead, the less-than-or-equal operator is simply defined as “not greater than,” and the greater-than-or-equal operator is defined as “not less than.” The one exception occurs when either operand is (or converts to) NaN, in which case all four comparison operators return false.
4.9.3 The in Operator The in operator expects a left-side operand that is or can be converted to a string. It expects a right-side operand that is an object. It evaluates to true if the left-side value is the name of a property of the right-side object. For example: var point = { x:1, y:1 }; "x" in point "z" in point "toString" in point
// // // //
Define an object => true: object has property named "x" => false: object has no "z" property. => true: object inherits toString method
var data = [7,8,9]; "0" in data 1 in data 3 in data
// // // //
An => => =>
array with elements 0, 1, and 2 true: array has an element "0" true: numbers are converted to strings false: no element 3
4.9.4 The instanceof Operator The instanceof operator expects a left-side operand that is an object and a right-side operand that identifies a class of objects. The operator evaluates to true if the left-side object is an instance of the right-side class and evaluates to false otherwise. Chapter 9 explains that, in JavaScript, classes of objects are defined by the constructor
74 | Chapter 4: Expressions and Operators
var d = new Date(); d instanceof Date; d instanceof Object; d instanceof Number; var a = [1, 2, 3]; a instanceof Array; a instanceof Object; a instanceof RegExp;
// // // // // // // //
Create a new object with the Date() constructor Evaluates to true; d was created with Date() Evaluates to true; all objects are instances of Object Evaluates to false; d is not a Number object Create an array with array literal syntax Evaluates to true; a is an array Evaluates to true; all arrays are objects Evaluates to false; arrays are not regular expressions
Note that all objects are instances of Object. instanceof considers the “superclasses” when deciding whether an object is an instance of a class. If the left-side operand of instanceof is not an object, instanceof returns false. If the right-hand side is not a function, it throws a TypeError. In order to understand how the instanceof operator works, you must understand the “prototype chain.” This is JavaScript’s inheritance mechanism, and it is described in §6.2.2. To evaluate the expression o instanceof f, JavaScript evaluates f.prototype, and then looks for that value in the prototype chain of o. If it finds it, then o is an instance of f (or of a superclass of f) and the operator returns true. If f.prototype is not one of the values in the prototype chain of o, then o is not an instance of f and instanceof returns false.
4.10 Logical Expressions The logical operators &&, ||, and ! perform Boolean algebra and are often used in conjunction with the relational operators to combine two relational expressions into one more complex expression. These operators are described in the subsections that follow. In order to fully understand them, you may want to review the concept of “truthy” and “falsy” values introduced in §3.3.
4.10.1 Logical AND (&&) The && operator can be understood at three different levels. At the simplest level, when used with boolean operands, && performs the Boolean AND operation on the two values: it returns true if and only if both its first operand and its second operand are true. If one or both of these operands is false, it returns false. && is often used as a conjunction to join two relational expressions: x == 0 && y == 0
// true if, and only if x and y are both 0
Relational expressions always evaluate to true or false, so when used like this, the && operator itself returns true or false. Relational operators have higher precedence than && (and ||), so expressions like these can safely be written without parentheses. But && does not require that its operands be boolean values. Recall that all JavaScript values are either “truthy” or “falsy.” (See §3.3 for details. The falsy values are false,
4.10 Logical Expressions | 75
Core JavaScript
function that initializes them. Thus, the right-side operand of instanceof should be a function. Here are examples:
null, undefined, 0, -0, NaN, and "". All other values, including all objects, are truthy.) The second level at which && can be understood is as a Boolean AND operator for truthy and falsy values. If both operands are truthy, the operator returns a truthy value. Otherwise, one or both operands must be falsy, and the operator returns a falsy value. In JavaScript, any expression or statement that expects a boolean value will work with a truthy or falsy value, so the fact that && does not always return true or false does not cause practical problems.
Notice that the description above says that the operator returns “a truthy value” or “a falsy value,” but does not specify what that value is. For that, we need to describe && at the third and final level. This operator starts by evaluating its first operand, the expression on its left. If the value on the left is falsy, the value of the entire expression must also be falsy, so && simply returns the value on the left and does not even evaluate the expression on the right. On the other hand, if the value on the left is truthy, then the overall value of the expression depends on the value on the right-hand side. If the value on the right is truthy, then the overall value must be truthy, and if the value on the right is falsy, then the overall value must be falsy. So when the value on the left is truthy, the && operator evaluates and returns the value on the right: var o = { x : 1 }; var p = null; o && o.x // => 1: o is truthy, so return value of o.x p && p.x // => null: p is falsy, so return it and don't evaluate p.x
It is important to understand that && may or may not evaluate its right-side operand. In the code above, the variable p is set to null, and the expression p.x would, if evaluated, cause a TypeError. But the code uses && in an idiomatic way so that p.x is evaluated only if p is truthy—not null or undefined. The behavior of && is sometimes called “short circuiting,” and you may sometimes see code that purposely exploits this behavior to conditionally execute code. For example, the following two lines of JavaScript code have equivalent effects: if (a == b) stop(); (a == b) && stop();
// Invoke stop() only if a == b // This does the same thing
In general, you must be careful whenever you write an expression with side effects (assignments, increments, decrements, or function invocations) on the right-hand side of &&. Whether those side effects occur depends on the value of the left-hand side. Despite the somewhat complex way that this operator actually works, it is most commonly used as a simple Boolean algebra operator that works on truthy and falsy values.
4.10.2 Logical OR (||) The || operator performs the Boolean OR operation on its two operands. If one or both operands is truthy, it returns a truthy value. If both operands are falsy, it returns a falsy value.
76 | Chapter 4: Expressions and Operators
As with the && operator, you should avoid right-side operands that include side effects, unless you purposely want to use the fact that the right-side expression may not be evaluated. An idiomatic usage of this operator is to select the first truthy value in a set of alternatives: // If max_width is defined, use that. Otherwise look for a value in // the preferences object. If that is not defined use a hard-coded constant. var max = max_width || preferences.max_width || 500;
This idiom is often used in function bodies to supply default values for parameters: // Copy the properties of o to p, and return p function copy(o, p) { p = p || {}; // If no object passed for p, use a newly created object. // function body goes here }
4.10.3 Logical NOT (!) The ! operator is a unary operator; it is placed before a single operand. Its purpose is to invert the boolean value of its operand. For example, if x is truthy !x evaluates to false. If x is falsy, then !x is true. Unlike the && and || operators, the ! operator converts its operand to a boolean value (using the rules described in Chapter 3) before inverting the converted value. This means that ! always returns true or false, and that you can convert any value x to its equivalent boolean value by applying this operator twice: !!x (see §3.8.2). As a unary operator, ! has high precedence and binds tightly. If you want to invert the value of an expression like p && q, you need to use parentheses: !(p && q). It is worth noting two theorems of Boolean algebra here that we can express using JavaScript syntax: // These two equalities hold for any values of p and q !(p && q) === !p || !q !(p || q) === !p && !q
4.11 Assignment Expressions JavaScript uses the = operator to assign a value to a variable or property. For example: i = 0 o.x = 1
// Set the variable i to 0. // Set the property x of object o to 1.
4.11 Assignment Expressions | 77
Core JavaScript
Although the || operator is most often used simply as a Boolean OR operator, it, like the && operator, has more complex behavior. It starts by evaluating its first operand, the expression on its left. If the value of this first operand is truthy, it returns that truthy value. Otherwise, it evaluates its second operand, the expression on its right, and returns the value of that expression.
The = operator expects its left-side operand to be an lvalue: a variable or object property (or array element). It expects its right-side operand to be an arbitrary value of any type. The value of an assignment expression is the value of the right-side operand. As a side effect, the = operator assigns the value on the right to the variable or property on the left so that future references to the variable or property evaluate to the value. Although assignment expressions are usually quite simple, you may sometimes see the value of an assignment expression used as part of a larger expression. For example, you can assign and test a value in the same expression with code like this: (a = b) == 0
If you do this, be sure you are clear on the difference between the = and == operators! Note that = has very low precedence and parentheses are usually necessary when the value of an assignment is to be used in a larger expression. The assignment operator has right-to-left associativity, which means that when multiple assignment operators appear in an expression, they are evaluated from right to left. Thus, you can write code like this to assign a single value to multiple variables: i = j = k = 0;
// Initialize 3 variables to 0
4.11.1 Assignment with Operation Besides the normal = assignment operator, JavaScript supports a number of other assignment operators that provide shortcuts by combining assignment with some other operation. For example, the += operator performs addition and assignment. The following expression: total += sales_tax
is equivalent to this one: total = total + sales_tax
As you might expect, the += operator works for numbers or strings. For numeric operands, it performs addition and assignment; for string operands, it performs concatenation and assignment. Similar operators include -=, *=, &=, and so on. Table 4-2 lists them all. Table 4-2. Assignment operators Operator
Example
Equivalent
+=
a += b
a = a + b
-=
a -= b
a = a - b
*=
a *= b
a = a * b
/=
a /= b
a = a / b
%=
a %= b
a = a % b
> b
>>>=
a >>>= b
a = a >>> b
&=
a &= b
a = a & b
|=
a |= b
a = a | b
^=
a ^= b
a = a ^ b
Core JavaScript
Operator
In most cases, the expression: a op= b
where op is an operator, is equivalent to the expression: a = a op b
In the first line, the expression a is evaluated once. In the second it is evaluated twice. The two cases will differ only if a includes side effects such as a function call or an increment operator. The following two assignments, for example, are not the same: data[i++] *= 2; data[i++] = data[i++] * 2;
4.12 Evaluation Expressions Like many interpreted languages, JavaScript has the ability to interpret strings of JavaScript source code, evaluating them to produce a value. JavaScript does this with the global function eval(): eval("3+2")
// => 5
Dynamic evaluation of strings of source code is a powerful language feature that is almost never necessary in practice. If you find yourself using eval(), you should think carefully about whether you really need to use it. The subsections below explain the basic use of eval() and then explain two restricted versions of it that have less impact on the optimizer.
Is eval() a Function or an Operator? eval() is a function, but it is included in this chapter on expressions because it really should have been an operator. The earliest versions of the language defined an eval()
function, and ever since then language designers and interpreter writers have been placing restrictions on it that make it more and more operator-like. Modern JavaScript interpreters perform a lot of code analysis and optimization. The problem with eval() is that the code it evaluates is, in general, unanalyzable. Generally speaking, if a function calls eval(), the interpreter cannot optimize that function. The problem with defining eval() as a function is that it can be given other names: var f = eval; var g = f;
4.12 Evaluation Expressions | 79
If this is allowed, then the interpreter can’t safely optimize any function that calls g(). This issue could have been avoided if eval was an operator (and a reserved word). We’ll learn below (in §4.12.2 and §4.12.3) about restrictions placed on eval() to make it more operator-like.
4.12.1 eval() eval() expects one argument. If you pass any value other than a string, it simply returns
that value. If you pass a string, it attempts to parse the string as JavaScript code, throwing a SyntaxError if it fails. If it successfully parses the string, then it evaluates the code and returns the value of the last expression or statement in the string or undefined if the last expression or statement had no value. If the string throws an exception, the eval() propagates that expression. The key thing about eval() (when invoked like this) is that it uses the variable environment of the code that calls it. That is, it looks up the values of variables and defines new variables and functions in the same way that local code does. If a function defines a local variable x and then calls eval("x"), it will obtain the value of the local variable. If it calls eval("x=1"), it changes the value of the local variable. And if the function calls eval("var y = 3;"), it has declared a new local variable y. Similarly a function can declare a local function with code like this: eval("function f() { return x+1; }");
If you call eval() from top-level code, it operates on global variables and global functions, of course. Note that the string of code you pass to eval() must make syntactic sense on its own— you cannot use it to paste code fragments into a function. It makes no sense to write eval("return;"), for example, because return is only legal within functions, and the fact that the evaluated string uses the same variable environment as the calling function does not make it part of that function. If your string would make sense as a standalone script (even a very short one like x=0 ), it is legal to pass to eval(). Otherwise eval() will throw a SyntaxError.
4.12.2 Global eval() It is the ability of eval() to change local variables that is so problematic to JavaScript optimizers. As a workaround, however, interpreters simply do less optimization on any function that calls eval(). But what should a JavaScript interpreter do, however, if a script defines an alias for eval() and then calls that function by another name? In order to simplify the job of JavaScript implementors, the ECMAScript 3 standard declared that interpreters did not have to allow this. If the eval() function was invoked by any name other than “eval”, it was allowed to throw an EvalError. In practice, most implementors did something else. When invoked by any other name, eval() would evaluate the string as if it were top-level global code. The evaluated code might define new global variables or global functions, and it might set global variables, 80 | Chapter 4: Expressions and Operators
ECMAScript 5 deprecates EvalError and standardizes the de facto behavior of eval(). A “direct eval” is a call to the eval() function with an expression that uses the exact, unqualified name “eval” (which is beginning to feel like a reserved word). Direct calls to eval() use the variable environment of the calling context. Any other call—an indirect call—uses the global object as its variable environment and cannot read, write, or define local variables or functions. The following code demonstrates: var geval = eval; // Using another name does a global eval var x = "global", y = "global"; // Two global variables function f() { // This function does a local eval var x = "local"; // Define a local variable eval("x += 'changed';"); // Direct eval sets local variable return x; // Return changed local variable } function g() { // This function does a global eval var y = "local"; // A local variable geval("y += 'changed';"); // Indirect eval sets global variable return y; // Return unchanged local variable } console.log(f(), x); // Local variable changed: prints "localchanged global": console.log(g(), y); // Global variable changed: prints "local globalchanged":
Notice that the ability to do a global eval is not just an accommodation to the needs of the optimizer, it is actually a tremendously useful feature: it allows you to execute strings of code as if they were independent, top-level scripts. As noted at the beginning of this section, it is rare to truly need to evaluate a string of code. But if you do find it necessary, you are more likely to want to do a global eval than a local eval. Before IE9, IE differs from other browsers: it does not do a global eval when eval() is invoked by a different name. (It doesn’t throw an EvalError either: it simply does a local eval.) But IE does define a global function named execScript() that executes its string argument as if it were a top-level script. (Unlike eval(), however, execScript() always returns null.)
4.12.3 Strict eval() ECMAScript 5 strict mode (see §5.7.3) imposes further restrictions on the behavior of the eval() function and even on the use of the identifier “eval”. When eval() is called from strict mode code, or when the string of code to be evaluated itself begins with a “use strict” directive, then eval() does a local eval with a private variable environment. This means that in strict mode, evaluated code can query and set local variables, but it cannot define new variables or functions in the local scope. Furthermore, strict mode makes eval() even more operator-like by effectively making “eval” into a reserved word. You are not allowed to overwrite the eval() function with a new value. And you are not allowed to declare a variable, function, function parameter, or catch block parameter with the name “eval”.
4.12 Evaluation Expressions | 81
Core JavaScript
but it could not use or modify any variables local to the calling function, and would not, therefore, interfere with local optimizations.
4.13 Miscellaneous Operators JavaScript supports a number of other miscellaneous operators, described in the following sections.
4.13.1 The Conditional Operator (?:) The conditional operator is the only ternary operator (three operands) in JavaScript and is sometimes actually called the ternary operator. This operator is sometimes written ?:, although it does not appear quite that way in code. Because this operator has three operands, the first goes before the ?, the second goes between the ? and the :, and the third goes after the :. It is used like this: x > 0 ? x : -x
// The absolute value of x
The operands of the conditional operator may be of any type. The first operand is evaluated and interpreted as a boolean. If the value of the first operand is truthy, then the second operand is evaluated, and its value is returned. Otherwise, if the first operand is falsy, then the third operand is evaluated and its value is returned. Only one of the second and third operands is evaluated, never both. While you can achieve similar results using the if statement (§5.4.1), the ?: operator often provides a handy shortcut. Here is a typical usage, which checks to be sure that a variable is defined (and has a meaningful, truthy value) and uses it if so or provides a default value if not: greeting = "hello " + (username ? username : "there");
This is equivalent to, but more compact than, the following if statement: greeting = "hello "; if (username) greeting += username; else greeting += "there";
4.13.2 The typeof Operator typeof is a unary operator that is placed before its single operand, which can be of any
type. Its value is a string that specifies the type of the operand. The following table specifies the value of the typeof operator for any JavaScript value: x
typeof x
undefined
"undefined"
null
"object"
true or false
"boolean"
any number or NaN
"number"
any string
"string"
82 | Chapter 4: Expressions and Operators
typeof x
any function
"function"
any nonfunction native object
"object"
any host object
An implementation-defined string, but not “undefined”, “boolean”, “number”, or “string”.
You might use the typeof operator in an expression like this: (typeof value == "string") ? "'" + value + "'" : value
The typeof operator is also useful when used with the switch statement (§5.4.3). Note that you can place parentheses around the operand to typeof, which makes typeof look like the name of a function rather than an operator keyword: typeof(i)
Note that typeof returns “object” if the operand value is null. If you want to distinguish null from objects, you’ll have to explicitly test for this special-case value. typeof may return a string other than “object” for host objects. In practice, however, most host objects in client-side JavaScript have a type of “object”. Because typeof evaluates to “object” for all object and array values other than functions, it is useful only to distinguish objects from other, primitive types. In order to distinguish one class of object from another, you must use other techniques, such as the instanceof operator (see §4.9.4), the class attribute (see §6.8.2), or the constructor property (see §6.8.1 and §9.2.2). Although functions in JavaScript are a kind of object, the typeof operator considers functions to be sufficiently different that they have their own return value. JavaScript makes a subtle distinction between functions and “callable objects.” All functions are callable, but it is possible to have a callable object—that can be invoked just like a function—that is not a true function. The ECMAScript 3 spec says that the typeof operator returns “function” for all native object that are callable. The ECMAScript 5 specification extends this to require that typeof return “function” for all callable objects, whether native objects or host objects. Most browser vendors use native JavaScript function objects for the methods of their host objects. Microsoft, however, has always used non-native callable objects for their client-side methods, and before IE 9 the typeof operator returns “object” for them, even though they behave like functions. In IE9 these client-side methods are now true native function objects. See §8.7.7 for more on the distinction between true functions and callable objects.
4.13 Miscellaneous Operators | 83
Core JavaScript
x
4.13.3 The delete Operator delete is a unary operator that attempts to delete the object property or array element
specified as its operand.1 Like the assignment, increment, and decrement operators, delete is typically used for its property deletion side effect, and not for the value it returns. Some examples: var o = { x: 1, y: 2}; // Start with an object delete o.x; // Delete one of its properties "x" in o // => false: the property does not exist anymore var a = [1,2,3]; delete a[2]; a.length
// Start with an array // Delete the last element of the array // => 2: array only has two elements now
Note that a deleted property or array element is not merely set to the undefined value. When a property is deleted, the property ceases to exist. Attempting to read a nonexistent property returns undefined, but you can test for the actual existence of a property with the in operator (§4.9.3). delete expects its operand to be an lvalue. If it is not an lvalue, the operator takes no action and returns true. Otherwise, delete attempts to delete the specified lvalue. delete returns true if it successfully deletes the specified lvalue. Not all properties can
be deleted, however: some built-in core and client-side properties are immune from deletion, and user-defined variables declared with the var statement cannot be deleted. Functions defined with the function statement and declared function parameters cannot be deleted either. In ECMAScript 5 strict mode, delete raises a SyntaxError if its operand is an unqualified identifier such as a variable, function, or function parameter: it only works when the operand is a property access expression (§4.4). Strict mode also specifies that delete raises a TypeError if asked to delete any nonconfigurable property (see §6.7). Outside of strict mode, no exception occurs in these cases and delete simply returns false to indicate that the operand could not be deleted. Here are some example uses of the delete operator: var o = {x:1, y:2}; delete o.x; typeof o.x; delete o.x; delete o; delete 1; this.x = 1; delete x;
// // // // // // // // //
Define a variable; initialize it to an object Delete one of the object properties; returns true Property does not exist; returns "undefined" Delete a nonexistent property; returns true Can't delete a declared variable; returns false. Would raise an exception in strict mode. Argument is not an lvalue: returns true Define a property of the a global object without var Try to delete it: returns true in non-strict mode
1. If you are a C++ programmer, note that the delete keyword in JavaScript is nothing like the delete keyword in C++. In JavaScript, memory deallocation is handled automatically by garbage collection, and you never have to worry about explicitly freeing up memory. Thus, there is no need for a C++-style delete to delete entire objects.
84 | Chapter 4: Expressions and Operators
// Exception in strict mode. Use 'delete this.x' instead // Runtime error: x is not defined
We’ll see the delete operator again in §6.3.
4.13.4 The void Operator void is a unary operator that appears before its single operand, which may be of any
type. This operator is unusual and infrequently used: it evaluates its operand, then discards the value and returns undefined. Since the operand value is discarded, using the void operator makes sense only if the operand has side effects. The most common use for this operator is in a client-side javascript: URL, where it allows you to evaluate an expression for its side effects without the browser displaying the value of the evaluated expression. For example, you might use the void operator in an HTML tag as follows: Open New Window
This HTML could be more cleanly written using an onclick event handler rather than a javascript: URL, of course, and the void operator would not be necessary in that case.
4.13.5 The Comma Operator (,) The comma operator is a binary operator whose operands may be of any type. It evaluates its left operand, evaluates its right operand, and then returns the value of the right operand. Thus, the following line: i=0, j=1, k=2;
evaluates to 2 and is basically equivalent to: i = 0; j = 1; k = 2;
The left-hand expression is always evaluated, but its value is discarded, which means that it only makes sense to use the comma operator when the left-hand expression has side effects. The only situation in which the comma operator is commonly used is with a for loop (§5.5.3) that has multiple loop variables: // The first comma below is part of the syntax of the var statement // The second comma is the comma operator: it lets us squeeze 2 // expressions (i++ and j--) into a statement (the for loop) that expects 1. for(var i=0,j=10; i < j; i++,j--) console.log(i+j);
4.13 Miscellaneous Operators | 85
Core JavaScript
x;
Download from Wow! eBook
CHAPTER 5
Statements
Chapter 4 described expressions as JavaScript phrases. By that analogy, statements are JavaScript sentences or commands. Just as English sentences are terminated and separated from each other with periods, JavaScript statements are terminated with semicolons (§2.5). Expressions are evaluated to produce a value, but statements are executed to make something happen. One way to “make something happen” is to evaluate an expression that has side effects. Expressions with side effects, such as assignments and function invocations, can stand alone as statements, and when used this way they are known as expression statements. A similar category of statements are the declaration statements that declare new variables and define new functions. JavaScript programs are nothing more than a sequence of statements to execute. By default, the JavaScript interpreter executes these statements one after another in the order they are written. Another way to “make something happen” is to alter this default order of execution, and JavaScript has a number of statements or control structures that do just this: • Conditionals are statements like if and switch that make the JavaScript interpreter execute or skip other statements depending on the value of an expression. • Loops are statements like while and for that execute other statements repetitively. • Jumps are statements like break, return, and throw that cause the interpreter to jump to another part of the program. The sections that follow describe the various statements in JavaScript and explain their syntax. Table 5-1, at the end of the chapter, summarizes the syntax. A JavaScript program is simply a sequence of statements, separated from one another with semicolons, so once you are familiar with the statements of JavaScript, you can begin writing JavaScript programs.
87
5.1 Expression Statements The simplest kinds of statements in JavaScript are expressions that have side effects. (But see §5.7.3 for an important expression statement without side effects.) This sort of statement was shown in Chapter 4. Assignment statements are one major category of expression statements. For example: greeting = "Hello " + name; i *= 3;
The increment and decrement operators, ++ and --, are related to assignment statements. These have the side effect of changing a variable value, just as if an assignment had been performed: counter++;
The delete operator has the important side effect of deleting an object property. Thus, it is almost always used as a statement, rather than as part of a larger expression: delete o.x;
Function calls are another major category of expression statements. For example: alert(greeting); window.close();
These client-side function calls are expressions, but they have side effects that affect the web browser and are used here as statements. If a function does not have any side effects, there is no sense in calling it, unless it is part of a larger expression or an assignment statement. For example, you wouldn’t just compute a cosine and discard the result: Math.cos(x);
But you might well compute the value and assign it to a variable for future use: cx = Math.cos(x);
Note that each line of code in each of these examples is terminated with a semicolon.
5.2 Compound and Empty Statements Just as the comma operator (§4.13.5) combines multiple expressions into a single expression, a statement block combines multiple statements into a single compound statement. A statement block is simply a sequence of statements enclosed within curly braces. Thus, the following lines act as a single statement and can be used anywhere that JavaScript expects a single statement: {
x = Math.PI; cx = Math.cos(x); console.log("cos(π) = " + cx);
}
88 | Chapter 5: Statements
Combining statements into larger statement blocks is extremely common in JavaScript programming. Just as expressions often contain subexpressions, many JavaScript statements contain substatements. Formally, JavaScript syntax usually allows a single substatement. For example, the while loop syntax includes a single statement that serves as the body of the loop. Using a statement block, you can place any number of statements within this single allowed substatement. A compound statement allows you to use multiple statements where JavaScript syntax expects a single statement. The empty statement is the opposite: it allows you to include no statements where one is expected. The empty statement looks like this: ;
The JavaScript interpreter takes no action when it executes an empty statement. The empty statement is occasionally useful when you want to create a loop that has an empty body. Consider the following for loop (for loops will be covered in §5.5.3): // Initialize an array a for(i = 0; i < a.length; a[i++] = 0) ;
In this loop, all the work is done by the expression a[i++] = 0, and no loop body is necessary. JavaScript syntax requires a statement as a loop body, however, so an empty statement—just a bare semicolon—is used. Note that the accidental inclusion of a semicolon after the right parenthesis of a for loop, while loop, or if statement can cause frustrating bugs that are difficult to detect. For example, the following code probably does not do what the author intended: if ((a == 0) || (b == 0)); o = null;
// Oops! This line does nothing... // and this line is always executed.
When you intentionally use the empty statement, it is a good idea to comment your code in a way that makes it clear that you are doing it on purpose. For example: for(i = 0; i < a.length; a[i++] = 0) /* empty */ ;
5.3 Declaration Statements The var and function are declaration statements—they declare or define variables and functions. These statements define identifiers (variable and function names) that can be used elsewhere in your program and assign values to those identifiers. Declaration statements don’t do much themselves, but by creating variables and functions they, in an important sense, define the meaning of the other statements in your program.
5.3 Declaration Statements | 89
Core JavaScript
There are a few things to note about this statement block. First, it does not end with a semicolon. The primitive statements within the block end in semicolons, but the block itself does not. Second, the lines inside the block are indented relative to the curly braces that enclose them. This is optional, but it makes the code easier to read and understand. Finally, recall that JavaScript does not have block scope and variables declared within a statement block are not private to the block (see §3.10.1 for details).
The subsections that follow explain the var statement and the function statement, but do not cover variables and functions comprehensively. See §3.9 and §3.10 for more on variables. And see Chapter 8 for complete details on functions.
5.3.1 var The var statement declares a variable or variables. Here’s the syntax: var name_1 [ = value_1] [ ,..., name_n [= value_n]]
The var keyword is followed by a comma-separated list of variables to declare; each variable in the list may optionally have an initializer expression that specifies its initial value. For example: var var var var var var var
i; j = 0; p, q; greeting = "hello" + name; x = 2.34, y = Math.cos(0.75), r, theta; x = 2, y = x*x; x = 2, f = function(x) { return x*x }, y = f(x);
// // // // // // // //
One simple variable One var, one value Two variables A complex initializer Many variables Second var uses the first Multiple variables... each on its own line
If a var statement appears within the body of a function, it defines local variables, scoped to that function. When var is used in top-level code, it declares global variables, visible throughout the JavaScript program. As noted in §3.10.2, global variables are properties of the global object. Unlike other global properties, however, properties created with var cannot be deleted. If no initializer is specified for a variable with the var statement, the variable’s initial value is undefined. As described in §3.10.1, variables are defined throughout the script or function in which they are declared—their declaration is “hoisted” up to the start of the script or function. Initialization, however, occurs at the location of the var statement, and the value of the variable is undefined before that point in the code. Note that the var statement can also appear as part of the for and for/in loops. (These variables are hoisted, just like variables declared outside of a loop.) Here are examples repeated from §3.9: for(var i = 0; i < 10; i++) console.log(i); for(var i = 0, j=10; i < 10; i++,j--) console.log(i*j); for(var i in o) console.log(i);
Note that it is harmless to declare the same variable multiple times.
90 | Chapter 5: Statements
The function keyword is used to define functions. We saw it in function definition expressions in §4.3. It can also be used in statement form. Consider the following two functions: var f = function(x) { return x+1; } function f(x) { return x+1; }
// Expression assigned to a variable // Statement includes variable name
A function declaration statement has the following syntax: function funcname([arg1 [, arg2 [..., argn]]]) { statements }
funcname is an identifier that names the function being declared. The function name is
followed by a comma-separated list of parameter names in parentheses. These identifiers can be used within the body of the function to refer to the argument values passed when the function is invoked. The body of the function is composed of any number of JavaScript statements, contained within curly braces. These statements are not executed when the function is defined. Instead, they are associated with the new function object for execution when the function is invoked. Note that the curly braces are a required part of the function statement. Unlike statement blocks used with while loops and other statements, a function body requires curly braces, even if the body consists of only a single statement. Here are some more examples of function declarations: function hypotenuse(x, y) { return Math.sqrt(x*x + y*y); } function factorial(n) { if (n 3: x and y are inherited from o and p
Now suppose you assign to the property x of the object o. If o already has an own (noninherited) property named x, then the assignment simply changes the value of this existing property. Otherwise, the assignment creates a new property named x on the object o. If o previously inherited the property x, that inherited property is now hidden by the newly created own property with the same name. Property assignment examines the prototype chain to determine whether the assignment is allowed. If o inherits a read-only property named x, for example, then the assignment is not allowed. (Details about when a property may be set are in §6.2.3.) If the assignment is allowed, however, it always creates or sets a property in the original object and never modifies the prototype chain. The fact that inheritance occurs when querying properties but not when setting them is a key feature of JavaScript because it allows us to selectively override inherited properties: var unitcircle = { r:1 }; var c = inherit(unitcircle); c.x = 1; c.y = 1; c.r = 2; unitcircle.r;
// // // // //
An object to inherit from c inherits the property r c defines two properties of its own c overrides its inherited property => 1: the prototype object 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.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
122 | Chapter 6: Objects
6.2.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. 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: book.subtitle;
// => undefined: property doesn't exist
It is an error, however, to attempt to query a property of an object that does not exist. The null and undefined values have no properties, and it is an error to query properties of these values. Continuing the above example: // Raises a TypeError exception. undefined doesn't have a length property var len = book.subtitle.length;
Unless you are certain that both book and book.subtitle are (or behave like) objects, you shouldn’t write the expression book.subtitle.length, since it might raise an exception. Here are two ways to guard against this kind of exception: // A verbose and explicit technique var len = undefined; if (book) { if (book.subtitle) len = book.subtitle.length; } // A concise and idiomatic alternative to get subtitle length or undefined var len = book && book.subtitle && book.subtitle.length;
To understand why this idiomatic expression works to prevent TypeError exceptions, you might want to review the short-circuiting behavior of the && operator in §4.10.1. Attempting to set a property on null or undefined also causes a TypeError, of course. Attempts to set properties on other values do not always succeed, either: some properties are read-only and cannot be set, and some objects do not allow the addition of new properties. Curiously, however, these failed attempts to set properties usually fail silently: // The prototype properties of built-in constructors are read-only. Object.prototype = 0; // Assignment fails silently; Object.prototype unchanged
This historical quirk of JavaScript is rectified in the strict mode of ECMAScript 5. In strict mode, any failed attempt to set a property throws a TypeError exception.
6.2 Querying and Setting Properties | 123
Core JavaScript
setter method defines any properties, it will do so on o, and it will again leave the prototype chain unmodified.
The rules that specify when a property assignment succeeds and when it fails are intuitive but difficult to express concisely. An attempt to set a property p of an object o fails in these circumstances: • o has an own property p that is read-only: it is not possible to set read-only properties. (See the defineProperty() method, however, for an exception that allows configurable read-only properties to be set.) • o has an inherited property p that is read-only: it is not possible to hide an inherited read-only property with an own property of the same name. • 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 §6.8.3) is false. If p does not already exist on 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.3 Deleting Properties The delete operator (§4.13.3) 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; delete book["main title"];
// The book object now has no author property. // Now it doesn't have "main title", either.
The delete operator only deletes own properties, not inherited ones. (To delete an inherited property, you must delete it from the prototype object in which it is defined. Doing this affects every object that inherits from that prototype.) A delete expression evaluates to true if the delete succeeded or if the delete had no effect (such as deleting a nonexistent property). delete also evaluates to true when used (meaninglessly) with an expression that is not a property access expression: o = {x:1}; delete o.x; delete o.x; delete o.toString; delete 1;
// // // // //
o has own property x and inherits property toString Delete x, and return true Do nothing (x doesn't exist), and return true Do nothing (toString isn't an own property), return true Nonsense, but evaluates to true
delete does not remove properties that have a configurable attribute of false. (Though it will remove configurable properties of nonextensible objects.) Certain properties of built-in objects are nonconfigurable, as are properties of the global object created by variable declaration and function declaration. In strict mode, attempting to delete a nonconfigurable property causes a TypeError. In non-strict mode (and in ECMAScript 3), delete simply evaluates to false in this case: delete Object.prototype; var x = 1; delete this.x; function f() {} delete this.f;
124 | Chapter 6: Objects
// // // // //
Can't delete; property is non-configurable Declare a global variable Can't delete this property Declare a global function Can't delete this property either
this.x = 1; delete x;
// Create a configurable global property (no var) // And delete it
In strict mode, however, delete raises a SyntaxError if its operand is an unqualified identifier like x, and you have to be explicit about the property access: delete x; delete this.x;
// SyntaxError in strict mode // This works
6.4 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 in operator expects a property name (as a string) 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: var o = { x: 1 } "x" in o; "y" in o; "toString" in o;
// true: o has an own property "x" // false: o doesn't have a property "y" // true: o inherits a toString property
The hasOwnProperty() method of an object tests whether that object has an own property with the given name. It returns false for inherited properties: var o = { x: 1 } o.hasOwnProperty("x"); // true: o has an own property x o.hasOwnProperty("y"); // false: o doesn't have a property y o.hasOwnProperty("toString"); // false: toString is an inherited property
The propertyIsEnumerable() refines the hasOwnProperty() test. It returns true only if the named property is an own property and its enumerable attribute is true. Certain built-in properties are not enumerable. Properties created by normal JavaScript code are enumerable unless you’ve used one of the ECMAScript 5 methods shown later to make them nonenumerable. var o = inherit({ y: 2 }); o.x = 1; o.propertyIsEnumerable("x"); // true: o has an own enumerable property x o.propertyIsEnumerable("y"); // false: y is inherited, not own Object.prototype.propertyIsEnumerable("toString"); // false: not enumerable
Instead of using the in operator it is often sufficient to simply query the property and use !== to make sure it is not undefined: var o = { x: 1 } o.x !== undefined;
// true: o has a property x
6.4 Testing Properties | 125
Core JavaScript
When deleting configurable properties of the global object in non-strict mode, you can omit the reference to the global object and simply follow the delete operator with the property name:
o.y !== undefined; // false: o doesn't have a property y o.toString !== undefined; // true: o inherits a toString property
There is one thing the in operator can do that the simple property access technique shown above cannot do. in can distinguish between properties that do not exist and properties that exist but have been set to undefined. Consider this code: var o = { x: undefined } o.x !== undefined o.y !== undefined "x" in o "y" in o delete o.x; "x" in o
// // // // // // //
Property is explicitly set to undefined false: property exists but is undefined false: property doesn't even exist true: the property exists false: the property doesn't exists Delete the property x false: it doesn't exist anymore
Note that the code above uses the !== operator instead of !=. !== and === distinguish between undefined and null. Sometimes, however, you don’t want to make this distinction: // If o has a property x whose value is not null or undefined, double it. if (o.x != null) o.x *= 2; // If o has a property x whose value does not convert to false, double it. // If x is undefined, null, false, "", 0, or NaN, leave it alone. if (o.x) o.x *= 2;
6.5 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. This is usually done with the for/in loop, although ECMAScript 5 provides two handy alternatives. The for/in loop was covered in §5.5.4. 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 (unless you use one of the functions described later to make them nonenumerable). For example: var o = {x:1, y:2, z:3}; o.propertyIsEnumerable("toString") for(p in o) console.log(p);
// // // //
Three enumerable own properties => false: not enumerable Loop through the properties Prints x, y, and z, but not toString
Some utility libraries add new methods (or other properties) to Object.prototype so that they are inherited by, and available to, all objects. Prior to ECMAScript 5, however, there is no way to make these added methods nonenumerable, so they are enumerated by for/in loops. To guard against this, you might want to filter the properties returned by for/in. Here are two ways you might do so: for(p in o) { if (!o.hasOwnProperty(p)) continue; }
126 | Chapter 6: Objects
// Skip inherited properties
Example 6-2 defines utility functions that use for/in loops to manipulate object properties in helpful ways. The extend() function, in particular, is one that is commonly included in JavaScript utility libraries.2 Example 6-2. Object utility functions that enumerate properties /* * Copy the enumerable properties of p to o, and return o. * If o and p have a property by the same name, o's property is overwritten. * This function does not handle getters and setters or copy attributes. */ function extend(o, p) { for(prop in p) { // For all props in p. o[prop] = p[prop]; // Add the property to o. } return o; } /* * Copy the enumerable properties of p to o, and return o. * If o and p have a property by the same name, o's property is left alone. * This function does not handle getters and setters or copy attributes. */ function merge(o, p) { for(prop in p) { // For all props in p. if (o.hasOwnProperty[prop]) continue; // Except those already in o. o[prop] = p[prop]; // Add the property to o. } return o; } /* * Remove properties from o if there is not a property with the same name in p. * Return o. */ function restrict(o, p) { for(prop in o) { // For all props in o if (!(prop in p)) delete o[prop]; // Delete if not in p } return o; } /* * For each property of p, delete the property with the same name from o. * Return o. */ function subtract(o, p) {
2. The implementation of extend() shown here is correct but does not compensate for a well-known bug in Internet Explorer. We’ll see a more robust version of extend() in Example 8-3.
6.5 Enumerating Properties | 127
Core JavaScript
for(p in o) { if (typeof o[p] === "function") continue; // Skip methods }
for(prop in p) { delete o[prop];
}
// For all props in p // Delete from o (deleting a // nonexistent prop is harmless)
} return o; /* * Return a new object that holds the properties of both o and p. * If o and p have properties by the same name, the values from o are used. */ function union(o,p) { return extend(extend({},o), p); } /* * Return a new object that holds only the properties of o that also appear * in p. This is something like the intersection of o and p, but the values of * the properties in p are discarded */ function intersection(o,p) { return restrict(extend({}, o), p); } /* * Return an array that holds the names of the enumerable own properties of o. */ function keys(o) { if (typeof o !== "object") throw TypeError(); // Object argument required var result = []; // The array we will return for(var prop in o) { // For all enumerable properties if (o.hasOwnProperty(prop)) // If it is an own property result.push(prop); // add it to the array. } return result; // Return the array. }
In addition to the for/in loop, ECMAScript 5 defines two functions that enumerate property names. The first is Object.keys(), which returns an array of the names of the enumerable own properties of an object. It works just like the keys() utility function shown in Example 6-2. The second ECMAScript 5 property enumeration function is Object.getOwnProperty Names(). It works like Object.keys() but returns the names of all the own properties of the specified object, not just the enumerable properties. There is no way to write this function in ECMAScript 3, because ECMAScript 3 does not provide a way to obtain the nonenumerable properties of an object.
6.6 Property Getters and Setters We’ve said that an object property is a name, a value, and a set of attributes. In ECMAScript 53 the value may be replaced by one or two methods, known as a getter
3. And in recent ECMAScript 3 versions of major browsers other than IE.
128 | Chapter 6: Objects
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 right-hand 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. Accessor properties do not have a writable attribute as data properties do. If a property has both a getter and a setter method, it is a read/write property. If it has only a getter method, it is a read-only property. And if it has only a setter method, it is a write-only property (something that is not possible with data properties) and attempts to read it always evaluate to undefined. The easiest way to define accessor properties is with an extension to the object literal syntax: var o = { // An ordinary data property data_prop: value,
};
// An accessor property defined as a pair of functions get accessor_prop() { /* function body here */ }, set accessor_prop(value) { /* function body here */ }
Accessor properties are defined as one or two functions whose name is the same as the property name, and with the function keyword replaced with get and/or set. Note that no colon is used to separate the name of the property from the functions that access that property, but that a comma is still required after the function body to separate the method from the next method or data property. As an example, consider the following object that represents a 2D Cartesian point. It has ordinary data properties to represent the X and Y coordinates of the point, and it has accessor properties for the equivalent polar coordinates of the point: var p // x: y:
= { x and y are regular read-write data properties. 1.0, 1.0,
// r is a read-write accessor property with getter and setter. // Don't forget to put a comma after accessor methods. get r() { return Math.sqrt(this.x*this.x + this.y*this.y); }, set r(newvalue) { var oldvalue = Math.sqrt(this.x*this.x + this.y*this.y); var ratio = newvalue/oldvalue; this.x *= ratio; this.y *= ratio; },
6.6 Property Getters and Setters | 129
Core JavaScript
and a setter. Properties defined by getters and setters are sometimes known as accessor properties to distinguish them from data properties that have a simple value.
};
// theta is a read-only accessor property with getter only. get theta() { return Math.atan2(this.y, this.x); }
Note the use of the keyword this in the getters and setter above. JavaScript invokes these functions as methods of the object on which they are defined, which means that within the body of the function this refers to the point object. So the getter method for the r property can refer to the x and y properties as this.x and this.y. Methods and the this keyword are covered in more detail in §8.2.2. Accessor properties are inherited, just as data properties are, so you can use the object p defined above as a prototype for other points. You can give the new objects their own x and y properties, and they’ll inherit the r and theta properties: var q = inherit(p); // Create a new object that inherits getters and setters q.x = 0, q.y = 0; // Create q's own data properties console.log(q.r); // And use the inherited accessor properties console.log(q.theta);
The code above uses accessor properties to define an API that provides two representations (Cartesian coordinates and polar coordinates) of a single set of data. Other reasons to use accessor properties include sanity checking of property writes and returning different values on each property read: // This object generates strictly increasing serial numbers var serialnum = { // This data property holds the next serial number. // The $ in the property name hints that it is a private property. $n: 0, // Return the current value and increment it get next() { return this.$n++; },
};
// Set a new value of n, but only if it is larger than current set next(n) { if (n >= this.$n) this.$n = n; else throw "serial number can only be set to a larger value"; }
Finally, here is one more example that uses a getter method to implement a property with “magical” behavior. // This object has accessor properties that return random numbers. // The expression "random.octet", for example, yields a random number // between 0 and 255 each time it is evaluated. var random = { get octet() { return Math.floor(Math.random()*256); }, get uint16() { return Math.floor(Math.random()*65536); }, get int16() { return Math.floor(Math.random()*65536)-32768; } };
This section has shown only how to define accessor properties when creating a new object from an object literal. The next section shows how to add accessor properties to existing objects. 130 | Chapter 6: Objects
In addition to a name and value, properties have attributes that specify whether they can be written, enumerated, and configured. In ECMAScript 3, there is no way to set these attributes: all properties created by ECMAScript 3 programs are writable, enumerable, and configurable, and there is no way to change this. This section explains the ECMAScript 5 API for querying and setting property attributes. This API is particularly important to library authors because: • It allows them to add methods to prototype objects and make them nonenumerable, like built-in methods. • It allows them to “lock down” their objects, defining properties that cannot be changed or deleted. For the purposes of this section, we are going to consider getter and setter methods of an accessor property to be property attributes. Following this logic, we’ll even say that the value of a data property is an attribute as well. Thus, we can say that a property has a name and four attributes. The four attributes of a data property are value, writable, enumerable, and configurable. Accessor properties don’t have a value attribute or a writable attribute: their writability is determined by the presence or absence of a setter. So the four attributes of an accessor property are get, set, enumerable, and configurable. The ECMAScript 5 methods for querying and setting the attributes of a property use an object called a property descriptor to represent the set of four attributes. A property descriptor object has properties with the same names as the attributes of the property it describes. Thus, the property descriptor object of a data property has properties named value, writable, enumerable, and configurable. And the descriptor for an accessor property has get and set properties instead of value and writable. The writa ble, enumerable, and configurable properties are boolean values, and the get and set properties are function values, of course. To obtain the property descriptor for a named property of a specified object, call Object.getOwnPropertyDescriptor(): // Returns {value: 1, writable:true, enumerable:true, configurable:true} Object.getOwnPropertyDescriptor({x:1}, "x"); // Now query the octet property of the random object defined above. // Returns { get: /*func*/, set:undefined, enumerable:true, configurable:true} Object.getOwnPropertyDescriptor(random, "octet"); // Returns undefined for inherited properties and properties that don't exist. Object.getOwnPropertyDescriptor({}, "x"); // undefined, no such prop Object.getOwnPropertyDescriptor({}, "toString"); // undefined, inherited
As its name implies, Object.getOwnPropertyDescriptor() works only for own properties. To query the attributes of inherited properties, you must explicitly traverse the prototype chain (see Object.getPrototypeOf() in §6.8.1).
6.7 Property Attributes | 131
Core JavaScript
6.7 Property Attributes
To set the attributes of a property, or to create a new property with the specified attributes, call Object.defineProperty(), passing the object to be modified, the name of the property to be created or altered, and the property descriptor object: var o = {}; // Start with no properties at all // Add a nonenumerable data property x with value 1. Object.defineProperty(o, "x", { value : 1, writable: true, enumerable: false, configurable: true}); // Check that the property is there but is nonenumerable o.x; // => 1 Object.keys(o) // => [] // Now modify the property x so that it is read-only Object.defineProperty(o, "x", { writable: false }); // Try to change the value of the property o.x = 2; // Fails silently or throws TypeError in strict mode o.x // => 1 // The property is still configurable, so we can change its value like this: Object.defineProperty(o, "x", { value: 2 }); o.x // => 2 // Now change x from a data property to an accessor property Object.defineProperty(o, "x", { get: function() { return 0; } }); o.x // => 0
The property descriptor you pass to Object.defineProperty() does not have to include all four attributes. If you’re creating a new property, then omitted attributes are taken to be false or undefined. If you’re modifying an existing property, then the attributes you omit are simply left unchanged. Note that this method alters an existing own property or creates a new own property, but it will not alter an inherited property. If you want to create or modify more than one property at a time, use Object.define Properties(). The first argument is the object that is to be modified. The second argument is an object that maps the names of the properties to be created or modified to the property descriptors for those properties. For example: var p = Object.defineProperties({}, { x: { value: 1, writable: true, enumerable:true, configurable:true }, y: { value: 1, writable: true, enumerable:true, configurable:true }, r: { get: function() { return Math.sqrt(this.x*this.x + this.y*this.y) }, enumerable:true, configurable:true } });
This code starts with an empty object, then adds two data properties and one read-only accessor property to it. It relies on the fact that Object.defineProperties() returns the modified object (as does Object.defineProperty()).
132 | Chapter 6: Objects
Object.defineProperty() and Object.defineProperties() throw TypeError if the at-
tempt to create or modify a property is not allowed. This happens if you attempt to add a new property to a nonextensible (see §6.8.3) object. The other reasons that these methods might throw TypeError have to do with the attributes themselves. The writable attribute governs attempts to change the value attribute. And the configurable attribute governs attempts to change the other attributes (and also specifies whether a property can be deleted). The rules are not completely straightforward, however. It is possible to change the value of a nonwritable property if that property is configurable, for example. Also, it is possible to change a property from writable to nonwritable even if that property is nonconfigurable. Here are the complete rules. Calls to Object.defineProperty() or Object.defineProperties() that attempt to violate them throw TypeError: • If an object is not extensible, you can edit its existing own properties, but you cannot add new properties to it. • If a property is not configurable, you cannot change its configurable or enumerable attributes. • If an accessor property is not configurable, you cannot change its getter or setter method, and you cannot change it to a data property. • If a data property is not configurable, you cannot change it to an accessor property. • If a data property is not configurable, you cannot change its writable attribute from false to true, but you can change it from true to false. • If a data property is not configurable and not writable, you cannot change its value. You can change the value of a property that is configurable but nonwritable, however (because that would be the same as making it writable, then changing the value, then converting it back to nonwritable). Example 6-2 included an extend() function that copied properties from one object to another. That function simply copied the name and value of the properties and ignored their attributes. Furthermore, it did not copy the getter and setter methods of accessor properties, but simply converted them into static data properties. Example 6-3 shows a new version of extend() that uses Object.getOwnPropertyDescriptor() and Object.defineProperty() to copy all property attributes. Rather than being written as a function, this version is defined as a new Object method and is added as a nonenumerable property to Object.prototype.
6.7 Property Attributes | 133
Core JavaScript
We saw the ECMAScript 5 method Object.create() in §6.1. We learned there that the first argument to that method is the prototype object for the newly created object. This method also accepts a second optional argument, which is the same as the second argument to Object.defineProperties(). If you pass a set of property descriptors to Object.create(), then they are used to add properties to the newly created object.
Example 6-3. Copying property attributes /* * Add a nonenumerable extend() method to Object.prototype. * This method extends the object on which it is called by copying properties * from the object passed as its argument. All property attributes are * copied, not just the property value. All own properties (even non* enumerable ones) of the argument object are copied unless a property * with the same name already exists in the target object. */ Object.defineProperty(Object.prototype, "extend", // Define Object.prototype.extend { writable: true, enumerable: false, // Make it nonenumerable configurable: true, value: function(o) { // Its value is this function // Get all own props, even nonenumerable ones var names = Object.getOwnPropertyNames(o); // Loop through them for(var i = 0; i < names.length; i++) { // Skip props already in this object if (names[i] in this) continue; // Get property description from o var desc = Object.getOwnPropertyDescriptor(o,names[i]); // Use it to create property on this Object.defineProperty(this, names[i], desc); } } });
6.7.1 Legacy API for Getters and Setters The object literal syntax for accessor properties described in §6.6 allows us to define accessor properties in new objects, but it doesn’t allow us to query the getter and setter methods or to add new accessor properties to existing objects. In ECMAScript 5 we can use Object.getOwnPropertyDescriptor() and Object.defineProperty() to do these things. Most JavaScript implementations (with the major exception of the IE web browser) supported the object literal get and set syntax even before the adoption of ECMAScript 5. These implementations support a nonstandard legacy API for querying and setting getters and setters. This API consists of four methods available on all objects. __lookupGetter__() and __lookupSetter__() return the getter or setter method for a named property. And __defineGetter__() and __defineSetter__() define a getter or setter: pass the property name first and the getter or setter method second. The names of each of these methods begin and end with double underscores to indicate that they are nonstandard methods. These nonstandard methods are not documented in the reference section.
134 | Chapter 6: Objects
Every object has associated prototype, class, and extensible attributes. The subsections that follow explain what these attributes do and (where possible) how to query and set them.
6.8.1 The prototype Attribute An object’s prototype attribute specifies the object from which it inherits properties. (Review §6.1.3 and §6.2.2 for more on prototypes and property inheritance.) This is such an important attribute that we’ll usually simply say “the prototype of o” rather than “the prototype attribute of o.” Also, it is important to understand that when prototype appears in code font, it refers to an ordinary object property, not to the prototype attribute. The prototype attribute is set when an object is created. Recall from §6.1.3 that objects created from object literals use Object.prototype as their prototype. Objects created with new use the value of the prototype property of their constructor function as their prototype. And objects created with Object.create() use the first argument to that function (which may be null) as their prototype. In ECMAScript 5, you can query the prototype of any object by passing that object to Object.getPrototypeOf(). There is no equivalent function in ECMAScript 3, but it is often possible to determine the prototype of an object o using the expression o.constructor.prototype. Objects created with a new expression usually inherit a constructor property that refers to the constructor function used to create the object. And, as described above, constructor functions have a prototype property that specifies the prototype for objects created using that constructor. This is explained in more detail in §9.2, which also explains why it is not a completely reliable method for determining an object’s prototype. Note that objects created by object literals or by Object.create() have a constructor property that refers to the Object() constructor. Thus, constructor.prototype refers to the correct prototype for object literals, but does not usually do so for objects created with Object.create(). To determine whether one object is the prototype of (or is part of the prototype chain of) another object, use the isPrototypeOf() method. To find out if p is the prototype of o write p.isPrototypeOf(o). For example: var p = {x:1}; var o = Object.create(p); p.isPrototypeOf(o) Object.prototype.isPrototypeOf(o)
// // // //
Define a prototype object. Create an object with that prototype. => true: o inherits from p => true: p inherits from Object.prototype
Note that isPrototypeOf() performs a function similar to the instanceof operator (see §4.9.4). Mozilla’s implementation of JavaScript has (since the early days of Netscape) exposed the prototype attribute through the specially named __proto__ property, and you can use this property to directly query or set the prototype of any object. Using __proto__ 6.8 Object Attributes | 135
Core JavaScript
6.8 Object Attributes
is not portable: it has not been (and probably never will be) implemented by IE or Opera, although it is currently supported by Safari and Chrome. Versions of Firefox that implement ECMAScript 5 still support __proto__, but restrict its ability to change the prototype of nonextensible objects.
6.8.2 The class Attribute An object’s class attribute is a string that provides information about the type of the object. Neither ECMAScript 3 nor ECMAScript 5 provide any way to set this attribute, and there is only an indirect technique for querying it. The default toString() method (inherited from Object.prototype) returns a string of the form: [object class]
So to obtain the class of an object, you can invoke this toString() method on it, and extract the eighth through the second-to-last characters of the returned string. The tricky part is that many objects inherit other, more useful toString() methods, and to invoke the correct version of toString(), we must do so indirectly, using the Function.call() method (see §8.7.3). Example 6-4 defines a function that returns the class of any object you pass it. Example 6-4. A classof() function function classof(o) { if (o === null) return "Null"; if (o === undefined) return "Undefined"; return Object.prototype.toString.call(o).slice(8,-1); }
This classof() function works for any JavaScript value. Numbers, strings, and booleans behave like objects when the toString() method is invoked on them, and the function includes special cases for null and undefined. (The special cases are not required in ECMAScript 5.) Objects created through built-in constructors such as Array and Date have class attributes that match the names of their constructors. Host objects typically have meaningful class attributes as well, though this is implementation-dependent. Objects created through object literals or by Object.create have a class attribute of “Object”. If you define your own constructor function, any objects you create with it will have a class attribute of “Object”: there is no way to specify the class attribute for your own classes of objects: classof(null) classof(1) classof("") classof(false) classof({}) classof([]) classof(/./) classof(new Date()) classof(window) function f() {}; classof(new f());
136 | Chapter 6: Objects
// // // // // // // // // // //
=> "Null" => "Number" => "String" => "Boolean" => "Object" => "Array" => "Regexp" => "Date" => "Window" (a client-side host object) Define a custom constructor => "Object"
The extensible attribute of an object specifies whether new properties can be added to the object or not. In ECMAScript 3, all built-in and user-defined objects are implicitly extensible, and the extensibility of host objects is implementation defined. In ECMAScript 5, all built-in and user-defined objects are extensible unless they have been converted to be nonextensible, and the extensibility of host objects is again implementation defined. ECMAScript 5 defines functions for querying and setting the extensibility of an object. To determine whether an object is extensible, pass it to Object.isExtensible(). To make an object nonextensible, pass it to Object.preventExtensions(). Note that there is no way to make an object extensible again once you have made it nonextensible. Also note that calling preventExtensions() only affects the extensibility of the object itself. If new properties are added to the prototype of a nonextensible object, the nonextensible object will inherit those new properties. The purpose of the extensible attribute is to be able to “lock down” objects into a known state and prevent outside tampering. The extensible object attribute is often used in conjunction with the configurable and writable property attributes, and ECMAScript 5 defines functions that make it easy to set these attributes together. Object.seal() works like Object.preventExtensions(), but in addition to making the
object nonextensible, it also makes all of the own properties of that object nonconfigurable. This means that new properties cannot be added to the object, and existing properties cannot be deleted or configured. Existing properties that are writable can still be set, however. There is no way to unseal a sealed object. You can use Object.isSealed() to determine whether an object is sealed. Object.freeze() locks objects down even more tightly. In addition to making the object
nonextensible and its properties nonconfigurable, it also makes all of the object’s own data properties read-only. (If the object has accessor properties with setter methods, these are not affected and can still be invoked by assignment to the property.) Use Object.isFrozen() to determine if an object is frozen. It is important to understand that Object.seal() and Object.freeze() affect only the object they are passed: they have no effect on the prototype of that object. If you want to thoroughly lock down an object, you probably need to seal or freeze the objects in the prototype chain as well. Object.preventExtensions(), Object.seal(), and Object.freeze() all return the object
that they are passed, which means that you can use them in nested function invocations: // Create a sealed object with a frozen prototype and a nonenumerable property var o = Object.seal(Object.create(Object.freeze({x:1}), {y: {value: 2, writable: true}}));
6.8 Object Attributes | 137
Core JavaScript
6.8.3 The extensible Attribute
6.9 Serializing Objects Object serialization is the process of converting an object’s state to a string from which it can later be restored. ECMAScript 5 provides native functions JSON.stringify() and JSON.parse() to 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: o = {x:1, y:{z:[false,null,""]}}; // Define a test object s = JSON.stringify(o); // s is '{"x":1,"y":{"z":[false,null,""]}}' p = JSON.parse(s); // p is a deep copy of o
The native implementation of these functions in ECMAScript 5 was modeled very closely after the public-domain ECMAScript 3 implementation available at http://json .org/json2.js. For practical purposes, the implementations are the same, and you can use these ECMAScript 5 functions in ECMAScript 3 with this json2.js module. 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 the reference section.
6.10 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(), propertyIsEnumerable(), and isPrototypeOf() methods. (And we’ve also already covered quite a few static functions defined on the Object constructor, such as Object.create() and Object.getPrototypeOf().) This section explains a handful of universal object methods that are defined on Object.prototype, but which are intended to be overridden by other, more specialized classes.
138 | Chapter 6: Objects
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 saw in §6.8.2). For example, the following line of code simply evaluates to the string “[object Object]”: var s = { x:1, y:1 }.toString();
Because this default method does not display much useful information, many classes define their own versions of toString(). For example, when an array is converted to a string, you obtain a list of the array elements, themselves each converted to a string, and when a function is converted to a string, you obtain the source code for the function. These customized versions of the toString() method are documented in the reference section. See Array.toString(), Date.toString(), and Function.toString(), for example. §9.6.3 describes how to define a custom toString() method for your own classes.
6.10.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 toLocale String() methods instead of their toString() methods.
6.10.3 The toJSON() Method Object.prototype does not actually define a toJSON() method, but the JSON.stringify() method (see §6.9) 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. See Date.toJSON() for an example.
6.10 Object Methods | 139
Core JavaScript
6.10.1 The toString() Method
6.10.4 The valueOf() Method
Download from Wow! eBook
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 (see Date.valueOf(), for example). §9.6.3 explains how to define a valueOf() method for custom object types you define.
140 | Chapter 6: Objects
CHAPTER 7
Arrays
An array is an ordered collection of values. Each value is called an element, and each element has a numeric position in the array, known as its index. JavaScript arrays are untyped: an array element may be of any type, and different elements of the same array may be of different types. Array elements may even be objects or other arrays, which allows you to create complex data structures, such as arrays of objects and arrays of arrays. JavaScript arrays are zero-based and use 32-bit indexes: the index of the first element is 0, and the highest possible index is 4294967294 (232−2), for a maximum array size of 4,294,967,295 elements. JavaScript arrays are dynamic: they grow or shrink as needed and there is no need to declare a fixed size for the array when you create it or to reallocate it when the size changes. JavaScript arrays may be sparse: the elements need not have contiguous indexes and there may be gaps. Every JavaScript array has a length property. For nonsparse arrays, this property specifies the number of elements in the array. For sparse arrays, length is larger than the index of all elements. JavaScript arrays are a specialized form of JavaScript object, and array indexes are really little more than property names that happen to be integers. We’ll talk more about the specializations of arrays elsewhere in this chapter. Implementations typically optimize arrays so that access to numerically indexed array elements is generally significantly faster than access to regular object properties. Arrays inherit properties from Array.prototype, which defines a rich set of array manipulation methods, covered in §7.8 and §7.9. Most of these methods are generic, which means that they work correctly not only for true arrays, but for any “array-like object.” We’ll discuss array-like objects in §7.11. In ECMAScript 5, strings behave like arrays of characters, and we’ll discuss this in §7.12.
7.1 Creating Arrays The easiest way to create an array is with an array literal, which is simply a commaseparated list of array elements within square brackets. For example:
141
var empty = []; // An array with no elements var primes = [2, 3, 5, 7, 11]; // An array with 5 numeric elements var misc = [ 1.1, true, "a", ]; // 3 elements of various types + trailing comma
The values in an array literal need not be constants; they may be arbitrary expressions: var base = 1024; var table = [base, base+1, base+2, base+3];
Array literals can contain object literals or other array literals: var b = [[1,{x:1, y:2}], [2, {x:3, y:4}]];
If you omit a value from an array literal, the omitted element is given the value undefined: var count = [1,,3]; // An array with 3 elements, the middle one undefined. var undefs = [,,]; // An array with 2 elements, both undefined.
Array literal syntax allows an optional trailing comma, so [,,] has only two elements, not three. 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: var a = new Array();
This method creates an empty array with no elements and is equivalent to the array literal []. • Call it with a single numeric argument, which specifies a length: var a = new Array(10);
This technique creates an array with the specified length. This form of the Array() constructor can be used to preallocate an array when you know in advance how many elements will be required. Note that no values are stored in the array, and the array index properties “0”, “1”, and so on are not even defined for the array. • Explicitly specify two or more array elements or a single non-numeric element for the array: var 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.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
142 | Chapter 7: Arrays
var a = ["world"]; var value = a[0]; a[1] = 3.14; i = 2; a[i] = 3; a[i + 1] = "hello"; a[a[i]] = a[0];
// Start with a one-element array // Read element 0 // Write element 1 // Write element 2 // Write element 3 // Read elements 0 and 2, write element 3
Remember that arrays are a specialized kind of object. The square brackets used to access array elements work just like the square brackets used to access object properties. JavaScript converts the numeric array index you specify to a string—the index 1 becomes the string "1"—then uses that string as a property name. There is nothing special about the conversion of the index from a number to a string: you can do that with regular objects, too: o = {}; o[1] = "one";
// Create a plain object // Index it with an integer
What is special about arrays is that when you use property names that are non-negative integers less than 232, the array automatically maintains the value of the length property for you. Above, for example, we created an array a with a single element. We then assigned values at indexes 1, 2, and 3. The length property of the array changed as we did so: a.length
// => 4
It is helpful to clearly distinguish an array index from an object property name. All indexes are property names, but only property names that are integers between 0 and 232–1 are indexes. All arrays are objects, and you can create properties of any name on them. If you use properties that are array indexes, however, arrays have the special behavior of updating their length property as needed. Note that you can index an array using numbers that are negative or that are not integers. When you do this, the number is converted to a string, and that string is used as the property name. Since the name is not a non-negative integer, it is treated as a regular object property, not an array index. Also, if you index an array with a string that happens to be a non-negative integer, it behaves as an array index, not an object property. The same is true if you use a floating-point number that is the same as an integer: a[-1.23] = true; a["1000"] = 0; a[1.000]
// This creates a property named "-1.23" // This the 1001st element of the array // Array index 1. Same as a[1]
The fact that array indexes are simply a special type of object property name means that JavaScript arrays have no notion of an “out of bounds” error. When you try to query a nonexistent property of any object, you don’t get an error, you simply get undefined. This is just as true for arrays as it is for objects:
7.2 Reading and Writing Array Elements | 143
Core JavaScript
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:
a = [true, false]; a[2] a[-1]
// This array has elements at indexes 0 and 1 // => undefined. No element at this index. // => undefined. No property with this name.
Since arrays are objects, they can inherit elements from their prototype. In ECMAScript 5, they can even have array elements defined by getter and setter methods (§6.6). If an array does inherit elements or use getters and setters for elements, you should expect it to use a nonoptimized code path: the time to access an element of such an array would be similar to regular object property lookup times.
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 = new Array(5); a = []; a[1000] = 0;
// No elements, but a.length is 5. // Create an array with no elements and length = 0. // Assignment adds one element but sets length to 1001.
We’ll see later that you can also make an array sparse with the delete operator. Arrays that are sufficiently sparse are typically implemented in a slower, more memoryefficient way than dense arrays are, and looking up elements in such an array will take about as much time as regular object property lookup. Note that when you omit value in an array literal, you are not creating a sparse array. The omitted element exists in the array and has the value undefined. This is subtly different than array elements that do not exist at all. You can detect the difference between these two cases with the in operator: var a1 = [,,,]; var a2 = new Array(3); 0 in a1 0 in a2
// // // //
This array is [undefined, undefined, undefined] This array has no values at all => true: a1 has an element with index 0 => false: a2 has no element with index 0
The difference between a1 and a2 is also apparent when you use a for/in loop. See §7.6. 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 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
144 | Chapter 7: Arrays
[].length ['a','b','c'].length
// => 0: the array has no elements // => 3: highest index is 2, length is 3
When an array is sparse, the length property is greater than the number of elements, and all we can say about it is that length is guaranteed to be larger than the index of every element in the array. Or, put another way, an array (sparse or not) will never have an element whose index is greater than or equal to its length. In order to maintain this invariant, arrays have two special behaviors. The first was described above: if you assign a value to an array element whose index i is greater than or equal to the array’s current length, the value of the length property is set to i+1. The second special behavior that arrays implement in order to maintain the length invariant is that if you set the length property to a non-negative integer n smaller than its current value, any array elements whose index is greater than or equal to n are deleted from the array: a = [1,2,3,4,5]; a.length = 3; a.length = 0; a.length = 5;
// // // //
Start with a 5-element array. a is now [1,2,3]. Delete all elements. a is []. 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. In ECMAScript 5, you can make the length property of an array read-only with Object.defineProperty() (see §6.7): a = [1,2,3]; Object.defineProperty(a, "length", {writable: false}); a.length = 0;
// // // //
Start with a 3-element array. Make the length property readonly. a is unchanged.
Similarly, if you make an array element nonconfigurable, it cannot be deleted. If it cannot be deleted, then the length property cannot be set to less than the index of the nonconfigurable element. (See §6.7 and the Object.seal() and Object.freeze() methods in §6.8.3.)
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: a = [] a[0] = "zero"; a[1] = "one";
// Start with an empty array. // And add elements to it.
You can also use the push() method to add one or more values to the end of an array:
7.5 Adding and Deleting Array Elements | 145
Core JavaScript
property specifies the number of elements in the array. Its value is one more than the highest index in the array:
a = []; // Start with an empty array a.push("zero") // Add a value at the end. a = ["zero"] a.push("one", "two") // Add two more values. a = ["zero", "one", "two"]
Pushing a value onto an array a is the same as assigning the value to a[a.length]. You can use the unshift() method (described in §7.8) to insert a value at the beginning of an array, shifting the existing array elements to higher indexes. You can delete array elements with the delete operator, just as you can delete object properties: a = [1,2,3]; delete a[1]; 1 in a a.length
// a now has no element at index 1 // => false: no array index 1 is defined // => 3: delete does not affect array length
Deleting an array element is similar to (but subtly different than) assigning undefined to that element. Note that using delete on an array element does not alter the length property and does not shift elements with higher indexes down to fill in the gap that is left by the deleted property. If you delete an element from an array, the array becomes sparse. As we saw above, you can also delete elements from the end of an array simply by setting the length property to the new desired length. Arrays have a pop() method (it works with push()) that reduces the length of an array by 1 but also returns the value of the deleted element. There is also a shift() method (which goes with unshift()) to remove an element from the beginning of an array. Unlike delete, the shift() method shifts all elements down to an index one lower than their current index. pop() and shift() are covered in §7.8 and in the reference section. 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 The most common way to loop through the elements of an array is with a for loop (§5.5.3): var keys = Object.keys(o); // Get an array of property names for object o var values = [] // Store matching property values in this array for(var i = 0; i < keys.length; i++) { // For each index in the array var key = keys[i]; // Get the key at that index values[i] = o[key]; // Store the value in the values array }
In nested loops, or other contexts where performance is critical, you may sometimes see this basic array iteration loop optimized so that the array length is only looked up once rather than on each iteration:
146 | Chapter 7: Arrays
These examples assume that the array is dense and that all elements contain valid data. If this is not the case, you should test the array elements before using them. If you want to exclude null, undefined, and nonexistent elements, you can write this: for(var i = 0; i < a.length; i++) { if (!a[i]) continue; // Skip null, undefined, and nonexistent elements // loop body here }
If you only want to skip undefined and nonexistent elements, you might write: for(var i = 0; i < a.length; i++) { if (a[i] === undefined) continue; // Skip undefined + nonexistent elements // loop body here }
Finally, if you only want to skip indexes for which no array element exists but still want to handle existing undefined elements, do this: for(var i = 0; i < a.length; i++) { if (!(i in a)) continue ; // Skip nonexistent elements // loop body here }
You can also use a for/in loop (§5.5.4) with sparse arrays. This loop assigns enumerable property names (including array indexes) to the loop variable one at a time. Indexes that do not exist will not be iterated: for(var index in sparseArray) { var value = sparseArray[index]; // Now do something with index and value }
As noted in §6.5, a for/in loop can return the names of inherited properties, such as the names of methods that have been added to Array.prototype. For this reason you should not use a for/in loop on an array unless you include an additional test to filter out unwanted properties. You might use either of these tests: for(var i in a) { if (!a.hasOwnProperty(i)) continue; // loop body here }
// Skip inherited properties
for(var i in a) { // Skip i if it is not a non-negative integer if (String(Math.floor(Math.abs(Number(i)))) !== i) continue; }
The ECMAScript specification allows the for/in loop to iterate the properties of an object in any order. Implementations typically iterate array elements in ascending order, but this is not guaranteed. In particular, if an array has both object properties and array elements, the property names may be returned in the order they were created, 7.6 Iterating Arrays | 147
Core JavaScript
for(var i = 0, len = keys.length; i < len; i++) { // loop body remains the same }
rather than in numeric order. Implementations differ in how they handle this case, so if iteration order matters for your algorithm, it is best to use a regular for loop instead of for/in. ECMAScript 5 defines a number of new methods for iterating array elements by passing each one, in index order, to a function that you define. The forEach() method is the most general of these methods: var data = [1,2,3,4,5]; // This is the array we want to iterate var sumOfSquares = 0; // We want to compute the sum of the squares of data data.forEach(function(x) { // Pass each element of data to this function sumOfSquares += x*x; // add up the squares }); sumOfSquares // =>55 : 1+4+9+16+25
forEach() and related iteration methods enable a simple and powerful functional pro-
gramming style for working with arrays. They are covered in §7.9, and we’ll return to them in §8.8, when we cover functional programming.
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 var table = new Array(10); for(var i = 0; i < table.length; i++) table[i] = new Array(10);
// 10 rows of the table // Each row has 10 columns
// Initialize the array for(var row = 0; row < table.length; row++) { for(col = 0; col < table[row].length; col++) { table[row][col] = row*col; } } // Use the multidimensional array to compute 5*7 var product = table[5][7]; // 35
7.8 Array Methods ECMAScript 3 defines a number of useful array manipulation functions on Array.prototype, which means that they are available as methods of any array. These ECMAScript 3 methods are introduced in the subsections below. As usual, complete details can be found under Array in the client-side reference section. ECMAScript 5 adds new array iteration methods; those methods are covered in §7.9. 148 | Chapter 7: Arrays
The Array.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. For example, the following lines of code produce the string “1,2,3”: var a = [1, 2, 3]; a.join(); a.join(" "); a.join(""); var b = new Array(10); b.join('-')
// // // // // //
Create a new array with these three elements => "1,2,3" => "1 2 3" => "123" An array of length 10 with no elements => '---------': a string of 9 hyphens
The Array.join() method is the inverse of the String.split() method, which creates an array by breaking a string into pieces.
7.8.2 reverse() The Array.reverse() method reverses the order of the elements of an array and returns the reversed array. It does this in place; in other words, it doesn’t create a new array with the elements rearranged but instead rearranges them in the already existing array. For example, the following code, which uses the reverse() and join() methods, produces the string “3,2,1”: var a = [1,2,3]; a.reverse().join()
// => "3,2,1" and a is now [3,2,1]
7.8.3 sort() Array.sort() sorts the elements of an array in place and returns the sorted array. When sort() is called with no arguments, it sorts the array elements in alphabetical order
(temporarily converting them to strings to perform the comparison, if necessary): var a = new Array("banana", "cherry", "apple"); a.sort(); var s = a.join(", "); // s == "apple, banana, cherry"
If an array contains undefined elements, they are sorted to the end of the array. To sort an array into some order other than alphabetical, you must pass a comparison function as an argument to sort(). This function decides which of its two arguments should appear first in the sorted array. If the first argument should appear before the second, the comparison function should return a number less than zero. If the first argument should appear after the second in the sorted array, the function should return a number greater than zero. And if the two values are equivalent (i.e., if their order is irrelevant), the comparison function should return 0. So, for example, to sort array elements into numerical rather than alphabetical order, you might do this: var a = [33, 4, 1111, 222]; a.sort(); // Alphabetical order: 1111, 222, 33, 4 a.sort(function(a,b) { // Numerical order: 4, 33, 222, 1111
7.8 Array Methods | 149
Core JavaScript
7.8.1 join()
return a-b; // Returns < 0, 0, or > 0, depending on order }); a.sort(function(a,b) {return b-a}); // Reverse numerical order
Note the convenient use of unnamed function expressions in this code. Since the comparison functions are used only once, there is no need to give them names. As another example of sorting array items, you might perform a case-insensitive alphabetical sort on an array of strings by passing a comparison function that converts both of its arguments to lowercase (with the toLowerCase() method) before comparing them: a = ['ant', 'Bug', 'cat', 'Dog'] a.sort(); // case-sensitive sort: ['Bug','Dog','ant',cat'] a.sort(function(s,t) { // Case-insensitive sort var a = s.toLowerCase(); var b = t.toLowerCase(); if (a < b) return -1; if (a > b) return 1; return 0; }); // => ['ant','Bug','cat','Dog']
7.8.4 concat() The Array.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. Here are some examples: var a = [1,2,3]; a.concat(4, 5) a.concat([4,5]); a.concat([4,5],[6,7]) a.concat(4, [5,[6,7]])
// // // //
Returns Returns Returns Returns
[1,2,3,4,5] [1,2,3,4,5] [1,2,3,4,5,6,7] [1,2,3,4,5,[6,7]]
7.8.5 slice() The Array.slice() method returns a slice, or subarray, of the specified array. Its two arguments specify the start and end of the slice to be returned. The returned array contains the element specified by the first argument and all subsequent elements up to, but not including, the element specified by the second argument. If only one argument is specified, the returned array contains all elements from the start position to the end of the array. If either argument is negative, it specifies an array element relative to the last element in the array. An argument of -1, for example, specifies the last element in the array, and an argument of -3 specifies the third from last element of the array. Note that slice() does not modify the array on which it is invoked. Here are some examples: var a = [1,2,3,4,5]; a.slice(0,3); // Returns [1,2,3]
150 | Chapter 7: Arrays
// Returns [4,5] // Returns [2,3,4] // Returns [3]
Core JavaScript
a.slice(3); a.slice(1,-1); a.slice(-3,-2);
7.8.6 splice() The Array.splice() method is a general-purpose method for inserting or removing elements from an array. Unlike slice() and concat(), splice() modifies the array on which it is invoked. Note that splice() and slice() have very similar names but perform substantially different operations. splice() can delete elements from an array, insert new elements into an array, or per-
form both operations at the same time. Elements of the array that come after the insertion or deletion point have their indexes increased or decreased as necessary so that they remain contiguous with the rest of the array. The first argument to splice() specifies the array position at which the insertion and/or deletion is to begin. The second argument specifies the number of elements that should be deleted from (spliced out of) the array. If this second argument is omitted, all array elements from the start element to the end of the array are removed. splice() returns an array of the deleted elements, or an empty array if no elements were deleted. For example: var a = [1,2,3,4,5,6,7,8]; a.splice(4); // Returns [5,6,7,8]; a is [1,2,3,4] a.splice(1,2); // Returns [2,3]; a is [1,4] a.splice(1,1); // Returns [4]; a is [1]
The first two arguments to splice() specify which array elements are to be deleted. These arguments may be followed by any number of additional arguments that specify elements to be inserted into the array, starting at the position specified by the first argument. For example: var a = [1,2,3,4,5]; a.splice(2,0,'a','b'); a.splice(2,2,[1,2],3);
// Returns []; a is [1,2,'a','b',3,4,5] // Returns ['a','b']; a is [1,2,[1,2],3,3,4,5]
Note that, unlike concat(), splice() inserts arrays themselves, not the elements of those arrays.
7.8.7 push() and pop() 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. 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 rather than produce a modified copy of the array. The combination of push() and pop() allows you to use a JavaScript array to implement a first-in, last-out stack. For example: var stack = []; stack.push(1,2); stack.pop();
// stack: [] // stack: [1,2] // stack: [1]
Returns 2 Returns 2
7.8 Array Methods | 151
stack.push(3); stack.pop(); stack.push([4,5]); stack.pop() stack.pop();
// // // // //
stack: stack: stack: stack: stack:
[1,3] [1] [1,[4,5]] [1] []
Returns Returns Returns Returns Returns
2 3 2 [4,5] 1
7.8.8 unshift() and shift() The unshift() and shift() methods behave much like push() and pop(), except that they insert and remove elements from the beginning of an array rather than from the end. unshift() adds an element or elements to the beginning of the array, shifts the existing array elements up to higher indexes to make room, and returns the new length of the array. shift() removes and returns the first element of the array, shifting all subsequent elements down one place to occupy the newly vacant space at the start of the array. For example: var a = []; a.unshift(1); a.unshift(22); a.shift(); a.unshift(3,[4,5]); a.shift(); a.shift(); a.shift();
// // // // // // // //
a:[] a:[1] a:[22,1] a:[1] a:[3,[4,5],1] a:[[4,5],1] a:[1] a:[]
Returns: Returns: Returns: Returns: Returns: Returns: Returns:
1 2 22 3 3 [4,5] 1
Note the possibly surprising behavior of unshift() when it’s invoked with multiple arguments. Instead of being inserted into the array one at a time, arguments are inserted all at once (as with the splice() method). This means that they appear in the resulting array in the same order in which they appeared in the argument list. Had the elements been inserted one at a time, their order would have been reversed.
7.8.9 toString() and toLocaleString() An array, like any JavaScript object, has a toString() method. For an array, this method converts each of its elements to a string (calling the toString() methods of its elements, if necessary) and outputs a comma-separated list of those strings. Note that the output does not include square brackets or any other sort of delimiter around the array value. For example: [1,2,3].toString() ["a", "b", "c"].toString() [1, [2,'c']].toString()
// Yields '1,2,3' // Yields 'a,b,c' // Yields '1,2,c'
Note that the join() method returns the same string when it is invoked with no arguments. 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 concat-
enates the resulting strings using a locale-specific (and implementation-defined) separator string.
152 | Chapter 7: Arrays
ECMAScript 5 defines nine new array methods for iterating, mapping, filtering, testing, reducing, and searching arrays. The subsections below describe these methods. Before we cover the details, however, it is worth making some generalizations about these ECMAScript 5 array methods. First, most of the 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. Most of the ECMAScript 5 array methods that accept a function as their first argument accept an optional second argument. If specified, the function is invoked as if it is a method of this second argument. That is, the second argument you pass becomes the value of the this keyword inside of the function you pass. The return value of the function you pass is important, but different methods handle the return value in different ways. None of the ECMAScript 5 array methods modify the array on which they are invoked. If you pass a function to these methods, that function may modify the array, of course.
7.9.1 forEach() The forEach() method iterates through an array, invoking a function you specify for each element. As described above, you pass the function as the first argument to forEach(). forEach() then invokes your function with three arguments: the value of the array element, the index of the array element, and the array itself. If you only care about the value of the array element, you can write a function with only one parameter—the additional arguments will be ignored: var data = [1,2,3,4,5]; // Compute the sum of the array elements var sum = 0; data.forEach(function(value) { sum += value; }); sum
// An array to sum // Start at 0 // Add each value to sum // => 15
// Now increment each array element data.forEach(function(v, i, a) { a[i] = v + 1; }); data // => [2,3,4,5,6]
Note that forEach() does not provide a way to terminate iteration before all elements have been passed to the function. That is, there is no equivalent of the break statement you can use with a regular for loop. If you need to terminate early, you must throw an exception, and place the call to forEach() within a try block. The following code defines a foreach() function that calls the forEach() method within such a try block. If the function passed to foreach() throws foreach.break, the loop will terminate early: function foreach(a,f,t) { try { a.forEach(f,t); }
7.9 ECMAScript 5 Array Methods | 153
Core JavaScript
7.9 ECMAScript 5 Array Methods
catch(e) { if (e === foreach.break) return; else throw e; }
} foreach.break = new Error("StopIteration");
7.9.2 map() The map() method passes each element of the array on which it is invoked to the function you specify, and returns an array containing the values returned by that function. For example: a = [1, 2, 3]; b = a.map(function(x) { return x*x; });
// b is [1, 4, 9]
The function you pass to map() is invoked in the same way as a function passed to forEach(). For the map() method, however, the function you pass should return a value. Note that map() returns a new array: it does not modify the array it is invoked on. If that array is sparse, the returned array will be sparse in the same way: it will have the same length and the same missing elements.
7.9.3 filter() The filter() method returns an array containing a subset of the elements of the array on which it is invoked. The function you pass to it should be predicate: a function that returns true or false. The predicate is invoked just as for forEach() and map(). If the return value is true, or a value that converts to true, then the element passed to the predicate is a member of the subset and is added to the array that will become the return value. Examples: a = [5, 4, 3, 2, 1]; smallvalues = a.filter(function(x) { return x < 3 }); // [2, 1] everyother = a.filter(function(x,i) { return i%2==0 }); // [5, 3, 1]
Note that filter() skips missing elements in sparse arrays, and that its return value is always dense. To close the gaps in a sparse array, you can do this: var dense = sparse.filter(function() { return true; });
And to close gaps and remove undefined and null elements you can use filter like this: a = a.filter(function(x) { return x !== undefined && x != null; });
7.9.4 every() and some() The every() and some() methods are array predicates: they apply a predicate function you specify to the elements of the array, and then return true or false. The every() method is like the mathematical “for all” quantifier ∀: it returns true if and only if your predicate function returns true for all elements in the array:
154 | Chapter 7: Arrays
The some() method is like the mathematical “there exists” quantifier ∃: it returns true if there exists at least one element in the array for which the predicate returns true, and returns false if and only if the predicate returns false for all elements of the array: a = [1,2,3,4,5]; a.some(function(x) { return x%2===0; }) a.some(isNaN)
// => true a has some even numbers. // => false: a has no non-numbers.
Note that both every() and some() stop iterating array elements as soon as they know what value to return. some() returns true the first time your predicate returns true, and only iterates through the entire array if your predicate always returns false. every() is the opposite: it returns false the first time your predicate returns false, and only iterates all elements if your predicate always returns true. Note also that by mathematical convention, every() returns true and some returns false when invoked on an empty array.
7.9.5 reduce(), reduceRight() The reduce() and reduceRight() methods combine the elements of an array, using the function you specify, to produce a single value. This is a common operation in functional programming and also goes by the names “inject” and “fold.” Examples help illustrate how it works: var var var var
a = [1,2,3,4,5] sum = a.reduce(function(x,y) { return x+y }, 0); // Sum of values product = a.reduce(function(x,y) { return x*y }, 1); // Product of values max = a.reduce(function(x,y) { return (x>y)?x:y; }); // Largest value
reduce() takes two arguments. The first is the function that performs the reduction
operation. The task of this reduction function is to somehow combine or reduce two values into a single value, and to return that reduced value. In the examples above, the functions combine two values by adding them, multiplying them, and choosing the largest. The second (optional) argument is an initial value to pass to the function. Functions used with reduce() are different than the functions used with forEach() and map(). The familiar value, index, and array values are passed as the second, third, and fourth arguments. The first argument is the accumulated result of the reduction so far. On the first call to the function, this first argument is the initial value you passed as the second argument to reduce(). On subsequent calls, it is the value returned by the previous invocation of the function. In the first example above, the reduction function is first called with arguments 0 and 1. It adds these and returns 1. It is then called again with arguments 1 and 2 and it returns 3. Next it computes 3+3=6, then 6+4=10, and finally 10+5=15. This final value, 15, becomes the return value of reduce().
7.9 ECMAScript 5 Array Methods | 155
Core JavaScript
a = [1,2,3,4,5]; a.every(function(x) { return x < 10; }) // => true: all values < 10. a.every(function(x) { return x % 2 === 0; }) // => false: not all values even.
You may have noticed that the third call to reduce() above has only a single argument: there is no initial value specified. When you invoke reduce() like this with no initial value, it uses the first element of the array as the initial value. This means that the first call to the reduction function will have the first and second array elements as its first and second arguments. In the sum and product examples above, we could have omitted the initial value argument. Calling reduce() on an empty array with no initial value argument causes a TypeError. If you call it with only one value—either an array with one element and no initial value or an empty array and an initial value—it simply returns that one value without ever calling the reduction function. reduceRight() works just like reduce(), except that it processes the array from highest
index to lowest (right-to-left), rather than from lowest to highest. You might want to do this if the reduction operation has right-to-left precedence, for example: var a = [2, 3, 4] // Compute 2^(3^4). Exponentiation has right-to-left precedence var big = a.reduceRight(function(accumulator,value) { return Math.pow(value,accumulator); });
Note that neither reduce() nor reduceRight() accepts an optional argument that specifies the this value on which the reduction function is to be invoked. The optional initial value argument takes its place. See the Function.bind() method if you need your reduction function invoked as a method of a particular object. It is worth noting that the every() and some() methods described above perform a kind of array reduction operation. They differ from reduce(), however, in that they terminate early when possible, and do not always visit every array element. The examples shown so far have been numeric for simplicity, but reduce() and reduce Right() are not intended solely for mathematical computations. Consider the union() function from Example 6-2. It computes the “union” of two objects and returns a new object that has the properties of both. This function expects two objects and returns another object, so it works as a reduction function, and we can use reduce() to generalize it and compute the union of any number of objects: var objects = [{x:1}, {y:2}, {z:3}]; var merged = objects.reduce(union);
// => {x:1, y:2, z:3}
Recall that when two objects have properties with the same name, the union() function uses the value of that property from the first argument. Thus reduce() and reduce Right() may give different results when used with union(): var objects = [{x:1,a:1}, {y:2,a:2}, {z:3,a:3}]; var leftunion = objects.reduce(union); // {x:1, y:2, z:3, a:1} var rightunion = objects.reduceRight(union); // {x:1, y:2, z:3, a:3}
156 | Chapter 7: Arrays
indexOf() and lastIndexOf() search an array for an element with a specified value, and return the index of the first such element found, or –1 if none is found. indexOf() searches the array from beginning to end, and lastIndexOf() searches from end to beginning. a = [0,1,2,1,0]; a.indexOf(1) a.lastIndexOf(1) a.indexOf(3)
// => 1: a[1] is 1 // => 3: a[3] is 1 // => -1: no element has value 3
Unlike the other methods described in this section, indexOf() and lastIndexOf() do not take a function argument. The first argument is the value to search for. The second argument is optional: it specifies the array index at which to begin the search. If this argument is omitted, indexOf() starts at the beginning and lastIndexOf() starts at the end. Negative values are allowed for the second argument and are treated as an offset from the end of the array, as they are for the splice() method: a value of –1, for example, specifies the last element of the array. The following function searches an array for a specified value and returns an array of all matching indexes. This demonstrates how the second argument to indexOf() can be used to find matches beyond the first. // Find all occurrences of a value x in an array a and return an array // of matching indexes function findall(a, x) { var results = [], // The array of indexes we'll return len = a.length, // The length of the array to be searched pos = 0; // The position to search from while(pos < len) { // While more elements to search... pos = a.indexOf(x, pos); // Search if (pos === -1) break; // If nothing found, we're done. results.push(pos); // Otherwise, store index in array pos = pos + 1; // And start next search at next element } return results; // Return array of indexes }
Note that strings have indexOf() and lastIndexOf() methods that work like these array methods.
7.10 Array Type We’ve seen throughout this chapter that arrays are objects with some special behavior. Given an unknown object, it is often useful to be able to determine whether it is an array or not. In ECMAScript 5, you can do this with the Array.isArray() function: Array.isArray([]) Array.isArray({})
// => true // => false
7.10 Array Type | 157
Core JavaScript
7.9.6 indexOf() and lastIndexOf()
Prior to ECMAScript 5, however, distinguishing arrays from nonarray objects was surprisingly difficult. The typeof operator does not help here: it returns “object” for arrays (and for all objects other than functions). The instanceof operator works in simple cases: [] instanceof Array ({}) instanceof Array
// => true // => false
The problem with using instanceof is that in web browsers, there can be more than one window or frame open. Each has its own JavaScript environment, with its own global object. And each global object has its own set of constructor functions. Therefore an object from one frame will never be an instance of a constructor from another frame. While interframe confusion does not arise often, it is enough of a problem that the instanceof operator is not deemed a reliable test for arrays. The solution is to inspect the class attribute (see §6.8.2) of the object. For arrays, this attribute will always have the value “Array”, and we can therefore write an isArray() function in ECMAScript 3 like this: var isArray = Function.isArray || function(o) { return typeof o === "object" && Object.prototype.toString.call(o) === "[object Array]"; };
This test of the class attribute is, in fact, exactly what the ECMAScript 5 Array.isArray() function does. The technique for obtaining the class of an object using Object.prototype.toString() is explained in §6.8.2 and demonstrated in Example 6-4.
7.11 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. Setting length to a smaller value truncates the array. Arrays inherit useful methods from Array.prototype. Arrays have a class attribute of “Array”.
These are the features that make JavaScript arrays distinct from regular objects. But they are not the essential features that define an array. It is often perfectly reasonable to treat any object with a numeric length property and corresponding non-negative integer properties as a kind of array. These “array-like” objects actually do occasionally appear in practice, and although you cannot directly invoke array methods on them or expect special behavior from the length property, you can still iterate through them with the same code you’d use for a true array. It turns out that many array algorithms work just as well with array-like
158 | Chapter 7: Arrays
The following code takes a regular object, adds properties to make it an array-like object, and then iterates through the “elements” of the resulting pseudo-array: var a = {};
// Start with a regular empty object
// Add properties to make it "array-like" var i = 0; while(i < 10) { a[i] = i * i; i++; } a.length = i; // Now iterate through it as if it were a real array var total = 0; for(var j = 0; j < a.length; j++) total += a[j];
The Arguments object that’s described in §8.3.2 is an array-like object. In client-side JavaScript, a number of DOM methods, such as document.getElementsByTagName(), return array-like objects. Here’s a function you might use to test for objects that work like arrays: // Determine if o is an array-like object. // Strings and functions have numeric length properties, but are // excluded by the typeof test. In client-side JavaScript, DOM text // nodes have a numeric length property, and may need to be excluded // with an additional o.nodeType != 3 test. function isArrayLike(o) { if (o && // o is not null, undefined, etc. typeof o === "object" && // o is an object isFinite(o.length) && // o.length is a finite number o.length >= 0 && // o.length is non-negative o.length===Math.floor(o.length) && // o.length is an integer o.length < 4294967296) // o.length < 2^32 return true; // Then o is array-like else return false; // Otherwise it is not }
We’ll see in §7.12 that ECMAScript 5 strings behave like arrays (and that some browsers made strings indexable before ECMAScript 5). Nevertheless, tests like the one above for array-like objects typically return false for strings—they are usually best handled as strings, not as arrays. The JavaScript array methods are purposely defined to be generic, so that they work correctly when applied to array-like objects in addition to true arrays. In ECMAScript 5, all array methods are generic. In ECMAScript 3, all methods except toString() and toLocaleString() are generic. (The concat() method is an exception: although it can be invoked on an array-like object, it does not property expand that object into the returned array.) Since array-like objects do not inherit from 7.11 Array-Like Objects | 159
Core JavaScript
objects as they do with real arrays. This is especially true if your algorithms treat the array as read-only or if they at least leave the array length unchanged.
Array.prototype, you cannot invoke array methods on them directly. You can invoke them indirectly using the Function.call method, however: var a = {"0":"a", "1":"b", "2":"c", length:3}; // An array-like object Array.prototype.join.call(a, "+") // => "a+b+c" Array.prototype.slice.call(a, 0) // => ["a","b","c"]: true array copy Array.prototype.map.call(a, function(x) { return x.toUpperCase(); }) // => ["A","B","C"]:
We’ve seen this call() technique before in the isArray() method of §7.10. The call() method of Function objects is covered in more detail in §8.7.3. The ECMAScript 5 array methods were introduced in Firefox 1.5. Because they were written generically, Firefox also introduced versions of these methods as functions defined directly on the Array constructor. With these versions of the methods defined, the examples above can be rewritten like this: var a = {"0":"a", "1":"b", "2":"c", length:3}; // An array-like object Array.join(a, "+") Array.slice(a, 0) Array.map(a, function(x) { return x.toUpperCase(); })
These static function versions of the array methods are quite useful when working with array-like objects, but since they are nonstandard, you can’t count on them to be defined in all browsers. You can write code like this to ensure that the functions you need exist before you use them: Array.join = Array.join || function(a,sep) { return Array.prototype.join.call(a,sep); }; Array.slice = Array.slice || function(a,from,to) { return Array.prototype.slice.call(a,from,to); }; Array.map = Array.map || function(a, f, thisArg) { return Array.prototype.map.call(a, f, thisArg); }
7.12 Strings As Arrays In ECMAScript 5 (and in many recent browser implementations—including IE8— prior to ECMAScript 5), strings behave like read-only arrays. Instead of accessing individual characters with the charAt() method, you can use square brackets: var s = test; s.charAt(0) s[1]
// => "t" // => "e"
The typeof operator still returns “string” for strings, of course, and the Array.isArray() method returns false if you pass it a string. The primary benefit of indexable strings is simply that we can replace calls to charAt() with square brackets, which are more concise and readable, and potentially
160 | Chapter 7: Arrays
s = "JavaScript" Array.prototype.join.call(s, " ") Array.prototype.filter.call(s, function(x) { return x.match(/[^aeiou]/); }).join("")
// => "J a v a S c r i p t" // Filter the characters of the string // Only match nonvowels // => "JvScrpt"
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.12 Strings As Arrays | 161
Core JavaScript
more efficient. The fact that strings behave like arrays also means, however, that we can apply generic array methods to them. For example:
CHAPTER 8
Functions
A function is a block of JavaScript code that is defined once but may be executed, or invoked, any number of times. You may already be familiar with the concept of a function under a name such as subroutine or procedure. JavaScript functions are parameterized: a function definition may include a list of identifiers, known as parameters, that work as local variables for the body of the function. Function invocations provide values, or arguments, for the function’s parameters. Functions often use their argument values to compute a return value that becomes the value of the function-invocation expression. In addition to the arguments, each invocation has another value—the invocation context—that is the value of the this keyword. If a function is assigned to the property of an object, it is known as a method of that object. When a function is invoked on or through an object, that object is the invocation context or this value for the function. Functions designed to initialize a newly created object are called constructors. Constructors were described in §6.1 and will be covered again in Chapter 9. In JavaScript, functions are objects, and they can be manipulated by programs. JavaScript can assign functions to variables and pass them to other functions, for example. Since functions are objects, you can set properties on them, and even invoke methods on them. 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.
163
8.1 Defining Functions Functions are defined with the function keyword, which can be used in a function definition expression (§4.3) or in a function declaration statement (§5.3.2). In either form, function definitions begin with the keyword function followed by these components: • An identifier that names the function. The name is a required part of function declaration statements: it is used as the name of a variable, and the newly defined function object is assigned to the variable. For function definition expressions, the name is optional: if present, the name refers to the function object only within the body of the function itself. • A pair of parentheses around a comma-separated list of zero or more identifiers. These identifiers are the parameter names for the function, and they behave like local variables within the body of the function. • A pair of curly braces with zero or more JavaScript statements inside. These statements are the body of the function: they are executed whenever the function is invoked. Example 8-1 shows some function definitions using both statement and expression forms. Notice that a function defined as an expression is only useful if it is part of a larger expression, such as an assignment or invocation, that does something with the newly defined function. Example 8-1. Defining JavaScript functions // Print the name and value of each property of o. function printprops(o) { for(var p in o) console.log(p + ": " + o[p] + "\n"); }
Return undefined.
// Compute the distance between Cartesian points (x1,y1) and (x2,y2). function distance(x1, y1, x2, y2) { var dx = x2 - x1; var dy = y2 - y1; return Math.sqrt(dx*dx + dy*dy); } // A recursive function (one that calls itself) that computes factorials // Recall that x! is the product of x and all positive integers less than it. function factorial(x) { if (x 16 // => 16
Functions can also be assigned to object properties rather than variables. When you do this, they’re called methods: var o = {square: function(x) { return x*x; }}; // An object literal var y = o.square(16); // y equals 256
Functions don’t even require names at all, as when they’re assigned to array elements: var a = [function(x) { return x*x; }, 20]; a[0](a[1]);
// An array literal // => 400
The syntax of this last example looks strange, but it is still a legal function invocation expression!
3. This may not seem like a particularly interesting point unless you are familiar with languages such as Java, in which functions are part of a program but cannot be manipulated by the program.
176 | Chapter 8: Functions
Example 8-2. Using functions as data // We define some simple functions here function add(x,y) { return x + y; } function subtract(x,y) { return x - y; } function multiply(x,y) { return x * y; } function divide(x,y) { return x / y; } // Here's a function that takes one of the above functions // as an argument and invokes it on two operands function operate(operator, operand1, operand2) { return operator(operand1, operand2); } // We could invoke this function like this to compute the value (2+3) + (4*5): var i = operate(add, operate(add, 2, 3), operate(multiply, 4, 5)); // For the sake of the example, we implement the simple functions again, // this time using function literals within an object literal; var operators = { add: function(x,y) { return x+y; }, subtract: function(x,y) { return x-y; }, multiply: function(x,y) { return x*y; }, divide: function(x,y) { return x/y; }, pow: Math.pow // Works for predefined functions too }; // This function takes the name of an operator, looks up that operator // in the object, and then invokes it on the supplied operands. Note // the syntax used to invoke the operator function. function operate2(operation, operand1, operand2) { if (typeof operators[operation] === "function") return operators[operation](operand1, operand2); else throw "unknown operator"; } // Compute the value ("hello" + " " + "world") like this: var j = operate2("add", "hello", operate2("add", " ", "world")); // Using the predefined Math.pow() function: var k = operate2("pow", 10, 2);
As another example of functions as values, consider the Array.sort() method. This method sorts the elements of an array. Because there are many possible orders to sort by (numerical order, alphabetical order, date order, ascending, descending, and so on), the sort() method optionally takes a function as an argument to tell it how to perform the sort. This function has a simple job: for any two values it is passed, it returns a value that specifies which element would come first in a sorted array. This function argument makes Array.sort() perfectly general and infinitely flexible; it can sort any type of data into any conceivable order. Examples are shown in §7.8.3.
8.4 Functions As Values | 177
Core JavaScript
Example 8-2 demonstrates the kinds of things that can be done when functions are used as values. This example may be a little tricky, but the comments explain what is going on.
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, instead of cluttering up the namespace by defining a global variable. 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. // Function declarations are hoisted so we really can // do this assignment before the function declaration. uniqueInteger.counter = 0; // This function returns a different integer each time it is called. // It uses a property of itself to remember the next value to be returned. function uniqueInteger() { return uniqueInteger.counter++; // Increment and return counter property }
As another example, consider the following factorial() function that uses properties of itself (treating itself as an array) to cache previously computed results: // Compute factorials and cache results as properties of the function itself. function factorial(n) { if (isFinite(n) && n>0 && n==Math.round(n)) { // Finite, positive ints only if (!(n in factorial)) // If no cached result factorial[n] = n * factorial(n-1); // Compute and cache it return factorial[n]; // Return the cached result } else return NaN; // If input was bad } factorial[1] = 1; // Initialize the cache to hold this base case.
8.5 Functions As Namespaces Recall from §3.10.1 that JavaScript has function scope: variables declared within a function are visible throughout the function (including within nested functions) but do not exist outside of the function. Variables declared outside of a function are global variables and are visible throughout your JavaScript program. JavaScript does not define any way to declare variables that are hidden within a single block of code, and 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 polluting the global namespace.
178 | Chapter 8: Functions
function mymodule() { // Module code goes here. // Any variables used by the module are local to this function // instead of cluttering up the global namespace. } mymodule(); // But don't forget to invoke the function!
This code defines only a single global variable: the function name “mymodule”. If defining even a single property is too much, you can define and invoke an anonymous function in a single expression: (function() { // mymodule function rewritten as an unnamed expression // Module code goes here. }()); // end the function literal and invoke it now.
This technique of defining and invoking a function in a single expression is used frequently enough that it has become idiomatic. Note the use of parentheses in the code above. The open parenthesis before function is required because without it, the JavaScript interpreter tries to parse the function keyword as a function declaration statement. With the parenthesis, the interpreter correctly recognizes this as a function definition expression. It is idiomatic to use the parentheses, even when they are not required, around a function that is to be invoked immediately after being defined. Example 8-3 demonstrates this namespace technique. It defines an anonymous function that returns an extend() function like the one shown in Example 6-2. The code in the anonymous function tests whether a well-known Internet Explorer bug is present and, if so, returns a patched version of the function. In addition, the anonymous function’s namespace serves to hide an array of property names. Example 8-3. The extend() function, patched if necessary // Define an extend function that copies the properties of its second and // subsequent arguments onto its first argument. // We work around an IE bug here: in many versions of IE, the for/in loop // won't enumerate an enumerable property of o if the prototype of o has // a nonenumerable property by the same name. This means that properties // like toString are not handled correctly unless we explicitly check for them. var extend = (function() { // Assign the return value of this function // First check for the presence of the bug before patching it. for(var p in {toString:null}) { // If we get here, then the for/in loop works correctly and we return // a simple version of the extend() function return function extend(o) {
8.5 Functions As Namespaces | 179
Core JavaScript
Suppose, for example, you have a module 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 module will be used in many different programs, you don’t know whether the variables it creates will conflict with variables used by the programs that import it. The solution, of course, is to put the code into a function and then invoke the function. This way, variables that would have been global become local to the function:
for(var i = 1; i < arguments.length; i++) { var source = arguments[i]; for(var prop in source) o[prop] = source[prop]; } return o; }; } // If we get here, it means that the for/in loop did not enumerate // the toString property of the test object. So return a version // of the extend() function that explicitly tests for the nonenumerable // properties of Object.prototype. return function patched_extend(o) { for(var i = 1; i < arguments.length; i++) { var source = arguments[i]; // Copy all the enumerable properties for(var prop in source) o[prop] = source[prop]; // And now check the special-case properties for(var j = 0; j < protoprops.length; j++) { prop = protoprops[j]; if (source.hasOwnProperty(prop)) o[prop] = source[prop]; } };
} return o; // This is the list of special-case properties we check for var protoprops = ["toString", "valueOf", "constructor", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable","toLocaleString"]; }());
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 current scope chain. (Before reading the rest of this section, you may want to review the material on variable scope and the scope chain in §3.10 and §3.10.3.) 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.4 Technically, all JavaScript functions are closures: they are objects, and they have a scope chain associated with them. Most functions are invoked using the same scope chain that was in effect when the function was defined, and it doesn’t really matter that there is a closure involved. Closures become interesting when they are invoked under a
4. This is an old term that refers to the fact that the function’s variables have bindings in the scope chain and that therefore the function is “closed over” its variables.
180 | Chapter 8: Functions
The first step to understanding closures is to review the lexical scoping rules for nested functions. Consider the following code (which is similar to code you’ve already seen in §3.10): var scope = "global scope"; function checkscope() { var scope = "local scope"; function f() { return scope; } return f(); } checkscope()
// A global variable // A local variable // Return the value in scope here // => "local scope"
The checkscope() function declares a local variable and then defines and invokes a function that returns the value of that variable. It should be clear to you why the call to checkscope() returns “local scope”. Now let’s change the code just slightly. Can you tell what this code will return? var scope = "global scope"; function checkscope() { var scope = "local scope"; function f() { return scope; } return f; } checkscope()()
// A global variable // A local variable // Return the value in scope here // What does this return?
In this code, a pair of parentheses has moved from inside checkscope() to outside of it. Instead of invoking the nested function and returning its result, checkscope() now just returns the nested function object itself. What happens when we invoke that nested function (with the second pair of parentheses in the last line of code) outside of the function in which it was defined? Remember the fundamental rule of lexical scoping: JavaScript functions are executed using the scope chain that was in effect when they were defined. The nested function f() was defined under a scope chain in which the variable scope was bound to the value “local scope”. That binding is still in effect when f is executed, wherever it is executed from. So the last line of code above returns “local scope”, not “global scope”. This, in a nutshell, is the surprising and powerful nature of closures: they capture the local variable (and parameter) bindings of the outer function within which they are defined.
8.6 Closures | 181
Core JavaScript
different scope chain than the one that was in effect when they were defined. 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.
Implementing Closures Closures are easy to understand if you simply accept the lexical scoping rule: functions are executed using the scope chain that was in effect when they were defined. Some programmers find closures confusing, however, because they get caught up in implementation details. Surely, they think, the local variables defined in the outer function cease to exist when the outer function returns, so how can the nested function execute using a scope chain that does not exist anymore? If you’re wondering about this yourself, then you have probably been exposed to low-level programming languages like C and to stack-based CPU architectures: if a function’s local variables are defined on a CPU stack, then they would indeed cease to exist when the function returned. But remember our definition of scope chain from §3.10.3. We described it as a list of objects, not a stack of bindings. Each time a JavaScript function is invoked, a new object is created to hold the local variables for that invocation, and that object is added to the scope chain. When the function returns, that variable binding object is removed from the scope chain. If there were no nested functions, there are no more references to the binding object and it gets garbage collected. If there were nested functions defined, then each of those functions has a reference to the scope chain, and that scope chain refers to the variable binding object. If those nested functions objects remained within their outer function, however, then they themselves will be garbage collected, along with the variable binding object they referred to. But if the function defines a nested function and returns it or stores it into a property somewhere, then there will be an external reference to the nested function. It won’t be garbage collected, and the variable binding object it refers to won’t be garbage collected either.
In §8.4.1 we defined a uniqueInteger() function that used a property of the function itself to keep track of the next value to be returned. A shortcoming of that approach is that buggy or malicious code could reset the counter or set it to a noninteger, causing the uniqueInteger() function to violate the “unique” or the “integer” part of its contract. Closures capture the local variables of a single function invocation and can use those variables as private state. Here is how we could rewrite the uniqueInteger() function using closures: var uniqueInteger = (function() { // Define and invoke var counter = 0; // Private state of function below return function() { return counter++; }; }());
In order to understand this code, you have to read it carefully. At first glance, the first line of code looks like it is assigning a function to the variable uniqueInteger. In fact, the code is defining and invoking (as hinted by the open parenthesis on the first line) a function, so it is the return value of the function that is being assigned to uniqueInteger. Now, if we study the body of the function, we see that its return value is another function. It is this nested function object that gets assigned to uniqueInteger. The nested function has access to the variables in scope, and can use the counter variable defined in the outer function. Once that outer function returns, no other code can see the counter variable: the inner function has exclusive access to it. 182 | Chapter 8: Functions
function counter() { var n = 0; return { count: function() { return n++; }, reset: function() { n = 0; } }; } var c = counter(), d = counter(); c.count() d.count() c.reset() c.count() d.count()
// // // // // //
Create two counters => 0 => 0: they count independently reset() and count() methods share state => 0: because we reset c => 1: d was not reset
The counter() function returns a “counter” object. This object has two methods: count() returns the next integer, and reset() resets the internal state. The first thing to understand is that the two methods share access to the private variable n. The second thing to understand is that each invocation of counter() creates a new scope chain and a new private variable. So if you call counter() twice, you get two counter objects with different private variables. Calling count() or reset() on one counter object has no effect on the other. It is worth noting here that you can combine this closure technique with property getters and setters. The following version of the counter() function is a variation on code that appeared in §6.6, but it uses closures for private state rather than relying on a regular object property: function counter(n) { // Function argument n is the private variable return { // Property getter method returns and increments private counter var. get count() { return n++; }, // Property setter doesn't allow the value of n to decrease set count(m) { if (m >= n) n = m; else throw Error("count can only be set to a larger value"); } }; } var c = counter(1000); c.count // => 1000 c.count // => 1001 c.count = 2000 c.count // => 2000 c.count = 2000 // => Error!
8.6 Closures | 183
Core JavaScript
Private variables like counter need not be exclusive to a single closure: it is perfectly possible for two or more nested functions to be defined within the same outer function and share the same scope chain. Consider the following code:
Note that this version of the counter() function does not declare a local variable, but just uses its parameter n to hold the private state shared by the property accessor methods. This allows the caller of counter() to specify the initial value of the private variable. Example 8-4 is a generalization of the shared private state through closures technique we’ve been demonstrating here. This example defines an addPrivateProperty() function that defines a private variable and two nested functions to get and set the value of that variable. It adds these nested functions as methods of the object you specify: Example 8-4. Private property accessor methods using closures // This function adds property accessor methods for a property with // the specified name to the object o. The methods are named get // and set. If a predicate function is supplied, the setter // method uses it to test its argument for validity before storing it. // If the predicate returns false, the setter method throws an exception. // // The unusual thing about this function is that the property value // that is manipulated by the getter and setter methods is not stored in // the object o. Instead, the value is stored only in a local variable // in this function. The getter and setter methods are also defined // locally to this function and therefore have access to this local variable. // This means that the value is private to the two accessor methods, and it // cannot be set or modified except through the setter method. function addPrivateProperty(o, name, predicate) { var value; // This is the property value // The getter method simply returns the value. o["get" + name] = function() { return value; };
}
// The setter method stores the value or throws an exception if // the predicate rejects the value. o["set" + name] = function(v) { if (predicate && !predicate(v)) throw Error("set" + name + ": invalid value " + v); else value = v; }; // The following code demonstrates the addPrivateProperty() method. var o = {}; // Here is an empty object // Add property accessor methods getName and setName() // Ensure that only string values are allowed addPrivateProperty(o, "Name", function(x) { return typeof x == "string"; }); o.setName("Frank"); // Set the property value console.log(o.getName()); // Get the property value o.setName(0); // Try to set a value of the wrong type
We’ve now seen a number of examples in which two closures are defined in the same scope chain and share access to the same private variable or variables. This is an
184 | Chapter 8: Functions
// This function returns a function that always returns v function constfunc(v) { return function() { return v; }; } // Create an array of constant functions: var funcs = []; for(var i = 0; i < 10; i++) funcs[i] = constfunc(i); // The function at array element 5 returns the value 5. funcs[5]() // => 5
When working with code like this that creates multiple closures using a loop, it is a common error to try to move the loop within the function that defines the closures. Think about the following code, for example: // Return an array of functions that return the values 0-9 function constfuncs() { var funcs = []; for(var i = 0; i < 10; i++) funcs[i] = function() { return i; }; return funcs; } var funcs = constfuncs(); funcs[5]() // What does this return?
The code above creates 10 closures, and stores them in an array. The closures are all defined within the same invocation of the function, so they share access to the variable i. When constfuncs() returns, the value of the variable i is 10, and all 10 closures share this value. Therefore, all the functions in the returned array of functions return the same value, which is not what we wanted at all. It is important to remember that the scope chain associated with a closure is “live.” Nested functions do not make private copies of the scope or make static snapshots of the variable bindings. Another thing to remember when writing closures is that this is a JavaScript keyword, not a variable. As discussed earlier, every function invocation has a this value, and a closure cannot access the this value of its outer function unless the outer function has saved that value into a variable: var self = this;
// Save this value in a variable for use by nested funcs.
The arguments binding is similar. This is not a language keyword, but it is automatically declared for every function invocation. Since a closure has its own binding for arguments, it cannot access the outer function’s arguments array unless the outer function has saved that array into a variable by a different name: var outerArguments = arguments;
// Save for use by nested functions
Example 8-5, later in this chapter, defines a closure that uses these techniques to refer to both the this and arguments values of the outer function.
8.6 Closures | 185
Core JavaScript
important technique, but it is just as important to recognize when closures inadvertently share access to a variable that they should not share. Consider the following code:
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 function properties and methods and the Function() constructor. You can also read about these in the reference section.
8.7.1 The length Property Within the body of a function, arguments.length specifies the number of arguments that were passed to the function. The length property of a function itself, however, has a different meaning. This read-only property returns 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. The following code defines a function named check() that is passed the arguments array from another function. It compares arguments.length (the number of arguments actually passed) to arguments.callee.length (the number expected) to determine whether the function was passed the right number of arguments. If not, it throws an exception. The check() function is followed by a test function f() that demonstrates how check() can be used: // This function uses arguments.callee, so it won't work in strict mode. function check(args) { var actual = args.length; // The actual number of arguments var expected = args.callee.length; // The expected number of arguments if (actual !== expected) // Throw an exception if they differ. throw Error("Expected " + expected + "args; got " + actual); } function f(x, y, z) { check(arguments); return x + y + z; }
// Check that the actual # of args matches expected #. // Now do the rest of the function normally.
8.7.2 The prototype Property Every function has 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.1.3 and will be covered again in Chapter 9.
186 | Chapter 8: Functions
call() and apply() allow you to indirectly invoke (§8.2.4) a function as if it were a method of some other object. (We used the call() method in Example 6-4 to invoke Object.prototype.toString on an object whose class we wanted to determine, for example.) The first argument to both call() and apply() is the object on which the func-
tion 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.apply(o);
Either of the lines of code above are similar to the following (which assume that o does not already have a property named m): o.m = f; o.m(); delete o.m;
// Make f a temporary method of o. // Invoke it, passing no arguments. // Remove the temporary method.
In ECMAScript 5 strict mode the first argument to call() or apply() becomes the value of this, even if it is a primitive value or null or undefined. In ECMAScript 3 and nonstrict mode, a value of null or undefined is replaced with the global object and a primitive value is replaced with the corresponding wrapper object. Any arguments to call() after the first invocation context argument are the values that are passed to the function that is invoked. For example, to pass two numbers to the function f() and invoke it as if it were a method of the object o, you could use code like this: f.call(o, 1, 2);
The apply() method is like the call() method, except that the arguments to be passed to the function are specified as an array: f.apply(o, [1,2]);
If a function is defined to accept an arbitrary number of arguments, the apply() method allows you to invoke that function on the contents of an array of arbitrary length. For example, to find the largest number in an array of numbers, you could use the apply() method to pass the elements of the array to the Math.max() function: var biggest = Math.max.apply(Math, array_of_numbers);
Note that apply() works with array-like objects as well as true arrays. In particular, you can invoke a function with the same arguments as the current function by passing the arguments array directly to apply(). The following code demonstrates: // Replace the method named m of the object o with a version that logs // messages before and after invoking the original method. function trace(o, m) { var original = o[m]; // Remember original method in the closure. o[m] = function() { // Now define the new method. console.log(new Date(), "Entering:", m); // Log message.
8.7 Function Properties, Methods, and Constructor | 187
Core JavaScript
8.7.3 The call() and apply() Methods
var result = original.apply(this, arguments); // Invoke original. console.log(new Date(), "Exiting:", m); // Log message. return result; // Return result. }
};
This trace() function is passed an object and a method name. It replaces the specified method with a new method that “wraps” additional functionality around the original method. This kind of dynamic alteration of existing methods is sometimes called “monkey-patching.”
8.7.4 The bind() Method The bind() method was added in ECMAScript 5, but it is easy to simulate in ECMAScript 3. As its name implies, 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 var o = { x : 1 }; // An object we'll bind to var g = f.bind(o); // Calling g(x) invokes o.f(x) g(2) // => 3
It is easy to accomplish this kind of binding with code like the following: // Return a function that invokes f as a method of o, passing all its arguments. function bind(f, o) { if (f.bind) return f.bind(o); // Use the bind method, if there is one else return function() { // Otherwise, bind it like this return f.apply(o, arguments); }; }
The ECMAScript 5 bind() method does more than just bind a function to an object. It also performs partial application: any arguments you pass to bind() after the first are bound along with the this value. Partial application is a common technique in functional programming and is sometimes called currying. Here are some examples of the bind() method used for partial application: var sum = function(x,y) { return x + y }; // Return the sum of 2 args // Create a new function like sum, but with the this value bound to null // and the 1st argument bound to 1. This new function expects just one arg. var succ = sum.bind(null, 1); succ(2) // => 3: x is bound to 1, and we pass 2 for the y argument function f(y,z) { return this.x + y + z }; // Another function that adds var 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
We can bind the this value and perform partial application in ECMAScript 3. The standard bind() method can be simulated with code like that shown in Example 8-5.
188 | Chapter 8: Functions
Example 8-5. A Function.bind() method for ECMAScript 3 if (!Function.prototype.bind) { Function.prototype.bind = function(o /*, args */) { // Save the this and arguments values into variables so we can // use them in the nested function below. var self = this, boundArgs = arguments; // The return value of the bind() method is a function return function() { // Build up an argument list, starting with any args passed // to bind after the first one, and follow those with all args // passed to this function. var args = [], i; for(i = 1; i < boundArgs.length; i++) args.push(boundArgs[i]); for(i = 0; i < arguments.length; i++) args.push(arguments[i]);
}
};
};
// Now invoke self as a method of o, with those arguments return self.apply(o, args);
Notice that the function returned by this bind() method is a closure that uses the variables self and boundArgs declared in the outer function, even though that inner function has been returned from the outer function and is invoked after the outer function has returned. The bind() method defined by ECMAScript 5 does have some features that cannot be simulated with the ECMAScript 3 code shown above. First, the true bind() method returns a function object with its length property properly set to the arity of the bound function minus the number of bound arguments (but not less than zero). Second, the ECMAScript 5 bind() method can be used for partial application of constructor functions. If the function returned by bind() is used as a constructor, the this passed to bind() is ignored, and the original function is invoked as a constructor, with some arguments already bound. Functions returned by the bind() method do not have a prototype property (the prototype property of regular functions cannot be deleted) and objects created when these bound functions are used as constructors inherit from the prototype of the original, unbound constructor. Also, a bound constructor works just like the unbound constructor for the purposes of the instanceof operator.
8.7.5 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. 8.7 Function Properties, Methods, and Constructor | 189
Core JavaScript
Note that we save this method as Function.prototype.bind, so that all function objects inherit it. This technique is explained in detail in §9.4.
8.7.6 The Function() Constructor Functions are usually defined using the function keyword, either in the form of a function definition statement or a function literal expression. But functions can also be defined with the Function() constructor. For example: var f = new Function("x", "y", "return x*y;");
This line of code creates a new function that is more or less equivalent to a function defined with the familiar syntax: var f = function(x, y) { return x*y; }
The Function() constructor expects any number of string arguments. The last argument is the text of the function body; it can contain arbitrary JavaScript statements, separated from each other by semicolons. All other arguments to the constructor are strings that specify the parameters names for the function. If you are defining a function that takes no arguments, you simply pass a single string—the function body—to the constructor. Notice that the Function() constructor is not passed any argument that specifies a name for the function it creates. Like function literals, the Function() constructor creates anonymous functions. There are a few points that are important to understand about the Function() constructor: • The Function() constructor allows JavaScript functions to be dynamically created and compiled at runtime. • The Function() constructor parses the function body and creates a new function object each time it is called. If the call to the constructor appears within a loop or within a frequently called function, this process can be inefficient. By contrast, nested functions and function definition expressions that appear within loops are not recompiled each time they are encountered. • A last, very important point about the Function() constructor is that the functions it creates do not use lexical scoping; instead, they are always compiled as if they were top-level functions, as the following code demonstrates: var scope = "global"; function constructFunction() { var scope = "local"; return new Function("return scope"); // Does not capture the local scope! } // This line returns "global" because the function returned by the // Function() constructor does not use the local scope. 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 should rarely need to use this constructor in your code.
190 | Chapter 8: Functions
We learned in §7.11 that there are “array-like” objects that are not true arrays but can be treated like arrays for most purposes. A similar situation exists for functions. A callable object is any object that can be invoked in a function invocation expression. All functions are callable, but not all callable objects are functions. Callable objects that are not functions are encountered in two situations in today’s JavaScript implementations. First, the IE web browser (version 8 and before) implements client-side methods such as Window.alert() and Document.getElementsById() using callable host objects rather than native Function objects. These methods work the same in IE as they do in other browsers, but they are not actually Function objects. IE9 switches to using true functions, so this kind of callable object will gradually become less common. The other common form of callable objects are RegExp objects—in many browsers, you can invoke a RegExp object directly as a shortcut for invoking its exec() method. This is a completely nonstandard feature of JavaScript that was introduced by Netscape and copied by other vendors for compatibility. Do not write code that relies on the callability of RegExp objects: this feature is likely to be deprecated and removed in the future. The typeof operator is not interoperable for callable RegExps. In some browsers it returns “function” and in others it returns “object”. If you want to determine whether an object is a true function object (and has function methods) you can test its class attribute (§6.8.2) using the technique shown in Example 6-4: function isFunction(x) { return Object.prototype.toString.call(x) === "[object Function]"; }
Note that this isFunction() function is quite similar to the isArray() function shown in §7.10.
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. The ECMAScript 5 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.5
5. If this piques your interest, you may be interested in using (or at least reading about) Oliver Steele’s Functional JavaScript library. See http://osteele.com/sources/javascript/functional/.
8.8 Functional Programming | 191
Core JavaScript
8.7.7 Callable Objects
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: var data = [1,1,3,5,5];
// This is our array of numbers
// The mean is the sum of the elements divided by the number of elements var total = 0; for(var i = 0; i < data.length; i++) total += data[i]; var mean = total/data.length; // The mean of our data is 3 // To compute the standard deviation, we first sum the squares of // the deviation of each element from the mean. total = 0; for(var i = 0; i < data.length; i++) { var deviation = data[i] - mean; total += deviation * deviation; } var stddev = Math.sqrt(total/(data.length-1)); // The standard deviation is 2
We can perform these same computations in concise functional style using the array methods map() and reduce() like this (see §7.9 to review these methods): // First, define two simple functions var sum = function(x,y) { return x+y; }; var square = function(x) { return x*x; }; // Then use those functions with Array methods to compute mean and stddev var data = [1,1,3,5,5]; var mean = data.reduce(sum)/data.length; var deviations = data.map(function(x) {return x-mean;}); var stddev = Math.sqrt(deviations.map(square).reduce(sum)/(data.length-1));
What if we’re using ECMAScript 3 and don’t have access to these newer array methods? We can define our own map() and reduce() functions that use the built-in methods if they exist: // Call the function f for each element of array a and return // an array of the results. Use Array.prototype.map if it is defined. var map = Array.prototype.map ? function(a, f) { return a.map(f); } // Use map method if it exists : function(a,f) { // Otherwise, implement our own var results = []; for(var i = 0, len = a.length; i < len; i++) { if (i in a) results[i] = f.call(null, a[i], i, a); } return results; }; // Reduce the array a to a single value using the function f and // optional initial value. Use Array.prototype.reduce if it is defined. var reduce = Array.prototype.reduce ? function(a, f, initial) { // If the reduce() method exists. if (arguments.length > 2) return a.reduce(f, initial); // If an initial value was passed.
192 | Chapter 8: Functions
// Start with the specified initial value, or the first value in a if (arguments.length > 2) accumulator = initial; else { // Find the first defined index in the array if (len == 0) throw TypeError(); while(i < len) { if (i in a) { accumulator = a[i++]; break; } else i++; } if (i == len) throw TypeError(); } // Now call f for each remaining element in the array while(i < len) { if (i in a) accumulator = f.call(undefined, accumulator, a[i], i, a); i++; } };
return accumulator;
With these map() and reduce() functions defined, our code to compute the mean and standard deviation now looks like this: var var var var var var
data = [1,1,3,5,5]; sum = function(x,y) { return x+y; }; square = function(x) { return x*x; }; mean = reduce(data, sum)/data.length; deviations = map(data, function(x) {return x-mean;}); stddev = Math.sqrt(reduce(map(deviations, square), sum)/(data.length-1));
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: // This higher-order function returns a new function that passes its // arguments to f and returns the logical negation of f's return value; function not(f) { return function() { // Return a new function var result = f.apply(this, arguments); // that calls f return !result; // and negates its result. }; } var even = function(x) { // A function to determine if a number is even return x % 2 === 0; };
8.8 Functional Programming | 193
Core JavaScript
else return a.reduce(f); // Otherwise, no initial value. } : function(a, f, initial) { // This algorithm from the ES5 specification var i = 0, len = a.length, accumulator;
var odd = not(even); [1,1,3,5,5].every(odd);
// A new function that does the opposite // => true: every element of the array is odd
The not() function above is a higher-order function because it takes a function argument and returns a new function. As another example, consider the mapper() function below. It takes a function argument and returns a new function that maps one array to another using that function. This function uses the map() function defined earlier, and it is important that you understand how the two functions are different: // Return a function that expects an array argument and applies f to // each element, returning the array of return values. // Contrast this with the map() function from earlier. function mapper(f) { return function(a) { return map(a, f); }; } var increment = function(x) { return x+1; }; var incrementer = mapper(increment); incrementer([1,2,3]) // => [2,3,4]
Here is another, more general, example that takes two functions f and g and returns a new function that computes f(g()): // Return a new function that computes f(g(...)). // The returned function h passes all of its arguments to g, and then passes // the return value of g to f, and then returns the return value of f. // Both f and g are invoked with the same this value as h was invoked with. function compose(f,g) { return function() { // We use call for f because we're passing a single value and // apply for g because we're passing an array of values. return f.call(this, g.apply(this, arguments)); }; } var square = function(x) { return x*x; }; var sum = function(x,y) { return x+y; }; var squareofsum = compose(square, sum); squareofsum(2,3) // => 25
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 The bind() method of a function f (§8.7.4) 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:
194 | Chapter 8: Functions
// The arguments to this function are passed on the left function partialLeft(f /*, ...*/) { var args = arguments; // Save the outer arguments array return function() { // And return this function var a = array(args, 1); // Start with the outer args from 1 on. a = a.concat(array(arguments)); // Then add all the inner arguments. return f.apply(this, a); // Then invoke f on that argument list. }; } // The arguments to this function are passed on the right function partialRight(f /*, ...*/) { var args = arguments; // Save the outer arguments array return function() { // And return this function var a = array(arguments); // Start with the inner arguments. a = a.concat(array(args,1)); // Then add the outer args from 1 on. return f.apply(this, a); // Then invoke f on that argument list. }; } // The arguments to this function serve as a template. Undefined values // in the argument list are filled in with values from the inner set. function partial(f /*, ... */) { var args = arguments; // Save the outer arguments array return function() { var a = array(args, 1); // Start with an array of outer args var i=0, j=0; // Loop through those args, filling in undefined values from inner for(; i < a.length; i++) if (a[i] === undefined) a[i] = arguments[j++]; // Now append any remaining inner arguments a = a.concat(array(arguments, j)) return f.apply(this, a); }; } // Here is a function with three arguments var f = function(x,y,z) { return x * (y - z); }; // Notice how these three partial applications differ partialLeft(f, 2)(3,4) // => -2: Bind first argument: 2 * (3 - 4) partialRight(f, 2)(3,4) // => 6: Bind last argument: 3 * (4 - 2) partial(f, undefined, 2)(3,4) // => -6: Bind middle argument: 3 * (2 - 4)
These partial application functions allow us to easily define interesting functions out of functions we already have defined. Here are some examples: var increment = partialLeft(sum, 1); var cuberoot = partialRight(Math.pow, 1/3); String.prototype.first = partial(String.prototype.charAt, 0); String.prototype.last = partial(String.prototype.substr, -1, 1);
8.8 Functional Programming | 195
Core JavaScript
// A utility function to convert an array-like object (or suffix of it) // to a true array. Used below to convert arguments objects to real arrays. function array(a, n) { return Array.prototype.slice.call(a, n || 0); }
Partial application becomes even more interesting when we combine it with other higher-order functions. Here, for example, is a way to define the not() function shown above using composition and partial application: var var var var
not = partialLeft(compose, function(x) { return !x; }); even = function(x) { return x % 2 === 0; }; odd = not(even); isNumber = not(isNaN)
We can also use composition and partial application to redo our mean and standard deviation calculations in extreme functional style: var var var var var var var
data = [1,1,3,5,5]; sum = function(x,y) { return x+y; }; product = function(x,y) { return x*y; }; neg = partial(product, -1); square = partial(Math.pow, undefined, 2); sqrt = partial(Math.pow, undefined, .5); reciprocal = partial(Math.pow, undefined,
// Our data // Two elementary functions // Define some others -1);
// Now compute the mean and standard deviation. This is all function // invocations with no operators, and it starts to look like Lisp code! var mean = product(reduce(data, sum), reciprocal(data.length)); var stddev = sqrt(product(reduce(map(data, compose(square, partial(sum, neg(mean)))), sum), reciprocal(sum(data.length,-1))));
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 below 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. // It only works if arguments to f all have distinct string representations. function memoize(f) { var cache = {}; // Value cache stored in the closure.
}
return function() { // Create a string version of the arguments to use as a cache key. var key = arguments.length + Array.prototype.join.call(arguments,","); if (key in cache) return cache[key]; else return cache[key] = f.apply(this, arguments); };
The memoize() function creates a new object to use as the cache and assigns this object to a local variable, so that it is private to (in the closure of) the returned function. The returned function converts its arguments array to a string, and uses that string as a property name for the cache object. If a value exists in the cache, it returns it directly.
196 | Chapter 8: Functions
Download from Wow! eBook
// Return the Greatest Common Divisor of two integers, using the Euclidian // algorithm: http://en.wikipedia.org/wiki/Euclidean_algorithm function gcd(a,b) { // Type checking for a and b has been omitted var t; // Temporary variable for swapping values if (a < b) t=b, b=a, a=t; // Ensure that a >= b while(b != 0) t=b, b = a%b, a=t; // This is Euclid's algorithm for GCD return a; } var gcdmemo = memoize(gcd); gcdmemo(85, 187) // => 17 // Note that when we write a recursive function that we will be memoizing, // we typically want to recurse to the memoized version, not the original. var factorial = memoize(function(n) { return (n 120. Also caches values for 4, 3, 2 and 1.
8.8 Functional Programming | 197
Core JavaScript
Otherwise, it calls the specified function to compute the value for these arguments, caches that value, and returns it. Here is how we might use memoize():
CHAPTER 9
Classes and Modules
JavaScript objects were covered in Chapter 6. That chapter treated each object as a unique set of properties, different from every other object. It is often useful, however, to define a class of objects that share certain properties. Members, or instances, of the class have their own properties to hold or define their state, but they also have properties (typically methods) that define their behavior. This behavior is defined by the class and is shared by all instances. Imagine a class named Complex to represent and perform arithmetic on complex numbers, for example. A Complex instance would have properties to hold the real and imaginary parts (state) of the complex number. And the Complex class would define methods to perform addition and multiplication (behavior) of those numbers. In JavaScript, classes are based on JavaScript’s prototype-based inheritance mechanism. If two objects inherit properties from the same prototype object, then we say that they are instances of the same class. JavaScript prototypes and inheritance were covered in §6.1.3 and §6.2.2, and you must be familiar with the material in those sections to understand this chapter. This chapter covers prototypes in §9.1. If two objects inherit from the same prototype, this typically (but not necessarily) means that they were created and initialized by the same constructor function. Constructors have been covered in §4.6, §6.1.2, and §8.2.3, and this chapter has more in §9.2. 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.3 demonstrates classical classes in JavaScript. One of the important features of JavaScript classes is that they are dynamically extendable. §9.4 explains how to do this. Classes can be thought of as types, and §9.5 explains several ways to test or determine the class of an object. That section also covers a
199
programming philosophy known as “duck-typing” that de-emphasizes object type in favor of object capability. After covering all of these fundamentals of object-oriented programming in JavaScript, the chapter shifts to more practical and less architectural matters. §9.6 includes two nontrivial example classes and demonstrates a number of practical object-oriented techniques for improving those classes. §9.7 demonstrates (with many examples) how to extend or subclass other classes and how to define class hierarchies in JavaScript. §9.8 covers some of the things you can do with classes using the new features of ECMAScript 5. Defining classes is a way of writing modular, reusable code, and the last section of this chapter talks about JavaScript modules more generally.
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. In Example 6-1 we defined an inherit() function that returns a newly created object that inherits from a specified prototype object. If we define a prototype object, and then use inherit() 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 // range.js: A class representing a range of values. // This is a factory function that returns a new range object. function range(from, to) { // Use the inherit() function to create an object that inherits from the // prototype object defined below. The prototype object is stored as // a property of this function, and defines the shared methods (behavior) // for all range objects. var r = inherit(range.methods); // Store the start and end points (state) of this new range object. // These are noninherited properties that are unique to this object. r.from = from; r.to = to;
}
// Finally return the new object return r; // This prototype object defines methods inherited by all range objects. range.methods = { // Return true if x is in the range, false otherwise
200 | Chapter 9: Classes and Modules
}; // Here are example uses var r = range(1,3); r.includes(2); r.foreach(console.log); console.log(r);
of // // // //
a range object. Create a range object => true: 2 is in the range Prints 1 2 3 Prints (1...3)
There are a few things worth noting in the code of Example 9-1. This code defines a factory function range() for creating new range objects. Notice that we use a property of this range() function range.methods as a convenient place to store the prototype object that defines the class. There is nothing special or idiomatic about putting the prototype object here. Second, notice that the range() function defines from and to properties on each range object. These are the unshared, noninherited properties that define the unique state of each individual range object. Finally, notice that the shared, inherited methods defined in range.methods all use these from and to properties, and 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 Example 9-1 demonstrates one 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. This means that all objects created with the same constructor 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. A Range class using a constructor // range2.js: Another class representing a range of values. // This is a constructor function that initializes new Range objects. // Note that it does not create or return the object. It just initializes this.
9.2 Classes and Constructors | 201
Core JavaScript
// This method works for textual and Date ranges as well as numeric. includes: function(x) { return this.from that.rank) return 1; return 0; }; // A function for ordering cards as you would in poker
1. This example is based on a Java example by Joshua Bloch, available at http://jcp.org/aboutJava/ communityprocess/jsr/tiger/enum.html.
218 | Chapter 9: Classes and Modules
// A function for ordering cards as you would in bridge Card.orderBySuit = function(a,b) { if (a.suit < b.suit) return -1; if (a.suit > b.suit) return 1; if (a.rank < b.rank) return -1; if (a.rank > b.rank) return 1; return 0; }; // Define a class to represent a standard deck of cards function Deck() { var cards = this.cards = []; // A deck is just an array of cards Card.Suit.foreach(function(s) { // Initialize the array Card.Rank.foreach(function(r) { cards.push(new Card(s,r)); }); }); } // Shuffle method: shuffles cards in place and returns the Deck.prototype.shuffle = function() { // For each element in the array, swap with a randomly var deck = this.cards, len = deck.length; for(var i = len-1; i > 0; i--) { var r = Math.floor(Math.random()*(i+1)), temp; temp = deck[i], deck[i] = deck[r], deck[r] = temp; } return this; };
deck chosen lower element // Random number // Swap
// Deal method: returns an array of cards Deck.prototype.deal = function(n) { if (this.cards.length < n) throw "Out of cards"; return this.cards.splice(this.cards.length-n, n); }; // Create a new deck of cards, shuffle it, and deal a bridge hand var deck = (new Deck()).shuffle(); var hand = deck.deal(13).sort(Card.orderBySuit);
9.6.3 Standard Conversion Methods §3.8.3 and §6.10 described important methods used for type conversion of objects, some of which are invoked automatically by the JavaScript interpreter when conversion is necessary. You do not need to implement these methods for every class you write, but they are important methods, and if you do not implement them for your classes, it should be a conscious choice not to implement them rather than mere oversight. The first, and most important, method is toString(). The purpose of this method is to return a string representation of an object. JavaScript automatically invokes this method if you use an object where a string is expected—as a property name, for example,
9.6 Object-Oriented Techniques in JavaScript | 219
Core JavaScript
Card.orderByRank = function(a,b) { return a.compareTo(b); };
or with the + operator to perform string concatenation. If you don’t implement this method, your class will inherit the default implementation from Object.prototype and will convert to the useless string “[object Object]”. A toString() method might return a human-readable string suitable for display to end users of your program. Even if this is not necessary, however, it is often useful to define toString() for ease of debugging. The Range and Complex classes in Examples 9-2 and 9-3 have toString() methods, as do the enumerated types of Example 9-7. We’ll define a toString() method for the Set class of Example 9-6 below. The toLocaleString() is closely related to toString(): it should convert an object to a string in a locale-sensitive way. By default, objects inherit a toLocaleString() method that simply calls their toString() method. Some built-in types have useful toLocale String() methods that actually return locale-dependent strings. If you find yourself writing a toString() method that converts other objects to strings, you should also define a toLocaleString() method that performs those conversions by invoking the toLocaleString() method on the objects. We’ll do this for the Set class below. The third method is valueOf(). Its job is to convert an object to a primitive value. The valueOf() method is invoked automatically when an object is used in a numeric context, with arithmetic operators (other than +) and with the relational operators, for example. Most objects do not have a reasonable primitive representation and do not define this method. The enumerated types in Example 9-7 demonstrate a case in which the valueOf() method is important, however. The fourth method is toJSON(), which is invoked automatically by JSON.stringify(). The JSON format is intended for serialization of data structures and can handle JavaScript primitive values, arrays, and plain objects. It does not know about classes, and when serializing an object, it ignores the object’s prototype and constructor. If you call JSON.stringify() on a Range or Complex object, for example, it returns a string like {"from":1, "to":3} or {"r":1, "i":-1}. If you pass these strings to JSON.parse(), you’ll obtain a plain object with properties appropriate for Range and Complex objects, but which do not inherit the Range and Complex methods. This kind of serialization is appropriate for classes like Range and Complex, but for other classes you may want to write a toJSON() method to define some other serialization format. If an object has a toJSON() method, JSON.stringify() does not serialize the object but instead calls toJSON() and serializes the value (either primitive or object) that it returns. Date objects, for example, have a toJSON() method that returns a string representation of the date. The enumerated types of Example 9-7 do the same: their toJSON() method is the same as their toString() method. The closest JSON analog to a set is an array, so we’ll define a toJSON() method below that converts a Set object to an array of values. The Set class of Example 9-6 does not define any of these methods. A set has no primitive representation, so it doesn’t make sense to define a valueOf() method, but the class should probably have toString(), toLocaleString(), and toJSON() methods. We
220 | Chapter 9: Classes and Modules
Download from Wow! eBook
// Add these methods to the Set prototype object. extend(Set.prototype, { // Convert a set to a string toString: function() { var s = "{", i = 0; this.foreach(function(v) { s += ((i++ > 0)?", ":"") + v; }); return s + "}"; }, // Like toString, but call toLocaleString on all values toLocaleString : function() { var s = "{", i = 0; this.foreach(function(v) { if (i++ > 0) s += ", "; if (v == null) s += v; // null & undefined else s += v.toLocaleString(); // all others }); return s + "}"; }, // Convert a set to an array of values toArray: function() { var a = []; this.foreach(function(v) { a.push(v); }); return a; } }); // Treat sets like arrays for the purposes of JSON stringification. Set.prototype.toJSON = Set.prototype.toArray;
9.6.4 Comparison Methods JavaScript equality operators compare objects by reference, not by value. That is, given two object references, they look to see if both references are to the same object. They do not check to see if two different objects have the same property names and values. It is often useful to be able to compare two distinct objects for equality or even for relative order (as the < and > operators do). If you define a class and want to be able to compare instances of that class, you should define appropriate methods to perform those comparisons. The Java programming language uses methods for object comparison, and adopting the Java conventions is a common and useful thing to do in JavaScript. To enable instances of your class to be tested for equality, define an instance method named equals(). It should take a single argument and return true if that argument is equal to the object it is invoked on. Of course it is up to you to decide what “equal” means in the context of your own class. For simple classes you can often simply compare the constructor properties to ensure that the two objects are of the same type and then compare the instance properties of the two objects to ensure that they have the same values. The Complex class in Example 9-3 has an equals() method of this sort, and we can easily write a similar one for the Range class: 9.6 Object-Oriented Techniques in JavaScript | 221
Core JavaScript
can do that with code like the following. Note the use of the extend() function (Example 6-2) to add methods to Set.prototype:
// The Range class overwrote its constructor property. So add it now. Range.prototype.constructor = Range; // A Range is not equal to any nonrange. // Two ranges are equal if and only if their endpoints are equal. Range.prototype.equals = function(that) { if (that == null) return false; // Reject null and undefined if (that.constructor !== Range) return false; // Reject non-ranges // Now return true if and only if the two endpoints are equal. return this.from == that.from && this.to == that.to; }
Defining an equals() method for our Set class is somewhat trickier. We can’t just compare the values property of two sets but must perform a deeper comparison: Set.prototype.equals = function(that) { // Shortcut for trivial case if (this === that) return true; // // // // // if
If the that object is not a set, it is not equal to this one. We use instanceof to allow any subclass of Set. We could relax this test if we wanted true duck-typing. Or we could strengthen it to check this.constructor == that.constructor Note that instanceof properly rejects null and undefined values (!(that instanceof Set)) return false;
// If two sets don't have the same size, they're not equal if (this.size() != that.size()) return false;
};
// Now check whether every element in this is also in that. // Use an exception to break out of the foreach if the sets are not equal. try { this.foreach(function(v) { if (!that.contains(v)) throw false; }); return true; // All elements matched: sets are equal. } catch (x) { if (x === false) return false; // An element in this is not in that. throw x; // Some other exception: rethrow it. }
It is sometimes useful to compare objects according to some ordering. That is, for some classes, it is possible to say that one instance is “less than” or “greater than” another instance. You might order Range object based on the value of their lower bound, for example. Enumerated types could be ordered alphabetically by name, or numerically by the associated value (assuming the associated value is a number). Set objects, on the other hand, do not really have a natural ordering. If you try to use objects with JavaScript’s relation operators, such as < and = 0
a == b
a.compareTo(b) == 0
a != b
a.compareTo(b) != 0
The Card class of Example 9-8 defines a compareTo() method of this kind, and we can write a similar method for the Range class to order ranges by their lower bound: Range.prototype.compareTo = function(that) { return this.from - that.from; };
Notice that the subtraction performed by this method correctly returns a value less than zero, equal to zero, or greater than zero, according to the relative order of the two Ranges. Because the Card.Rank enumeration in Example 9-8 has a valueOf() method, we could have used this same idiomatic trick in the compareTo() method of the Card class. The equals() methods above perform type checking on their argument and return false to indicate inequality if the argument is of the wrong type. The compareTo() method does not have any return value that indicates “those two values are not comparable,” so a compareTo() method that does type checking should typically throw an error when passed an argument of the wrong type. Notice that the compareTo() method we defined for the Range class above returns 0 when two ranges have the same lower bound. This means that as far as compareTo() is concerned, any two ranges that start at the same spot are equal. This definition of equality is inconsistent with the definition used by the equals() method, which requires both endpoints to match. Inconsistent notions of equality can be a pernicious source of bugs, and it is best to make your equals() and compareTo() methods consistent. Here is a revised compareTo() method for the Range class. It is consistent with equals() and also throws an error if called with an incomparable value:
9.6 Object-Oriented Techniques in JavaScript | 223
Core JavaScript
own choosing, you can (again, following Java convention) define a method named compareTo().
// Order ranges by lower bound, or upper bound if the lower bounds are equal. // Throws an error if passed a non-Range value. // Returns 0 if and only if this.equals(that). Range.prototype.compareTo = function(that) { if (!(that instanceof Range)) throw new Error("Can't compare a Range with " + that); var diff = this.from - that.from; // Compare lower bounds if (diff == 0) diff = this.to - that.to; // If equal, compare upper bounds return diff; };
One reason to define a compareTo() method for a class is so that arrays of instances of that class can be sorted. The Array.sort() method accepts as an optional argument a comparison function that uses the same return-value conventions as the compareTo() method. Given the compareTo() method shown above, it is easy to sort an array of Range objects with code like this: ranges.sort(function(a,b) { return a.compareTo(b); });
Sorting is important enough that you should consider defining this kind of twoargument comparison function as a class method for any class for which you define a compareTo() instance method. One can easily be defined in terms of the other. For example: Range.byLowerBound = function(a,b) { return a.compareTo(b); };
With a method like this defined, sorting becomes simpler: ranges.sort(Range.byLowerBound);
Some classes can be ordered in more than one way. The Card class, for example, defines one class method that orders cards by suit and another that orders them by rank.
9.6.5 Borrowing Methods There is nothing special about methods in JavaScript: they are simply functions assigned to object properties and invoked “through” or “on” an object. A single function can be assigned to two properties, and it then serves as two methods. We did this for our Set class, for example, when we copied the toArray() method and made it do dualduty as a toJSON() method as well. A single function can even be used as a method of more than one class. Most of the built-in methods of the Array class, for example, are defined generically, and if you define a class whose instances are array-like objects, you can copy functions from Array.prototype to the prototype object of your class. If you view JavaScript through the lens of classical object-oriented languages, the use of methods of one class as methods of another class can be thought of as a form of multiple inheritance. JavaScript is not a classical object-oriented language, however, and I prefer to describe this kind of method reuse using the informal term borrowing. It is not only Array methods that can be borrowed: we can write our own generic methods. Example 9-9 defines generic toString() and equals() methods that are suit-
224 | Chapter 9: Classes and Modules
Range.prototype.equals = generic.equals;
Note that the generic.equals() method does only a shallow comparison, and it is not suitable for use with classes whose instance properties refer to objects with their own equals() methods. Also notice that this method includes special case code to handle the property added to objects when they are inserted into a Set (Example 9-6). Example 9-9. Generic methods for borrowing var generic = { // Returns a string that includes the name of the constructor function // if available and the names and values of all noninherited, nonfunction // properties. toString: function() { var s = '['; // If the object has a constructor and the constructor has a name, // use that class name as part of the returned string. Note that // the name property of functions is nonstandard and not supported // everywhere. if (this.constructor && this.constructor.name) s += this.constructor.name + ": ";
},
};
// Now enumerate all noninherited, nonfunction properties var n = 0; for(var name in this) { if (!this.hasOwnProperty(name)) continue; // skip inherited props var value = this[name]; if (typeof value === "function") continue; // skip methods if (n++) s += ", "; s += name + '=' + value; } return s + ']'; // Tests for equality by comparing the constructors and instance properties // of this and that. Only works for classes whose instance properties are // primitive values that can be compared with ===. // As a special case, ignore the special property added by the Set class. equals: function(that) { if (that == null) return false; if (this.constructor !== that.constructor) return false; for(var name in this) { if (name === "|**objectid**|") continue; // skip special prop. if (!this.hasOwnProperty(name)) continue; // skip inherited if (this[name] !== that[name]) return false; // compare values } return true; // If all properties matched, objects are equal. }
9.6 Object-Oriented Techniques in JavaScript | 225
Core JavaScript
able for use by simple classes like our Range, Complex, and Card classes. If the Range class did not have an equals() method, we could borrow the generic equals() like this:
9.6.6 Private State In classical object-oriented programming, it is often a goal to encapsulate or hide the state of an object within the object, allowing access to that state only through the methods of the object, and now allowing the important state variables to be read or written directly. To achieve this goal, languages like Java allow the declaration of “private” instance fields of a class that are only accessible to the instance method of the class and cannot be seen outside of the class. We can approximate private instance fields using variables (or arguments) captured in the closure of the constructor invocation that creates an instance. To do this, we define functions inside the constructor (so they have access to the constructor’s arguments and variables) and assign those functions to properties of the newly created object. Example 9-10 shows how we can do this to create an encapsulated version of our Range class. Instead of having from and to properties that give the endpoints of the range, instances of this new version of the class have from and to methods that return the endpoints of the range. These from() and to() methods are defined on the individual Range object and are not inherited from the prototype. The other Range methods are defined on the prototype as usual, but modified to call the from() and to() methods rather than read the endpoints directly from properties. Example 9-10. A Range class with weakly encapsulated endpoints function Range(from, to) { // Don't store the endpoints as properties of this object. Instead // define accessor functions that return the endpoint values. // These values are stored in the closure. this.from = function() { return from; }; this.to = function() { return to; }; } // The methods on the prototype can't see the endpoints directly: they have // to invoke the accessor methods just like everyone else. Range.prototype = { constructor: Range, includes: function(x) { return this.from()