ponyfoo.com

ES6 Object Changes in Depth

Fix
A relevant ad will be displayed here soon. These ads help pay for my hosting.
Please consider disabling your ad blocker on Pony Foo. These ads help pay for my hosting.
You can support Pony Foo directly through Patreon or via PayPal.

Howdy. You’re reading ES6 – “I vehemently Object to come up with a better tagline” – in Depth series. If you’ve never been around here before, start with A Brief History of ES6 Tooling. Then, make your way through destructuring, template literals, arrow functions, the spread operator and rest parameters, improvements coming to object literals, the new classes sugar on top of prototypes, let, const, and the “Temporal Dead Zone”, iterators, generators, Symbols, Maps, WeakMaps, Sets, and WeakSets, proxies, proxy traps, more proxy traps, reflection, Number, Math, and Array. Today we’ll learn about changes to Object.

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 an assignment(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.

Liked the article? Subscribe below to get an email when new articles come out! Also, follow @ponyfoo on Twitter and @ponyfoo on Facebook.
One-click unsubscribe, anytime. Learn more.

Comments