Created
December 22, 2011 22:20
-
-
Save showell/1512100 to your computer and use it in GitHub Desktop.
scoping in CoffeeScript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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. | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment