Constant width1 ES6 effected a profound change in the JavaScript language, introducing multiple syntax improvements and a few handfuls of new methods. This book assumes familiarity with JavaScript after ES6. You can learn more about ES6 syntax by visiting the Pony Foo blog for a crash course.
2 JavaScript Application Design is a book I published through Manning in 2015. It revolves around build processes, but also features chapters on managing complexity, sensible asynchronous flow control code, REST API design, and JavaScript testing concerns.
<script>varinitialized=falseif(!initialized){init()}functioninit(){initialized=trueconsole.log('init')}</script><script>if(initialized){console.log('was initialized!')}// even `init` has been implicitly made a global variableconsole.log('init'inwindow)</script>
(function(){console.log('IIFE using parenthesis')})()~function(){console.log('IIFE using a bitwise operator')}()voidfunction(){console.log('IIFE using the void operator')}()
voidfunction(){window.mathlib=window.mathlib||{}window.mathlib.sum=sumfunctionsum(...values){returnvalues.reduce((a,b)=>a+b,0)}}()mathlib.sum(1,2,3)// <- 6
define(function(){returnsumfunctionsum(...values){returnvalues.reduce((a,b)=>a+b,0)}})
define(['mathlib/sum'],function(sum){return{sum}})
require(['mathlib'],function(mathlib){mathlib.sum(1,2,3)// <- 6})
module.factory('calculator',function(mathlib){// ...})
module.factory('calculator',['mathlib',function(mathlib){// ...}])
constmathlib=require('./mathlib')
importmathlibfrom'./mathlib'import('./mathlib').then(mathlib=>{// ...})
Writing code under ES6 and beyond, but then transpiling parts of that code down to ES5 to attain broader browser support
Shared rendering, using the same code on both server and client to render a page quickly on initial page load and continue to load pages quickly upon navigation
Automated code bundling, packing the modules that an application comprises into a single bundle for optimized delivery
Bundle-splitting along routes so that there are several bundles outputted, each optimized for the initially visited route; CSS bundling at the JavaScript module level so that CSS (which doesn’t feature a native module syntax) can also be split across bundles
Myriad ways of optimizing assets such as images at compile time, improving productivity during development while keeping production deployments highly performant
1 You can dive into the specifics by reading “The Current State of Implementation and Planning for ESModules” by a member of the Node.js team, Myles Borins.
2 Code splitting lets you split your application into several bundles based on different entry points, and also lets you extract dependencies shared across bundles into a single reusable bundle.
“Characterized by a very complicated or involved arrangement of parts, units, etc.”
“So complicated or intricate as to be hard to understand or deal with”
importinsanefrom'insane'importmailApifrom'mail-api'import{mailApiSecret}from'./secrets'functionsanitize(template,...expressions){returntemplate.reduce((result,part,i)=>result+insane(expressions[i-1])+part)}exportdefaultfunctionsend(options,done){const{to,subject,model:{title,body,tags}}=optionsconsthtml=sanitize`<h1>${title}</h1><div>${body}</div><div>${tags.map(tag=>`${<span>${tag}</span> }`).join(` `)}</div>`constclient=mailApi({mailApiSecret})client.send({from:`hello@mjavascript.com`,to,subject,html},done)}
importmailApifrom'mail-api'import{mailApiSecret}from'./secrets'exportdefaultfunctionsend(options,done){const{to,subject,html}=optionsconstclient=mailApi({mailApiSecret})client.send({from:`hello@mjavascript.com`,to,subject,html},done)}
exportdefaultfunctionsend(options,done){const{to,subject,html}=optionsconsole.log(`Sending email.To:${to}Subject:${subject}${html}`)done()}
importinsanefrom'insane'functionsanitize(template,...expressions){returntemplate.reduce((result,part,i)=>result+insane(expressions[i-1])+part)}exportdefaultfunctioncompile(model){const{title,body,tags}=modelconsthtml=sanitize`<h1>${title}</h1><div>${body}</div><div>${tags.map(tag=>`${<span>${tag}</span> }`).join(` `)}</div>`returnhtml}
import{send}from'./email/log-provider'import{compile}from'./templating/static'exportdefaultfunctionsend(options,done){const{to,subject,model}=optionsconsthtml=compile(model)send({to,subject,html},done)}
import{createClient}from'./elasticsearch'import{elasticsearchHost}from'./secrets'constclient=createClient({host:elasticsearchHost})client.get({index:`blog`,type:`articles`,body:{query:{match:{tags:[`modularity`,`javascript`]}}}}).then(response=>{// ...})
constcounter={_state:0,increment(){counter._state++},decrement(){counter._state--},read(){returncounter._state}}exportdefaultcounter
constcounter={_state:0,increment(){counter._state++},decrement(){counter._state--},read(){returncounter._state}}const{increment,decrement,read}=counterconstapi={increment,decrement,read}exportdefaultapi
(function(){constcounter={_state:0,increment(){counter._state++},decrement(){counter._state--},read(){returncounter._state}}const{increment,decrement,read}=counterconstapi={increment,decrement,read}returnapi})()
functionsum(numbers){returnnumbers.reduce((a,b)=>a+b,0)}
letcount=0constincrement=()=>count++exportdefaultincrement
constfactory=()=>{letcount=0constincrement=()=>count++returnincrement}exportdefaultfactory
$()
$(selector)
$(selector, context)
$(element)
$(elementArray)
$(object)
$(selection)
$(html)
$(html, ownerDocument)
$(html, attributes)
$(callback)
$(callback) binds a function to be executed when the DOM has finished loading.
$(html) overloads create elements out of the provided html.
Every other overload matches elements in the DOM against the provided input.
$('{div}')// <- Uncaught Error: unrecognized expression: {div}
awaitfetch('/api/users')awaitfetch('/api/users/rob',{method:'DELETE'})
event.initKeyEvent(type,bubbles,cancelable,viewArg,ctrlKeyArg,altKeyArg,shiftKeyArg,metaKeyArg,keyCodeArg,charCodeArg)
event.initKeyEvent(type,{bubbles,cancelable,viewArg,ctrlKeyArg,altKeyArg,shiftKeyArg,metaKeyArg,keyCodeArg,charCodeArg})
Consumers can declare options in any order, as the arguments are no longer positional inside the options object.
The API can offer default values for each option. This helps the consumer avoid specifying defaults just so that they can change another positional parameter.9
Consumers don’t need to concern themselves with options they don’t need.
Developers reading pieces of code that consume the API can immediately understand which parameters are being used, because they’re explicitly named in the options object.
constres=awaitfetch('/api/users/john')console.log(res.statusCode)// <- 200
constres=awaitfetch('/api/users/john')constdata=res.json()console.log(data.name)// <- 'John Doe'
awaitfetch('/api/users/john',{method:`DELETE`})
1 Further details of the dictionary definition might help shed light on this topic.
2 For example, one implementation might merely compile an HTML email by using inline templates, another might use HTML template files, another could rely on a third-party service, and yet another could compile emails as plain-text instead.
3 You can check out the Elasticsearch Query DSL documentation.
4 The options parameter is an optional configuration object that’s relatively new to the web API. We can set flags such as capture, which has the same behavior as passing a useCapture flag; passive, which suppresses calls to event.preventDefault() in the listener; and once, which indicates that the event listener should be removed after being invoked for the first time.
5 You can find request on GitHub.
6 For a given set of inputs, an idempotent function always produces the same output.
7 When a function has overloaded signatures which can handle two or more types (such as an array or an object) in the same position, the parameter is said to be polymorphic. Polymorphic parameters make functions harder for compilers to optimize, resulting in slower code execution. When this polymorphism is in a hot path—that is, a function that gets called very often—the performance implications have a larger negative impact. Read more about the compiler implications in “What’s Up with Monomorphism” by Vyacheslav Egorov.
8 See the MDN documentation.
9 Assuming we have a createButton(size = 'normal', type = 'primary', color = 'red') method and we want to change its color, we’d have to use createButton('normal', 'primary', 'blue') to accomplish that, only because the API doesn’t have an options object. If the API ever changes its defaults, we’d have to change any function calls accordingly as well.
Draggable elements must have a parent with a draggable-list class.
Draggable elements mustn’t have a draggable-frozen class.
Dragging must initiate from a child with a drag-handle class.
Elements may be dropped into containers with a draggable-dropzone class.
Elements may be dropped into containers with at most six children.
Elements may not be dropped into the container they’re being dragged from.
Elements must be sortable in the container they’re dragged from, but they can’t be dropped into other containers.
1 In A/B testing, a form of user testing, a small portion of users are presented with a different experience than that used for the general user base. We then track engagement among the two groups, and if the engagement is higher for the users with the new experience, then we might go ahead and present that to our entire user base. It is an effective way of reducing risk when we want to modify our user experience, by testing our assumptions in small experiments before we introduce changes to the majority of our users.
2 Monkey-patching is the intentional modification of the public interface of a component from the outside in order to add, remove, or change its functionality. Monkey-patching can be helpful when we want to change the behavior of a component that we don’t control, such as a library or dependency. Patching is error-prone because we might be affecting other consumers of this API who are unaware of our patches. The API itself or its internals may also change, breaking the assumptions made about them in our patch. Although it’s generally best avoided, sometimes it’s the only choice at hand.
getProducts(products=>{getProductPrices(products,prices=>{getProductDetails({products,prices},details=>{// ...})})})
if(auth!==undefined&&auth.token!==undefined&&auth.expires>Date.now()){// we have a valid token that hasn't expired yetreturn}
functionhasValidToken(auth){if(auth===undefined||auth.token===undefined){returnfalse}consthasNotExpiredYet=auth.expires>Date.now()returnhasNotExpiredYet}
if(hasValidToken(auth)){return}
consthasToken=auth===undefined||auth.token===undefinedconsthasValidToken=hasToken&&auth.expires>Date.now()if(hasValidToken){return}
if(response){if(!response.errors){// ... use `response`}else{returnfalse}}else{returnfalse}
if(!response){returnfalse}if(response.errors){returnfalse}// ... use `response`
double(6)// TypeError: double is not a functionvardouble=function(x){returnx*2}
double(6)// TypeError: double is not definedconstdouble=function(x){returnx*2}
double(6)// 12functiondouble(x){returnx*2}
functiongetUserModels(done){findUsers((err,users)=>{if(err){done(err)return}constmodels=users.map(user=>{const{name,}=userconstmodel={name,}if(user.type.includes('admin')){model.admin=true}returnmodel})done(null,models)})}
functiongetUserModels(done){findUsers((err,users)=>{if(err){done(err)return}constmodels=users.map(toUserModel)done(null,models)})}functiontoUserModel(user){const{name,}=userconstmodel={name,}if(user.type.includes('admin')){model.admin=true}returnmodel}
// ...letwebsite=nullif(user.details){website=user.details.website}elseif(user.website){website=user.website}// ...
// ...constwebsite=getUserWebsite(user)// ...functiongetUserWebsite(user){if(user.details){returnuser.details.website}if(user.website){returnuser.website}returnnull}
a(function(){b(function(){c(function(){d(function(){console.log('hi!')})})})})
a(a1)functiona1(){b(b1)}functionb1(){c(c1)}functionc1(){d(d1)}functiond1(){console.log('hi!')}
a(a1)functiona1(){b(b1)}functionb1(){c(c1)}functionc1(){d(()=>d1('hi!'))}functiond1(salute){console.log(salute)// <- 'hi!'}
async.series([next=>setTimeout(()=>next(),1000),next=>setTimeout(()=>next(),1000),next=>setTimeout(()=>next(),1000)],err=>console.log(err?'failed!':'done!'))
Promise.resolve(1).then(()=>Promise.resolve(2).then(()=>Promise.resolve(3).then(()=>Promise.resolve(4).then(value=>{console.log(value)// <- 4}))))
Promise.resolve(1).then(()=>Promise.resolve(2)).then(()=>Promise.resolve(3)).then(()=>Promise.resolve(4)).then(value=>{console.log(value)// <- 4})
asyncfunctionmain(){awaitPromise.resolve(1)awaitPromise.resolve(2)awaitPromise.resolve(3)constvalue=awaitPromise.resolve(4)console.log(value)// <- 4}
functionabsolutizeHtml(html,origin){const$dom=$(html)$dom.find('a[href]').each(function(){const$element=$(this)consthref=$element.attr('href')constabsolute=absolutize(href,origin)$element.attr('href',absolute)})$dom.find('img[src]').each(function(){const$element=$(this)constsrc=$element.attr('src')constabsolute=absolutize(src,origin)$element.attr('src',absolute)})return$dom.html()}
constattributes=[['a','href'],['img','src'],['iframe','src'],['script','src'],['link','href']]functionabsolutizeHtml(html,origin){const$dom=$(html)attributes.forEach(absolutizeAttribute)return$dom.html()functionabsolutizeAttribute([tag,property]){$dom.find(`${tag}[${property}]`).each(function(){const$element=$(this)constvalue=$element.attr(property)constabsolute=absolutize(value,origin)$element.attr(property,absolute)})}}
movies.map(movie=>{movie.profit=movie.gross-movie.budgetreturnmovie})
for(constmovieofmovies){movie.profit=movie.gross-movie.budget}
for(constmovieofmovies){movie.profit=movie.amount*movie.unitCost}constsuccessfulMovies=movies.filter(movie=>movie.profit>15)
constmovieModels=movies.map(movie=>({...movie,profit:movie.amount*movie.unitCost}))constsuccessfulMovies=movieModels.filter(movie=>movie.profit>15)
[{slug:'understanding-javascript-async-await',title:'Understanding JavaScript’s async await',contents:'...'},{slug:'pattern-matching-in-ecmascript',title:'Pattern Matching in ECMAScript',contents:'...'},...]
{'understanding-javascript-async-await':{slug:'understanding-javascript-async-await',title:'Understanding JavaScript’s async await',contents:'...'},'pattern-matching-in-ecmascript':{slug:'pattern-matching-in-ecmascript',title:'Pattern Matching in ECMAScript',contents:'...'},...}
newMap([['understanding-javascript-async-await',{slug:'understanding-javascript-async-await',title:'Understanding JavaScript’s async await',contents:'...'}],['pattern-matching-in-ecmascript',{slug:'pattern-matching-in-ecmascript',title:'Pattern Matching in ECMAScript',contents:'...'}],...])
classValue{constructor(value){this.state=value}add(value){this.state+=valuereturnthis}multiply(value){this.state*=valuereturnthis}valueOf(){returnthis.state}}console.log(+newValue(5).add(3).multiply(2))// <- 16
functionadd(current,value){returncurrent+value}functionmultiply(current,value){returncurrent*value}console.log(multiply(add(5,3),2))// <- 16
1 In the example, we immediately return false when the token isn’t present.
'Hello '+name+', I\'m Nicolás!'
`Hello${name}, I'm Nicolás!`
importinsanefrom'insane'functionsanitize(template,...expressions){returntemplate.reduce((accumulator,part,i)=>{returnaccumulator+insane(expressions[i-1])+part})}
constcomment='exploit time! <iframe src="http://evil.corp"></iframe>'consthtml=sanitize`<div>${comment}</div>`console.log(html)// <- '<div>exploit time! </div>'
const{low,high,ask,...details}=ticker
const{title,description,askingPrice,features:{area,bathrooms,bedrooms,amenities},contact:{name,phone,}}=response
const{title,description,askingPrice,features:{area,bathrooms,bedrooms,amenities},contact:{name:responseContactName,phone,}}=response
const{title,description,askingPrice,features:{area,bathrooms,bedrooms,amenities},contact:responseContact,contact:{name:responseContactName,phone,}}=response
constfaxCopy={...fax}constnewCopy={...fax,date:newDate()}
// ...lettype='contributor'if(user.administrator){type='administrator'}elseif(user.roles.includes('edit_articles')){type='editor'}// ...
// ...consttype=getUserType(user)// ...functiongetUserType(user){if(user.administrator){return'administrator'}if(user.roles.includes('edit_articles')){return'editor'}return'contributor'}
letvalues=[1,2,3,4,5]values=values.map(value=>value*2)values=values.filter(value=>value>5)// <- [6, 8, 10]
constfinalValues=[1,2,3,4,5].map(value=>value*2).filter(value=>value>5)// <- [6, 8, 10]
constinitialValues=[1,2,3,4,5]constdoubledValues=initialValues.map(value=>value*2)constfinalValues=doubledValues.filter(value=>value>5)// <- [6, 8, 10]
Callbacks are typically a solid choice, but we often need to involve libraries when we want to execute our work concurrently.
Promises might be hard to understand at first, but they offer a few utilities like Promise#all for concurrent work, yet they might be hard to debug under some circumstances.
Async functions require a bit of understanding on top of being comfortable with promises, but they’re easier to debug and often result in simpler code, plus they can be interspersed with synchronous functions rather easily as well.
Iterators and generators are powerful tools, but they don’t have all that many practical use cases, so we must consider whether we’re using them because they fit our needs or just because we can.
functiondelay(timeout){constresolver=resolve=>{setTimeout(()=>{resolve()},timeout)}returnnewPromise(resolver)}delay(2000).then(...)
import{promisify}from'util'import{readFile}from'fs'constreadFilePromise=promisify(readFile)readFilePromise('./data.json','utf8').then(data=>{console.log(`Data:${data}`)})
// promisify.jsexportdefaultfunctionpromisify(fn){return(...rest)=>{returnnewPromise((resolve,reject)=>{fn(...rest,(err,result)=>{if(err){reject(err)return}resolve(result)})})}}
importpromisifyfrom'./promisify'import{readFile}from'fs'constreadFilePromise=promisify(readFile)readFilePromise('./data.json','utf8').then(data=>{console.log(`Data:${data}`)})
functionunpromisify(p,done){p.then(data=>done(null,data),error=>done(error))}unpromisify(delay(2000),err=>{// ...})
functionmakeEmitter(target){constlisteners=[]target.on=(eventType,listener)=>{if(!(eventTypeinlisteners)){listeners[eventType]=[]}listeners[eventType].push(listener)}target.emit=(eventType,...params)=>{if(!(eventTypeinlisteners)){return}listeners[eventType].forEach(listener=>{listener(...params)})}returntarget}constperson=makeEmitter({name:'Artemisa',age:27})person.on('move',(x,y)=>{console.log(`${person.name}moved to [${x},${y}].`)})person.emit('move',23,5)// <- 'Artemisa moved to [23, 5].'
constemitters=newWeakMap()functiononEvent(target,eventType,listener){if(!emitters.has(target)){emitters.set(target,newMap())}constlisteners=emitters.get(target)if(!(eventTypeinlisteners)){listeners.set(eventType,[])}listeners.get(eventType).push(listener)}functionemitEvent(target,eventType,...params){if(!emitters.has(target)){return}constlisteners=emitters.get(target)if(!listeners.has(eventType)){return}listeners.get(eventType).forEach(listener=>{listener(...params)})}constperson={name:'Artemisa',age:27}onEvent(person,'move',(x,y)=>{console.log(`${person.name}moved to [${x},${y}].`)})emitEvent(person,'move',23,5)// <- 'Artemisa moved to [23, 5].'
constoperations=[]letstate=0exportfunctionadd(value){operations.push(()=>{state+=value})}exportfunctionmultiply(value){operations.push(()=>{state*=value})}exportfunctioncalculate(){operations.forEach(op=>op())returnstate}
import{add,multiply,calculate}from'./calculator'add(3)add(4)multiply(-2)calculate()// <- -14
// a.jsimport{add,calculate}from'./calculator'add(3)setTimeout(()=>{add(4)calculate()// <- 14, an extra 7 because of b.js},100)// b.jsimport{add,calculate}from'./calculator'add(2)calculate()// <- 5, an extra 3 from a.js
constoperations=[]exportfunctionadd(value){operations.push(state=>state+value)}exportfunctionmultiply(value){operations.push(state=>state*value)}exportfunctioncalculate(){returnoperations.reduce((result,op)=>op(result),0)}
// a.jsimport{add,calculate}from'./calculator'add(3)setTimeout(()=>{add(4)calculate()// <- 9, an extra 2 from b.js},100)// b.jsimport{add,calculate}from'./calculator'add(2)calculate()// <- 5, an extra 3 from a.js
exportfunctiongetCalculator(){constoperations=[]functionadd(value){operations.push(state=>state+value)}functionmultiply(value){operations.push(state=>state*value)}functioncalculate(){returnoperations.reduce((result,op)=>op(result),0)}return{add,multiply,calculate}}
import{getCalculator}from'./calculator'const{add,multiply,calculate}=getCalculator()add(3)add(4)multiply(-2)calculate()// <- -14
// a.jsimport{getCalculator}from'./calculator'const{add,calculate}=getCalculator()add(3)setTimeout(()=>{add(4)calculate()// <- 7},100)// b.jsimport{getCalculator}from'./calculator'const{add,calculate}=getCalculator()add(2)calculate()// <- 2
classPerson{constructor(name,address){this.name=namethis.address=address}greet(){console.log(`Hi! My name is${this.name}.`)}}constrwanda=newPerson('Rwanda','123 Main St')
1 You can read a blog post I wrote about why template literals are better than strings at the Pony Foo site. Practical Modern JavaScript (O’Reilly, 2017) is the first book in the Modular JavaScript series. You’re currently reading the second book of that series.
2 Note also that, starting in Node.js v10.0.0, the native fs.promises interface can be used to access promise-based versions of the fs module’s methods.
3 Up until recently, JSON wasn’t, strictly speaking, a proper subset of ECMA-262. A recent proposal has amended the ECMAScript specification to consider bits of JSON that were previously invalid JavaScript to be valid JavaScript.
{"PORT":3000,"MONGO_URI":"mongodb://localhost/mjavascript","SESSION_SECRET":"ditch-foot-husband-conqueror"}
.env.defaults.json can be used to define default values that aren’t necessarily overwritten across environments, such as the application listening port, the NODE_ENV variable, and configurable options you don’t want to hardcode into your application code. These default settings should be safe to check into source control.
.env.production.json, .env.staging.json, and others can be used for environment-specific settings, such as the various production connection strings for databases, cookie encoding secrets, API keys, and so on.
.env.json could be your local, machine-specific settings, useful for secrets or configuration changes that shouldn’t be shared with other team members.
// envimportnconffrom'nconf'nconf.env()nconf.file('environment',`.env.${nodeEnv()}.json`)nconf.file('machine','.env.json')nconf.file('defaults','.env.defaults.json')process.env.NODE_ENV=nodeEnv()// consistencyfunctionnodeEnv(){returnaccessor('NODE_ENV')}functionaccessor(key){returnnconf.get(key)}exportdefaultaccessor
importenvfrom'./env'constport=env('PORT')
{"NODE_ENV":"development"}
{"NODE_ENV":"development","BROWSER_ENV":{"MIXPANEL_API_KEY":"some-api-key","GOOGLE_MAPS_API_KEY":"another-api-key"}}
// print-browser-envimportenvfrom'./env'constbrowserEnv=env('BROWSER_ENV')constprettyJson=JSON.stringify(browserEnv,null,2)console.log(prettyJson)
node print-browser-env > browser/.env.browser.json
// browser/envimportenvfrom'./env.browser.json'exportdefaultfunctionaccessor(key){if(typeofkey!=='string'){returnenv}returnkeyinenv?env[key]:null}
{"name":"A","version":"0.1.0",//metadata..."dependencies":{"B":{"version":"0.0.1","resolved":"https://registry.npmjs.org/B/-/B-0.0.1.tgz","integrity":"sha512-DeAdb33F+""dependencies":{"C":{"version":"git://github.com/org/C.git#5c380ae3"}}}}}
1 You can find the original Twelve-Factor App methodology and its documentation online.
2 When we run npm install, npm also executes a rebuild step after npm install ends. The rebuild step recompiles native binaries, building different assets depending on the execution environment and the local machine’s operating system.