1.
2.
3.
i. CompilerTheory
ii. UnderstandingScope
iii. NestedScope
iv. Errors
4.
i. Lex-time
ii. CheatingLexical
5.
Chapter3:Functionvs.BlockScope
i. ScopeFromFunctions
ii. HidingInPlainScope
iii. FunctionsAsScopes
iv. BlocksAsScopes
6.
i. ChickenOrTheEgg?
ii. TheCompilerStrikesAgain
iii. FunctionsFirst
7.
i. Enlightenment
ii. NittyGritty
iii. NowICanSee
iv. Loops+Closure
v. Modules
8.
9.
AppendixB:PolyfillingBlockScope
10.
11.
TableofContents
YouDon'tKnowJS:Scope&Closures
2
ThisisaseriesofbooksdivingdeepintothecoremechanismsoftheJavaScriptlanguage.
PleasefeelfreetocontributetothequalityofthiscontentbysubmittingPR'sforimprovementstocodesnippets,
explanations,etc.Whiletypofixesarewelcomed,theywilllikelybecaughtthroughnormaleditingprocesses,andarethus
notnecessarilyasimportantforthisrepository.
Toreadmoreaboutthemotivationsandperspectivebehindthisbookseries,checkoutthe
.
Readonline(free!):
"Up&Going"
,Published:
,ebookformatisfree!
Readonline(free!):
"Scope&Closures"
Readonline(free!):
"this&ObjectPrototypes"
Readonline(free!):
"Types&Grammar"
,Published:
Readonline(free!):
"Async&Performance"
,Published:
Readonline(free!):
"ES6&Beyond"
(inproduction)
Thesebooksarebeingreleasedhereasdrafts,freetoread,butarealsobeingedited,produced,andpublishedthrough
O'Reilly.
Ifyoulikethecontentyoufindhere,andwanttosupportmorecontentlikeit,pleasepurchasethebooksoncetheyare
availableforsale,throughyournormalbooksources.:)
Ifyou'dliketocontributefinanciallytowardstheeffort(oranyofmyotherOSSwork)asidefrompurchasingthebooks,Ido
thatIwouldalwaysappreciateyourgenerositytowards.
ThecontentforthesebooksderivesheavilyfromaseriesoftrainingmaterialsIteachprofessionally(inbothpublicand
private-corporateworkshopformat),called"AdvancedJS:The'WhatYouNeedToKnow'Parts".
Ifyoulikethiscontentandwouldliketocontactmeregardingconductingtrainingonthese,orothervarious
JS/HTML5/node.jstopics,pleasereachouttomethroughanyofthesechannelslistedhere:
IalsohavesomeJStrainingmaterialavailableinon-demandvideoformat.Iteachcoursesthrough
,like
YouDon'tKnowJS(bookseries)
Titles
Publishing
In-personTraining
OnlineVideoTraining
YouDon'tKnowJS:Scope&Closures
3
Introduction
my
workshop(morecoursescomingsoon!).
.
Anycontributionsyoumaketothiseffortareofcoursegreatlyappreciated.
However,ifyouchoosetocontributecontent(notjusttypocorrections)tothisrepo,youagreethatyou'regivingmeanon-
exclusivelicensetousethatcontentforthebookseries,asI(andO'Reilly)deemappropriate.Youprobablyguessedthat
already,butwejusthavetomakethelawyershappybyexplicitlystatingit.
So:blah,blah,blah...:)
Thematerialshereinareall(c)2013-2015KyleSimpson.
CreativeCommonsAttribution-NonCommercial-NoDerivs3.0UnportedLicense
.
ContentContributions
License&Copyright
YouDon'tKnowJS:Scope&Closures
4
Introduction
I'msureyounoticed,but"JS"inthebookseriestitleisnotanabbreviationforwordsusedtocurseaboutJavaScript,
thoughcursingatthelanguage'squirksissomethingwecanprobablyallidentifywith!
Fromtheearliestdaysoftheweb,JavaScripthasbeenafoundationaltechnologythatdrivesinteractiveexperiencearound
thecontentweconsume.Whileflickeringmousetrailsandannoyingpop-uppromptsmaybewhereJavaScriptstarted,
nearly2decadeslater,thetechnologyandcapabilityofJavaScripthasgrownmanyordersofmagnitude,andfewdoubtits
importanceattheheartoftheworld'smostwidelyavailablesoftwareplatform:theweb.
Butasalanguage,ithasperpetuallybeenatargetforagreatdealofcriticism,owingpartlytoitsheritagebutevenmoreto
itsdesignphilosophy.Eventhenameevokes,asBrendanEichonceputit,"dumbkidbrother"statusnexttoitsmore
matureolderbrother"Java".Butthenameismerelyanaccidentofpoliticsandmarketing.Thetwolanguagesarevastly
differentinmanyimportantways."JavaScript"isasrelatedto"Java"as"Carnival"isto"Car".
BecauseJavaScriptborrowsconceptsandsyntaxidiomsfromseverallanguages,includingproudC-styleproceduralroots
aswellassubtle,lessobviousScheme/Lisp-stylefunctionalroots,itisexceedinglyapproachabletoabroadaudienceof
developers,eventhosewithjustlittletonoprogrammingexperience.The"HelloWorld"ofJavaScriptissosimplethatthe
languageisinvitingandeasytogetcomfortablewithinearlyexposure.
WhileJavaScriptisperhapsoneoftheeasiestlanguagestogetupandrunningwith,itseccentricitiesmakesolidmastery
ofthelanguageavastlylesscommonoccurrencethaninmanyotherlanguages.Whereittakesaprettyin-depth
knowledgeofalanguagelikeCorC++towriteafull-scaleprogram,full-scaleproductionJavaScriptcan,andoftendoes,
barelyscratchthesurfaceofwhatthelanguagecando.
Sophisticatedconceptswhicharedeeplyrootedintothelanguagetendinsteadtosurfacethemselvesinseemingly
simplisticways,suchaspassingaroundfunctionsascallbacks,whichencouragestheJavaScriptdevelopertojustusethe
languageas-isandnotworrytoomuchaboutwhat'sgoingonunderthehood.
Itissimultaneouslyasimple,easy-to-uselanguagethathasbroadappeal,andacomplexandnuancedcollectionof
languagemechanicswhichwithoutcarefulstudywilleludetrueunderstandingevenforthemostseasonedofJavaScript
developers.
ThereinliestheparadoxofJavaScript,theAchilles'Heelofthelanguage,thechallengewearepresentlyaddressing.
BecauseJavaScriptcanbeusedwithoutunderstanding,theunderstandingofthelanguageisoftenneverattained.
IfateverypointthatyouencounterasurpriseorfrustrationinJavaScript,yourresponseistoaddittotheblacklist,as
someareaccustomedtodoing,yousoonwillberelegatedtoahollowshelloftherichnessofJavaScript.
Whilethissubsethasbeenfamouslydubbed"TheGoodParts",Iwouldimploreyou,dearreader,toinsteadconsideritthe
"TheEasyParts","TheSafeParts",oreven"TheIncompleteParts".
ThisYouDon'tKnowJavaScriptbookseriesoffersacontrarychallenge:learnanddeeplyunderstandallofJavaScript,
evenandespecially"TheToughParts".
Here,weaddressheadonthetendencyofJSdeveloperstolearn"justenough"togetby,withouteverforcingthemselves
tolearnexactlyhowandwhythelanguagebehavesthewayitdoes.Furthermore,weeschewthecommonadviceto
retreatwhentheroadgetsrough.
Iamnotcontent,norshouldyoube,atstoppingoncesomethingjustworks,andnotreallyknowingwhy.Igentlychallenge
Preface
Mission
YouDon'tKnowJS:Scope&Closures
5
Preface
youtojourneydownthatbumpy"roadlesstraveled"andembraceallthatJavaScriptisandcando.Withthatknowledge,
notechnique,noframework,nopopularbuzzwordacronymoftheweek,willbebeyondyourunderstanding.
Thesebookseachtakeonspecificcorepartsofthelanguagewhicharemostcommonlymisunderstoodorunder-
understood,anddiveverydeepandexhaustivelyintothem.Youshouldcomeawayfromreadingwithafirmconfidencein
yourunderstanding,notjustofthetheoretical,butthepractical"whatyouneedtoknow"bits.
TheJavaScriptyouknowrightnowisprobablypartshandeddowntoyoubyotherswho'vebeenburnedbyincomplete
understanding.ThatJavaScriptisbutashadowofthetruelanguage.Youdon'treallyknowJavaScript,yet,butifyoudig
intothisseries,youwill.Readon,myfriends.JavaScriptawaitsyou.
JavaScriptisawesome.It'seasytolearnpartially,andmuchhardertolearncompletely(orevensufficiently).When
developersencounterconfusion,theyusuallyblamethelanguageinsteadoftheirlackofunderstanding.Thesebooksaim
tofixthat,inspiringastrongappreciationforthelanguageyoucannow,andshould,deeplyknow.
Note:Manyoftheexamplesinthisbookassumemodern(andfuture-reaching)JavaScriptengineenvironments,suchas
ES6.Somecodemaynotworkasdescribedifruninolder(pre-ES6)engines.
Summary
YouDon'tKnowJS:Scope&Closures
6
Preface
Oneofthemostfundamentalparadigmsofnearlyallprogramminglanguagesistheabilitytostorevaluesinvariables,and
laterretrieveormodifythosevalues.Infact,theabilitytostorevaluesandpullvaluesoutofvariablesiswhatgivesa
programstate.
Withoutsuchaconcept,aprogramcouldperformsometasks,buttheywouldbeextremelylimitedandnotterribly
interesting.
Buttheinclusionofvariablesintoourprogrambegetsthemostinterestingquestionswewillnowaddress:wheredothose
variableslive?Inotherwords,wherearetheystored?And,mostimportantly,howdoesourprogramfindthemwhenit
needsthem?
Thesequestionsspeaktotheneedforawell-definedsetofrulesforstoringvariablesinsomelocation,andforfinding
thosevariablesatalatertime.We'llcallthatsetofrules:Scope.
But,whereandhowdotheseScoperulesgetset?
Itmaybeself-evident,oritmaybesurprising,dependingonyourlevelofinteractionwithvariouslanguages,butdespite
thefactthatJavaScriptfallsunderthegeneralcategoryof"dynamic"or"interpreted"languages,itisinfactacompiled
language.Itisnotcompiledwellinadvance,asaremanytraditionally-compiledlanguages,noraretheresultsof
compilationportableamongvariousdistributedsystems.
But,nevertheless,theJavaScriptengineperformsmanyofthesamesteps,albeitinmoresophisticatedwaysthanwemay
commonlybeaware,ofanytraditionallanguage-compiler.
Intraditionalcompiled-languageprocess,achunkofsourcecode,yourprogram,willundergotypicallythreestepsbeforeit
isexecuted,roughlycalled"compilation":
1. Tokenizing/Lexing:breakingupastringofcharactersintomeaningful(tothelanguage)chunks,calledtokens.For
instance,considertheprogram:
vara=2;
.Thisprogramwouldlikelybebrokenupintothefollowingtokens:
var
,
a
,
=
,
2
,and
;
.Whitespacemayormaynotbepersistedasatoken,dependingonwhetherit'smeaningfulornot.
Note:Thedifferencebetweentokenizingandlexingissubtleandacademic,butitcentersonwhetherornotthese
tokensareidentifiedinastatelessorstatefulway.Putsimply,ifthetokenizerweretoinvokestatefulparsingrulesto
figureoutwhether
a
shouldbeconsideredadistincttokenorjustpartofanothertoken,thatwouldbelexing.
2. Parsing:takingastream(array)oftokensandturningitintoatreeofnestedelements,whichcollectivelyrepresentthe
grammaticalstructureoftheprogram.Thistreeiscalledan"AST"(AbstractSyntaxTree).
Thetreefor
vara=2;
mightstartwithatop-levelnodecalled
VariableDeclaration
,withachildnodecalled
Identifier
(whosevalueis
a
),andanotherchildcalled
AssignmentExpression
whichitselfhasachildcalled
NumericLiteral
(whosevalueis
2
).
3. Code-Generation:theprocessoftakinganASTandturningitintoexecutablecode.Thispartvariesgreatly
dependingonthelanguage,theplatformit'stargeting,etc.
So,ratherthangetmiredindetails,we'lljusthandwaveandsaythatthere'sawaytotakeourabovedescribedASTfor
vara=2;
andturnitintoasetofmachineinstructionstoactuallycreateavariablecalled
a
(includingreserving
memory,etc.),andthenstoreavalueinto
a
.
Chapter1:WhatisScope?
CompilerTheory
YouDon'tKnowJS:Scope&Closures
7
Chapter1:WhatisScope?
Note:Thedetailsofhowtheenginemanagessystemresourcesaredeeperthanwewilldig,sowe'lljusttakeitfor
grantedthattheengineisabletocreateandstorevariablesasneeded.
TheJavaScriptengineisvastlymorecomplexthanjustthosethreesteps,asaremostotherlanguagecompilers.For
instance,intheprocessofparsingandcode-generation,therearecertainlystepstooptimizetheperformanceofthe
execution,includingcollapsingredundantelements,etc.
So,I'mpaintingonlywithbroadstrokeshere.ButIthinkyou'llseeshortlywhythesedetailswedocover,evenatahigh
level,arerelevant.
Foronething,JavaScriptenginesdon'tgettheluxury(likeotherlanguagecompilers)ofhavingplentyoftimetooptimize,
becauseJavaScriptcompilationdoesn'thappeninabuildstepaheadoftime,aswithotherlanguages.
ForJavaScript,thecompilationthatoccurshappens,inmanycases,meremicroseconds(orless!)beforethecodeis
executed.Toensurethefastestperformance,JSenginesuseallkindsoftricks(likeJITs,whichlazycompileandevenhot
re-compile,etc.)whicharewellbeyondthe"scope"ofourdiscussionhere.
Let'sjustsay,forsimplicity'ssake,thatanysnippetofJavaScripthastobecompiledbefore(usuallyrightbefore!)it's
executed.So,theJScompilerwilltaketheprogram
vara=2;
andcompileitfirst,andthenbereadytoexecuteit,usually
rightaway.
Thewaywewillapproachlearningaboutscopeistothinkoftheprocessintermsofaconversation.But,whoishavingthe
conversation?
Let'smeetthecastofcharactersthatinteracttoprocesstheprogram
vara=2;
,soweunderstandtheirconversations
thatwe'lllisteninonshortly:
1. Engine:responsibleforstart-to-finishcompilationandexecutionofourJavaScriptprogram.
2. Compiler:oneofEngine'sfriends;handlesallthedirtyworkofparsingandcode-generation(seeprevioussection).
3. Scope:anotherfriendofEngine;collectsandmaintainsalook-uplistofallthedeclaredidentifiers(variables),and
enforcesastrictsetofrulesastohowtheseareaccessibletocurrentlyexecutingcode.
ForyoutofullyunderstandhowJavaScriptworks,youneedtobegintothinklikeEngine(andfriends)think,askthe
questionstheyask,andanswerthosequestionsthesame.
Whenyouseetheprogram
vara=2;
,youmostlikelythinkofthatasonestatement.Butthat'snothowournewfriend
Engineseesit.Infact,Engineseestwodistinctstatements,onewhichCompilerwillhandleduringcompilation,andone
whichEnginewillhandleduringexecution.
So,let'sbreakdownhowEngineandfriendswillapproachtheprogram
vara=2;
.
ThefirstthingCompilerwilldowiththisprogramisperformlexingtobreakitdownintotokens,whichitwillthenparseintoa
tree.ButwhenCompilergetstocode-generation,itwilltreatthisprogramsomewhatdifferentlythanperhapsassumed.
AreasonableassumptionwouldbethatCompilerwillproducecodethatcouldbesummedupbythispseudo-code:
"Allocatememoryforavariable,labelit
a
,thenstickthevalue
2
intothatvariable."Unfortunately,that'snotquite
UnderstandingScope
TheCast
Back&Forth
YouDon'tKnowJS:Scope&Closures
8
Chapter1:WhatisScope?
accurate.
Compilerwillinsteadproceedas:
1. Encountering
vara
,CompilerasksScopetoseeifavariable
a
alreadyexistsforthatparticularscopecollection.If
so,Compilerignoresthisdeclarationandmoveson.Otherwise,CompilerasksScopetodeclareanewvariablecalled
a
forthatscopecollection.
2. CompilerthenproducescodeforEnginetolaterexecute,tohandlethe
a=2
assignment.ThecodeEnginerunswill
firstaskScopeifthereisavariablecalled
a
accessibleinthecurrentscopecollection.Ifso,Engineusesthat
variable.Ifnot,Enginelookselsewhere(seenestedScopesectionbelow).
IfEngineeventuallyfindsavariable,itassignsthevalue
2
toit.Ifnot,Enginewillraiseitshandandyelloutanerror!
Tosummarize:twodistinctactionsaretakenforavariableassignment:First,Compilerdeclaresavariable(ifnotpreviously
declaredinthecurrentscope),andsecond,whenexecuting,EnginelooksupthevariableinScopeandassignstoit,if
found.
Weneedalittlebitmorecompilerterminologytoproceedfurtherwithunderstanding.
WhenEngineexecutesthecodethatCompilerproducedforstep(2),ithastolook-upthevariable
a
toseeifithasbeen
declared,andthislook-upisconsultingScope.Butthetypeoflook-upEngineperformsaffectstheoutcomeofthelook-up.
Inourcase,itissaidthatEnginewouldbeperformingan"LHS"look-upforthevariable
a
.Theothertypeoflook-upis
called"RHS".
Ibetyoucanguesswhatthe"L"and"R"mean.Thesetermsstandfor"Left-handSide"and"Right-handSide".
Side...ofwhat?Ofanassignmentoperation.
Inotherwords,anLHSlook-upisdonewhenavariableappearsontheleft-handsideofanassignmentoperation,andan
RHSlook-upisdonewhenavariableappearsontheright-handsideofanassignmentoperation.
Actually,let'sbealittlemoreprecise.AnRHSlook-upisindistinguishable,forourpurposes,fromsimplyalook-upofthe
valueofsomevariable,whereastheLHSlook-upistryingtofindthevariablecontaineritself,sothatitcanassign.Inthis
way,RHSdoesn'treallymean"right-handsideofanassignment"perse,itjust,moreaccurately,means"notleft-hand
side".
Beingslightlyglibforamoment,youcouldalsothink"RHS"insteadmeans"retrievehis/hersource(value)",implyingthat
RHSmeans"gogetthevalueof...".
Let'sdigintothatdeeper.
WhenIsay:
console
.log(a);
Thereferenceto
a
isanRHSreference,becausenothingisbeingassignedto
a
here.Instead,we'relooking-upto
retrievethevalueof
a
,sothatthevaluecanbepassedto
console.log(..)
.
Bycontrast:
CompilerSpeak
YouDon'tKnowJS:Scope&Closures
9
Chapter1:WhatisScope?
a=
2
;
Thereferenceto
a
hereisanLHSreference,becausewedon'tactuallycarewhatthecurrentvalueis,wesimplywantto
findthevariableasatargetforthe
=2
assignmentoperation.
Note:LHSandRHSmeaning"left/right-handsideofanassignment"doesn'tnecessarilyliterallymean"left/rightsideofthe
=
assignmentoperator".Thereareseveralotherwaysthatassignmentshappen,andsoit'sbettertoconceptuallythink
aboutitas:"who'sthetargetoftheassignment(LHS)"and"who'sthesourceoftheassignment(RHS)".
Considerthisprogram,whichhasbothLHSandRHSreferences:
function
foo
(
a
)
{
console
.log(a);
//2
}
foo(
2
);
Thelastlinethatinvokes
foo(..)
asafunctioncallrequiresanRHSreferenceto
foo
,meaning,"golook-upthevalueof
foo
,andgiveittome."Moreover,
(..)
meansthevalueof
foo
shouldbeexecuted,soit'dbetteractuallybeafunction!
There'sasubtlebutimportantassignmenthere.Didyouspotit?
Youmayhavemissedtheimplied
a=2
inthiscodesnippet.Ithappenswhenthevalue
2
ispassedasanargumentto
the
foo(..)
function,inwhichcasethe
2
valueisassignedtotheparameter
a
.To(implicitly)assigntoparameter
a
,an
LHSlook-upisperformed.
There'salsoanRHSreferenceforthevalueof
a
,andthatresultingvalueispassedto
console.log(..)
.
console.log(..)
needsareferencetoexecute.It'sanRHSlook-upforthe
console
object,thenaproperty-resolutionoccurstoseeifithas
amethodcalled
log
.
Finally,wecanconceptualizethatthere'sanLHS/RHSexchangeofpassingthevalue
2
(bywayofvariable
a
'sRHS
look-up)into
log(..)
.Insideofthenativeimplementationof
log(..)
,wecanassumeithasparameters,thefirstofwhich
(perhapscalled
arg1
)hasanLHSreferencelook-up,beforeassigning
2
toit.
Note:Youmightbetemptedtoconceptualizethefunctiondeclaration
functionfoo(a){...
asanormalvariable
declarationandassignment,suchas
varfoo
and
foo=function(a){...
.Insodoing,itwouldbetemptingtothinkofthis
functiondeclarationasinvolvinganLHSlook-up.
However,thesubtlebutimportantdifferenceisthatCompilerhandlesboththedeclarationandthevaluedefinitionduring
code-generation,suchthatwhenEngineisexecutingcode,there'snoprocessingnecessaryto"assign"afunctionvalueto
foo
.Thus,it'snotreallyappropriatetothinkofafunctiondeclarationasanLHSlook-upassignmentinthewaywe're
discussingthemhere.
function
foo
(
a
)
{
console
.log(a);
//2
}
foo(
2
);
Let'simaginetheaboveexchange(whichprocessesthiscodesnippet)asaconversation.Theconversationwouldgoa
littlesomethinglikethis:
Engine/ScopeConversation
YouDon'tKnowJS:Scope&Closures
10
Chapter1:WhatisScope?
Engine:HeyScope,IhaveanRHSreferencefor
foo
.Everheardofit?
Scope:Whyyes,Ihave.Compilerdeclareditjustasecondago.He'safunction.Hereyougo.
Engine:Great,thanks!OK,I'mexecuting
foo
.
Engine:Hey,Scope,I'vegotanLHSreferencefor
a
,everheardofit?
Scope:Whyyes,Ihave.Compilerdeclareditasaformalparameterto
foo
justrecently.Hereyougo.
Engine:Helpfulasalways,Scope.Thanksagain.Now,timetoassign
2
to
a
.
Engine:Hey,Scope,sorrytobotheryouagain.IneedanRHSlook-upfor
console
.Everheardofit?
Scope:Noproblem,Engine,thisiswhatIdoallday.Yes,I'vegot
console
.He'sbuilt-in.Hereyago.
Engine:Perfect.Lookingup
log(..)
.OK,great,it'safunction.
Engine:Yo,Scope.CanyouhelpmeoutwithanRHSreferenceto
a
.IthinkIrememberit,butjustwanttodouble-
check.
Scope:You'reright,Engine.Sameguy,hasn'tchanged.Hereyago.
Engine:Cool.Passingthevalueof
a
,whichis
2
,into
log(..)
.
...
Checkyourunderstandingsofar.MakesuretoplaythepartofEngineandhavea"conversation"withtheScope:
function
foo
(
a
)
{
var
b=a;
return
a+b;
}
var
c=foo(
2
);
1. IdentifyalltheLHSlook-ups(thereare3!).
2. IdentifyalltheRHSlook-ups(thereare4!).
Note:Seethechapterreviewforthequizanswers!
WesaidthatScopeisasetofrulesforlookingupvariablesbytheiridentifiername.There'susuallymorethanoneScope
toconsider,however.
Justasablockorfunctionisnestedinsideanotherblockorfunction,scopesarenestedinsideotherscopes.So,ifa
variablecannotbefoundintheimmediatescope,Engineconsultsthenextoutercontainingscope,continuinguntilfoundor
untiltheoutermost(aka,global)scopehasbeenreached.
Consider:
Quiz
NestedScope
YouDon'tKnowJS:Scope&Closures
11
Chapter1:WhatisScope?
function
foo
(
a
)
{
console
.log(a+b);
}
var
b=
2
;
foo(
2
);
//4
TheRHSreferencefor
b
cannotberesolvedinsidethefunction
foo
,butitcanberesolvedintheScopesurroundingit(in
thiscase,theglobal).
So,revisitingtheconversationsbetweenEngineandScope,we'doverhear:
Engine:"Hey,Scopeof
foo
,everheardof
b
?GotanRHSreferenceforit."
Scope:"Nope,neverheardofit.Gofish."
Engine:"Hey,Scopeoutsideof
foo
,ohyou'retheglobalScope,okcool.Everheardof
b
?GotanRHSreference
forit."
Scope:"Yep,surehave.Hereyago."
ThesimplerulesfortraversingnestedScope:EnginestartsatthecurrentlyexecutingScope,looksforthevariablethere,
thenifnotfound,keepsgoinguponelevel,andsoon.Iftheoutermostglobalscopeisreached,thesearchstops,whether
itfindsthevariableornot.
TovisualizetheprocessofnestedScoperesolution,Iwantyoutothinkofthistallbuilding.
Thebuildingrepresentsourprogram'snestedScoperuleset.Thefirstfloorofthebuildingrepresentsyourcurrently
executingScope,whereveryouare.ThetoplevelofthebuildingistheglobalScope.
BuildingonMetaphors
YouDon'tKnowJS:Scope&Closures
12
Chapter1:WhatisScope?
YouresolveLHSandRHSreferencesbylookingonyourcurrentfloor,andifyoudon'tfindit,takingtheelevatortothenext
floor,lookingthere,thenthenext,andsoon.Onceyougettothetopfloor(theglobalScope),youeitherfindwhatyou're
lookingfor,oryoudon't.Butyouhavetostopregardless.
WhydoesitmatterwhetherwecallitLHSorRHS?
Becausethesetwotypesoflook-upsbehavedifferentlyinthecircumstancewherethevariablehasnotyetbeendeclared
(isnotfoundinanyconsultedScope).
Consider:
function
foo
(
a
)
{
console
.log(a+b);
b=a;
}
foo(
2
);
WhentheRHSlook-upoccursfor
b
thefirsttime,itwillnotbefound.Thisissaidtobean"undeclared"variable,because
itisnotfoundinthescope.
IfanRHSlook-upfailstoeverfindavariable,anywhereinthenestedScopes,thisresultsina
ReferenceError
being
thrownbytheEngine.It'simportanttonotethattheerrorisofthetype
ReferenceError
.
Bycontrast,iftheEngineisperforminganLHSlook-up,anditarrivesatthetopfloor(globalScope)withoutfindingit,ifthe
programisnotrunningin"StrictMode"
,thentheglobalScopewillcreateanewvariableofthatnameinthe
globalscope,andhanditbacktoEngine.
"No,therewasn'tonebefore,butIwashelpfulandcreatedoneforyou."
"StrictMode"
,whichwasaddedinES5,hasanumberofdifferentbehaviorsfromnormal/relaxed/lazymode.
Onesuchbehavioristhatitdisallowstheautomatic/implicitglobalvariablecreation.Inthatcase,therewouldbenoglobal
Scope'dvariabletohandbackfromanLHSlook-up,andEnginewouldthrowa
ReferenceError
similarlytotheRHScase.
Now,ifavariableisfoundforanRHSlook-up,butyoutrytodosomethingwithitsvaluethatisimpossible,suchastryingto
execute-as-functionanon-functionvalue,orreferenceapropertyona
null
or
undefined
value,thenEnginethrowsa
differentkindoferror,calleda
TypeError
.
ReferenceError
isScoperesolution-failurerelated,whereas
TypeError
impliesthatScoperesolutionwassuccessful,but
thattherewasanillegal/impossibleactionattemptedagainsttheresult.
Scopeisthesetofrulesthatdetermineswhereandhowavariable(identifier)canbelooked-up.Thislook-upmaybefor
thepurposesofassigningtothevariable,whichisanLHS(left-hand-side)reference,oritmaybeforthepurposesof
retrievingitsvalue,whichisanRHS(right-hand-side)reference.
LHSreferencesresultfromassignmentoperations.Scope-relatedassignmentscanoccureitherwiththe
=
operatororby
passingargumentsto(assignto)functionparameters.
TheJavaScriptEnginefirstcompilescodebeforeitexecutes,andinsodoing,itsplitsupstatementslike
vara=2;
into
Errors
Review(TL;DR)
YouDon'tKnowJS:Scope&Closures
13
Chapter1:WhatisScope?
twoseparatesteps:
1. First,
vara
todeclareitinthatScope.Thisisperformedatthebeginning,beforecodeexecution.
2. Later,
a=2
tolookupthevariable(LHSreference)andassigntoitiffound.
BothLHSandRHSreferencelook-upsstartatthecurrentlyexecutingScope,andifneedbe(thatis,theydon'tfindwhat
they'relookingforthere),theyworktheirwayupthenestedScope,onescope(floor)atatime,lookingfortheidentifier,
untiltheygettotheglobal(topfloor)andstop,andeitherfindit,ordon't.
UnfulfilledRHSreferencesresultin
ReferenceError
sbeingthrown.UnfulfilledLHSreferencesresultinanautomatic,
implicitly-createdglobalofthatname(ifnotin"StrictMode"
function
foo
(
a
)
{
var
b=a;
return
a+b;
}
var
c=foo(
2
);
1. IdentifyalltheLHSlook-ups(thereare3!).
c=..
,
a=2
(implicitparamassignment)and
b=..
2. IdentifyalltheRHSlook-ups(thereare4!).
foo(2..
,
=a;
,
a+..
and
..+b
note-strictmode
QuizAnswers
YouDon'tKnowJS:Scope&Closures
14
Chapter1:WhatisScope?
InChapter1,wedefined"scope"asthesetofrulesthatgovernhowtheEnginecanlookupavariablebyitsidentifier
nameandfindit,eitherinthecurrentScope,orinanyoftheNestedScopesit'scontainedwithin.
Therearetwopredominantmodelsforhowscopeworks.Thefirstoftheseisbyfarthemostcommon,usedbythevast
majorityofprogramminglanguages.It'scalledLexicalScope,andwewillexamineitin-depth.Theothermodel,whichis
stillusedbysomelanguages(suchasBashscripting,somemodesinPerl,etc.)iscalledDynamicScope.
DynamicScopeiscoveredinAppendixA.ImentionithereonlytoprovideacontrastwithLexicalScope,whichisthe
scopemodelthatJavaScriptemploys.
AswediscussedinChapter1,thefirsttraditionalphaseofastandardlanguagecompileriscalledlexing(aka,tokenizing).
Ifyourecall,thelexingprocessexaminesastringofsourcecodecharactersandassignssemanticmeaningtothetokens
asaresultofsomestatefulparsing.
Itisthisconceptwhichprovidesthefoundationtounderstandwhatlexicalscopeisandwherethenamecomesfrom.
Todefineitsomewhatcircularly,lexicalscopeisscopethatisdefinedatlexingtime.Inotherwords,lexicalscopeisbased
onwherevariablesandblocksofscopeareauthored,byyou,atwritetime,andthusis(mostly)setinstonebythetimethe
lexerprocessesyourcode.
Note:Wewillseeinalittlebittherearesomewaystocheatlexicalscope,therebymodifyingitafterthelexerhaspassed
by,butthesearefrownedupon.Itisconsideredbestpracticetotreatlexicalscopeas,infact,lexical-only,andthusentirely
author-timeinnature.
Let'sconsiderthisblockofcode:
function
foo
(
a
)
{
var
b=a*
2
;
function
bar
(
c
)
{
console
.log(a,b,c);
}
bar(b*
3
);
}
foo(
2
);
//2412
Therearethreenestedscopesinherentinthiscodeexample.Itmaybehelpfultothinkaboutthesescopesasbubbles
insideofeachother.
Chapter2:LexicalScope
Lex-time
YouDon'tKnowJS:Scope&Closures
15
Chapter2:LexicalScope
Bubble1encompassestheglobalscope,andhasjustoneidentifierinit:
foo
.
Bubble2encompassesthescopeof
foo
,whichincludesthethreeidentifiers:
a
,
bar
and
b
.
Bubble3encompassesthescopeof
bar
,anditincludesjustoneidentifier:
c
.
Scopebubblesaredefinedbywheretheblocksofscopearewritten,whichoneisnestedinsidetheother,etc.Inthenext
chapter,we'lldiscussdifferentunitsofscope,butfornow,let'sjustassumethateachfunctioncreatesanewbubbleof
scope.
Thebubblefor
bar
isentirelycontainedwithinthebubblefor
foo
,because(andonlybecause)that'swherewechoseto
definethefunction
bar
.
Noticethatthesenestedbubblesarestrictlynested.We'renottalkingaboutVenndiagramswherethebubblescancross
boundaries.Inotherwords,nobubbleforsomefunctioncansimultaneouslyexist(partially)insidetwootherouterscope
bubbles,justasnofunctioncanpartiallybeinsideeachoftwoparentfunctions.
ThestructureandrelativeplacementofthesescopebubblesfullyexplainstotheEnginealltheplacesitneedstolookto
findanidentifier.
Intheabovecodesnippet,theEngineexecutesthe
console.log(..)
statementandgoeslookingforthethreereferenced
variables
a
,
b
,and
c
.Itfirststartswiththeinnermostscopebubble,thescopeofthe
bar(..)
function.Itwon'tfind
a
there,soitgoesuponelevel,outtothenextnearestscopebubble,thescopeof
foo(..)
.Itfinds
a
there,andsoituses
that
a
.Samethingfor
b
.But
c
,itdoesfindinsideof
bar(..)
.
Hadtherebeena
c
bothinsideof
bar(..)
andinsideof
foo(..)
,the
console.log(..)
statementwouldhavefoundand
usedtheonein
bar(..)
,nevergettingtotheonein
foo(..)
.
Scopelook-upstopsonceitfindsthefirstmatch.Thesameidentifiernamecanbespecifiedatmultiplelayersof
nestedscope,whichiscalled"shadowing"(theinneridentifier"shadows"theouteridentifier).Regardlessofshadowing,
scopelook-upalwaysstartsattheinnermostscopebeingexecutedatthetime,andworksitswayoutward/upwarduntilthe
firstmatch,andstops.
Note:Globalvariablesarealsoautomaticallypropertiesoftheglobalobject(
window
inbrowsers,etc.),soitispossibleto
referenceaglobalvariablenotdirectlybyitslexicalname,butinsteadindirectlyasapropertyreferenceoftheglobalobject.
Look-ups
YouDon'tKnowJS:Scope&Closures
16
Chapter2:LexicalScope
window
.a
Thistechniquegivesaccesstoaglobalvariablewhichwouldotherwisebeinaccessibleduetoitbeingshadowed.
However,non-globalshadowedvariablescannotbeaccessed.
Nomatterwhereafunctionisinvokedfrom,orevenhowitisinvoked,itslexicalscopeisonlydefinedbywherethe
functionwasdeclared.
Thelexicalscopelook-upprocessonlyappliestofirst-classidentifiers,suchasthe
a
,
b
,and
c
.Ifyouhadareferenceto
foo.bar.baz
inapieceofcode,thelexicalscopelook-upwouldapplytofindingthe
foo
identifier,butonceitlocatesthat
variable,objectproperty-accessrulestakeovertoresolvethe
bar
and
baz
properties,respectively.
Iflexicalscopeisdefinedonlybywhereafunctionisdeclared,whichisentirelyanauthor-timedecision,howcouldthere
possiblybeawayto"modify"(aka,cheat)lexicalscopeatrun-time?
JavaScripthastwosuchmechanisms.Bothofthemareequallyfrowned-uponinthewidercommunityasbadpracticesto
useinyourcode.Butthetypicalargumentsagainstthemareoftenmissingthemostimportantpoint:cheatinglexical
scopeleadstopoorerperformance.
BeforeIexplaintheperformanceissue,though,let'slookathowthesetwomechanismswork.
The
eval(..)
functioninJavaScripttakesastringasanargument,andtreatsthecontentsofthestringasifithadactually
beenauthoredcodeatthatpointintheprogram.Inotherwords,youcanprogrammaticallygeneratecodeinsideofyour
authoredcode,andrunthegeneratedcodeasifithadbeenthereatauthortime.
Evaluating
eval(..)
(punintended)inthatlight,itshouldbeclearhow
eval(..)
allowsyoutomodifythelexicalscope
environmentbycheatingandpretendingthatauthor-time(aka,lexical)codewasthereallalong.
Onsubsequentlinesofcodeafteran
eval(..)
hasexecuted,theEnginewillnot"know"or"care"thatthepreviouscodein
questionwasdynamicallyinterpretedandthusmodifiedthelexicalscopeenvironment.TheEnginewillsimplyperformits
lexicalscopelook-upsasitalwaysdoes.
Considerthefollowingcode:
function
foo
(
str,a
)
{
eval
(str);
//cheating!
console
.log(a,b);
}
var
b=
2
;
foo(
"varb=3;"
,
1
);
//1,3
Thestring
"varb=3;"
istreated,atthepointofthe
eval(..)
call,ascodethatwasthereallalong.Becausethatcode
happenstodeclareanewvariable
b
,itmodifiestheexistinglexicalscopeof
foo(..)
.Infact,asmentionedabove,this
codeactuallycreatesvariable
b
insideof
foo(..)
thatshadowsthe
b
thatwasdeclaredintheouter(global)scope.
Whenthe
console.log(..)
calloccurs,itfindsboth
a
and
b
inthescopeof
foo(..)
,andneverfindstheouter
b
.Thus,
weprintout"1,3"insteadof"1,2"aswouldhavenormallybeenthecase.
CheatingLexical
eval
YouDon'tKnowJS:Scope&Closures
17
Chapter2:LexicalScope
Note:Inthisexample,forsimplicity'ssake,thestringof"code"wepassinwasafixedliteral.Butitcouldeasilyhavebeen
programmaticallycreatedbyaddingcharacterstogetherbasedonyourprogram'slogic.
eval(..)
isusuallyusedto
executedynamicallycreatedcode,asdynamicallyevaluatingessentiallystaticcodefromastringliteralwouldprovideno
realbenefittojustauthoringthecodedirectly.
Bydefault,ifastringofcodethat
eval(..)
executescontainsoneormoredeclarations(eithervariablesorfunctions),this
actionmodifiestheexistinglexicalscopeinwhichthe
eval(..)
resides.Technically,
eval(..)
canbeinvoked"indirectly",
throughvarioustricks(beyondourdiscussionhere),whichcausesittoinsteadexecuteinthecontextoftheglobalscope,
thusmodifyingit.Butineithercase,
eval(..)
canatruntimemodifyanauthor-timelexicalscope.
Note:
eval(..)
whenusedinastrict-modeprogramoperatesinitsownlexicalscope,whichmeansdeclarationsmade
insideoftheeval()donotactuallymodifytheenclosingscope.
function
foo
(
str
)
{
"usestrict";
eval
(str);
console
.log(a);
//ReferenceError:aisnotdefined
}
foo(
"vara=2"
);
ThereareotherfacilitiesinJavaScriptwhichamounttoaverysimilareffectto
eval(..)
.
setTimeout(..)
and
setInterval(..)
cantakeastringfortheirrespectivefirstargument,thecontentsofwhichare
eval
uatedasthecodeofa
dynamically-generatedfunction.Thisisold,legacybehaviorandlong-sincedeprecated.Don'tdoit!
The
newFunction(..)
functionconstructorsimilarlytakesastringofcodeinitslastargumenttoturnintoadynamically-
generatedfunction(thefirstargument(s),ifany,arethenamedparametersforthenewfunction).Thisfunction-constructor
syntaxisslightlysaferthan
eval(..)
,butitshouldstillbeavoidedinyourcode.
Theuse-casesfordynamicallygeneratingcodeinsideyourprogramareincrediblyrare,astheperformancedegradations
arealmostneverworththecapability.
Theotherfrowned-upon(andnowdeprecated!)featureinJavaScriptwhichcheatslexicalscopeisthe
with
keyword.
Therearemultiplevalidwaysthat
with
canbeexplained,butIwillchooseheretoexplainitfromtheperspectiveofhowit
interactswithandaffectslexicalscope.
with
istypicallyexplainedasashort-handformakingmultiplepropertyreferencesagainstanobjectwithoutrepeatingthe
objectreferenceitselfeachtime.
Forexample:
var
obj={
a:
1
,
b:
2
,
c:
3
};
//more"tedious"torepeat"obj"
obj.a=
2
;
obj.b=
3
;
obj.c=
4
;
//"easier"short-hand
with
(obj){
a=
3
;
b=
4
;
c=
5
;
with
YouDon'tKnowJS:Scope&Closures
18
Chapter2:LexicalScope
}
However,there'smuchmoregoingonherethanjustaconvenientshort-handforobjectpropertyaccess.Consider:
function
foo
(
obj
)
{
with
(obj){
a=
2
;
}
}
var
o1={
a:
3
};
var
o2={
b:
3
};
foo(o1);
console
.log(o1.a);
//2
foo(o2);
console
.log(o2.a);
//undefined
console
.log(a);
//2--Oops,leakedglobal!
Inthiscodeexample,twoobjects
o1
and
o2
arecreated.Onehasan
a
property,andtheotherdoesnot.The
foo(..)
functiontakesanobjectreference
obj
asanargument,andcalls
with(obj){..}
onthereference.Insidethe
with
block,wemakewhatappearstobeanormallexicalreferencetoavariable
a
,anLHSreferenceinfact(seeChapter1),to
assigntoitthevalueof
2
.
Whenwepassin
o1
,the
a=2
assignmentfindstheproperty
o1.a
andassignsitthevalue
2
,asreflectedinthe
subsequent
console.log(o1.a)
statement.However,whenwepassin
o2
,sinceitdoesnothavean
a
property,nosuch
propertyiscreated,and
o2.a
remains
undefined
.
Butthenwenoteapeculiarside-effect,thefactthataglobalvariable
a
wascreatedbythe
a=2
assignment.Howcan
thisbe?
The
with
statementtakesanobject,onewhichhaszeroormoreproperties,andtreatsthatobjectasifitisawholly
separatelexicalscope,andthustheobject'spropertiesaretreatedaslexicallydefinedidentifiersinthat"scope".
Note:Eventhougha
with
blocktreatsanobjectlikealexicalscope,anormal
var
declarationinsidethat
with
blockwill
notbescopedtothat
with
block,butinsteadthecontainingfunctionscope.
Whilethe
eval(..)
functioncanmodifyexistinglexicalscopeifittakesastringofcodewithoneormoredeclarationsinit,
the
with
statementactuallycreatesawholenewlexicalscopeoutofthinair,fromtheobjectyoupasstoit.
Understoodinthisway,the"scope"declaredbythe
with
statementwhenwepassedin
o1
was
o1
,andthat"scope"had
an"identifier"initwhichcorrespondstothe
o1.a
property.Butwhenweused
o2
asthe"scope",ithadnosuch
a
"identifier"init,andsothenormalrulesofLHSidentifierlook-up(seeChapter1)occurred.
Neitherthe"scope"of
o2
,northescopeof
foo(..)
,northeglobalscopeeven,hasan
a
identifiertobefound,sowhen
a=2
isexecuted,itresultsintheautomatic-globalbeingcreated(sincewe'reinnon-strictmode).
Itisastrangesortofmind-bendingthoughttosee
with
turning,atruntime,anobjectanditspropertiesintoa"scope"with
"identifiers".ButthatistheclearestexplanationIcangivefortheresultswesee.
Note:Inadditiontobeingabadideatouse,both
eval(..)
and
with
areaffected(restricted)byStrictMode.
with
is
outrightdisallowed,whereasvariousformsofindirectorunsafe
eval(..)
aredisallowedwhileretainingthecore
functionality.
YouDon'tKnowJS:Scope&Closures
19
Chapter2:LexicalScope
Both
eval(..)
and
with
cheattheotherwiseauthor-timedefinedlexicalscopebymodifyingorcreatingnewlexicalscope
atruntime.
So,what'sthebigdeal,youask?Iftheyoffermoresophisticatedfunctionalityandcodingflexibility,aren'tthesegood
features?No.
TheJavaScriptEnginehasanumberofperformanceoptimizationsthatitperformsduringthecompilationphase.Someof
theseboildowntobeingabletoessentiallystaticallyanalyzethecodeasitlexes,andpre-determinewhereallthevariable
andfunctiondeclarationsare,sothatittakeslessefforttoresolveidentifiersduringexecution.
ButiftheEnginefindsan
eval(..)
or
with
inthecode,itessentiallyhastoassumethatallitsawarenessofidentifier
locationmaybeinvalid,becauseitcannotknowatlexingtimeexactlywhatcodeyoumaypassto
eval(..)
tomodifythe
lexicalscope,orthecontentsoftheobjectyoumaypassto
with
tocreateanewlexicalscopetobeconsulted.
Inotherwords,inthepessimisticsense,mostofthoseoptimizationsitwouldmakearepointlessif
eval(..)
or
with
are
present,soitsimplydoesn'tperformtheoptimizationsatall.
Yourcodewillalmostcertainlytendtorunslowersimplybythefactthatyouincludean
eval(..)
or
with
anywhereinthe
code.NomatterhowsmarttheEnginemaybeabouttryingtolimittheside-effectsofthesepessimisticassumptions,
there'snogettingaroundthefactthatwithouttheoptimizations,coderunsslower.
Lexicalscopemeansthatscopeisdefinedbyauthor-timedecisionsofwherefunctionsaredeclared.Thelexingphaseof
compilationisessentiallyabletoknowwhereandhowallidentifiersaredeclared,andthuspredicthowtheywillbelooked-
upduringexecution.
TwomechanismsinJavaScriptcan"cheat"lexicalscope:
eval(..)
and
with
.Theformercanmodifyexistinglexical
scope(atruntime)byevaluatingastringof"code"whichhasoneormoredeclarationsinit.Thelatteressentiallycreatesa
wholenewlexicalscope(again,atruntime)bytreatinganobjectreferenceasa"scope"andthatobject'spropertiesas
scopedidentifiers.
ThedownsidetothesemechanismsisthatitdefeatstheEngine'sabilitytoperformcompile-timeoptimizationsregarding
scopelook-up,becausetheEnginehastoassumepessimisticallythatsuchoptimizationswillbeinvalid.Codewillrun
slowerasaresultofusingeitherfeature.Don'tusethem.
Performance
Review(TL;DR)
YouDon'tKnowJS:Scope&Closures
20
Chapter2:LexicalScope
AsweexploredinChapter2,scopeconsistsofaseriesof"bubbles"thateachactasacontainerorbucket,inwhich
identifiers(variables,functions)aredeclared.Thesebubblesnestneatlyinsideeachother,andthisnestingisdefinedat
author-time.
Butwhatexactlymakesanewbubble?Isitonlythefunction?CanotherstructuresinJavaScriptcreatebubblesofscope?
ThemostcommonanswertothosequestionsisthatJavaScripthasfunction-basedscope.Thatis,eachfunctionyou
declarecreatesabubbleforitself,butnootherstructurescreatetheirownscopebubbles.Aswe'llseeinjustalittlebit,this
isnotquitetrue.
Butfirst,let'sexplorefunctionscopeanditsimplications.
Considerthiscode:
function
foo
(
a
)
{
var
b=
2
;
//somecode
function
bar
()
{
//...
}
//morecode
var
c=
3
;
}
Inthissnippet,thescopebubblefor
foo(..)
includesidentifiers
a
,
b
,
c
and
bar
.Itdoesn'tmatterwhereinthescope
adeclarationappears,thevariableorfunctionbelongstothecontainingscopebubble,regardless.We'llexplorehow
exactlythatworksinthenextchapter.
bar(..)
hasitsownscopebubble.Sodoestheglobalscope,whichhasjustoneidentifierattachedtoit:
foo
.
Because
a
,
b
,
c
,and
bar
allbelongtothescopebubbleof
foo(..)
,theyarenotaccessibleoutsideof
foo(..)
.That
is,thefollowingcodewouldallresultin
ReferenceError
errors,astheidentifiersarenotavailabletotheglobalscope:
bar();
//fails
console
.log(a,b,c);
//all3fail
However,alltheseidentifiers(
a
,
b
,
c
,
foo
,and
bar
)areaccessibleinsideof
foo(..)
,andindeedalsoavailableinside
of
bar(..)
(assumingtherearenoshadowidentifierdeclarationsinside
bar(..)
).
Functionscopeencouragestheideathatallvariablesbelongtothefunction,andcanbeusedandre-usedthroughoutthe
entiretyofthefunction(andindeed,accessibleeventonestedscopes).Thisdesignapproachcanbequiteuseful,and
certainlycanmakefulluseofthe"dynamic"natureofJavaScriptvariablestotakeonvaluesofdifferenttypesasneeded.
Ontheotherhand,ifyoudon'ttakecarefulprecautions,variablesexistingacrosstheentiretyofascopecanleadtosome
unexpectedpitfalls.
Chapter3:Functionvs.BlockScope
ScopeFromFunctions
YouDon'tKnowJS:Scope&Closures
21
Chapter3:Functionvs.BlockScope
Thetraditionalwayofthinkingaboutfunctionsisthatyoudeclareafunction,andthenaddcodeinsideit.Buttheinverse
thinkingisequallypowerfulanduseful:takeanyarbitrarysectionofcodeyou'vewritten,andwrapafunctiondeclaration
aroundit,whichineffect"hides"thecode.
Thepracticalresultistocreateascopebubblearoundthecodeinquestion,whichmeansthatanydeclarations(variableor
function)inthatcodewillnowbetiedtothescopeofthenewwrappingfunction,ratherthanthepreviouslyenclosing
scope.Inotherwords,youcan"hide"variablesandfunctionsbyenclosingtheminthescopeofafunction.
Whywould"hiding"variablesandfunctionsbeausefultechnique?
There'savarietyofreasonsmotivatingthisscope-basedhiding.Theytendtoarisefromthesoftwaredesignprinciple
"PrincipleofLeastPrivilege"
,alsosometimescalled"LeastAuthority"or"LeastExposure".Thisprinciple
statesthatinthedesignofsoftware,suchastheAPIforamodule/object,youshouldexposeonlywhatisminimally
necessary,and"hide"everythingelse.
Thisprincipleextendstothechoiceofwhichscopetocontainvariablesandfunctions.Ifallvariablesandfunctionswerein
theglobalscope,theywouldofcoursebeaccessibletoanynestedscope.Butthiswouldviolatethe"Least..."principlein
thatyouare(likely)exposingmanyvariablesorfunctionswhichyoushouldotherwisekeepprivate,asproperuseofthe
codewoulddiscourageaccesstothosevariables/functions.
Forexample:
function
doSomething
(
a
)
{
b=a+doSomethingElse(a*
2
);
console
.log(b*
3
);
}
function
doSomethingElse
(
a
)
{
return
a-
1
;
}
var
b;
doSomething(
2
);
//15
Inthissnippet,the
b
variableandthe
doSomethingElse(..)
functionarelikely"private"detailsofhow
doSomething(..)
doesitsjob.Givingtheenclosingscope"access"to
b
and
doSomethingElse(..)
isnotonlyunnecessarybutalsopossibly
"dangerous",inthattheymaybeusedinunexpectedways,intentionallyornot,andthismayviolatepre-condition
assumptionsof
doSomething(..)
.
Amore"proper"designwouldhidetheseprivatedetailsinsidethescopeof
doSomething(..)
,suchas:
function
doSomething
(
a
)
{
function
doSomethingElse
(
a
)
{
return
a-
1
;
}
var
b;
b=a+doSomethingElse(a*
2
);
console
.log((b*
3
));
}
doSomething(
2
);
//15
HidingInPlainScope
YouDon'tKnowJS:Scope&Closures
22
Chapter3:Functionvs.BlockScope
Now,
b
and
doSomethingElse(..)
arenotaccessibletoanyoutsideinfluence,insteadcontrolledonlyby
doSomething(..)
.
Thefunctionalityandend-resulthasnotbeenaffected,butthedesignkeepsprivatedetailsprivate,whichisusually
consideredbettersoftware.
Anotherbenefitof"hiding"variablesandfunctionsinsideascopeistoavoidunintendedcollisionbetweentwodifferent
identifierswiththesamenamebutdifferentintendedusages.Collisionresultsofteninunexpectedoverwritingofvalues.
Forexample:
function
foo
()
{
function
bar
(
a
)
{
i=
3
;
//changingthe`i`intheenclosingscope'sfor-loop
console
.log(a+i);
}
for
(
var
i=
0
;i<
10
;i++){
bar(i*
2
);
//oops,infiniteloopahead!
}
}
foo();
The
i=3
assignmentinsideof
bar(..)
overwrites,unexpectedly,the
i
thatwasdeclaredin
foo(..)
atthefor-loop.In
thiscase,itwillresultinaninfiniteloop,because
i
issettoafixedvalueof
3
andthatwillforeverremain
<10
.
Theassignmentinside
bar(..)
needstodeclarealocalvariabletouse,regardlessofwhatidentifiernameischosen.
var
i=3;
wouldfixtheproblem(andwouldcreatethepreviouslymentioned"shadowedvariable"declarationfor
i
).An
additional,notalternate,optionistopickanotheridentifiernameentirely,suchas
varj=3;
.Butyoursoftwaredesign
maynaturallycallforthesameidentifiername,soutilizingscopeto"hide"yourinnerdeclarationisyourbest/onlyoptionin
thatcase.
Aparticularlystrongexampleof(likely)variablecollisionoccursintheglobalscope.Multiplelibrariesloadedintoyour
programcanquiteeasilycollidewitheachotheriftheydon'tproperlyhidetheirinternal/privatefunctionsandvariables.
Suchlibrariestypicallywillcreateasinglevariabledeclaration,oftenanobject,withasufficientlyuniquename,intheglobal
scope.Thisobjectisthenusedasa"namespace"forthatlibrary,whereallspecificexposuresoffunctionalityaremadeas
propertiesoffthatobject(namespace),ratherthanastop-levellexicallyscopedidentifiersthemselves.
Forexample:
var
MyReallyCoolLibrary={
awesome:
"stuff"
,
doSomething:
function
()
{
//...
},
doAnotherThing:
function
()
{
//...
}
};
Anotheroptionforcollisionavoidanceisthemoremodern"module"approach,usinganyofvariousdependencymanagers.
CollisionAvoidance
Global"Namespaces"
ModuleManagement
YouDon'tKnowJS:Scope&Closures
23
Chapter3:Functionvs.BlockScope
Usingthesetools,nolibrarieseveraddanyidentifierstotheglobalscope,butareinsteadrequiredtohavetheiridentifier(s)
beexplicitlyimportedintoanotherspecificscopethroughusageofthedependencymanager'svariousmechanisms.
Itshouldbeobservedthatthesetoolsdonotpossess"magic"functionalitythatisexemptfromlexicalscopingrules.They
simplyusetherulesofscopingasexplainedheretoenforcethatnoidentifiersareinjectedintoanysharedscope,andare
insteadkeptinprivate,non-collision-susceptiblescopes,whichpreventsanyaccidentalscopecollisions.
Assuch,youcancodedefensivelyandachievethesameresultsasthedependencymanagersdowithoutactuallyneeding
tousethem,ifyousochoose.SeetheChapter5formoreinformationaboutthemodulepattern.
We'veseenthatwecantakeanysnippetofcodeandwrapafunctionaroundit,andthateffectively"hides"anyenclosed
variableorfunctiondeclarationsfromtheoutsidescopeinsidethatfunction'sinnerscope.
Forexample:
var
a=
2
;
function
foo
()
{
//<--insertthis
var
a=
3
;
console
.log(a);
//3
}
//<--andthis
foo();
//<--andthis
console
.log(a);
//2
Whilethistechnique"works",itisnotnecessarilyveryideal.Thereareafewproblemsitintroduces.Thefirstisthatwe
havetodeclareanamed-function
foo()
,whichmeansthattheidentifiername
foo
itself"pollutes"theenclosingscope
(global,inthiscase).Wealsohavetoexplicitlycallthefunctionbyname(
foo()
)sothatthewrappedcodeactually
executes.
Itwouldbemoreidealifthefunctiondidn'tneedaname(or,rather,thenamedidn'tpollutetheenclosingscope),andifthe
functioncouldautomaticallybeexecuted.
Fortunately,JavaScriptoffersasolutiontobothproblems.
var
a=
2
;
(
function
foo
()
{
//<--insertthis
var
a=
3
;
console
.log(a);
//3
})();
//<--andthis
console
.log(a);
//2
Let'sbreakdownwhat'shappeninghere.
First,noticethatthewrappingfunctionstatementstartswith
(function...
asopposedtojust
function...
.Whilethismay
seemlikeaminordetail,it'sactuallyamajorchange.Insteadoftreatingthefunctionasastandarddeclaration,thefunction
istreatedasafunction-expression.
Note:Theeasiestwaytodistinguishdeclarationvs.expressionisthepositionoftheword"function"inthestatement(not
FunctionsAsScopes
YouDon'tKnowJS:Scope&Closures
24
Chapter3:Functionvs.BlockScope
justaline,butadistinctstatement).If"function"istheveryfirstthinginthestatement,thenit'safunctiondeclaration.
Otherwise,it'safunctionexpression.
Thekeydifferencewecanobserveherebetweenafunctiondeclarationandafunctionexpressionrelatestowhereits
nameisboundasanidentifier.
Comparetheprevioustwosnippets.Inthefirstsnippet,thename
foo
isboundintheenclosingscope,andwecallit
directlywith
foo()
.Inthesecondsnippet,thename
foo
isnotboundintheenclosingscope,butinsteadisboundonly
insideofitsownfunction.
Inotherwords,
(functionfoo(){..})
asanexpressionmeanstheidentifier
foo
isfoundonlyinthescopewherethe
..
indicates,notintheouterscope.Hidingthename
foo
insideitselfmeansitdoesnotpollutetheenclosingscope
unnecessarily.
Youareprobablymostfamiliarwithfunctionexpressionsascallbackparameters,suchas:
setTimeout(
function
()
{
console
.log(
"Iwaited1second!"
);
},
1000
);
Thisiscalledan"anonymousfunctionexpression",because
function()...
hasnonameidentifieronit.Function
expressionscanbeanonymous,butfunctiondeclarationscannotomitthename--thatwouldbeillegalJSgrammar.
Anonymousfunctionexpressionsarequickandeasytotype,andmanylibrariesandtoolstendtoencouragethisidiomatic
styleofcode.However,theyhaveseveraldraw-backstoconsider:
1. Anonymousfunctionshavenousefulnametodisplayinstacktraces,whichcanmakedebuggingmoredifficult.
2. Withoutaname,ifthefunctionneedstorefertoitself,forrecursion,etc.,thedeprecated
arguments.callee
reference
isunfortunatelyrequired.Anotherexampleofneedingtoself-referenceiswhenaneventhandlerfunctionwantsto
unbinditselfafteritfires.
3. Anonymousfunctionsomitanamewhichisoftenhelpfulinprovidingmorereadable/understandablecode.A
descriptivenamehelpsself-documentthecodeinquestion.
Inlinefunctionexpressionsarepowerfulanduseful--thequestionofanonymousvs.nameddoesn'tdetractfromthat.
Providinganameforyourfunctionexpressionquiteeffectivelyaddressesallthesedraw-backs,buthasnotangible
downsides.Thebestpracticeistoalwaysnameyourfunctionexpressions:
setTimeout(
function
timeoutHandler
()
{
//<--Look,Ihaveaname!
console
.log(
"Iwaited1second!"
);
},
1000
);
var
a=
2
;
(
function
foo
()
{
var
a=
3
;
console
.log(a);
//3
})();
Anonymousvs.Named
InvokingFunctionExpressionsImmediately
YouDon'tKnowJS:Scope&Closures
25
Chapter3:Functionvs.BlockScope
console
.log(a);
//2
Nowthatwehaveafunctionasanexpressionbyvirtueofwrappingitina
()
pair,wecanexecutethatfunctionbyadding
another
()
ontheend,like
(functionfoo(){..})()
.Thefirstenclosing
()
pairmakesthefunctionanexpression,and
thesecond
()
executesthefunction.
Thispatternissocommon,afewyearsagothecommunityagreedonatermforit:IIFE,whichstandsforImmediately
InvokedFunctionExpression.
Ofcourse,IIFE'sdon'tneednames,necessarily--themostcommonformofIIFEistouseananonymousfunction
expression.Whilecertainlylesscommon,naminganIIFEhasalltheaforementionedbenefitsoveranonymousfunction
expressions,soit'sagoodpracticetoadopt.
var
a=
2
;
(
function
IIFE
()
{
var
a=
3
;
console
.log(a);
//3
})();
console
.log(a);
//2
There'saslightvariationonthetraditionalIIFEform,whichsomeprefer:
(function(){..}())
.Lookcloselytoseethe
difference.Inthefirstform,thefunctionexpressioniswrappedin
()
,andthentheinvoking
()
pairisontheoutsideright
afterit.Inthesecondform,theinvoking
()
pairismovedtotheinsideoftheouter
()
wrappingpair.
Thesetwoformsareidenticalinfunctionality.It'spurelyastylisticchoicewhichyouprefer.
AnothervariationonIIFE'swhichisquitecommonistousethefactthattheyare,infact,justfunctioncalls,andpassin
argument(s).
Forinstance:
var
a=
2
;
(
function
IIFE
(
global
)
{
var
a=
3
;
console
.log(a);
//3
console
.log(global.a);
//2
})(
window
);
console
.log(a);
//2
Wepassinthe
window
objectreference,butwenametheparameter
global
,sothatwehaveaclearstylisticdelineation
forglobalvs.non-globalreferences.Ofcourse,youcanpassinanythingfromanenclosingscopeyouwant,andyoucan
nametheparameter(s)anythingthatsuitsyou.Thisismostlyjuststylisticchoice.
Anotherapplicationofthispatternaddressesthe(minorniche)concernthatthedefault
undefined
identifiermighthaveits
valueincorrectlyoverwritten,causingunexpectedresults.Bynamingaparameter
undefined
,butnotpassinganyvaluefor
thatargument,wecanguaranteethatthe
undefined
identifierisinfacttheundefinedvalueinablockofcode:
undefined
=
true
;
//settingaland-mineforothercode!avoid!
YouDon'tKnowJS:Scope&Closures
26
Chapter3:Functionvs.BlockScope
(
function
IIFE
(
undefined
)
{
var
a;
if
(a===
undefined
){
console
.log(
"Undefinedissafehere!"
);
}
})();
StillanothervariationoftheIIFEinvertstheorderofthings,wherethefunctiontoexecuteisgivensecond,afterthe
invocationandparameterstopasstoit.ThispatternisusedintheUMD(UniversalModuleDefinition)project.Somepeople
finditalittlecleanertounderstand,thoughitisslightlymoreverbose.
var
a=
2
;
(
function
IIFE
(
def
)
{
def(
window
);
})(
function
def
(
global
)
{
var
a=
3
;
console
.log(a);
//3
console
.log(global.a);
//2
});
The
def
functionexpressionisdefinedinthesecond-halfofthesnippet,andthenpassedasaparameter(alsocalled
def
)tothe
IIFE
functiondefinedinthefirsthalfofthesnippet.Finally,theparameter
def
(thefunction)isinvoked,
passing
window
inasthe
global
parameter.
Whilefunctionsarethemostcommonunitofscope,andcertainlythemostwide-spreadofthedesignapproachesinthe
majorityofJSincirculation,otherunitsofscopearepossible,andtheusageoftheseotherscopeunitscanleadtoeven
better,cleanertomaintaincode.
ManylanguagesotherthanJavaScriptsupportBlockScope,andsodevelopersfromthoselanguagesareaccustomedto
themindset,whereasthosewho'veprimarilyonlyworkedinJavaScriptmayfindtheconceptslightlyforeign.
Butevenifyou'veneverwrittenasinglelineofcodeinblock-scopedfashion,youarestillprobablyfamiliarwiththis
extremelycommonidiominJavaScript:
for
(
var
i=
0
;i<
10
;i++){
console
.log(i);
}
Wedeclarethevariable
i
directlyinsidethefor-loophead,mostlikelybecauseourintentistouse
i
onlywithinthe
contextofthatfor-loop,andessentiallyignorethefactthatthevariableactuallyscopesitselftotheenclosingscope
(functionorglobal).
That'swhatblock-scopingisallabout.Declaringvariablesascloseaspossible,aslocalaspossible,towheretheywillbe
used.Anotherexample:
var
foo=
true
;
if
(foo){
var
bar=foo*
2
;
bar=something(bar);
BlocksAsScopes
YouDon'tKnowJS:Scope&Closures
27
Chapter3:Functionvs.BlockScope
console
.log(bar);
}
Weareusinga
bar
variableonlyinthecontextoftheif-statement,soitmakesakindofsensethatwewoulddeclareit
insidetheif-block.However,wherewedeclarevariablesisnotrelevantwhenusing
var
,becausetheywillalwaysbelong
totheenclosingscope.Thissnippetisessentially"fake"block-scoping,forstylisticreasons,andrelyingonself-
enforcementnottoaccidentallyuse
bar
inanotherplaceinthatscope.
Blockscopeisatooltoextendtheearlier"PrincipleofLeastPrivilegeExposure"
functionstohidinginformationinblocksofourcode.
Considerthefor-loopexampleagain:
for
(
var
i=
0
;i<
10
;i++){
console
.log(i);
}
Whypollutetheentirescopeofafunctionwiththe
i
variablethatisonlygoingtobe(oronlyshouldbe,atleast)usedfor
thefor-loop?
Butmoreimportantly,developersmayprefertocheckthemselvesagainstaccidentally(re)usingvariablesoutsideoftheir
intendedpurpose,suchasbeingissuedanerroraboutanunknownvariableifyoutrytouseitinthewrongplace.Block-
scoping(ifitwerepossible)forthe
i
variablewouldmake
i
availableonlyforthefor-loop,causinganerrorif
i
is
accessedelsewhereinthefunction.Thishelpsensurevariablesarenotre-usedinconfusingorhard-to-maintainways.
But,thesadrealityisthat,onthesurface,JavaScripthasnofacilityforblockscope.
Thatis,untilyoudigalittlefurther.
Welearnedabout
with
inChapter2.Whileitisafrowneduponconstruct,itisanexampleof(aformof)blockscope,in
thatthescopethatiscreatedfromtheobjectonlyexistsforthelifetimeofthat
with
statement,andnotintheenclosing
scope.
It'saverylittleknownfactthatJavaScriptinES3specifiedthevariabledeclarationinthe
catch
clauseofa
try/catch
to
beblock-scopedtothe
catch
block.
Forinstance:
try
{
undefined
();
//illegaloperationtoforceanexception!
}
catch
(err){
console
.log(err);
//works!
}
console
.log(err);
//ReferenceError:`err`notfound
Asyoucansee,
err
existsonlyinthe
catch
clause,andthrowsanerrorwhenyoutrytoreferenceitelsewhere.
Note:WhilethisbehaviorhasbeenspecifiedandtrueofpracticallyallstandardJSenvironments(exceptperhapsoldIE),
manylintersseemtostillcomplainifyouhavetwoormore
catch
clausesinthesamescopewhicheachdeclaretheirerror
with
try/catch
YouDon'tKnowJS:Scope&Closures
28
Chapter3:Functionvs.BlockScope
variablewiththesameidentifiername.Thisisnotactuallyare-definition,sincethevariablesaresafelyblock-scoped,but
thelintersstillseemto,annoyingly,complainaboutthisfact.
Toavoidtheseunnecessarywarnings,somedevswillnametheir
catch
variables
err1
,
err2
,etc.Otherdevswillsimply
turnoffthelintingcheckforduplicatevariablenames.
Theblock-scopingnatureof
catch
mayseemlikeauselessacademicfact,butseeAppendixBformoreinformationon
justhowusefulitmightbe.
Thusfar,we'veseenthatJavaScriptonlyhassomestrangenichebehaviorswhichexposeblockscopefunctionality.Ifthat
wereallwehad,anditwasformany,manyyears,thenblockscopingwouldnotbeterriblyusefultotheJavaScript
developer.
Fortunately,ES6changesthat,andintroducesanewkeyword
let
whichsitsalongside
var
asanotherwaytodeclare
variables.
The
let
keywordattachesthevariabledeclarationtothescopeofwhateverblock(commonlya
{..}
pair)it'scontained
in.Inotherwords,
let
implicitlyhijacksanyblock'sscopeforitsvariabledeclaration.
var
foo=
true
;
if
(foo){
let
bar=foo*
2
;
bar=something(bar);
console
.log(bar);
}
console
.log(bar);
//ReferenceError
Using
let
toattachavariabletoanexistingblockissomewhatimplicit.Itcanconfuseifyou'renotpayingcloseattention
towhichblockshavevariablesscopedtothem,andareinthehabitofmovingblocksaround,wrappingtheminother
blocks,etc.,asyoudevelopandevolvecode.
Creatingexplicitblocksforblock-scopingcanaddresssomeoftheseconcerns,makingitmoreobviouswherevariablesare
attachedandnot.Usually,explicitcodeispreferableoverimplicitorsubtlecode.Thisexplicitblock-scopingstyleiseasyto
achieve,andfitsmorenaturallywithhowblock-scopingworksinotherlanguages:
var
foo=
true
;
if
(foo){
{
//<--explicitblock
let
bar=foo*
2
;
bar=something(bar);
console
.log(bar);
}
}
console
.log(bar);
//ReferenceError
Wecancreateanarbitraryblockfor
let
tobindtobysimplyincludinga
{..}
pairanywhereastatementisvalid
grammar.Inthiscase,we'vemadeanexplicitblockinsidetheif-statement,whichmaybeeasierasawholeblocktomove
aroundlaterinrefactoring,withoutaffectingthepositionandsemanticsoftheenclosingif-statement.
Note:Foranotherwaytoexpressexplicitblockscopes,seeAppendixB.
InChapter4,wewilladdresshoisting,whichtalksaboutdeclarationsbeingtakenasexistingfortheentirescopeinwhich
let
YouDon'tKnowJS:Scope&Closures
29
Chapter3:Functionvs.BlockScope
theyoccur.
However,declarationsmadewith
let
willnothoisttotheentirescopeoftheblocktheyappearin.Suchdeclarationswill
notobservably"exist"intheblockuntilthedeclarationstatement.
{
console
.log(bar);
//ReferenceError!
let
bar=
2
;
}
Anotherreasonblock-scopingisusefulrelatestoclosuresandgarbagecollectiontoreclaimmemory.We'llbrieflyillustrate
here,buttheclosuremechanismisexplainedindetailinChapter5.
Consider:
function
process
(
data
)
{
//dosomethinginteresting
}
var
someReallyBigData={..};
process(someReallyBigData);
var
btn=
document
.getElementById(
"my_button"
);
btn.addEventListener(
"click"
,
function
click
(
evt
)
{
console
.log(
"buttonclicked"
);
},
/*capturingPhase=*/
false
);
The
click
functionclickhandlercallbackdoesn'tneedthe
someReallyBigData
variableatall.Thatmeans,theoretically,
after
process(..)
runs,thebigmemory-heavydatastructurecouldbegarbagecollected.However,it'squitelikely(though
implementationdependent)thattheJSenginewillstillhavetokeepthestructurearound,sincethe
click
functionhasa
closureovertheentirescope.
Block-scopingcanaddressthisconcern,makingitclearertotheenginethatitdoesnotneedtokeep
someReallyBigData
around:
function
process
(
data
)
{
//dosomethinginteresting
}
//anythingdeclaredinsidethisblockcangoawayafter!
{
let
someReallyBigData={..};
process(someReallyBigData);
}
var
btn=
document
.getElementById(
"my_button"
);
btn.addEventListener(
"click"
,
function
click
(
evt
)
{
console
.log(
"buttonclicked"
);
},
/*capturingPhase=*/
false
);
Declaringexplicitblocksforvariablestolocallybindtoisapowerfultoolthatyoucanaddtoyourcodetoolbox.
GarbageCollection
let
Loops
YouDon'tKnowJS:Scope&Closures
30
Chapter3:Functionvs.BlockScope
Aparticularcasewhere
let
shinesisinthefor-loopcaseaswediscussedpreviously.
for
(
let
i=
0
;i<
10
;i++){
console
.log(i);
}
console
.log(i);
//ReferenceError
Notonlydoes
let
inthefor-loopheaderbindthe
i
tothefor-loopbody,butinfact,itre-bindsittoeachiterationofthe
loop,makingsuretore-assignitthevaluefromtheendofthepreviousloopiteration.
Here'sanotherwayofillustratingtheper-iterationbindingbehaviorthatoccurs:
{
let
j;
for
(j=
0
;j<
10
;j++){
let
i=j;
//re-boundforeachiteration!
console
.log(i);
}
}
Thereasonwhythisper-iterationbindingisinterestingwillbecomeclearinChapter5whenwediscussclosures.
Because
let
declarationsattachtoarbitraryblocksratherthantotheenclosingfunction'sscope(orglobal),therecanbe
gotchaswhereexistingcodehasahiddenrelianceonfunction-scoped
var
declarations,andreplacingthe
var
with
let
mayrequireadditionalcarewhenrefactoringcode.
Consider:
var
foo=
true
,baz=
10
;
if
(foo){
var
bar=
3
;
if
(baz>bar){
console
.log(baz);
}
//...
}
Thiscodeisfairlyeasilyre-factoredas:
var
foo=
true
,baz=
10
;
if
(foo){
var
bar=
3
;
//...
}
if
(baz>bar){
console
.log(baz);
}
But,becarefulofsuchchangeswhenusingblock-scopedvariables:
var
foo=
true
,baz=
10
;
YouDon'tKnowJS:Scope&Closures
31
Chapter3:Functionvs.BlockScope
if
(foo){
let
bar=
3
;
if
(baz>bar){
//<--don'tforget`bar`whenmoving!
console
.log(baz);
}
}
SeeAppendixBforanalternate(moreexplicit)styleofblock-scopingwhichmayprovideeasiertomaintain/refactorcode
that'smorerobusttothesescenarios.
Inadditionto
let
,ES6introduces
const
,whichalsocreatesablock-scopedvariable,butwhosevalueisfixed(constant).
Anyattempttochangethatvalueatalatertimeresultsinanerror.
var
foo=
true
;
if
(foo){
var
a=
2
;
const
b=
3
;
//block-scopedtothecontaining`if`
a=
3
;
//justfine!
b=
4
;
//error!
}
console
.log(a);
//3
console
.log(b);
//ReferenceError!
FunctionsarethemostcommonunitofscopeinJavaScript.Variablesandfunctionsthataredeclaredinsideanother
functionareessentially"hidden"fromanyoftheenclosing"scopes",whichisanintentionaldesignprincipleofgood
software.
Butfunctionsarebynomeanstheonlyunitofscope.Block-scopereferstotheideathatvariablesandfunctionscan
belongtoanarbitraryblock(generally,any
{..}
pair)ofcode,ratherthanonlytotheenclosingfunction.
StartingwithES3,the
try/catch
structurehasblock-scopeinthe
catch
clause.
InES6,the
let
keyword(acousintothe
var
keyword)isintroducedtoallowdeclarationsofvariablesinanyarbitrary
blockofcode.
if(..){leta=2;}
willdeclareavariable
a
thatessentiallyhijacksthescopeofthe
if
's
{..}
block
andattachesitselfthere.
Thoughsomeseemtobelieveso,blockscopeshouldnotbetakenasanoutrightreplacementof
var
functionscope.Both
functionalitiesco-exist,anddeveloperscanandshouldusebothfunction-scopeandblock-scopetechniqueswhere
respectivelyappropriatetoproducebetter,morereadable/maintainablecode.
note-leastprivilege
const
Review(TL;DR)
YouDon'tKnowJS:Scope&Closures
32
Chapter3:Functionvs.BlockScope
Bynow,youshouldbefairlycomfortablewiththeideaofscope,andhowvariablesareattachedtodifferentlevelsofscope
dependingonwhereandhowtheyaredeclared.Bothfunctionscopeandblockscopebehavebythesamerulesinthis
regard:anyvariabledeclaredwithinascopeisattachedtothatscope.
Butthere'sasubtledetailofhowscopeattachmentworkswithdeclarationsthatappearinvariouslocationswithinascope,
andthatdetailiswhatwewillexaminehere.
There'satemptationtothinkthatallofthecodeyouseeinaJavaScriptprogramisinterpretedline-by-line,top-downin
order,astheprogramexecutes.Whilethatissubstantiallytrue,there'sonepartofthatassumptionwhichcanleadto
incorrectthinkingaboutyourprogram.
Considerthiscode:
a=
2
;
var
a;
console
.log(a);
Whatdoyouexpecttobeprintedinthe
console.log(..)
statement?
Manydeveloperswouldexpect
undefined
,sincethe
vara
statementcomesafterthe
a=2
,anditwouldseemnaturalto
assumethatthevariableisre-defined,andthusassignedthedefault
undefined
.However,theoutputwillbe
2
.
Consideranotherpieceofcode:
console
.log(a);
var
a=
2
;
Youmightbetemptedtoassumethat,sincetheprevioussnippetexhibitedsomeless-than-top-downlookingbehavior,
perhapsinthissnippet,
2
willalsobeprinted.Othersmaythinkthatsincethe
a
variableisusedbeforeitisdeclared,this
mustresultina
ReferenceError
beingthrown.
Unfortunately,bothguessesareincorrect.
undefined
istheoutput.
So,what'sgoingonhere?Itwouldappearwehaveachicken-and-the-eggquestion.Whichcomesfirst,thedeclaration
("egg"),ortheassignment("chicken")?
Toanswerthisquestion,weneedtoreferbacktoChapter1,andourdiscussionofcompilers.RecallthattheEngine
actuallywillcompileyourJavaScriptcodebeforeitinterpretsit.Partofthecompilationphasewastofindandassociateall
declarationswiththeirappropriatescopes.Chapter2showedusthatthisistheheartofLexicalScope.
So,thebestwaytothinkaboutthingsisthatalldeclarations,bothvariablesandfunctions,areprocessedfirst,beforeany
Chapter4:Hoisting
ChickenOrTheEgg?
TheCompilerStrikesAgain
YouDon'tKnowJS:Scope&Closures
33
Chapter4:Hoisting
partofyourcodeisexecuted.
Whenyousee
vara=2;
,youprobablythinkofthatasonestatement.ButJavaScriptactuallythinksofitastwo
statements:
vara;
and
a=2;
.Thefirststatement,thedeclaration,isprocessedduringthecompilationphase.The
secondstatement,theassignment,isleftinplacefortheexecutionphase.
Ourfirstsnippetthenshouldbethoughtofasbeinghandledlikethis:
var
a;
a=
2
;
console
.log(a);
...wherethefirstpartisthecompilationandthesecondpartistheexecution.
Similarly,oursecondsnippetisactuallyprocessedas:
var
a;
console
.log(a);
a=
2
;
So,onewayofthinking,sortofmetaphorically,aboutthisprocess,isthatvariableandfunctiondeclarationsare"moved"
fromwheretheyappearintheflowofthecodetothetopofthecode.Thisgivesrisetothename"Hoisting".
Inotherwords,theegg(declaration)comesbeforethechicken(assignment).
Note:Onlythedeclarationsthemselvesarehoisted,whileanyassignmentsorotherexecutablelogicareleftinplace.If
hoistingweretore-arrangetheexecutablelogicofourcode,thatcouldwreakhavoc.
foo();
function
foo
()
{
console
.log(a);
//undefined
var
a=
2
;
}
Thefunction
foo
'sdeclaration(whichinthiscaseincludestheimpliedvalueofitasanactualfunction)ishoisted,suchthat
thecallonthefirstlineisabletoexecute.
It'salsoimportanttonotethathoistingisper-scope.Sowhileourprevioussnippetsweresimplifiedinthattheyonly
includedglobalscope,the
foo(..)
functionwearenowexaminingitselfexhibitsthat
vara
ishoistedtothetopof
foo(..)
(not,obviously,tothetopoftheprogram).Sotheprogramcanperhapsbemoreaccuratelyinterpretedlikethis:
function
foo
()
{
var
a;
console
.log(a);
//undefined
a=
2
;
YouDon'tKnowJS:Scope&Closures
34
Chapter4:Hoisting
}
foo();
Functiondeclarationsarehoisted,aswejustsaw.Butfunctionexpressionsarenot.
foo();
//notReferenceError,butTypeError!
var
foo=
function
bar
()
{
//...
};
Thevariableidentifier
foo
ishoistedandattachedtotheenclosingscope(global)ofthisprogram,so
foo()
doesn'tfailas
a
ReferenceError
.But
foo
hasnovalueyet(asitwouldifithadbeenatruefunctiondeclarationinsteadofexpression).
So,
foo()
isattemptingtoinvokethe
undefined
value,whichisa
TypeError
illegaloperation.
Alsorecallthateventhoughit'sanamedfunctionexpression,thenameidentifierisnotavailableintheenclosingscope:
foo();
//TypeError
bar();
//ReferenceError
var
foo=
function
bar
()
{
//...
};
Thissnippetismoreaccuratelyinterpreted(withhoisting)as:
var
foo;
foo();
//TypeError
bar();
//ReferenceError
foo=
function
()
{
var
bar=...self...
//...
}
Bothfunctiondeclarationsandvariabledeclarationsarehoisted.Butasubtledetail(thatcanshowupincodewithmultiple
"duplicate"declarations)isthatfunctionsarehoistedfirst,andthenvariables.
Consider:
foo();
//1
var
foo;
function
foo
()
{
console
.log(
1
);
}
foo=
function
()
{
console
.log(
2
);
};
1
isprintedinsteadof
2
!ThissnippetisinterpretedbytheEngineas:
FunctionsFirst
YouDon'tKnowJS:Scope&Closures
35
Chapter4:Hoisting
function
foo
()
{
console
.log(
1
);
}
foo();
//1
foo=
function
()
{
console
.log(
2
);
};
Noticethat
varfoo
wastheduplicate(andthusignored)declaration,eventhoughitcamebeforethe
functionfoo()...
declaration,becausefunctiondeclarationsarehoistedbeforenormalvariables.
Whilemultiple/duplicate
var
declarationsareeffectivelyignored,subsequentfunctiondeclarationsdooverrideprevious
ones.
foo();
//3
function
foo
()
{
console
.log(
1
);
}
var
foo=
function
()
{
console
.log(
2
);
};
function
foo
()
{
console
.log(
3
);
}
Whilethisallmaysoundlikenothingmorethaninterestingacademictrivia,ithighlightsthefactthatduplicatedefinitionsin
thesamescopeareareallybadideaandwilloftenleadtoconfusingresults.
Functiondeclarationsthatappearinsideofnormalblockstypicallyhoisttotheenclosingscope,ratherthanbeing
conditionalasthiscodeimplies:
foo();
//"b"
var
a=
true
;
if
(a){
function
foo
()
{
console
.log(
"a"
);}
}
else
{
function
foo
()
{
console
.log(
"b"
);}
}
However,it'simportanttonotethatthisbehaviorisnotreliableandissubjecttochangeinfutureversionsofJavaScript,so
it'sprobablybesttoavoiddeclaringfunctionsinblocks.
Wecanbetemptedtolookat
vara=2;
asonestatement,buttheJavaScriptEnginedoesnotseeitthatway.Itsees
var
a
and
a=2
astwoseparatestatements,thefirstoneacompiler-phasetask,andthesecondoneanexecution-phase
task.
Whatthisleadstoisthatalldeclarationsinascope,regardlessofwheretheyappear,areprocessedfirstbeforethecode
itselfisexecuted.Youcanvisualizethisasdeclarations(variablesandfunctions)being"moved"tothetopoftheir
respectivescopes,whichwecall"hoisting".
Review(TL;DR)
YouDon'tKnowJS:Scope&Closures
36
Chapter4:Hoisting
Declarationsthemselvesarehoisted,butassignments,evenassignmentsoffunctionexpressions,arenothoisted.
Becarefulaboutduplicatedeclarations,especiallymixedbetweennormalvardeclarationsandfunctiondeclarations--peril
awaitsifyoudo!
YouDon'tKnowJS:Scope&Closures
37
Chapter4:Hoisting
Wearriveatthispointwithhopefullyaveryhealthy,solidunderstandingofhowscopeworks.
Weturnourattentiontoanincrediblyimportant,butpersistentlyelusive,almostmythological,partofthelanguage:closure.
Ifyouhavefollowedourdiscussionoflexicalscopethusfar,thepayoffisthatclosureisgoingtobe,largely,anti-climatic,
almostself-obvious.There'samanbehindthewizard'scurtain,andwe'reabouttoseehim.No,hisnameisnotCrockford!
Ifhoweveryouhavenaggingquestionsaboutlexicalscope,nowwouldbeagoodtimetogobackandreviewChapter2
beforeproceeding.
ForthosewhoaresomewhatexperiencedinJavaScript,buthaveperhapsneverfullygraspedtheconceptofclosures,
understandingclosurecanseemlikeaspecialnirvanathatonemuststriveandsacrificetoattain.
IrecallyearsbackwhenIhadafirmgrasponJavaScript,buthadnoideawhatclosurewas.Thehintthattherewasthis
othersidetothelanguage,onewhichpromisedevenmorecapabilitythanIalreadypossessed,teasedandtauntedme.I
rememberreadingthroughthesourcecodeofearlyframeworkstryingtounderstandhowitactuallyworked.Iremember
thefirsttimesomethingofthe"modulepattern"begantoemergeinmymind.Irememberthea-ha!momentsquitevividly.
WhatIdidn'tknowbackthen,whattookmeyearstounderstand,andwhatIhopetoimparttoyoupresently,isthissecret:
closureisallaroundyouinJavaScript,youjusthavetorecognizeandembraceit.Closuresarenotaspecialopt-in
toolthatyoumustlearnnewsyntaxandpatternsfor.No,closuresarenotevenaweaponthatyoumustlearntowieldand
masterasLuketrainedinTheForce.
Closureshappenasaresultofwritingcodethatreliesonlexicalscope.Theyjusthappen.Youdonotevenreallyhaveto
intentionallycreateclosurestotakeadvantageofthem.Closuresarecreatedandusedforyoualloveryourcode.What
youaremissingisthepropermentalcontexttorecognize,embrace,andleverageclosuresforyourownwill.
Theenlightenmentmomentshouldbe:oh,closuresarealreadyoccurringallovermycode,Icanfinallyseethem
now.UnderstandingclosuresislikewhenNeoseestheMatrixforthefirsttime.
OK,enoughhyperboleandshamelessmoviereferences.
Here'sadown-n-dirtydefinitionofwhatyouneedtoknowtounderstandandrecognizeclosures:
Closureiswhenafunctionisabletorememberandaccessitslexicalscopeevenwhenthatfunctionisexecuting
outsideitslexicalscope.
Let'sjumpintosomecodetoillustratethatdefinition.
function
foo
()
{
var
a=
2
;
function
bar
()
{
console
.log(a);
//2
}
bar();
}
Chapter5:ScopeClosure
Enlightenment
NittyGritty
YouDon'tKnowJS:Scope&Closures
38
Chapter5:ScopeClosures
foo();
ThiscodeshouldlookfamiliarfromourdiscussionsofNestedScope.Function
bar()
hasaccesstothevariable
a
inthe
outerenclosingscopebecauseoflexicalscopelook-uprules(inthiscase,it'sanRHSreferencelook-up).
Isthis"closure"?
Well,technically...perhaps.Butbyourwhat-you-need-to-knowdefinitionabove...notexactly.Ithinkthemostaccurateway
toexplain
bar()
referencing
a
isvialexicalscopelook-uprules,andthoserulesareonly(animportant!)partofwhat
closureis.
Fromapurelyacademicperspective,whatissaidoftheabovesnippetisthatthefunction
bar()
hasaclosureoverthe
scopeof
foo()
(andindeed,evenovertherestofthescopesithasaccessto,suchastheglobalscopeinourcase).Put
slightlydifferently,it'ssaidthat
bar()
closesoverthescopeof
foo()
.Why?Because
bar()
appearsnestedinsideof
foo()
.Plainandsimple.
But,closuredefinedinthiswayisnotdirectlyobservable,nordoweseeclosureexercisedinthatsnippet.Weclearlysee
lexicalscope,butclosureremainssortofamysteriousshiftingshadowbehindthecode.
Letusthenconsidercodewhichbringsclosureintofulllight:
function
foo
()
{
var
a=
2
;
function
bar
()
{
console
.log(a);
}
return
bar;
}
var
baz=foo();
baz();
//2--Whoa,closurewasjustobserved,man.
Thefunction
bar()
haslexicalscopeaccesstotheinnerscopeof
foo()
.Butthen,wetake
bar()
,thefunctionitself,and
passitasavalue.Inthiscase,we
return
thefunctionobjectitselfthat
bar
references.
Afterweexecute
foo()
,weassignthevalueitreturned(ourinner
bar()
function)toavariablecalled
baz
,andthenwe
actuallyinvoke
baz()
,whichofcourseisinvokingourinnerfunction
bar()
,justbyadifferentidentifierreference.
bar()
isexecuted,forsure.Butinthiscase,it'sexecutedoutsideofitsdeclaredlexicalscope.
After
foo()
executed,normallywewouldexpectthattheentiretyoftheinnerscopeof
foo()
wouldgoaway,becausewe
knowthattheEngineemploysaGarbageCollectorthatcomesalongandfreesupmemoryonceit'snolongerinuse.Since
itwouldappearthatthecontentsof
foo()
arenolongerinuse,itwouldseemnaturalthattheyshouldbeconsideredgone.
Butthe"magic"ofclosuresdoesnotletthishappen.Thatinnerscopeisinfactstill"inuse",andthusdoesnotgoaway.
Who'susingit?Thefunction
bar()
itself.
Byvirtueofwhereitwasdeclared,
bar()
hasalexicalscopeclosureoverthatinnerscopeof
foo()
,whichkeepsthat
scopealivefor
bar()
toreferenceatanylatertime.
bar()
stillhasareferencetothatscope,andthatreferenceiscalledclosure.
So,afewmicrosecondslater,whenthevariable
baz
isinvoked(invokingtheinnerfunctionweinitiallylabeled
bar
),itduly
hasaccesstoauthor-timelexicalscope,soitcanaccessthevariable
a
justaswe'dexpect.
YouDon'tKnowJS:Scope&Closures
39
Chapter5:ScopeClosures
Thefunctionisbeinginvokedwelloutsideofitsauthor-timelexicalscope.Closureletsthefunctioncontinuetoaccessthe
lexicalscopeitwasdefinedinatauthor-time.
Ofcourse,anyofthevariouswaysthatfunctionscanbepassedaroundasvalues,andindeedinvokedinotherlocations,
areallexamplesofobserving/exercisingclosure.
function
foo
()
{
var
a=
2
;
function
baz
()
{
console
.log(a);
//2
}
bar(baz);
}
function
bar
(
fn
)
{
fn();
//lookma,Isawclosure!
}
Wepasstheinnerfunction
baz
overto
bar
,andcallthatinnerfunction(labeled
fn
now),andwhenwedo,itsclosure
overtheinnerscopeof
foo()
isobserved,byaccessing
a
.
Thesepassings-aroundoffunctionscanbeindirect,too.
var
fn;
function
foo
()
{
var
a=
2
;
function
baz
()
{
console
.log(a);
}
fn=baz;
//assign`baz`toglobalvariable
}
function
bar
()
{
fn();
//lookma,Isawclosure!
}
foo();
bar();
//2
Whateverfacilityweusetotransportaninnerfunctionoutsideofitslexicalscope,itwillmaintainascopereferenceto
whereitwasoriginallydeclared,andwhereverweexecuteit,thatclosurewillbeexercised.
Thepreviouscodesnippetsaresomewhatacademicandartificiallyconstructedtoillustrateusingclosure.ButIpromised
yousomethingmorethanjustacoolnewtoy.Ipromisedthatclosurewassomethingallaroundyouinyourexistingcode.
Letusnowseethattruth.
function
wait
(
message
)
{
setTimeout(
function
timer
()
{
console
.log(message);
},
1000
);
}
NowICanSee
YouDon'tKnowJS:Scope&Closures
40
Chapter5:ScopeClosures
wait(
"Hello,closure!"
);
Wetakeaninnerfunction(named
timer
)andpassitto
setTimeout(..)
.But
timer
hasascopeclosureoverthescopeof
wait(..)
,indeedkeepingandusingareferencetothevariable
message
.
Athousandmillisecondsafterwehaveexecuted
wait(..)
,anditsinnerscopeshouldotherwisebelonggone,that
anonymousfunctionstillhasclosureoverthatscope.
DeepdowninthegutsoftheEngine,thebuilt-inutility
setTimeout(..)
hasreferencetosomeparameter,probablycalled
fn
or
func
orsomethinglikethat.Enginegoestoinvokethatfunction,whichisinvokingourinner
timer
function,andthe
lexicalscopereferenceisstillintact.
Closure.
Or,ifyou'reofthejQuerypersuasion(oranyJSframework,forthatmatter):
function
setupBot
(
name,selector
)
{
$(selector).click(
function
activator
()
{
console
.log(
"Activating:"
+name);
});
}
setupBot(
"ClosureBot1"
,
"#bot_1"
);
setupBot(
"ClosureBot2"
,
"#bot_2"
);
Iamnotsurewhatkindofcodeyouwrite,butIregularlywritecodewhichisresponsibleforcontrollinganentireglobal
dronearmyofclosurebots,sothisistotallyrealistic!
(Some)jokingaside,essentiallywheneverandwhereveryoutreatfunctions(whichaccesstheirownrespectivelexical
scopes)asfirst-classvaluesandpassthemaround,youarelikelytoseethosefunctionsexercisingclosure.Bethattimers,
eventhandlers,Ajaxrequests,cross-windowmessaging,webworkers,oranyoftheotherasynchronous(orsynchronous!)
tasks,whenyoupassinacallbackfunction,getreadytoslingsomeclosurearound!
Note:Chapter3introducedtheIIFEpattern.WhileitisoftensaidthatIIFE(alone)isanexampleofobservedclosure,I
wouldsomewhatdisagree,byourdefinitionabove.
var
a=
2
;
(
function
IIFE
()
{
console
.log(a);
})();
Thiscode"works",butit'snotstrictlyanobservationofclosure.Why?Becausethefunction(whichwenamed"IIFE"here)
isnotexecutedoutsideitslexicalscope.It'sstillinvokedrightthereinthesamescopeasitwasdeclared(the
enclosing/globalscopethatalsoholds
a
).
a
isfoundvianormallexicalscopelook-up,notreallyviaclosure.
Whileclosuremighttechnicallybehappeningatdeclarationtime,itisnotstrictlyobservable,andso,astheysay,it'satree
fallingintheforestwithnoonearoundtohearit.
ThoughanIIFEisnotitselfanexampleofclosure,itabsolutelycreatesscope,andit'soneofthemostcommontoolswe
usetocreatescopewhichcanbeclosedover.SoIIFEsareindeedheavilyrelatedtoclosure,evenifnotexercisingclosure
themselves.
Putthisbookdownrightnow,dearreader.Ihaveataskforyou.GoopenupsomeofyourrecentJavaScriptcode.Lookfor
yourfunctions-as-valuesandidentifywhereyouarealreadyusingclosureandmaybedidn'tevenknowitbefore.
YouDon'tKnowJS:Scope&Closures
41
Chapter5:ScopeClosures
I'llwait.
Now...yousee!
Themostcommoncanonicalexampleusedtoillustrateclosureinvolvesthehumblefor-loop.
for
(
var
i=
1
;i<=
5
;i++){
setTimeout(
function
timer
()
{
console
.log(i);
},i*
1000
);
}
Note:Lintersoftencomplainwhenyouputfunctionsinsideofloops,becausethemistakesofnotunderstandingclosure
aresocommonamongdevelopers.Weexplainhowtodosoproperlyhere,leveragingthefullpowerofclosure.Butthat
subtletyisoftenlostonlintersandtheywillcomplainregardless,assumingyoudon'tactuallyknowwhatyou'redoing.
Thespiritofthiscodesnippetisthatwewouldnormallyexpectforthebehaviortobethatthenumbers"1","2",.."5"would
beprintedout,oneatatime,onepersecond,respectively.
Infact,ifyourunthiscode,youget"6"printedout5times,attheone-secondintervals.
Huh?
Firstly,let'sexplainwhere
6
comesfrom.Theterminatingconditionoftheloopiswhen
i
isnot
<=5
.Thefirsttimethat's
thecaseiswhen
i
is6.So,theoutputisreflectingthefinalvalueofthe
i
aftertheloopterminates.
Thisactuallyseemsobviousonsecondglance.Thetimeoutfunctioncallbacksareallrunningwellafterthecompletionof
theloop.Infact,astimersgo,evenifitwas
setTimeout(..,0)
oneachiteration,allthosefunctioncallbackswouldstillrun
strictlyafterthecompletionoftheloop,andthusprint
6
eachtime.
Butthere'sadeeperquestionatplayhere.What'smissingfromourcodetoactuallyhaveitbehaveaswesemantically
haveimplied?
What'smissingisthatwearetryingtoimplythateachiterationoftheloop"captures"itsowncopyof
i
,atthetimeofthe
iteration.But,thewayscopeworks,all5ofthosefunctions,thoughtheyaredefinedseparatelyineachloopiteration,all
areclosedoverthesamesharedglobalscope,whichhas,infact,onlyone
i
init.
Putthatway,ofcourseallfunctionsshareareferencetothesame
i
.Somethingabouttheloopstructuretendstoconfuse
usintothinkingthere'ssomethingelsemoresophisticatedatwork.Thereisnot.There'snodifferencethanifeachofthe5
timeoutcallbackswerejustdeclaredonerightaftertheother,withnoloopatall.
OK,so,backtoourburningquestion.What'smissing?Weneedmorecowbellclosuredscope.Specifically,weneedanew
closuredscopeforeachiterationoftheloop.
WelearnedinChapter3thattheIIFEcreatesscopebydeclaringafunctionandimmediatelyexecutingit.
Let'stry:
for
(
var
i=
1
;i<=
5
;i++){
(
function
()
{
setTimeout(
function
timer
()
{
console
.log(i);
},i*
1000
);
Loops+Closure
YouDon'tKnowJS:Scope&Closures
42
Chapter5:ScopeClosures
})();
}
Doesthatwork?Tryit.Again,I'llwait.
I'llendthesuspenseforyou.Nope.Butwhy?Wenowobviouslyhavemorelexicalscope.Eachtimeoutfunctioncallbackis
indeedclosingoveritsownper-iterationscopecreatedrespectivelybyeachIIFE.
It'snotenoughtohaveascopetocloseoverifthatscopeisempty.Lookclosely.OurIIFEisjustanemptydo-nothing
scope.Itneedssomethinginittobeusefultous.
Itneedsitsownvariable,withacopyofthe
i
valueateachiteration.
for
(
var
i=
1
;i<=
5
;i++){
(
function
()
{
var
j=i;
setTimeout(
function
timer
()
{
console
.log(j);
},j*
1000
);
})();
}
Eureka!Itworks!
Aslightvariationsomepreferis:
for
(
var
i=
1
;i<=
5
;i++){
(
function
(
j
)
{
setTimeout(
function
timer
()
{
console
.log(j);
},j*
1000
);
})(i);
}
Ofcourse,sincetheseIIFEsarejustfunctions,wecanpassin
i
,andwecancallit
j
ifweprefer,orwecanevencallit
i
again.Eitherway,thecodeworksnow.
TheuseofanIIFEinsideeachiterationcreatedanewscopeforeachiteration,whichgaveourtimeoutfunctioncallbacks
theopportunitytocloseoveranewscopeforeachiteration,onewhichhadavariablewiththerightper-iterationvalueinit
forustoaccess.
Problemsolved!
Lookcarefullyatouranalysisoftheprevioussolution.WeusedanIIFEtocreatenewscopeper-iteration.Inotherwords,
weactuallyneededaper-iterationblockscope.Chapter3showedusthe
let
declaration,whichhijacksablockand
declaresavariablerightthereintheblock.
Itessentiallyturnsablockintoascopethatwecancloseover.So,thefollowingawesomecode"justworks":
for
(
var
i=
1
;i<=
5
;i++){
let
j=i;
//yay,block-scopeforclosure!
setTimeout(
function
timer
()
{
console
.log(j);
},j*
1000
);
}
BlockScopingRevisited
YouDon'tKnowJS:Scope&Closures
43
Chapter5:ScopeClosures
But,that'snotall!(inmybestBobBarkervoice).There'saspecialbehaviordefinedfor
let
declarationsusedinthehead
ofafor-loop.Thisbehaviorsaysthatthevariablewillbedeclarednotjustoncefortheloop,buteachiteration.And,itwill,
helpfully,beinitializedateachsubsequentiterationwiththevaluefromtheendofthepreviousiteration.
for
(
let
i=
1
;i<=
5
;i++){
setTimeout(
function
timer
()
{
console
.log(i);
},i*
1000
);
}
Howcoolisthat?Blockscopingandclosureworkinghand-in-hand,solvingalltheworld'sproblems.Idon'tknowaboutyou,
butthatmakesmeahappyJavaScripter.
Thereareothercodepatternswhichleveragethepowerofclosurebutwhichdonotonthesurfaceappeartobeabout
callbacks.Let'sexaminethemostpowerfulofthem:themodule.
function
foo
()
{
var
something=
"cool"
;
var
another=[
1
,
2
,
3
];
function
doSomething
()
{
console
.log(something);
}
function
doAnother
()
{
console
.log(another.join(
"!"
));
}
}
Asthiscodestandsrightnow,there'snoobservableclosuregoingon.Wesimplyhavesomeprivatedatavariables
something
and
another
,andacoupleofinnerfunctions
doSomething()
and
doAnother()
,whichbothhavelexicalscope
(andthusclosure!)overtheinnerscopeof
foo()
.
Butnowconsider:
function
CoolModule
()
{
var
something=
"cool"
;
var
another=[
1
,
2
,
3
];
function
doSomething
()
{
console
.log(something);
}
function
doAnother
()
{
console
.log(another.join(
"!"
));
}
return
{
doSomething:doSomething,
doAnother:doAnother
};
}
var
foo=CoolModule();
foo.doSomething();
//cool
foo.doAnother();
//1!2!3
Modules
YouDon'tKnowJS:Scope&Closures
44
Chapter5:ScopeClosures
ThisisthepatterninJavaScriptwecallmodule.Themostcommonwayofimplementingthemodulepatternisoftencalled
"RevealingModule",andit'sthevariationwepresenthere.
Let'sexaminesomethingsaboutthiscode.
Firstly,
CoolModule()
isjustafunction,butithastobeinvokedfortheretobeamoduleinstancecreated.Withoutthe
executionoftheouterfunction,thecreationoftheinnerscopeandtheclosureswouldnotoccur.
Secondly,the
CoolModule()
functionreturnsanobject,denotedbytheobject-literalsyntax
{key:value,...}
.Theobject
wereturnhasreferencesonittoourinnerfunctions,butnottoourinnerdatavariables.Wekeepthosehiddenandprivate.
It'sappropriatetothinkofthisobjectreturnvalueasessentiallyapublicAPIforourmodule.
Thisobjectreturnvalueisultimatelyassignedtotheoutervariable
foo
,andthenwecanaccessthosepropertymethods
ontheAPI,like
foo.doSomething()
.
Note:Itisnotrequiredthatwereturnanactualobject(literal)fromourmodule.Wecouldjustreturnbackaninnerfunction
directly.jQueryisactuallyagoodexampleofthis.The
jQuery
and
$
identifiersarethepublicAPIforthejQuery"module",
buttheyare,themselves,justafunction(whichcanitselfhaveproperties,sinceallfunctionsareobjects).
The
doSomething()
and
doAnother()
functionshaveclosureovertheinnerscopeofthemodule"instance"(arrivedatby
actuallyinvoking
CoolModule()
).Whenwetransportthosefunctionsoutsideofthelexicalscope,bywayofproperty
referencesontheobjectwereturn,wehavenowsetupaconditionbywhichclosurecanbeobservedandexercised.
Tostateitmoresimply,therearetwo"requirements"forthemodulepatterntobeexercised:
1. Theremustbeanouterenclosingfunction,anditmustbeinvokedatleastonce(eachtimecreatesanewmodule
instance).
2. Theenclosingfunctionmustreturnbackatleastoneinnerfunction,sothatthisinnerfunctionhasclosureoverthe
privatescope,andcanaccessand/ormodifythatprivatestate.
Anobjectwithafunctionpropertyonitaloneisnotreallyamodule.Anobjectwhichisreturnedfromafunctioninvocation
whichonlyhasdatapropertiesonitandnoclosuredfunctionsisnotreallyamodule,intheobservablesense.
Thecodesnippetaboveshowsastandalonemodulecreatorcalled
CoolModule()
whichcanbeinvokedanynumberof
times,eachtimecreatinganewmoduleinstance.Aslightvariationonthispatterniswhenyouonlycaretohaveone
instance,a"singleton"ofsorts:
var
foo=(
function
CoolModule
()
{
var
something=
"cool"
;
var
another=[
1
,
2
,
3
];
function
doSomething
()
{
console
.log(something);
}
function
doAnother
()
{
console
.log(another.join(
"!"
));
}
return
{
doSomething:doSomething,
doAnother:doAnother
};
})();
foo.doSomething();
//cool
foo.doAnother();
//1!2!3
Here,weturnedourmodulefunctionintoanIIFE(seeChapter3),andweimmediatelyinvokeditandassigneditsreturn
YouDon'tKnowJS:Scope&Closures
45
Chapter5:ScopeClosures
valuedirectlytooursinglemoduleinstanceidentifier
foo
.
Modulesarejustfunctions,sotheycanreceiveparameters:
function
CoolModule
(
id
)
{
function
identify
()
{
console
.log(id);
}
return
{
identify:identify
};
}
var
foo1=CoolModule(
"foo1"
);
var
foo2=CoolModule(
"foo2"
);
foo1.identify();
//"foo1"
foo2.identify();
//"foo2"
AnotherslightbutpowerfulvariationonthemodulepatternistonametheobjectyouarereturningasyourpublicAPI:
var
foo=(
function
CoolModule
(
id
)
{
function
change
()
{
//modifyingthepublicAPI
publicAPI.identify=identify2;
}
function
identify1
()
{
console
.log(id);
}
function
identify2
()
{
console
.log(id.toUpperCase());
}
var
publicAPI={
change:change,
identify:identify1
};
return
publicAPI;
})(
"foomodule"
);
foo.identify();
//foomodule
foo.change();
foo.identify();
//FOOMODULE
ByretaininganinnerreferencetothepublicAPIobjectinsideyourmoduleinstance,youcanmodifythatmoduleinstance
fromtheinside,includingaddingandremovingmethods,properties,andchangingtheirvalues.
Variousmoduledependencyloaders/managersessentiallywrapupthispatternofmoduledefinitionintoafriendlyAPI.
Ratherthanexamineanyoneparticularlibrary,letmepresentaverysimpleproofofconceptforillustrationpurposes
(only):
var
MyModules=(
function
Manager
()
{
var
modules={};
function
define
(
name,deps,impl
)
{
for
(
var
i=
0
;i<deps.length;i++){
deps[i]=modules[deps[i]];
}
modules[name]=impl.apply(impl,deps);
ModernModules
YouDon'tKnowJS:Scope&Closures
46
Chapter5:ScopeClosures
}
function
get
(
name
)
{
return
modules[name];
}
return
{
define:define,
get:get
};
})();
Thekeypartofthiscodeis
modules[name]=impl.apply(impl,deps)
.Thisisinvokingthedefinitionwrapperfunctionfora
module(passinginanydependencies),andstoringthereturnvalue,themodule'sAPI,intoaninternallistofmodules
trackedbyname.
Andhere'showImightuseittodefinesomemodules:
MyModules.define(
"bar"
,[],
function
()
{
function
hello
(
who
)
{
return
"Letmeintroduce:"
+who;
}
return
{
hello:hello
};
});
MyModules.define(
"foo"
,[
"bar"
],
function
(
bar
)
{
var
hungry=
"hippo"
;
function
awesome
()
{
console
.log(bar.hello(hungry).toUpperCase());
}
return
{
awesome:awesome
};
});
var
bar=MyModules.get(
"bar"
);
var
foo=MyModules.get(
"foo"
);
console
.log(
bar.hello(
"hippo"
)
);
//Letmeintroduce:hippo
foo.awesome();
//LETMEINTRODUCE:HIPPO
Boththe"foo"and"bar"modulesaredefinedwithafunctionthatreturnsapublicAPI."foo"evenreceivestheinstanceof
"bar"asadependencyparameter,andcanuseitaccordingly.
Spendsometimeexaminingthesecodesnippetstofullyunderstandthepowerofclosuresputtouseforourowngood
purposes.Thekeytake-awayisthatthere'snotreallyanyparticular"magic"tomodulemanagers.Theyfulfillboth
characteristicsofthemodulepatternIlistedabove:invokingafunctiondefinitionwrapper,andkeepingitsreturnvalueas
theAPIforthatmodule.
Inotherwords,modulesarejustmodules,evenifyouputafriendlywrappertoolontopofthem.
ES6addsfirst-classsyntaxsupportfortheconceptofmodules.Whenloadedviathemodulesystem,ES6treatsafileasa
separatemodule.EachmodulecanbothimportothermodulesorspecificAPImembers,aswellexporttheirownpublicAPI
members.
FutureModules
YouDon'tKnowJS:Scope&Closures
47
Chapter5:ScopeClosures
Note:Function-basedmodulesaren'tastaticallyrecognizedpattern(somethingthecompilerknowsabout),sotheirAPI
semanticsaren'tconsidereduntilrun-time.Thatis,youcanactuallymodifyamodule'sAPIduringtherun-time(seeearlier
publicAPI
discussion).
Bycontrast,ES6ModuleAPIsarestatic(theAPIsdon'tchangeatrun-time).Sincethecompilerknowsthat,itcan(and
does!)checkduring(fileloadingand)compilationthatareferencetoamemberofanimportedmodule'sAPIactuallyexists.
IftheAPIreferencedoesn'texist,thecompilerthrowsan"early"erroratcompile-time,ratherthanwaitingfortraditional
dynamicrun-timeresolution(anderrors,ifany).
ES6modulesdonothavean"inline"format,theymustbedefinedinseparatefiles(onepermodule).The
browsers/engineshaveadefault"moduleloader"(whichisoverridable,butthat'swell-beyondourdiscussionhere)which
synchronouslyloadsamodulefilewhenit'simported.
Consider:
bar.js
function
hello
(
who
)
{
return
"Letmeintroduce:"
+who;
}
export
hello;
foo.js
//importonly`hello()`fromthe"bar"module
import
hello
from
"bar"
;
var
hungry=
"hippo"
;
function
awesome
()
{
console
.log(
hello(hungry).toUpperCase()
);
}
export
awesome;
//importtheentire"foo"and"bar"modules
module
foofrom
"foo"
;
module
barfrom
"bar"
;
console
.log(
bar.hello(
"rhino"
)
);
//Letmeintroduce:rhino
foo.awesome();
//LETMEINTRODUCE:HIPPO
Note:Separatefiles"foo.js"and"bar.js"wouldneedtobecreated,withthecontentsasshowninthefirsttwosnippets,
respectively.Then,yourprogramwouldload/importthosemodulestousethem,asshowninthethirdsnippet.
import
importsoneormoremembersfromamodule'sAPIintothecurrentscope,eachtoaboundvariable(
hello
inour
case).
module
importsanentiremoduleAPItoaboundvariable(
foo
,
bar
inourcase).
export
exportsanidentifier
(variable,function)tothepublicAPIforthecurrentmodule.Theseoperatorscanbeusedasmanytimesinamodule's
definitionasisnecessary.
Thecontentsinsidethemodulefilearetreatedasifenclosedinascopeclosure,justlikewiththefunction-closuremodules
seenearlier.
YouDon'tKnowJS:Scope&Closures
48
Chapter5:ScopeClosures
Closureseemstotheun-enlightenedlikeamysticalworldsetapartinsideofJavaScriptwhichonlythefewbravestsouls
canreach.Butit'sactuallyjustastandardandalmostobviousfactofhowwewritecodeinalexicallyscopedenvironment,
wherefunctionsarevaluesandcanbepassedaroundatwill.
Closureiswhenafunctioncanrememberandaccessitslexicalscopeevenwhenit'sinvokedoutsideitslexical
scope.
Closurescantripusup,forinstancewithloops,ifwe'renotcarefultorecognizethemandhowtheywork.Buttheyarealso
animmenselypowerfultool,enablingpatternslikemodulesintheirvariousforms.
Modulesrequiretwokeycharacteristics:1)anouterwrappingfunctionbeinginvoked,tocreatetheenclosingscope2)the
returnvalueofthewrappingfunctionmustincludereferencetoatleastoneinnerfunctionthatthenhasclosureoverthe
privateinnerscopeofthewrapper.
Nowwecanseeclosuresallaroundourexistingcode,andwehavetheabilitytorecognizeandleveragethemtoourown
benefit!
Review(TL;DR)
YouDon'tKnowJS:Scope&Closures
49
Chapter5:ScopeClosures
InChapter2,wetalkedabout"DynamicScope"asacontrasttothe"LexicalScope"model,whichishowscopeworksin
JavaScript(andinfact,mostotherlanguages).
Wewillbrieflyexaminedynamicscope,tohammerhomethecontrast.But,moreimportantly,dynamicscopeactuallyisa
nearcousintoanothermechanism(
this
)inJavaScript,whichwecoveredinthe"this&ObjectPrototypes"titleofthis
bookseries.
AswesawinChapter2,lexicalscopeisthesetofrulesabouthowtheEnginecanlook-upavariableandwhereitwillfind
it.Thekeycharacteristicoflexicalscopeisthatitisdefinedatauthor-time,whenthecodeiswritten(assumingyoudon't
cheatwith
eval()
or
with
).
Dynamicscopeseemstoimply,andforgoodreason,thatthere'samodelwherebyscopecanbedetermineddynamically
atruntime,ratherthanstaticallyatauthor-time.Thatisinfactthecase.Let'sillustrateviacode:
function
foo
()
{
console
.log(a);
//2
}
function
bar
()
{
var
a=
3
;
foo();
}
var
a=
2
;
bar();
LexicalscopeholdsthattheRHSreferenceto
a
in
foo()
willberesolvedtotheglobalvariable
a
,whichwillresultin
value
2
beingoutput.
Dynamicscope,bycontrast,doesn'tconcernitselfwithhowandwherefunctionsandscopesaredeclared,butrather
wheretheyarecalledfrom.Inotherwords,thescopechainisbasedonthecall-stack,notthenestingofscopesincode.
So,ifJavaScripthaddynamicscope,when
foo()
isexecuted,theoreticallythecodebelowwouldinsteadresultin
3
as
theoutput.
function
foo
()
{
console
.log(a);
//3(not2!)
}
function
bar
()
{
var
a=
3
;
foo();
}
var
a=
2
;
bar();
Howcanthisbe?Becausewhen
foo()
cannotresolvethevariablereferencefor
a
,insteadofsteppingupthenested
(lexical)scopechain,itwalksupthecall-stack,tofindwhere
foo()
wascalledfrom.Since
foo()
wascalledfrom
bar()
,it
checksthevariablesinscopefor
bar()
,andfindsan
a
therewithvalue
3
.
Strange?You'reprobablythinkingso,atthemoment.
AppendixA:DynamicScope
YouDon'tKnowJS:Scope&Closures
50
AppendixA:DynamicScope
Butthat'sjustbecauseyou'veprobablyonlyeverworkedon(oratleastdeeplyconsidered)codewhichislexicallyscoped.
Sodynamicscopingseemsforeign.Ifyouhadonlyeverwrittencodeinadynamicallyscopedlanguage,itwouldseem
natural,andlexicalscopewouldbetheodd-ball.
Tobeclear,JavaScriptdoesnot,infact,havedynamicscope.Ithaslexicalscope.Plainandsimple.Butthe
this
mechanismiskindoflikedynamicscope.
Thekeycontrast:lexicalscopeiswrite-time,whereasdynamicscope(and
this
!)areruntime.Lexicalscopecares
whereafunctionwasdeclared,butdynamicscopecareswhereafunctionwascalledfrom.
Finally:
this
careshowafunctionwascalled,whichshowshowcloselyrelatedthe
this
mechanismistotheideaof
dynamicscoping.Todigmoreinto
this
,readthetitle"this&ObjectPrototypes".
YouDon'tKnowJS:Scope&Closures
51
AppendixA:DynamicScope
InChapter3,weexploredBlockScope.Wesawthat
with
andthe
catch
clausearebothtinyexamplesofblockscope
thathaveexistedinJavaScriptsinceatleasttheintroductionofES3.
Butit'sES6'sintroductionof
let
thatfinallygivesfull,unfetteredblock-scopingcapabilitytoourcode.Therearemany
excitingthings,bothfunctionallyandcode-stylistically,thatblockscopewillenable.
Butwhatifwewantedtouseblockscopeinpre-ES6environments?
Considerthiscode:
{
let
a=
2
;
console
.log(a);
//2
}
console
.log(a);
//ReferenceError
ThiswillworkgreatinES6environments.Butcanwedosopre-ES6?
catch
istheanswer.
try
{
throw
2
}
catch
(a){
console
.log(a);
//2
}
console
.log(a);
//ReferenceError
Whoa!That'ssomeugly,weirdlookingcode.Weseea
try/catch
thatappearstoforciblythrowanerror,butthe"error"it
throwsisjustavalue
2
,andthenthevariabledeclarationthatreceivesitisinthe
catch(a)
clause.Mind:blown.
That'sright,the
catch
clausehasblock-scopingtoit,whichmeansitcanbeusedasapolyfillforblockscopeinpre-ES6
environments.
"But...",yousay."...noonewantstowriteuglycodelikethat!"That'strue.Noonewrites(someof)thecodeoutputbythe
CoffeeScriptcompiler,either.That'snotthepoint.
ThepointisthattoolscantranspileES6codetoworkinpre-ES6environments.Youcanwritecodeusingblock-scoping,
andbenefitfromsuchfunctionality,andletabuild-steptooltakecareofproducingcodethatwillactuallyworkwhen
deployed.
Thisisactuallythepreferredmigrationpathforall(ahem,most)ofES6:touseacodetranspilertotakeES6codeand
produceES5-compatiblecodeduringthetransitionfrompre-ES6toES6.
Googlemaintainsaprojectcalled"Traceur"
,whichisexactlytaskedwithtranspilingES6featuresintopre-ES6
(mostlyES5,butnotall!)forgeneralusage.TheTC39committeereliesonthistool(andothers)totestoutthesemanticsof
thefeaturestheyspecify.
WhatdoesTraceurproducefromoursnippet?Youguessedit!
{
AppendixB:PolyfillingBlockScope
Traceur
YouDon'tKnowJS:Scope&Closures
52
AppendixB:PolyfillingBlockScope
try
{
throw
undefined
;
}
catch
(a){
a=
2
;
console
.log(a);
}
}
console
.log(a);
So,withtheuseofsuchtools,wecanstarttakingadvantageofblockscoperegardlessofifwearetargetingES6ornot,
because
try/catch
hasbeenaround(andworkedthisway)fromES3days.
InChapter3,weidentifiedsomepotentialpitfallstocodemaintainability/refactorabilitywhenweintroduceblock-scoping.Is
thereanotherwaytotakeadvantageofblockscopebuttoreducethisdownside?
Considerthisalternateformof
let
,calledthe"letblock"or"letstatement"(contrastedwith"letdeclarations"frombefore).
let
(a=
2
){
console
.log(a);
//2
}
console
.log(a);
//ReferenceError
Insteadofimplicitlyhijackinganexistingblock,thelet-statementcreatesanexplicitblockforitsscopebinding.Notonly
doestheexplicitblockstandoutmore,andperhapsfaremorerobustlyincoderefactoring,itproducessomewhatcleaner
codeby,grammatically,forcingallthedeclarationstothetopoftheblock.Thismakesiteasiertolookatanyblockand
knowwhat'sscopedtoitandnot.
Asapattern,itmirrorstheapproachmanypeopletakeinfunction-scopingwhentheymanuallymove/hoistalltheir
var
declarationstothetopofthefunction.Thelet-statementputsthemthereatthetopoftheblockbyintent,andifyoudon't
use
let
declarationsstrewnthroughout,yourblock-scopingdeclarationsaresomewhateasiertoidentifyandmaintain.
But,there'saproblem.Thelet-statementformisnotincludedinES6.NeitherdoestheofficialTraceurcompileracceptthat
formofcode.
Wehavetwooptions.WecanformatusingES6-validsyntaxandalittlesprinkleofcodediscipline:
/*let*/
{
let
a=
2
;
console
.log(a);
}
console
.log(a);
//ReferenceError
But,toolsaremeanttosolveourproblems.Sotheotheroptionistowriteexplicitletstatementblocks,andletatoolconvert
themtovalid,workingcode.
So,Ibuiltatoolcalled"let-er"
toaddressjustthisissue.let-erisabuild-stepcodetranspiler,butitsonlytaskisto
findlet-statementformsandtranspilethem.Itwillleavealoneanyoftherestofyourcode,includinganylet-declarations.
Youcansafelyuselet-erasthefirstES6transpilerstep,andthenpassyourcodethroughsomethinglikeTraceurif
necessary.
Moreover,let-erhasaconfigurationflag
--es6
,whichwhenturnedon(offbydefault),changesthekindofcodeproduced.
Insteadofthe
try/catch
ES3polyfillhack,let-erwouldtakeoursnippetandproducethefullyES6-compliant,non-hacky:
Implicitvs.ExplicitBlocks
YouDon'tKnowJS:Scope&Closures
53
AppendixB:PolyfillingBlockScope
{
let
a=
2
;
console
.log(a);
}
console
.log(a);
//ReferenceError
So,youcanstartusinglet-errightaway,andtargetallpre-ES6environments,andwhenyouonlycareaboutES6,youcan
addtheflagandinstantlytargetonlyES6.
Andmostimportantly,youcanusethemorepreferableandmoreexplicitlet-statementformeventhoughitisnotan
officialpartofanyESversion(yet).
Letmeaddonelastquicknoteontheperformanceof
try/catch
,and/ortoaddressthequestion,"whynotjustuseanIIFE
tocreatethescope?"
Firstly,theperformanceof
try/catch
isslower,butthere'snoreasonableassumptionthatithastobethatway,oreven
thatitalwayswillbethatway.SincetheofficialTC39-approvedES6transpileruses
try/catch
,theTraceurteamhas
askedChrometoimprovetheperformanceof
try/catch
,andtheyareobviouslymotivatedtodoso.
Secondly,IIFEisnotafairapples-to-applescomparisonwith
try/catch
,becauseafunctionwrappedaroundanyarbitrary
codechangesthemeaning,insideofthatcode,of
this
,
return
,
break
,and
continue
.IIFEisnotasuitablegeneral
substitute.Itcouldonlybeusedmanuallyincertaincases.
Thequestionreallybecomes:doyouwantblock-scoping,ornot.Ifyoudo,thesetoolsprovideyouthatoption.Ifnot,keep
using
var
andgoonaboutyourcoding!
note-traceur
.
note-let_er
Performance
YouDon'tKnowJS:Scope&Closures
54
AppendixB:PolyfillingBlockScope
Thoughthistitledoesnotaddressthe
this
mechanisminanydetail,there'soneES6topicwhichrelates
this
tolexical
scopeinanimportantway,whichwewillquicklyexamine.
ES6addsaspecialsyntacticformoffunctiondeclarationcalledthe"arrowfunction".Itlookslikethis:
var
foo=a=>{
console
.log(a);
};
foo(
2
);
//2
Theso-called"fatarrow"isoftenmentionedasashort-handforthetediouslyverbose(sarcasm)
function
keyword.
Butthere'ssomethingmuchmoreimportantgoingonwitharrow-functionsthathasnothingtodowithsavingkeystrokesin
yourdeclaration.
Briefly,thiscodesuffersaproblem:
var
obj={
id:
"awesome"
,
cool:
function
coolFn
()
{
console
.log(
this
.id);
}
};
var
id=
"notawesome"
;
obj.cool();
//awesome
setTimeout(obj.cool,
100
);
//notawesome
Theproblemisthelossof
this
bindingonthe
cool()
function.Therearevariouswaystoaddressthatproblem,butone
often-repeatedsolutionis
varself=this;
.
Thatmightlooklike:
var
obj={
count:
0
,
cool:
function
coolFn
()
{
var
self=
this
;
if
(self.count<
1
){
setTimeout(
function
timer
()
{
self.count++;
console
.log(
"awesome?"
);
},
100
);
}
}
};
obj.cool();
//awesome?
Withoutgettingtoomuchintotheweedshere,the
varself=this
"solution"justends-aroundthewholeproblemof
understandingandproperlyusing
this
binding,andinsteadfallsbacktosomethingwe'reperhapsmorecomfortablewith:
lexicalscope.
self
becomesjustanidentifierthatcanberesolvedvialexicalscopeandclosure,andcaresnotwhat
AppendixC:Lexical-this
YouDon'tKnowJS:Scope&Closures
55
AppendixC:Lexical-this
happenedtothe
this
bindingalongtheway.
Peopledon'tlikewritingverbosestuff,especiallywhentheydoitoverandoveragain.So,amotivationofES6istohelp
alleviatethesescenarios,andindeed,fixcommonidiomproblems,suchasthisone.
TheES6solution,thearrow-function,introducesabehaviorcalled"lexicalthis".
var
obj={
count:
0
,
cool:
function
coolFn
()
{
if
(
this
.count<
1
){
setTimeout(()=>{
//arrow-functionftw?
this
.count++;
console
.log(
"awesome?"
);
},
100
);
}
}
};
obj.cool();
//awesome?
Theshortexplanationisthatarrow-functionsdonotbehaveatalllikenormalfunctionswhenitcomestotheir
this
binding.
Theydiscardallthenormalrulesfor
this
binding,andinsteadtakeonthe
this
valueoftheirimmediatelexicalenclosing
scope,whateveritis.
So,inthatsnippet,thearrow-functiondoesn'tgetits
this
unboundinsomeunpredictableway,itjust"inherits"the
this
bindingofthe
cool()
function(whichiscorrectifweinvokeitasshown!).
Whilethismakesforshortercode,myperspectiveisthatarrow-functionsarereallyjustcodifyingintothelanguagesyntaxa
commonmistakeofdevelopers,whichistoconfuseandconflate"thisbinding"ruleswith"lexicalscope"rules.
Putanotherway:whygotothetroubleandverbosityofusingthe
this
stylecodingparadigm,onlytocutitoffattheknees
bymixingitwithlexicalreferences.Itseemsnaturaltoembraceoneapproachortheotherforanygivenpieceofcode,and
notmixtheminthesamepieceofcode.
Note:oneotherdetractionfromarrow-functionsisthattheyareanonymous,notnamed.SeeChapter3forthereasons
whyanonymousfunctionsarelessdesirablethannamedfunctions.
Amoreappropriateapproach,inmyperspective,tothis"problem",istouseandembracethe
this
mechanismcorrectly.
var
obj={
count:
0
,
cool:
function
coolFn
()
{
if
(
this
.count<
1
){
setTimeout(
function
timer
()
{
this
.count++;
//`this`issafebecauseof`bind(..)`
console
.log(
"moreawesome"
);
}.bind(
this
),
100
);
//look,`bind()`!
}
}
};
obj.cool();
//moreawesome
Whetheryoupreferthenewlexical-thisbehaviorofarrow-functions,oryoupreferthetried-and-true
bind()
,it'simportant
tonotethatarrow-functionsarenotjustaboutlesstypingof"function".
Theyhaveanintentionalbehavioraldifferencethatweshouldlearnandunderstand,andifwesochoose,leverage.
Nowthatwefullyunderstandlexicalscoping(andclosure!),understandinglexical-thisshouldbeabreeze!
YouDon'tKnowJS:Scope&Closures
56
AppendixC:Lexical-this
YouDon'tKnowJS:Scope&Closures
57
AppendixC:Lexical-this
Ihavemanypeopletothankformakingthisbooktitleandtheoverallserieshappen.
First,ImustthankmywifeChristenSimpson,andmytwokidsEthanandEmily,forputtingupwithDadalwayspecking
awayatthecomputer.Evenwhennotwritingbooks,myobsessionwithJavaScriptgluesmyeyestothescreenfarmore
thanitshould.ThattimeIborrowfrommyfamilyisthereasonthesebookscansodeeplyandcompletelyexplain
JavaScripttoyou,thereader.Iowemyfamilyeverything.
I'dliketothankmyeditorsatO'Reilly,namelySimonSt.LaurentandBrianMacDonald,aswellastherestoftheeditorial
andmarketingstaff.Theyarefantastictoworkwith,andhavebeenespeciallyaccommodatingduringthisexperimentinto
"opensource"bookwriting,editing,andproduction.
Thankyoutothemanyfolkswhohaveparticipatedinmakingthisbookseriesbetterbyprovidingeditorialsuggestionsand
corrections,includingShelleyPowers,TimFerro,EvanBorden,ForrestL.Norvell,JenniferDavis,JesseHarlin,andmany
others.AbigthankyoutoShaneHudsonforwritingtheForewordforthistitle.
Thankyoutothecountlessfolksinthecommunity,includingmembersoftheTC39committee,whohavesharedsomuch
knowledgewiththerestofus,andespeciallytoleratedmyincessantquestionsandexplorationswithpatienceanddetail.
John-DavidDalton,Juriy"kangax"Zaytsev,MathiasBynens,AxelRauschmayer,NicholasZakas,AngusCroll,Reginald
Braithwaite,DaveHerman,BrendanEich,AllenWirfs-Brock,BradleyMeck,DomenicDenicola,DavidWalsh,TimDisney,
PetervanderZee,AndreaGiammarchi,KitCambridge,EricElliott,andsomanyothers,Ican'tevenscratchthesurface.
TheYouDon'tKnowJSbookserieswasbornonKickstarter,soIalsowishtothankallmy(nearly)500generousbackers,
withoutwhomthisbookseriescouldnothavehappened:
JanSzpila,nokiko,MuraliKrishnamoorthy,RyanJoy,CraigPatchett,pdqtrader,DaleFukami,rayhatfield,R0drigo
Perez[Mx],DanPetitt,JackFranklin,AndrewBerry,BrianGrinstead,RobSutherland,SergiMeseguer,Phillip
Gourley,MarkWatson,JeffCarouth,AlfredoSumaran,MartinSachse,MarcioBarrios,Dan,AimelyneM,Matt
Sullivan,DelnattePierre-Antoine,JakeSmith,EugenTudorancea,Iris,DavidTrinh,simonstl,RayDaly,UrosGruber,
JustinMyers,ShaiZonis,Mom&Dad,DevinClark,DennisPalmer,BrianPanahiJohnson,JoshMarshall,Marshall,
DennisKerr,MattSteele,ErikSlagter,Sacah,JustinRainbow,ChristianNilsson,Delapouite,D.Pereira,Nicolas
Hoizey,GeorgeV.Reilly,DanReeves,BrunoLaturner,ChadJennings,ShaneKing,JeremiahLeeCohick,od3n,
StanYamane,MarkoVucinic,JimB,StephenCollins,ÆgirÞorsteinsson,EricPederson,Owain,NathanSmith,
Jeanetteurphy,AlexandreELISÉ,ChrisPeterson,RikWatson,LukeMatthews,JustinLowery,MortenNielsen,
VernonKesner,ChetanShenoy,PaulTregoing,MarcGrabanski,DionAlmaer,AndrewSullivan,KeithElsass,Tom
Burke,BrianAshenfelter,DavidStuart,KarlSwedberg,Graeme,BrandonHays,JohnChristopher,Gior,manojreddy,
ChadSmith,JaredHarbour,MinoruTODA,ChrisWigley,DanielMee,Mike,Handyface,AlexJahraus,CarlFurrow,
RobFoulkrod,MaxShishkin,LeighPennyJr.,RobertFerguson,MikevanHoenselaar,HasseSchougaard,rajan
venkataguru,JeffAdams,TraeRobbins,RolfLangenhuijzen,JorgeAntunes,AlexKoloskov,HughGreenish,Tim
Jones,JoseOchoa,MichaelBrennan-White,NagaHarishMuvva,BarkócziDávid,KittHodsden,PaulMcGraw,
SaschaGoldhofer,AndrewMetcalf,MarkusKrogh,MichaelMathews,MattJared,Juanfran,GeorgieKirschner,
KennyLee,TedZhang,AmitPahwa,InbalSinai,DanRaine,SchabseLaks,MichaelTervoort,AlexandreAbreu,Alan
JosephWilliams,NicolasD,CindyWong,RegBraithwaite,LocalPCGuy,JonFriskics,ChrisMerriman,JohnPena,
JacobKatz,SueLockwood,MagnusJohansson,JeremyCrapsey,GrzegorzPawłowski,niconuzzaci,Christine
Wilks,HansBergren,charlesmontgomery,Arielבבל-רבFogel,IvanKolev,DanielCampos,HughWood,Christian
Bradford,FrédéricHarper,IonuţDanPopa,JeffTrimble,RupertWood,TreyCarrico,PanchoLopez,Joëlkuijten,
TomAMarra,JeffJewiss,JacobRios,PaoloDiStefano,SoledadPenades,ChrisGerber,AndreyDolganov,Wil
MooreIII,ThomasMartineau,Kareem,BenThouret,UdiNir,MorganLaupies,jorycarson-burson,NathanLSmith,
EricDamonWalters,DerryLozano-Hoyland,GeoffreyWiseman,mkeehner,KatieK,ScottMacFarlane,Brian
LaShomb,AdrienMas,christopherross,IanLittman,DanAtkinson,ElliotJobe,NickDozier,PeterWooley,John
Hoover,dan,MartinA.Jackson,HéctorFernandoHurtado,andyennamorato,PaulSeltmann,MelissaGore,Dave
AppendixD:Acknowledgments
YouDon'tKnowJS:Scope&Closures
58
AppendixD:ThankYou's!
Pollard,JackSmith,PhilipDaSilva,GuyIsraeli,@megalithic,DamianCrawford,FelixGliesche,AprilCarterGrant,
Heidi,jimtierney,AndreaGiammarchi,NicoVignola,DonJones,ChrisHartjes,AlexHowes,johngibbon,DavidJ.
Groom,BBox,Yu'Dilys'Sun,NateSteiner,BrandonSatrom,BrianWyant,WesleyHales,IanPouncey,Timothy
KevinOxley,GeorgeTerezakis,sanjayraj,JordanHarband,MarkoMcLion,WolfgangKaufmann,PascalPeuckert,
DaveNugent,MarkusLiebelt,WellingGuzman,NickCooley,DanielMesquita,RobertSyvarth,ChrisCoyier,Rémy
Bach,AdamDougal,AlistairDuggin,DavidLoidolt,EdRicher,BrianChenault,GoldFireStudios,CarlesAndrés,
CarlosCabo,YuyaSaito,robertoricardo,BarnettKlane,MikeMoore,KevinMarx,JustinLove,JoeTaylor,Paul
Dijou,MichaelKohler,RobCassie,MikeTierney,CodyLeroyLindley,tofuji,ShimonSchwartz,Raymond,LucDe
Brouwer,DavidHayes,RhysBrett-Bowen,Dmitry,AzizKhoury,Dean,ScottTolinski-LevelUp,ClementBoirie,
DjordjeLukic,AntonKotenko,RafaelCorral,PhilipHurwitz,JonathanPidgeon,JasonCampbell,JosephC.,
SwiftOne,JanHohner,DerickBailey,getify,DanielCousineau,ChrisCharlton,EricTurner,DavidTurner,Joël
Galeran,DharmaVagabond,adam,DirkvanBergen,dave♥♫
★furf,VedranZakanj,RyanMcAllen,NataliePatrice
Tucker,EricJ.Bivona,AdamSpooner,AaronCavano,KellyPacker,EricJ,MartinDrenovac,Emilis,Michael
Pelikan,ScottF.Walter,JoshFreeman,BrandonHudgeons,vijaychennupati,BillGlennon,RobinR.,TroyForster,
otaku_coder,Brad,Scott,FrederickOstrander,AdamBrill,SebFlippence,MichaelAnderson,Jacob,AdamRandlett,
Standard,JoshuaClanton,SebastianKouba,ChrisDeck,SwordFire,HannesPapenberg,RichardWoeber,hnzz,
RobCrowther,JedidiahBroadbent,SergeyChernyshev,Jay-ArJamon,BenCombee,lucianobonachela,Mark
Tomlinson,KitCambridge,MichaelMelgares,JacobAdams,AdrianBruinhout,BevWieber,ScottPuleo,Thomas
Herzog,AprilLeone,DanielMizieliński,KeesvanGinkel,JonAbrams,ErwinHeiser,AviLaviad,Davidnewell,Jean-
FrancoisTurcot,NikoRoberts,ErikDana,CharlesNeill,AaronHolmes,GrzegorzZiółkowski,NathanYoungman,
Timothy,JacobMather,MichaelAllan,MohitSeth,RyanEwing,BenjaminVanTreese,MarceloSantos,DenisWolf,
PhilKeys,ChrisYung,TimoTijhof,MartinLekvall,Agendine,GregWhitworth,HelenHumphrey,DougalCampbell,
JohannesHarth,BrunoGirin,BrianHough,DarrenNewton,CraigMcPheat,OlivierTille,DennisRoethig,Mathias
Bynens,BrendanStromberger,sundeep,JohnMeyer,RonMale,JohnFCrostonIII,gigante,CarlBergenhem,B.J.
May,RebekahTyler,TedFoxberry,JordanReese,TerrySuitor,afeliz,TomKiefer,DarraghDuffy,Kevin
Vanderbeken,AndyPearson,SimonMacDonald,AbidDin,ChrisJoel,TomasTheunissen,DavidDick,PaulGrock,
BrandonWood,JohnWeis,dgrebb,NickJenkins,ChuckLane,JohnnyMegahan,marzsman,TatuTamminen,
GeoffreyKnauth,AlexanderTarmolov,JeremyTymes,ChadAuld,SeanParmelee,RobStaenke,DanBender,
Yannickderwa,JoshuaJones,GeertPlaisier,TomLeZotte,ChristenSimpson,StefanBruvik,JustinFalcone,Carlos
Santana,MichaelWeiss,PabloVilloslada,PeterdeHaan,DimitrisIliopoulos,seyDoggy,AdamJordens,Noah
Kantrowitz,AmolM,MatthewWinnard,DirkGinader,PhinamBui,DavidRapson,AndrewBaxter,FlorianBougel,
MichaelGeorge,AlbanEscalier,DanielSellers,SashaRudan,JohnGreen,RobertKowalski,DavidI.Teixeira
(@ditma,CharlesCarpenter,JustinYost,SamS,DenisCiccale,KevinSheurs,YannickCroissant,PauFracés,
StephenMcGowan,ShawnSearcy,ChrisRuppel,KevinLamping,JessicaCampbell,ChristopherSchmitt,Sablons,
JonathanReisdorf,BunniGek,TeddyHuff,MichaelMullany,MichaelFürstenberg,CarlHenderson,RickYoesting,
ScottNichols,HernánCiudad,AndrewMaier,MikeStapp,JesseShawl,SérgioLopes,jsulak,ShawnPrice,Joel
Clermont,ChrisRidmann,SeanTimm,JasonFinch,AidenMontgomery,ElijahManor,DerekGathright,JesseHarlin,
DillonCurry,CourtneyMyers,DiegoCadenas,ArnedeBree,JoãoPauloDubas,JamesTaylor,PhilippKraeutli,
MihaiPăun,SamGharegozlou,joshjs,MattMurchison,EricWindham,TimoBehrmann,AndrewHall,joshuaprice,
ThéophileVillard
Thisbookseriesisbeingproducedinanopensourcefashion,includingeditingandproduction.WeoweGitHubadebtof
gratitudeformakingthatsortofthingpossibleforthecommunity!
ThankyouagaintoallthecountlessfolksIdidn'tnamebutwhoInonethelessowethanks.Maythisbookseriesbe"owned"
byallofusandservetocontributetoincreasingawarenessandunderstandingoftheJavaScriptlanguage,tothebenefitof
allcurrentandfuturecommunitycontributors.
YouDon'tKnowJS:Scope&Closures
59
AppendixD:ThankYou's!