Comments

JavaScript: Wrapper objects

One thing we have learned about OOP is that objects can have properties and methods, as opposed to primitive values, that represent a simple data. This concept looks like something basic (in fact, it is), however, some apparent contradictions arise when we try to apply it to JavaScript. Keep reading and you’ll learn what are wrapper objects and what they have to do with these “inconsistencies”.

The confusion

An object “borns” with a set of methods, such as toString and hasOwnProperty. It also can have properties, like __proto__ (which, by the way, is deprecated):

const app = {};
console.log(app.toString()); //=> "[Object object]
console.log(app.hasOwnProperty('name')); //=> false
console.log(app.__proto__); //=> {constructor: ƒ, __defineGetter__: ƒ, ...}

An array is also an object. It has methods like concat and sort. Arrays also have the well-known length property.

const years = [2004, 1986, 1992, 2019, 2009, 1985, 2013];
console.log(years.length); //=> 7
console.log(years.sort((a,b) => a - b)); //=> [1985, 1986, 1992, 2004, 2009, 2013, 2019]
console.log(years.concat([1994, 1996])); //=> [1985, 1986, 1992, 2004, 2009, 2013, 2019, 1994, 1996]

Finally, strings have a number of utility methods, such as substr, indexOf and replace. Besides, they also have a length property.

const name = 'John Scofield';
console.log(name.length); //=> 13
console.log(name.indexOf('S')); //=> 5
console.log(name.substr(2, 5)); //=> "hn Sc"
console.log(name.replace('John', 'Michael')); //=> "Michael Scofield"

Therefore, following the reasoning we’ve developed above, it seems obvious that strings are objects as well. Right?

Wrong!

JavaScript has three primitive types: strings, numbers and booleans. Therefore, strings are not objects.

I know that it seems to not make any sense. But before a deeper explanation, let’s see where things get really different. An object will allow you to define custom methods and properties:

const app = {};
app.init = function() {
  this.status = 'RUNNING';
};

app.status = 'PAUSED';
console.log(app.status); //=> "PAUSED"
app.init();
console.log(app.status); //=> "RUNNING"

You could do the same with an array:

const names = ['Joe', 'John', 'Jane', 'Julia'];
names.separator = ', ';
names.printNames = function() {
  console.log(this.join(this.separator));
};

names.printNames(); //=> "Joe, John, Jane, Julia"

It works! But, how about strings? Let’s give it a try:

const name = 'Darth Vader';
name.alignment = 'Lawful evil';
name.tellTheTruth = () => {
  console.log('Luke, I am your father!');
};

console.log(name.alignment); //=> undefined
name.tellTheTruth(); //=> Uncaught TypeError: name.tellTheTruth is not a function

In the above example, we defined a method and a property and tried to use them immediately after their initialization. As you can see, both were not recognized. The same problem occurs with booleans and numbers:

// Numbers
const number = 3.14159;
number.name = 'Pi';
number.print = () => {
  console.log(`The value of ${this.name}, with 5 digits of precision, is:${this}`);
};

console.log(number.name); //=> undefined
number.print(); //=> Uncaught TypeError: number.print is not a function


// Booleans
const someBoolean = false;
someBoolean.trueLabel = 'YES';
someBoolean.falseLabel = 'NO';
someBoolean.print = () => {
  console.log(this ? this.trueLabel : this.falseLabel);
};

console.log(someBoolean.trueLabel); //=> undefined
console.log(someBoolean.falseLabel); //=> undefined
someBoolean.print(); //=> Uncaught TypeError: someBoolean.print is not a function

Weird, isn’t it? Well, let’s find out why this happens.

Wrapper objects

When we treat a primitive value like it was an object (i.e. by accessing properties and methods), JavaScript creates, under the hood, a wrapper to wrap this value and expose it as an object. The JS engine never reuses a wrapper object, giving them to the garbage collector right after a single use.

If you write something like const surname = ‘Freddie Mercury’.substr(8, 7);, you’re not storing a reference to the created wrapper object in the surname variable; actually, what is being assigned is the primitive value itself. Therefore, if you run console.log(surname.toUpperCase());, JavaScript will create a brand new wrapper object to wrap the primitive value stored in the surname variable, expose its properties and utility methods (e.g. toUpperCase) and finally dispose it once again.

How the mapping happens

Wrapper objects for number values are instances of Number, string values are wrapped by instances of String and the type for a boolean’s wrapper is Boolean:

'A martini. Shaken, not stirred.'.constructor === String; //=> true
(4321).constructor === Number; //=> true
false.constructor === Boolean; //=> true

As we learned, wrapper objects’ instantiation is done on-the-fly by JavaScript’s engine, boxing primitive values in temporary objects. However, it’s possible to instantiate a wrapper object explicitly and, this way, make it behave like a regular object, preventing its automatic disposal. Check out this example:

const quote = new String('My precious');
quote.character = 'Gollum';
quote.showQuote = function() {
  console.log(`'${this}' is a quote from '${this.character}'.`);
};

quote.showQuote(); //=> "'My precious' is a quote from 'Gollum'."

For numbers and booleans, it’s the same thing:

const age = new Number(13);
age.hasComeOfAge = function() {
  console.log(`I am ${(this >= 18 ? 'an adult' : 'a minor')}.`);
};

age.hasComeOfAge(); //=> "I am a minor."

const answer = new Boolean(true);
answer.print = function() {
  return this.valueOf() ? 'Yes' : 'No';
};

console.log(answer.print()); //=> "Yes"

Once you instantiate a wrapper object by using a constructor and initialize a variable with it, what is actually being assigned is not the primitive value anymore, but a reference to the object itself. Each wrapper object will have a different reference. Therefore, if you compare a wrapped value to its equivalent primitive, they will differ:

// Two primitives with the same value are always equal
console.log('some string' === 'some string'); //=> true
console.log(42 === 42); //=> true
console.log(true === true); //=> false

// But a wrapper object and its equivalent primitive value will always differ
console.log(new String('some string') === 'some string'); //=> false
console.log(new Number(42) === 42); //=> false
console.log(new Boolean(true) === true); //=> false

// Even two objects wrapping the same value won't be the same
console.log(new String('some string') === new String('some string')); //=> false
console.log(new Number(42) === new Number(42)); //=> false
console.log(new Boolean(true) === new Boolean(true)); //=> false

You can use the method valueOf in order to retrieve the original primitive value that is wrapped inside an object:

console.log((new String('some string')).valueOf() === 'some string'); //=> true
console.log((new Number(42)).valueOf() === 42); //=> true
console.log((new Boolean(true)).valueOf() === true); //=> true

In practice, you probably will never need to instantiate a wrapper object explicitly. Even if you think it’s a good idea to assign properties or methods to a string, boolean or number, maybe it’s better to consider how you could represent this data by using an object instead.

Conclusion

  • Objects can have properties and methods while primitive values can’t
  • JavaScript has three primitive types: strings, numbers and booleans
  • When you interact with a primitive value like it was an object (by calling a method or reading a property from it), JavaScript creates a wrapper object on the fly.
  • You have read access to properties and methods of wrapper objects, like they were regular objects.
  • A wrapper object is disposed right after a single use.
  • Due to this auto-disposal mechanism, properties and methods injected into wrapper objects are immediately lost.
  • Each primitive type has a counterpart type for its wrapper objects. These types are String, Number and Boolean.
  • It’s possible to instantiate a wrapper object explicitly, by using its constructor. However, it’s rarely necessary (if at all). Normally, the better approach is to use regular objects instead.

Do you want to become a JavaScript master? If so, you need to check out Mosh’s JavaScript course. It’s the best JavaScript course out there! And if you liked this article, share it with others as well!

 

JavaScript hacker, front-end engineer and F/OSS lover.
Tags: ,

Leave a Reply

Connect with Me
  • Categories
  • Popular Posts