Like I did in previous articles on the series, I would love to point out that you should probably set up Babel and follow along the examples with either a REPL or the
babel-node
CLI and a file. That’ll make it so much easier for you to internalize the concepts discussed in the series. If you aren’t the “install things on my computer” kind of human, you might prefer to hop on CodePen and then click on the gear icon for JavaScript – they have a Babel preprocessor which makes trying out ES6 a breeze. Another alternative that’s also quite useful is to use Babel’s online REPL – it’ll show you compiled ES5 code to the right of your ES6 code for quick comparison.
Before getting into it, let me shamelessly ask for your support if you’re enjoying my ES6 in Depth series. Your contributions will go towards helping me keep up with the schedule, server bills, keeping me fed, and maintaining Pony Foo as a veritable source of JavaScript goodies.
Thanks for reading that, and let’s go into changes to Object
. Make sure to read some of the articles from earlier in the series to get comfortable with ES6 syntax changes.
Upcoming Object
Changes
Objects didn’t get as many new methods in ES6 as arrays did. In the case of objects, we get four new static methods, and no new instance methods or properties.
And just like arrays, objects are slated to get a few more static methods in ES2016 (ES7). We’re not going to cover these today.
Object.observe
Object.unobserve
Shall we?
Object.assign
This is another example of the kind of helper method that has been beaten to death by libraries like Underscore and Lodash. I even wrote my own implementation that’s around 20 lines of code. You can use Object.assign
to recursively overwrite properties on an object with properties from other objects. The first argument passed to Object.assign
, target
, will be used as the return value as well. Subsequent values are “applied” onto that object.
Object.assign({}, { a: 1 })
// <- { a: 1 }
If you already had a property, it’s overwritten.
Object.assign({ a: 1 }, { a: 2 })
// <- { a: 2 }
Properties that aren’t present in the object being assigned are left untouched.
Object.assign({ a: 1, b: 2 }, { a: 3 })
// <- { a: 3, b: 2 }
You can assign as many objects as you want. You can think of Object.assign(a, b, c)
as the equivalent of doing Object.assign(Object.assign(a, b), c)
, if that makes it easier for you to reason about it. I like to reason about it as a reduce operation.
Object.assign({ a: 1, b: 2 }, { a: 3 }, { c: 4 })
// <- { a: 3, b: 2, c: 4 }
Note that only enumerable own properties are copied over – think Object.keys
plus Object.getOwnPropertySymbols
. The example below shows an invisible
property that didn’t get copied over. Properties from the prototype chain aren’t taken into account either.
var a = { b: 'c' }
Object.defineProperty(a, 'invisible', { enumerable: false, value: 'boo! ahhh!' })
Object.assign({}, a)
// <- { b: 'c' }
You can use this API against arrays as well.
Object.assign([1, 2, 3], [4, 5])
// <- [4, 5, 3]
Properties using symbols as their keys are also copied over.
Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' })
// <- { a: 'b', Symbol(c): 'd' }
As long as they’re enumerable and found directly on the object, that is.
var a = {}
Object.defineProperty(a, Symbol('b'), { enumerable: false, value: 'c' })
Object.assign({}, a)
// <- {}
There’s a problem with Object.assign
. It doesn’t allow you to control how deep you want to go. You may be hoping for a way to do the following while preserving the target.a.d
property, but Object.assign
replaces target.a
entirely with source.a
.
var target = { a: { b: 'c', d: 'e' } }
var source = { a: { b: 'ahh!' } }
Object.assign(target, source)
// <- { a: { b: 'ahh!' } }
Most implementations in the wild work differently, at least giving you the option to make a “deep assign”. Take assignment
for instance. If it finds an object reference in target
for a given property, it has two options.
- If the value in
source[key]
is an object, it goes recursive with anassignment(target[key], source[key])
call - If the value is not an object, it just replaces it:
target[key] = source[key]
This means that the last example we saw would work differently with assignment
than how it did with Object.assign
, which only allows for shallow extensions.
var target = { a: { b: 'c', d: 'e' } }
var source = { a: { b: 'ahh!' } }
assignment(target, source)
// <- { a: { b: 'ahh!', d: 'e' } }
The assignment
approach is usually preferred when it comes to the most common use case of this type of method: providing sensible defaults that can be overwritten by the user. Consider the following example. It uses the well-known pattern of providing your “assign” method with an empty object, that’s then filled with default values, and then poured user preferences for good measure. Note that it doesn’t change the defaults object directly because those are supposed to stay the same across invocations.
function markdownEditor (user) {
var defaults = {
height: 400,
markdown: {
githubFlavored: true,
tables: false
}
}
var options = Object.assign({}, defaults, user)
console.log(options)
}
The problem with Object.assign
is that if the markdownEditor
consumer wants to change markdown.tables
to true
, all of the other defaults in markdown
will be lost!
markdownEditor({ markdown: { tables: true } })
// <- {
// height: 400,
// markdown: {
// tables: true
// }
// }
From both the library author’s perspective and the library’s user perspective, this is just unacceptable and weird. If we were to use assignment
we wouldn’t be having those issues, because assignment
is built with this particular use case in mind. Libraries like Lodash usually provide many different flavors of this method.
Note that when it comes to nested arrays, replacement probably is the behavior you want most of the time. Given defaults like { extensions: ['css', 'js', 'html'] }
, the following would be quite weird.
markdownEditor({ extensions: ['js'] })
// <- { extensions: ['js', 'js', 'html'] }
For that reason, assignment
replaces arrays entirely, just like Object.assign
would. This difference doesn’t make Object.assign
useless, but it’s still necessary to know about the difference between shallow and deep assignment.
Object.is
This method is pretty much a programmatic way to use the ===
operator. You pass in two arguments and it tells you whether they’re the same reference or the same primitive value.
Object.is('foo', 'foo')
// <- true
Object.is({}, {})
// <- false
There are two important differences, however. First off, -0
and +0
are considered unequal by this method, even though ===
returns true
.
-0 === +0
// <- true
Object.is(-0, +0)
// <- false
The other difference is when it comes to NaN
. The Object.is
method treats NaN
as equal to NaN
. This is a behavior we’ve already observed in maps and sets, which also treats NaN
as being the same value as NaN
.
NaN === NaN
// <- false
Object.is(NaN, NaN)
// <- true
While this may be convenient in some cases, I’d probably go for the more explicit Number.isNaN
most of the time.
Object.getOwnPropertySymbols
This method returns all own property symbols found on an object.
var a = {
[Symbol('b')]: 'c',
[Symbol('d')]: 'e',
'f': 'g',
'h': 'i'
}
Object.getOwnPropertySymbols(a)
// <- [Symbol(b), Symbol(d)]
We’ve already covered Object.getOwnPropertySymbols
in depth in the symbols dossier. If I were you, I’d read it!
Object.setPrototypeOf
Again, something we’ve covered earlier in the series. One of the articles about proxy traps covers this method tangentially. You can use Object.setPrototypeOf
to change the prototype of an object.
It is, in fact, the equivalent of setting __proto__
on runtimes that have that property.
Comments