Not everything that looks like an object is one. Here are some cases that look something like JavaScript objects but aren't. We'll also see some unusual things along the way. GOTO
in JavaScript?!
Code Blocks
The most basic example where curly braces don't mean an object are code blocks. We are accustomed to code blocks being defined for functions and conditional statements, but they don't need a special reason to exist. Code blocks can be almost anywhere.
const a = 'outside';
if (true) {
// The curly braces are a code block
console.log(a);
}
// But we don't need a condition to have a block
{
// We even get a different block scope
const a = 'inside';
console.log(a);
}
This ability to start a line with a code block even comes into play later on!
Arrow Functions
The expression body of arrow functions allows for no curly braces and an implicit return type, but just like in the above examples the interpreter must assume curly braces after the arrow mean a code block rather than an object. So we have to add parentheses to differentiate.
This one is fairly common, so it hopefully doesn't come as a surprise.
const a = 'one';
// Code block, returns undefined
const foo = () => {
a
};
// Object, returns the { a: 'one' }
const bar = () => ({
a
});
This is also a harder mistake to make if we require semicolons and trailing commas, because it clearly differentiates the object property from the statement.
Labeled Statements
Making this more confusing is the rarely-discussed concept of labeled statements.
For those of us with experience in BASIC or similar low-level languages, JavaScript has a GOTO
or JUMP
equivalent! Also, you shouldn't use it unless you really know what you are doing.
The key takeaway is we can create labels in the code that we can jump to when during continue
or break
out of loops...but we can also just create labels for no reason.
here: var a = 'outside';
there: if (true) {
// The curly braces are a code block
anywhere: console.log(a);
}
// But we don't need a condition to have a block
hither: {
thither: var a = 'inside';
yon: console.log(a);
}
We switched to var
in this last example because labels create a lexical scope. Declaring let
or const
in a one-line scope is a Syntax Error because they couldn't be accessed.
const a = 'one'
here: const b = 'two';
// SyntaxError: Lexical declaration cannot appear in a single-statement context.
But with code blocks, labelled statements, and a little help from automatic semicolon insertion again, we can have very confusing code.
// This is a code block that would return 'two'
// if it could be assigned to a value...which it can't.
{
foo: 'a'
bar: '1000'
baz: 'two'
}
I notice the syntax highlighting doesn't accommodate statement labels properly, which gives a sense for how uncommon they are. Here we had three labels and three statements.
Mixing arrow functions and labelled statements can really be confusing:
const myFn = () => {
foo: bar
};
It seems so similar to an object that we might be forgiven for missing it at first glance.
Finally, with what appears to be object shorthand but is actually the comma operator, we can end up with something that really looks like an object.
const a = 'one';
const b = 'two';
// Not an object, this code block would also return 'two'
{
a,
b
}
Code blocks can sometimes look like objects, especially when we don't require semicolons, or if we don't use a standardized code formatter/linter/prettier to provide consistency. Most of the time, though, we won't end up mistaking code blocks for objects.
Destructuring
We usually see destructuring associated with declarations like let
and const
:
const { foo, bar: baz } = data;
This is a reliable indicator we aren't working with plain objects, but sometimes destructuring is more difficult to spot.
Special Assignment
Is this an object or not?
({
foo,
bar: baz,
// ...
Unfortunately, the answer is, "It depends." We can't tell until we get to the end of the object.
// Object with foo and bar
({
foo,
bar: baz,
}).valueOf();
// Destructuring to foo and baz
({
foo,
bar: baz,
} = data);
Destructuring outside of a declaration requires special syntax with parentheses to ensure the interpreter knows what we intend.
Beyond that, there are lots of things that can happen in destructuring that looks like objects. There are a few that don't, like defaults.
({
missing = 2,
} = data);
Property Confusion
Destructuring allows us to assign values to object properties as well as directly to variables, so it can be hard to tell some objects from destructuring at a glance. Consider the following bizarre example.
// Create some variables
let one, two, three, four, five, six;
// Some source data
const data = {
one: 'one',
two: ['two'],
three: 3,
four: { four: 4 },
five: [1,2,3,4,5],
six: 'six',
};
// Destructure!
({
one,
two: three,
four: five,
six: five.six,
} = data);
console.log({ one, two, three, four, five, six });
This confusing code produces this equally confusing output:
{
one: 'one',
two: undefined,
three: [ 'two' ],
four: undefined,
five: { four: 4, six: 'six' },
six: undefined
}
Not only that, but because we assigned an object and then a property to it, we modified the original data object!
data.four;
// { four: 4, six: 'six' }
It's a strange example, but it is only six lines. If we were performing complex destructuring of a many-property React component or extracting information from a complex configuration object it could be difficult to track all the contextual clues that help us distinguish object from destructuring.
So destructuring can sometimes look like an object, but has lots of ways that differ from an object.
Import
The Syntax example at the top of the MDN import page really covers most of this section:
import defaultExport from "module-name";
import * as name from "module-name";
import { export1 } from "module-name";
import { export1 as alias1 } from "module-name";
import { default as alias } from "module-name";
import { export1, export2 } from "module-name";
import { export1, export2 as alias2, /* … */ } from "module-name";
import { "string name" as alias } from "module-name";
import defaultExport, { export1, /* … */ } from "module-name";
import defaultExport, * as name from "module-name";
import "module-name";
While some of these look a lot like objects, and obviously even more like object destructuring, it is a truly distinct use of curly braces. Imports must be static identifiers only; no computed properties here!
This can be especially confusing when an export is an object. We might want to combine object destructuring and import syntax, but we cannot.
Also, there are special cases that as
solves differently than any object.
// Rename the default or specific exports
import { default as alias } from "module-name";
import { export1 as alias1 } from "module-name";
// Import invalid identifiers that require strings to obtain
import { "string name" as alias } from "module-name";
The simple rule is that while we can import objects, import statements aren't objects.
Conclusion
There are several ways we use curly braces in JavaScript, and even when they look like objects that doesn't mean they are. We experience code blocks regularly, so we usually know when something is a block, but sometimes it's not as easy to differentiate.
Happy coding!
Top comments (0)