Today, I was reading this React Server pull request from the inimitable Sasha Aickin to our awesome new open source React server, cleverly named react-server when I came across this function signature
export default (routes, { workingDir = ‘./__clientTemp’, routesDir = ‘.’, ... } = {}) => { // function body omitted }
I generally like to think that I’m a pretty good JavaScript dev, and that I’ve been doing a good job keeping up with ES6, but I was completely lost on this one. I was pretty sure that this was destructuring a default argument, but based on the MDN documentation at the time, I would have expected this to look like this
export default (routes, { workingDir: workingDir, routesDir: routesDir} = { workingDir: ‘./__clientTemp’, routesDir: ‘.’ }) { // function body omitted }
(note that MDN has since updated their documentation). Other than the fact that I’m not sure that’s the most readable indentation, I was fairly certain that would work, but when I babelified Sasha’s implementation, I got code that looked exactly like what I would have expected
sasha.js export default (routes, { workingDir = './__clientTemp', routesDir = '.', } = {}) => { console.log(workingDir); console.log(routesDir); } dougwade ~/temp » babel sasha.js 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = function (routes) { var _ref = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; var _ref$workingDir = _ref.workingDir; var workingDir = _ref$workingDir === undefined ? './__clientTemp' : _ref$workingDir; var _ref$routesDir = _ref.routesDir; var routesDir = _ref$routesDir === undefined ? '.' : _ref$routesDir; console.log(workingDir); console.log(routesDir); }; module.exports = exports['default'];
And even more interesting was how unlike what I thought MDN was telling me I should do it was
mdn.js export default (routes, { workingDir: workingDir, routesDir: routesDir, } = { workingDir: './__clientTemp', routesDir: '.', }) => { console.log(workingDir); console.log(routesDir); } dougwade ~/temp » babel mdn.js 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = function (routes) { var _ref = arguments.length <= 1 || arguments[1] === undefined ? { workingDir: './__clientTemp', routesDir: '.' } : arguments[1]; var workingDir = _ref.workingDir; var routesDir = _ref.routesDir; console.log(workingDir); console.log(routesDir); };
And how much better behaved it is than what I was trying to do
harness.js import destruct from "./sasha" //changed to “./mdn” for mdnHarness.js let initialParam = {unused: true}; console.log('only one function param'); destruct(initialParam); console.log('pass a well-behaved second argument'); destruct(initialParam, { workingDir: './foo', routesDir: './bar', }) console.log('pass a partial second argument; hopefully routesDir is “.”'); destruct(initialParam, { workingDir: './foo', }); console.log('expliticly pass undefined'); destruct(initialParam); terminal dougwade ~/temp » babel --presets es2015 --plugins add-module-exports mdn.js > target/mdn.js dougwade ~/temp » babel --presets es2015 --plugins add-module-exports sasha.js > target/sasha.js dougwade ~/temp » babel --presets es2015 --plugins add-module-exports harness.js > sashaHarness.js # editing goes here dougwade ~/temp » babel --presets es2015 --plugins add-module-exports harness.js > mdnHarness.js dougwade ~/temp » node target/sashaHarness.js only one function param ./__clientTemp . pass a well-behaved second argument ./foo ./bar pass a partial second argument ./foo . explicitly pass undefined ./__clientTemp . dougwade ~/temp » node target/mdnHarness.js only one function param ./__clientTemp . pass a well-behaved second argument ./foo ./bar pass a partial second argument ./foo undefined expliticly pass undefined ./__clientTemp .
Note especially the case of a partial second argument — when you pass a partial argument to what I thought MDN was suggesting, the second argument is undefined, whereas in Sasha’s solution, you still get the default behavior. So, what’s going on; what sorcery is making this do?
My first thought was that I misunderstood destructuring, but there seemed to be no indication that { x = 1, y = 2 } = {} was valid destructuring from MDN (in case you hadn’t noticed, MDN is my go-to reference for how you JavaScript). My next thought was that maybe I didn’t understand the object literal, but that didn’t seem to have any hints either, and the default parameters article is what had gotten me into this mess in the first place. My first real clue came from the eminent Dr Axel Rauschmayer from his blog post Destructuring Algorithm, which has this example [{x=0, y=0} = {}] <- [{z:3}]. Turns out we’re in what Dr. Raushmayer calls rule 2d {key: pattern = default_value, <<properties>>}, and sure enough, when I dropped into the node repl, everything worked just as I expected with my new-found enlightenment
So, what happened to me? Check out this editorial aside in the Mozilla hacks article on destructuring
temp.js let { workingDir = './__clientTemp', } = { routesDir: '.' }; console.log(workingDir); console.log(routesDir); terminal dougwade ~/temp » babel temp.js > target/temp.js dougwade ~/temp » node target/temp.js ./__clientTemp /Users/doug.wade/temp/target/temp.js:8 console.log(routesDir); ^ ReferenceError: routesDir is not defined at Object.<anonymous> (/Users/doug.wade/temp/target/temp.js:8:13) at Module._compile (module.js:413:34) at Object.Module._extensions..js (module.js:422:10) at Module.load (module.js:357:32) at Function.Module._load (module.js:314:12) at Function.Module.runMain (module.js:447:10) at startup (node.js:140:18) at node.js:1001:3
(Editor’s note: This feature is currently implemented in Firefox only for the first two cases, not the third. See bug 932080.)
The third example referenced looks like this
var { x = 3 } = {}; console.log(x); // 3
My takeaway, then, is a cautionary tale. For me, MDN is far and away the most accessible, attractive, and well-written source of documentation for JavaScript, but it’s not intended to be the authoritative source on the ECMAScript standard, which is what Chrome and Node and Babel develop against, but instead the authoritative source on how Firefox and SpiderMonkey function. If you want to know what is in the ECMAScript standard, probably your best starting place is Exploring ES6.