JavaScript Tips: Redefining userAgent property
Imagine a use case where you are trying to check if a user accessed your app from a mobile device or not. Most likely you will need to use navigator.userAgent property and craft some smart regular expression to test for the presence of particular expression, like (/Mobi/.test(navigator.userAgent)
which seems to be the recommended way to do it. Ok, so we’re almost done with our feature, we just need to add some tests to make sure it works as expected. But there’s a problem - you can’t redefine userAgent
property with just using a setter! Fortunately, there is a way to solve this problem.
Anatomy of the problem
Let’s check what happens when we try to override navigator.userAgent
property with a setter in a browser.
> navigator.userAgent
< "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/602.3.12 (KHTML, like Gecko) Version/10.0.2 Safari/602.3.12"
> navigator.userAgent = "Mobile"
< "Mobile"
> navigator.userAgent
< "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/602.3.12 (KHTML, like Gecko) Version/10.0.2 Safari/602.3.12"
Well, that’s not exactly what we wanted to be returned. But we need to override this value somehow to test both behaviours - when the device is a mobile one and not a mobile one. Fortunately, JavaScript is quite powerful at this point and it’s possibly to redefine such property using Object.defineProperty
.
Object.defineProperty to the Rescue
Object.defineProperty
allows to define a new property or redefine an existing one on a given object. The syntax is following:
Object.defineProperty(obj, prop, descriptor)
descriptor
argument is a particularly interesting one - it allows to define a value
of the property, a getter, a setter, whether the property should be enumerable
(if it’s going to be included when iterating over the properties), if it’s writable
(if the value can be changed with an assignment operator) and configurable
(if the property can be changed and deleted from the object’s properties).
Looks like value
is exactly what we need. Let’s try it then:
> Object.defineProperty(window.navigator, 'userAgent', {
value: 'Mobile'
});
> navigator.userAgent;
< "Mobile"
Looks good so far. What if we wanted to override this property again?
> navigator.userAgent = 'Desktop';
> navigator.userAgent;
< "Mobile"; // whooops
Hmm, doesn’t work, maybe let’s try to redefine this propert again then?
> Object.defineProperty(window.navigator, 'userAgent', {
value: 'Mobile'
});
< TypeError: Attempting to change value of a readonly property.
Apparently it’s not that great as we thought it would be. However, that’s not a problem! We just need to make this property either configurable
or writable
! Let’s check both scenarios:
> Object.defineProperty(window.navigator, 'userAgent', {
value: 'Mobile',
configurable: true
});
> navigator.userAgent;
< "Mobile"
> Object.defineProperty(window.navigator, 'userAgent', {
value: 'Desktop',
configurable: true
});
> navigator.userAgent;
< "Desktop"
> navigator.userAgent = "Mobile";
> navigator.userAgent;
< "Desktop" // setter won't work here!
> Object.defineProperty(window.navigator, 'userAgent', {
value: 'Mobile',
writable: true
});
> navigator.userAgent;
< "Mobile"
> navigator.userAgent = 'Desktop';
> navigator.userAgent;
< "Desktop"
> Object.defineProperty(window.navigator, 'userAgent', {
value: 'Mobile',
writable: true
});
> navigator.userAgent;
< "Mobile"
Both of the ways work just fine, however, writable
is a bit more flexible as it allows to change the value
returned by a given property by redefining this property or using a simple setter. In case of configurable
you can only redefine a property.
Wrapping Up
Maybe JavaScript has some odd parts, nevertheless, it’s a quite powerful language. Changing the value of read-only properties is probably not a something do you will do often, but if you really need to do it, Object.defineProperty
will be your friend.