scoping in CoffeeScript

6 min read Original article ↗
# This code demonstrates how CS scoping works within a file. Scroll down to the end to # see how cross-file scoping works. # Here are the rules. # # Scoping is all lexical. Read the file from top to bottom to determine variable scopes. # # 1) When you encounter any variable in the top-level nesting, its scope is top level. # 2) Inside a function, if you encounter a variable name that still exists in an outer scope, then that # variable name refers to the variable in the outer scope. (This is "closure".) # 3) Inside a function, if you encounter a variable name that does not exist in an inner scope, then a # new local variable is created, and its scope only gets hoisted to the top of the immediately # enclosing functions. # 4) Top level variables go out of scope when you reach the bottom of the file. # 5) Function-scoped variables go out of scope when you reach the lexical end of the function. (As a # consequence, two lexically independent functions in a file can reuse common names like "i", "s", # etc. without side effects.) # 6) Scoping rules don't change according to casing: bar, Bar, BAR, and $bar all have the same # rules. # # If the rules above are hard to grok, then it's probably best to either experiment yourself or to # simply learn by example. BANNER = '\n--------------' # BASIC SCOPING: independent functions can declare local variables. # # The first example should not be surprising to most folks. The variables "a" in f1 are f2 # are not shared. f1 = -> console.log BANNER console.log a # undefined a = 1 console.log a # 1 # end of a's scope f2 = -> console.log BANNER console.log a # undefined a = 2 console.log a # 2 # end of a's scope # f1 and f2 are still in scope f1() # calls the f1 we defined above f2() # calls the f1 we defined above # there is no "a" at outer scope console.log BANNER console.log "Is a in scope? #{a?}" # false # CLOSURES: functions can easily alias variables in outer lexical scope # # CS has normal closure behavior Accumulator = (seed) -> initial_value = seed * 2 return -> initial_value += 1 # refers to initial_value above return initial_value # If you want to execute a few statements inside a new scope, CS lets # you create one with its "do ->" idiom. It translates to this is JS: # (function() { // stuff })(); do -> # new scope here, so we don't pollute the top level namespace console.log BANNER f_incr = Accumulator(5) # Accumulator refers to same Accumulator above console.log f_incr() # 11 console.log f_incr() # 12 # f_incr falls out of scope console.log f_incr? # false, out of scope # CLASSES: "this" is explicit, unlike Ruby self # # CS does not conflate local variables with instance variables. class Person constructor: (name) -> this.name = name add_friend: (name) -> # name and this.name are two different concepts, obviously console.log "#{name} and #{this.name} are friends" do -> console.log BANNER person = new Person("alice") person.add_friend("bob") # bob and alice are friends # At this point, we are back at top-level scope, and these variables are # defined: console.log BANNER # same string as above console.log f1, f2, Accumulator, Person # [Function] [Function] [Function] [Function: Person] console.log person? # false, person is not in scope # SHARED VARIABLES: CS is for consenting adults. # # We can define a new variable PLANET that is available at all scopes below it: PLANET = "Earth" do -> console.log BANNER f = -> g = -> h = -> mars = "Mars" # local variable # PLANET is already in scope, so it stays in its existing scope. PLANET = mars # hey, we're tired of Earth return PLANET return h return g console.log f()()() # Mars console.log PLANET # Mars (our call to the innermost function was touching the top-level PLANET) # Back out at top-level scope, f, g, h, and mars no longer exist console.log f?, g?, h?, mars? # all false # GOTCHAS?? # # Try to be tricky, and create subtle coupling at the top level by slyly putting a # variable into top-level scope without assigning to it first. try console.log BANNER console.log should_be_local # will be undefined catch e console.log "We got a ReferenceError, so no subtle bugs: #{e}" # GOOD PRACTICES: Namespace your top-level variables. Tree = root: "thing that goes into the ground to get minerals" bark: "protective covering" Dog = root: -> console.log "dog is rooting around for foot" bark: -> bark = "RUFF!!!" # no ambiguity with Dog.bark, this is just a local console.log bark # No ambiguity here. console.log BANNER console.log Tree.root console.log Tree.bark Dog.root() Dog.bark() # RUFF!!!! # Hopefully, this covers 99% of the scoping situations you're likely to create in your own codebase. # # Some final advice: # 1) Use descriptive names to avoid unintentional naming collisions. # 2) Use objects like Tree and Dog to create namespaces. # 3) Use functions and "do" to create smaller scopes. # 4) Use naming conventions for top-level variables, especially classes and constants. # Cross-file scoping. # # You don't have to worry about naming collisions between multiple files in CoffeeScript. # CoffeeScript automatically wraps all files in a function closure, which means all variables # inside a file, even top level variables, are scoped to the function wrapper. # # Of course, there are occasions when you need scoping to cross file boundaries. For very # small projects, you can use the -b option of the compiler to suppress the function closure # wrappers, but this is very brittle. The preferred approach is to selectively add variables # to exports or window. I'm not going to cover that here, because the details of how you # do that are really more of a Javascript issue than a Coffeescript issue, and the solutions # largely depend on the size of your project.