- 快召唤伙伴们来围观吧
- 微博 QQ QQ空间 贴吧
- 文档嵌入链接
- 复制
- 微信扫一扫分享
- 已成功复制到剪贴板
你不知道的JavaScript(英文)
展开查看详情
1 .
2 .
3 .Scope and Closures Kyle Simpson
4 .Scope and Closures by Kyle Simpson Copyright © 2014 Getify Solutions, Inc. 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 corporate@oreilly.com. Editors: Simon St. Laurent and Brian Mac‐ Cover Designer: Karen Montgomery Donald Interior Designer: David Futato Production Editor: Melanie Yarbrough Illustrator: Rebecca Demarest Proofreader: Linley Dolby March 2014: First Edition Revision History for the First Edition: 2014-03-06: First release See http://oreilly.com/catalog/errata.csp?isbn=9781449335588 for release details. Nutshell Handbook, the Nutshell Handbook logo, and the O’Reilly logo are registered trademarks of O’Reilly Media, Inc. You Don’t Know JavaScript: Scope and Closures, and related trade dress are trademarks of O’Reilly Media, Inc. Many of the designations used by manufacturers and sellers to distinguish their prod‐ ucts 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 author assume no responsibility for errors or omissions, or for damages resulting from the use of the information contained herein. ISBN: 978-1-449-33558-8 [LSI]
5 . Table of Contents Foreword. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . v Preface. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . vii 1. What Is Scope?. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 Compiler Theory 1 Understanding Scope 3 Nested Scope 8 Errors 10 Review 11 2. Lexical Scope. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 Lex-time 13 Cheating Lexical 16 Review 21 3. Function Versus Block Scope. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 Scope From Functions 23 Hiding in Plain Scope 24 Functions as Scopes 28 Blocks as Scopes 33 Review 39 4. Hoisting. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 Chicken or the Egg? 41 The Compiler Strikes Again 42 Functions First 44 iii
6 . Review 46 5. Scope Closure. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 Enlightenment 47 Nitty Gritty 48 Now I Can See 51 Loops and Closure 53 Modules 56 Review 63 A. Dynamic Scope. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 B. Polyfilling Block Scope. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 C. Lexical this. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 D. Acknowledgments. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 iv | Table of Contents
7 . Foreword When I was a young child, I would often enjoy taking things apart and putting them back together again—old mobile phones, hi-fi stereos, and anything else I could get my hands on. I was too young to really use these devices, but whenever one broke, I would instantly ask if I could figure out how it worked. I remember once looking at a circuit board for an old radio. It had this weird long tube with copper wire wrapped around it. I couldn’t work out its purpose, but I immediately went into research mode. What does it do? Why is it in a radio? It doesn’t look like the other parts of the circuit board, why? Why does it have copper wrapped around it? What happens if I remove the copper?! Now I know it was a loop antenna, made by wrapping copper wire around a ferrite rod, which are often used in transistor radios. Did you ever become addicted to figuring out all of the answers to every why question? Most children do. In fact it is probably my favorite thing about children—their desire to learn. Unfortunately, now I’m considered a professional and spend my days making things. When I was young, I loved the idea of one day making the things that I took apart. Of course, most things I make now are with JavaScript and not ferrite rods…but close enough! However, de‐ spite once loving the idea of making things, I now find myself longing for the desire to figure things out. Sure, I often figure out the best way to solve a problem or fix a bug, but I rarely take the time to question my tools. And that is exactly why I am so excited about this “You Don’t Know JS” series of books. Because it’s right. I don’t know JS. I use JavaScript v
8 .day in, day out and have done for many years, but do I really under‐ stand it? No. Sure, I understand a lot of it and I often read the specs and the mailing lists, but no, I don’t understand as much as my inner six-year-old wishes I did. Scope and Closures is a brilliant start to the series. It is very well targeted at people like me (and hopefully you, too). It doesn’t teach JavaScript as if you’ve never used it, but it does make you realize how little about the inner workings you probably know. It is also coming out at the perfect time: ES6 is finally settling down and implementation across browsers is going well. If you’ve not yet made time for learning the new features (such as let and const), this book will be a great intro‐ duction. So I hope that you enjoy this book, but moreso, that Kyle’s way of critically thinking about how every tiny bit of the language works will creep into your mindset and general workflow. Instead of just using the antenna, figure out how and why it works. —Shane Hudson www.shanehudson.net vi | Foreword
9 . Preface I’m sure you noticed, but “JS” in the book series title is not an abbre‐ viation for words used to curse about JavaScript, though cursing at the language’s quirks is something we can probably all identify with! From the earliest days of the Web, JavaScript has been a foundational technology that drives interactive experience around the content we consume. While flickering mouse trails and annoying pop-up prompts may be where JavaScript started, nearly two decades later, the technology and capability of JavaScript has grown many orders of magnitude, and few doubt its importance at the heart of the world’s most widely available software platform: the Web. But as a language, it has perpetually been a target for a great deal of criticism, owing partly to its heritage but even more to its design phi‐ losophy. Even the name evokes, as Brendan Eich once put it, “dumb kid brother” status next to its more mature older brother, Java. But the name is merely an accident of politics and marketing. The two lan‐ guages are vastly different in many important ways. “JavaScript” is as related to “Java” as “Carnival” is to “Car.” Because JavaScript borrows concepts and syntax idioms from several languages, including proud C-style procedural roots as well as subtle, less obvious Scheme/Lisp-style functional roots, it is exceedingly ap‐ proachable to a broad audience of developers, even those with just little to no programming experience. The “Hello World” of JavaScript is so simple that the language is inviting and easy to get comfortable with in early exposure. While JavaScript is perhaps one of the easiest languages to get up and running with, its eccentricities make solid mastery of the language a vii
10 .vastly less common occurrence than in many other languages. Where it takes a pretty in-depth knowledge of a language like C or C++ to write a full-scale program, full-scale production JavaScript can, and often does, barely scratch the surface of what the language can do. Sophisticated concepts that are deeply rooted into the language tend instead to surface themselves in seemingly simplistic ways, such as passing around functions as callbacks, which encourages the Java‐ Script developer to just use the language as-is and not worry too much about what’s going on under the hood. It is simultaneously a simple, easy-to-use language that has broad ap‐ peal and a complex and nuanced collection of language mechanics that without careful study will elude true understanding even for the most seasoned of JavaScript developers. Therein lies the paradox of JavaScript, the Achilles’ heel of the lan‐ guage, the challenge we are presently addressing. Because JavaScript can be used without understanding, the understanding of the language is often never attained. Mission If at every point that you encounter a surprise or frustration in Java‐ Script, your response is to add it to the blacklist, as some are accus‐ tomed to doing, you soon will be relegated to a hollow shell of the richness of JavaScript. While this subset has been famoulsy dubbed “The Good Parts,” I would implore you, dear reader, to instead consider it the “The Easy Parts,” “The Safe Parts,” or even “The Incomplete Parts.” This “You Don’t Know JavaScript” book series offers a contrary chal‐ lenge: learn and deeply understand all of JavaScript, even and espe‐ cially “The Tough Parts.” Here, we address head on the tendency of JS developers to learn “just enough” to get by, without ever forcing themselves to learn exactly how and why the language behaves the way it does. Furthermore, we eschew the common advice to retreat when the road gets rough. I am not content, nor should you be, at stopping once something just works, and not really knowing why. I gently challenge you to journey down that bumpy “road less traveled” and embrace all that JavaScript is and can do. With that knowledge, no technique, no framework, no viii | Preface
11 .popular buzzword acronym of the week, will be beyond your under‐ standing. These books each take on specific core parts of the language that are most commonly misunderstood or under-understood, and dive very deep and exhaustively into them. You should come away from reading with a firm confidence in your understanding, not just of the theo‐ retical, but the practical “what you need to know” bits. The JavaScript you know right now is probably parts handed down to you by others who’ve been burned by incomplete understanding. That JavaScript is but a shadow of the true language. You don’t really know JavaScript, yet, but if you dig into this series, you will. Read on, my friends. JavaScript awaits you. Review JavaScript is awesome. It’s easy to learn partially, but much harder to learn completely (or even sufficiently). When developers encounter confusion, they usually blame the language instead of their lack of understanding. These books aim to fix that, inspiring a strong appre‐ ciation for the language you can now, and should, deeply know. Many of the examples in this book assume modern (and future- reaching) JavaScript engine environments, such as ECMA‐ Script version 6 (ES6). Some code may not work as described if run in older (pre-ES6) environments. Conventions Used in This Book The following typographical conventions are used in this book: Italic Indicates new terms, URLs, email addresses, filenames, and file extensions. Constant width Used for program listings, as well as within paragraphs to refer to program elements such as variable or function names, databases, data types, environment variables, statements, and keywords. Preface | ix
12 .Constant width bold Shows commands or other text that should be typed literally by the user. Constant width italic Shows text that should be replaced with user-supplied values or by values determined by context. This element signifies a tip or suggestion. This element signifies a general note. This element indicates a warning or caution. Using Code Examples Supplemental material (code examples, exercises, etc.) is available for download at http://bit.ly/1c8HEWF. This book is here to help you get your job done. In general, if example code is offered with this book, you may use it in your programs and documentation. You do not need to contact us 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 code does not re‐ quire permission. Incorporating a significant amount of example code from this book into your product’s documentation does require per‐ mission. x | Preface
13 .We appreciate, but do not require, attribution. An attribution usually includes the title, author, publisher, and ISBN. For example: “Scope and Closures by Kyle Simpson (O’Reilly). Copyright 2014 Kyle Simp‐ son, 978-1-449-33558-8.” If you feel your use of code examples falls outside fair use or the per‐ mission given above, feel free to contact us at permissions@oreilly.com. Safari® Books Online Safari Books Online is an on-demand digital li‐ brary that delivers expert content in both book and video form from the world’s leading authors in technology and business. Technology professionals, software developers, web designers, and business and creative professionals use Safari Books Online as their primary resource for research, problem solving, learning, and certif‐ ication training. Safari Books Online offers a range of product mixes and pricing pro‐ grams for organizations, government agencies, and individuals. Sub‐ scribers have access to thousands of books, training videos, and pre‐ publication manuscripts in one fully searchable database from pub‐ lishers like O’Reilly Media, Prentice Hall Professional, Addison- Wesley Professional, Microsoft Press, Sams, Que, Peachpit Press, Focal Press, Cisco Press, John Wiley & Sons, Syngress, Morgan Kaufmann, IBM Redbooks, Packt, Adobe Press, FT Press, Apress, Manning, New Riders, McGraw-Hill, Jones & Bartlett, Course Technology, and doz‐ ens more. For more information about Safari Books Online, please visit us online. How to Contact Us Please address comments and questions concerning this book to the publisher: O’Reilly Media, Inc. 1005 Gravenstein Highway North Sebastopol, CA 95472 800-998-9938 (in the United States or Canada) 707-829-0515 (international or local) 707-829-0104 (fax) Preface | xi
14 .We have a web page for this book, where we list errata, examples, and any additional information. You can access this page at http://oreil.ly/ JS_scope_and_closures. To comment or ask technical questions about this book, send email to bookquestions@oreilly.com. For more information about our books, courses, conferences, and news, 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 Check out the full You Don’t Know JS series: http://YouDont KnowJS.com xii | Preface
15 . CHAPTER 1 What Is Scope? One of the most fundamental paradigms of nearly all programming languages is the ability to store values in variables, and later retrieve or modify those values. In fact, the ability to store values and pull values out of variables is what gives a program state. Without such a concept, a program could perform some tasks, but they would be extremely limited and not terribly interesting. But the inclusion of variables into our program begets the most in‐ teresting questions we will now address: where do those variables live? In other words, where are they stored? And, most important, how does our program find them when it needs them? These questions speak to the need for a well-defined set of rules for storing variables in some location, and for finding those variables at a later time. We’ll call that set of rules: scope. But, where and how do these scope rules get set? Compiler Theory It may be self-evident, or it may be surprising, depending on your level of interaction with various languages, but despite the fact that Java‐ Script falls under the general category of “dynamic” or “interpreted” languages, it is in fact a compiled language. It is not compiled well in advance, as are many traditionally compiled languages, nor are the results of compilation portable among various distributed systems. 1
16 .But, nevertheless, the JavaScript engine performs many of the same steps, albeit in more sophisticated ways than we may commonly be aware, of any traditional language compiler. In traditional compiled-language process, a chunk of source code, your program, will undergo typically three steps before it is executed, roughly called “compilation”: Tokenizing/Lexing Breaking up a string of characters into meaningful (to the lan‐ guage) chunks, called tokens. For instance, consider the program var a = 2;. This program would likely be broken up into the following tokens: var, a, =, 2, and ;. Whitespace may or may not be persisted as a token, depending on whether its meaningful or not. The difference between tokenizing and lexing is subtle and academic, but it centers on whether or not these tokens are identified in a stateless or stateful way. Put simply, if the tokenizer were to invoke stateful parsing rules to fig‐ ure out whether a should be considered a distinct token or just part of another token, that would be lexing. Parsing taking a stream (array) of tokens and turning it into a tree of nested elements, which collectively represent the grammatical structure of the program. This tree is called an “AST” (abstract syntax tree). The tree for var a = 2; might start with a top-level node called VariableDeclaration, with a child node called Identifier (whose value is a), and another child called AssignmentExpres sion, which itself has a child called NumericLiteral (whose value is 2). Code-Generation The process of taking an AST and turning it into executable code. This part varies greatly depending on the language, the platform it’s targeting, and so on. So, rather than get mired in details, we’ll just handwave and say that there’s a way to take our previously described AST for var a = 2; and turn it into a set of machine instructions to actually create 2 | Chapter 1: What Is Scope?
17 . a variable called a (including reserving memory, etc.), and then store a value into a. The details of how the engine manages system resources are deeper than we will dig, so we’ll just take it for gran‐ ted that the engine is able to create and store variables as needed. The JavaScript engine is vastly more complex than just those three steps, as are most other language compilers. For instance, in the process of parsing and code-generation, there are certainly steps to optimize the performance of the execution, including collapsing re‐ dundant elements, etc. So, I’m painting only with broad strokes here. But I think you’ll see shortly why these details we do cover, even at a high level, are relevant. For one thing, JavaScript engines don’t get the luxury (like other lan‐ guage compilers) of having plenty of time to optimize, because Java‐ Script compilation doesn’t happen in a build step ahead of time, as with other languages. For JavaScript, the compilation that occurs happens, in many cases, mere microseconds (or less!) before the code is executed. To ensure the fastest performance, JS engines use all kinds of tricks (like JITs, which lazy compile and even hot recompile, etc.) that are well beyond the “scope” of our discussion here. Let’s just say, for simplicity sake, that any snippet of JavaScript has to be compiled before (usually right before!) it’s executed. So, the JS com‐ piler will take the program var a = 2; and compile it first, and then be ready to execute it, usually right away. Understanding Scope The way we will approach learning about scope is to think of the pro‐ cess in terms of a conversation. But, who is having the conversation? The Cast Let’s meet the cast of characters that interact to process the program var a = 2;, so we understand their conversations that we’ll listen in on shortly: Understanding Scope | 3
18 .Engine Responsible for start-to-finish compilation and execution of our JavaScript program. Compiler One of Engine’s friends; handles all the dirty work of parsing and code-generation (see previous section). Scope Another friend of Engine; collects and maintains a look-up list of all the declared identifiers (variables), and enforces a strict set of rules as to how these are accessible to currently executing code. For you to fully understand how JavaScript works, you need to begin to think like Engine (and friends) think, ask the questions they ask, and answer those questions the same. Back and Forth When you see the program var a = 2;, you most likely think of that as one statement. But that’s not how our new friend Engine sees it. In fact, Engine sees two distinct statements, one that Compiler will handle during compilation, and one that Engine will handle during execution. So, let’s break down how Engine and friends will approach the program var a = 2;. The first thing Compiler will do with this program is perform lexing to break it down into tokens, which it will then parse into a tree. But when Compiler gets to code generation, it will treat this program somewhat differently than perhaps assumed. A reasonable assumption would be that Compiler will produce code that could be summed up by this pseudocode: “Allocate memory for a variable, label it a, then stick the value 2 into that variable.” Unfortu‐ nately, that’s not quite accurate. Compiler will instead proceed as: 1. Encountering var a, Compiler asks Scope to see if a variable a already exists for that particular scope collection. If so, Compiler ignores this declaration and moves on. Otherwise, Compiler asks Scope to declare a new variable called a for that scope collection. 2. Compiler then produces code for Engine to later execute, to han‐ dle the a = 2 assignment. The code Engine runs will first ask Scope 4 | Chapter 1: What Is Scope?
19 . if there is a variable called a accessible in the current scope col‐ lection. If so, Engine uses that variable. If not, Engine looks else‐ where (see “Nested Scope” on page 8). If Engine eventually finds a variable, it assigns the value 2 to it. If not, Engine will raise its hand and yell out an error! To summarize: two distinct actions are taken for a variable assignment: First, Compiler declares a variable (if not previously declared) in the current Scope, and second, when executing, Engine looks up the vari‐ able in Scope and assigns to it, if found. Compiler Speak We need a little bit more compiler terminology to proceed further with understanding. When Engine executes the code that Compiler produced for step 2, it has to look up the variable a to see if it has been declared, and this look-up is consulting Scope. But the type of look-up Engine performs affects the outcome of the look-up. In our case, it is said that Engine would be performing an LHS look- up for the variable a. The other type of look-up is called RHS. I bet you can guess what the “L” and “R” mean. These terms stand for lefthand side and righthand side. Side…of what? Of an assignment operation. In other words, an LHS look-up is done when a variable appears on the lefthand side of an assignment operation, and an RHS look-up is done when a variable appears on the righthand side of an assignment operation. Actually, let’s be a little more precise. An RHS look-up is indistin‐ guishable, for our purposes, from simply a look-up of the value of some variable, whereas the LHS look-up is trying to find the variable con‐ tainer itself, so that it can assign. In this way, RHS doesn’t really mean “righthand side of an assignment” per se, it just, more accurately, means “not lefthand side”. Being slightly glib for a moment, you could think RHS instead means “retrieve his/her source (value),” implying that RHS means “go get the value of…” Understanding Scope | 5
20 .Let’s dig into that deeper. When I say: console.log( a ); The reference to a is an RHS reference, because nothing is being as‐ signed to a here. Instead, we’re looking up to retrieve the value of a, so that the value can be passed to console.log(..). By contrast: a = 2; The reference to a here is an LHS reference, because we don’t actually care what the current value is, we simply want to find the variable as a target for the = 2 assignment operation. LHS and RHS meaning “left/righthand side of an assigment” doesn’t necessarily literally mean “left/right side of the = as‐ signment operator.” There are several other ways that assign‐ ments happen, and so it’s better to conceptually think about it as: “Who’s the target of the assignment (LHS)?” and “Who’s the source of the assignment (RHS)?” Consider this program, which has both LHS and RHS references: function foo(a) { console.log( a ); // 2 } foo( 2 ); The last line that invokes foo(..) as a function call requires an RHS reference to foo, meaning, “Go look up the value of foo, and give it to me.” Moreover, (..) means the value of foo should be executed, so it’d better actually be a function! There’s a subtle but important assignment here. You may have missed the implied a = 2 in this code snippet. It happens when the value 2 is passed as an argument to the foo(..) function, in which case the 2 value is assigned to the parameter a. To (implicitly) assign to parameter a, an LHS look-up is performed. There’s also an RHS reference for the value of a, and that resulting value is passed to console.log(..). console.log(..) needs a 6 | Chapter 1: What Is Scope?
21 .reference to execute. It’s an RHS look-up for the console object, then a property resolution occurs to see if it has a method called log. Finally, we can conceptualize that there’s an LHS/RHS exchange of passing the value 2 (by way of variable a’s RHS look-up) into log(..). Inside of the native implementation of log(..), we can as‐ sume it has parameters, the first of which (perhaps called arg1) has an LHS reference look-up, before assigning 2 to it. You might be tempted to conceptualize the function declara‐ tion function foo(a) {… as a normal variable declaration and assignment, such as var foo and foo = function(a){…. In so doing, it would be tempting to think of this function declara‐ tion as involving an LHS look-up. However, the subtle but important difference is that Compil‐ er handles both the declaration and the value definition dur‐ ing code-generation, such that when Engine is executing code, there’s no processing necessary to “assign” a function value to foo. Thus, it’s not really appropriate to think of a function declaration as an LHS look-up assignment in the way we’re discussing them here. Engine/Scope Conversation function foo(a) { console.log( a ); // 2 } foo( 2 ); Let’s imagine the above exchange (which processes this code snippet) as a conversation. The conversation would go a little something like this: Engine: Hey Scope, I have an RHS reference for foo. Ever heard of it? Scope: Why yes, I have. Compiler declared it just a second ago. It’s a function. Here you go. Engine: Great, thanks! OK, I’m executing foo. Engine: Hey, Scope, I’ve got an LHS reference for a, ever heard of it? Scope: Why yes, I have. Compiler declared it as a formal parameter to foo just recently. Here you go. Engine: Helpful as always, Scope. Thanks again. Now, time to assign 2 to a. Understanding Scope | 7
22 . Engine: Hey, Scope, sorry to bother you again. I need an RHS look- up for console. Ever heard of it? Scope: No problem, Engine, this is what I do all day. Yes, I’ve got console. It’s built-in. Here ya go. Engine: Perfect. Looking up log(..). OK, great, it’s a function. Engine: Yo, Scope. Can you help me out with an RHS reference to a. I think I remember it, but just want to double-check. Scope: You’re right, Engine. Same variable, hasn’t changed. Here ya go. Engine: Cool. Passing the value of a, which is 2, into log(..). … Quiz Check your understanding so far. Make sure to play the part of Engine and have a “conversation” with Scope: function foo(a) { var b = a; return a + b; } var c = foo( 2 ); 1. Identify all the LHS look-ups (there are 3!). 2. Identify all the RHS look-ups (there are 4!). See the chapter review for the quiz answers! Nested Scope We said that Scope is a set of rules for looking up variables by their identifier name. There’s usually more than one scope to consider, however. Just as a block or function is nested inside another block or function, scopes are nested inside other scopes. So, if a variable cannot be found in the immediate scope, Engine consults the next outercontaining 8 | Chapter 1: What Is Scope?
23 .scope, continuing until is found or until the outermost (a.k.a., global) scope has been reached. Consider the following: function foo(a) { console.log( a + b ); } var b = 2; foo( 2 ); // 4 The RHS reference for b cannot be resolved inside the function foo, but it can be resolved in the scope surrounding it (in this case, the global). So, revisiting the conversations between Engine and Scope, we’d over‐ hear: Engine: “Hey, Scope of foo, ever heard of b? Got an RHS reference for it.” Scope: “Nope, never heard of it. Go fish.” Engine: “Hey, Scope outside of foo, oh you’re the global scope, OK cool. Ever heard of b? Got an RHS reference for it.” Scope: “Yep, sure have. Here ya go.” The simple rules for traversing nested scope: Engine starts at the cur‐ rently executing scope, looks for the variable there, then if not found, keeps going up one level, and so on. If the outermost global scope is reached, the search stops, whether it finds the variable or not. Building on Metaphors To visualize the process of nested scope resolution, I want you to think of this tall building: Nested Scope | 9
24 .The building represents our program’s nested scope ruleset. The first floor of the building represents your currently executing scope, wher‐ ever you are. The top level of the building is the global scope. You resolve LHS and RHS references by looking on your current floor, and if you don’t find it, taking the elevator to the next floor, looking there, then the next, and so on. Once you get to the top floor (the global scope), you either find what you’re looking for, or you don’t. But you have to stop regardless. Errors Why does it matter whether we call it LHS or RHS? Because these two types of look-ups behave differently in the circum‐ stance where the variable has not yet been declared (is not found in any consulted scope). Consider: 10 | Chapter 1: What Is Scope?
25 . function foo(a) { console.log( a + b ); b = a; } foo( 2 ); When the RHS look-up occurs for b the first time, it will not be found. This is said to be an “undeclared” variable, because it is not found in the scope. If an RHS look-up fails to ever find a variable, anywhere in the nested scopes, this results in a ReferenceError being thrown by the engine. It’s important to note that the error is of the type ReferenceError. By contrast, if the engine is performing an LHS look-up, and it arrives at the top floor (global scope) without finding it, if the program is not running in “Strict Mode,”1 then the global scope will create a new vari‐ able of that name in the global scope, and hand it back to Engine. “No, there wasn’t one before, but I was helpful and created one for you.” “Strict Mode,” which was added in ES5, has a number of different be‐ haviors from normal/relaxed/lazy mode. One such behavior is that it disallows the automatic/implicit global variable creation. In that case, there would be no global scoped variable to hand back from an LHS look-up, and Engine would throw a ReferenceError similarly to the RHS case. Now, if a variable is found for an RHS look-up, but you try to do something with its value that is impossible, such as trying to execute- as-function a nonfunction value, or reference a property on a null or undefined value, then Engine throws a different kind of error, called a TypeError. ReferenceError is scope resolution-failure related, whereas TypeEr ror implies that scope resolution was successful, but that there was an illegal/impossible action attempted against the result. Review Scope is the set of rules that determines where and how a variable (identifier) can be looked up. This look-up may be for the purposes of 1. See the MDN’s break down of Strict Mode Review | 11
26 .assigning to the variable, which is an LHS (lefthand-side) reference, or it may be for the purposes of retrieving its value, which is an RHS (righthand-side) reference. LHS references result from assignment operations. Scope-related as‐ signments can occur either with the = operator or by passing argu‐ ments to (assign to) function parameters. The JavaScript engine first compiles code before it executes, and in so doing, it splits up statements like var a = 2; into two separate steps: 1. First, var a to declare it in that scope. This is performed at the beginning, before code execution. 2. Later, a = 2 to look up the variable (LHS reference) and assign to it if found. Both LHS and RHS reference look-ups start at the currently executing scope, and if need be (that is, they don’t find what they’re looking for there), they work their way up the nested scope, one scope (floor) at a time, looking for the identifier, until they get to the global (top floor) and stop, and either find it, or don’t. Unfulfilled RHS references result in ReferenceErrors being thrown. Unfulfilled LHS references result in an automatic, implicitly created global of that name (if not in Strict Mode), or a ReferenceError (if in Strict Mode). Quiz Answers function foo(a) { var b = a; return a + b; } var c = foo( 2 ); 1. Identify all the LHS look-ups (there are 3!). c = ..;, a = 2 (implicit param assignment) and b = .. 2. Identify all the RHS look-ups (there are 4!). foo(2.., = a;, a .. and .. b 12 | Chapter 1: What Is Scope?
27 . CHAPTER 2 Lexical Scope In Chapter 1, we defined “scope” as the set of rules that govern how the engine can look up a variable by its identifier name and find it, either in the current scope, or in any of the nested scopes it’s contained within. There are two predominant models for how scope works. The first of these is by far the most common, used by the vast majority of pro‐ gramming languages. It’s called lexical scope, and we will examine it in depth. The other model, which is still used by some languages (such as Bash scripting, some modes in Perl, etc) is called dynamic scope. Dynamic scope is covered in Appendix A. I mention it here only to provide a contrast with lexical scope, which is the scope model that JavaScript employs. Lex-time As we discussed in Chapter 1, the first traditional phase of a standard language compiler is called lexing (a.k.a., tokenizing). If you recall, the lexing process examines a string of source code characters and assigns semantic meaning to the tokens as a result of some stateful parsing. It is this concept that provides the foundation to understand what lexical scope is and where the name comes from. To define it somewhat circularly, lexical scope is scope that is defined at lexing time. In other words, lexical scope is based on where variables and blocks of scope are authored, by you, at write time, and thus is (mostly) set in stone by the time the lexer processes your code. 13
28 . We will see in a little bit that there are some ways to cheat lexical scope, thereby modifying it after the lexer has passed by, but these are frowned upon. It is considered best practice to treat lexical scope as, in fact, lexical-only, and thus entirely author- time in nature. Let’s consider this block of code: function foo(a) { var b = a * 2; function bar(c) { console.log( a, b, c ); } bar( b * 3 ); } foo( 2 ); // 2, 4, 12 There are three nested scopes inherent in this code example. It may be helpful to think about these scopes as bubbles inside of each other. Bubble 1 encompasses the global scope and has just one identifier in it: foo. Bubble 2 encompasses the scope of foo, which includes the three identifiers: a, bar, and b. Bubble 3 encompasses the scope of bar, and it includes just one iden‐ tifier: c. Scope bubbles are defined by where the blocks of scope are written, which one is nested inside the other, etc. In the next chapter, we’ll 14 | Chapter 2: Lexical Scope
29 .discuss different units of scope, but for now, let’s just assume that each function creates a new bubble of scope. The bubble for bar is entirely contained within the bubble for foo, because (and only because) that’s where we chose to define the function bar. Notice that these nested bubbles are strictly nested. We’re not talking about Venn diagrams where the bubbles can cross boundaries. In other words, no bubble for some function can simultaneously exist (parti‐ ally) inside two other outer scope bubbles, just as no function can partially be inside each of two parent functions. Look-ups The structure and relative placement of these scope bubbles fully ex‐ plains to the engine all the places it needs to look to find an identifier. In the previous code snippet, the engine executes the con sole.log(..) statement and goes looking for the three referenced variables a, b, and c. It first starts with the innermost scope bubble, the scope of the bar(..) function. It won’t find a there, so it goes up one level, out to the next nearest scope bubble, the scope of foo(..). It finds a there, and so it uses that a. Same thing for b. But c, it does find inside of bar(..). Had there been a c both inside of bar(..) and inside of foo(..), the console.log(..) statement would have found and used the one in bar(..), never getting to the one in foo(..). Scope look-up stops once it finds the first match. The same identifier name can be specified at multiple layers of nested scope, which is called “shadowing” (the inner identifer “shadows” the outer identifier). Re‐ gardless of shadowing, scope look-up always starts at the innermost scope being executed at the time, and works its way outward/upward until the first match, and stops. Lex-time | 15