Aakansha Doshi
Aakansha's Blog

Aakansha's Blog

Hacking Javascript Objects - I

Hacking Javascript Objects - I

An Introduction to Object.defineProperty

Aakansha Doshi's photo
Aakansha Doshi
Β·Jul 18, 2021Β·

11 min read

Subscribe to my newsletter and never miss my upcoming articles

Most of us deal with Objects pretty much every day and it's one of the most commonly used data structures. But few of us might know that it's also possible to control the behavior of the properties in an object or you can say "Hack" the properties of Objects πŸ˜‚. In this post, we will be diving into some of the internals of the Objects that can help in achieving the same and we will also have a fun Quiz towards the end of the post πŸ˜‰.

Before we dive into the internals of object let's start with some basics.

What are objects?

objects are a collection of properties where each property has a key and a value.

Objects can be created in three formsπŸ‘‡πŸ»

  1. The literal notation with curly braces {}. This is the most commonly used syntax.

     const myObj = { id: 1, name: 'Aakansha'};
     console.log(myObj);// { id: 1, name: 'Aakansha'}
    
  2. Using the Object constructor. Object class represents one of the data structures in Javascript. There would be hardly any need to use this form when creating objects but still good to know πŸ™‚

     const myObj = new Object();
     myObj.id = 1;
     myObj.name = 'Aakansha';
     console.log(myObj);// { id: 1, name: 'Aakansha'}
    

    The value of a property can be accessed using dot(.) notation or square brackets[].

     const obj = { id: 1}
     obj.id // 1
     obj['id'] // 1
    

    Most of the time dot notation should work, but for the cases, where key contains spaces or key is computed at runtime dot notation will not work and square brackets notation will be needed.

     const obj = { id: 1, "first name": "Aakansha" };
     obj["first name"] // Aakansha
     const key = "id";
     obj[key] // 1
    
  3. We will not be going into details about the third form but will share it later in this post πŸ™‚

Customizing the behavior of properties in an object

So it's fairly easy to get started with objects, but here comes the interesting fact, it's also possible to customize the behavior of the properties in an object. Let's see what all customizations are possible πŸ‘€.

Object class has a static method defineProperty which allows to add a new property or update an existing property of an object and also control the behavior of the property. The static properties are directly accessed using the class, instances need not be created to access the static properties.

Object.defineProperty

Usage

Object.defineProperty(obj, property, descriptor)
  • obj: The object whose properties need to be updated.
  • property: Name or Symbol of the property which needs to be added/updated. Since Symbol is not covered in this post so we will be using Name.
  • descriptor: An object denoting the attributes for the property. This is going to help us in controlling the behavior of a property in an objectπŸ˜‰. Going further we will be referring to these attributes as descriptors.

There are two types of descriptors

  1. Data Descriptors - The property descriptor has a value and may or may not be modifiable.
  2. Accessor Descriptors - The property descriptor has get and set methods with which the value of the property can be retrieved and updated respectively.

Data descriptors

NameTypeDefaultDescription
valueAny valid javascript typeundefinedThis denotes the value of the property. Defaults to undefined.
writeablebooleantrueImplies whether the value of the property can be updated with an assignment operator(=).

value

This denotes the value of the property. Defaults to undefined.

var myObj = Object.defineProperty({}, 'id', {value: 1}); // value is passed
console.log(myObj); // {id: 1}
myObj = Object.defineProperty({}, 'id', {}); // value not passed
console.log(myObj); // {id: undefined}

writeable

Implies whether the value of the property can be updated with an assignment operator(=). Defaults to `true.

// writeable will be set to true since not passed
var myObj = Object.defineProperty({}, 'id', {value: 1}); 
myObj.id = 10;
myObj.id; // 10

// writeable is false
myObj = Object.defineProperty({}, 'id', {value: 1, writeable: false}); 
myObj.id = 10;
myObj.id; // 1, as the value couldnt't be updated

Accessor descriptors

NameTypeDefaultDescription
getfunctionundefinedA function which returns the value of the property. This function gets called when the property is accessed.
setfunctionundefinedA function which sets the value of the property. This function gets called when the property value is set using assignment operator(=).

get

A function returns the value of the property. This function gets called when the property is accessed using dot(.) or square brackets([]).

var myObj = Object.defineProperty({}, 'id', {get: () => 10 });
myObj.id // 10

set

A function that sets the value of the property. This function gets called when the property value is set using assignment operator(=).

var myObj = Object.defineProperty({}, "id", { set: (val) => (id = val) });
myObj.id = 20;
myObj.id = 20;

myObj = Object.defineProperty({}, "id", {
  set: (val) => (id = val),
  get: () => id,
});
myObj.id = 20;
myObj.id; // 20

Note: An object can have either Data Descriptors or Accessor Descriptors but not both.

myObj = Object.defineProperty({}, 'id', { value: 10, set:() => id = 10 });
// Throws error as we are using both Data and Accessor descriptors
// value is a Data descriptor whereas set is an accessor descriptor

Additional attributes for descriptors

Apart from data descriptors and accessor descriptors, there are additional attributes common to both the descriptorsπŸ‘‡πŸ» that can be used to control the behavior as well.

NameTypeDefaultDescription
configurablebooleanfalseImplies whether the property can be deleted from the object.
enumerablebooleanfalseImplies whether the property will show during enumeration of the keys of the object.

configurable

Implies whether the property can be deleted from the object. Defaults to false.

var myObj = Object.defineProperty({}, 'id', { value: 10 });
delete myObj.id
myObj // { id: 10 } as its not configurable

myObj = Object.defineProperty({}, 'id', { value: 10, configurable: true });
delete myObj.id
myObj // {} as its configurable

enumerable

Implies whether the property will show during enumeration of the keys of the object eg when using Object.keys or for...in. Defaults to false.

var myObj = Object.defineProperty({}, 'id', { value: 10 });
Object.keys(myObj) // [] as the key "id" is not enumerable

myObj = Object.defineProperty({}, 'id', { value: 10, enumerable: true });
Object.keys(myObj) // ["id"] as the key "id" is enumerable

For updating multiple properties, we can use Object.defineProperties.

Object.defineProperties(obj, {
  property1: descriptor,
  property2: descriptor
});

As mentioned earlier, there is a third way to create an object as well and that is Object.create which helps to create an object with the specified prototype object and descriptors.

Now since you know about the descriptors with the help of which the behavior of the properties can be controlled, can you guess why πŸ‘‡πŸ» is possible?

var myObj = { id : 10 };
myObj.id; // 10
myObj.id = 100;
myObj.id; // 100
Object.keys(id); // ["id"];
delete myObj.id
myObj; // {}

This means when the objects are created using literal notation / Object() constructor or even a new property is added via assignment operator(.), the descriptors are set to πŸ‘‡πŸ»

NameValue
valueThe value which is set when creating / updating the object.
writeabletrue, hence update is possible
enumerabletrue, hence the keys are enumerable
configurabletrue, hence delete works

Retrieve descriptors of an existing Object?

Using descriptors we have a lot more control, but there should be some way to retrieve the descriptors of properties in an existing object as well. Well yes, there is a static method getOwnPropertyDescriptor in the Object class which helps in achieving the same😍.

Object.getOwnPropertyDescriptor

Usage

Object.getOwnPropertyDescriptor(obj, property)
  • obj: The object whose property descriptors need to be retrieved.
  • property: Name or Symbol of the property whose descriptors needs to be retrieved. Since Symbol is not covered in this post so we will be using Name.
var myObj = { id: 10 };
Object.getOwnPropertyDescriptor(myObj, 'id')// {value: 10, writable: true, enumerable: true, configurable: true}

To retrieve descriptors of all properties of an object we can use Object.getOwnPropertyDescriptors

Object.getOwnPropertyDescriptors(obj)

Quiz time

It's time to have some fun quiz now πŸ˜‰. Guess the output 🧐.

  1. An attempt to "Hack" console.log 😱

    Object.defineProperty(console, "log", {
     value: () => "console.log is hacked 😱",
    });
    console.log("Hello world!");
    
  2. What will be the value of arr and why πŸ€” ?

     var arr = [1, 2, 3, 4, 5];
     arr.length = 2;
     console.log(arr);
    
  3. Pass or Fail? 🀞🏻

     var myObj = { marks: 60 };
     myObj = Object.defineProperty(myObj, "result", {
     get: () => {
       if (myObj.marks < 50) return "Fail";
       else return "Pass";
       },
     });
    console.log(myObj.result);
    myObj.result = "modified result";
    console.log(myObj.result);
    
  4. Playing with console 😎

     Object.defineProperties(console, { happy: { value: () => "πŸ™‚" } });
     console.happy();
     delete console.happy;
     delete console.log;
     console.happy();
     console.log("hello");
    
  5. Trying out with Functions 🧐

     function Dev() {
       Object.defineProperties(this, {
         name: {
           set: (name) => {
             this.devName = name;
           },
           get: () => {
             return this.devName;
           },
         },
         type: {
           set: (devType) => {
             type = devType;
           },
           get: () => {
             return type;
           },
         },
       });
     }
     dev1 = new Dev();
     dev1.name = "Maria";
     dev1.type = "backend";
     dev2 = new Dev();
     dev2.name = "June";
     dev2.type = "frontend";
     console.log(`${dev1.name} is a ${dev1.type} developer`);
     console.log(`${dev2.name} is a ${dev2.type} developer`);
    

Quiz Solutions

  1. An attempt to "Hack" console.log 😱

    Object.defineProperty(console, "log", {
     value: () => "console.log is hacked 😱",
    });
    console.log("Hello world!");
    

    Explanation

    The output will beπŸ‘‡πŸ»

    console.log is hacked 😱
    

    Overriding the behavior of console.log is possible, as thelogproperty in the console object is writeable.

    Object.getOwnPropertyDescriptor(console, 'log').writable // true
    
  2. What will be the value of arr and why πŸ€” ?

     var arr = [1, 2, 3, 4, 5];
     arr.length = 2;
     console.log(arr);
    

    Explanation

    Arrays are list-like objects, and length property is writeable hence it reduces the length of the array when updating length. Hence the value of arr will be πŸ‘‡πŸ»

    [1,2]
    

    If you increase the length then it will add empty slots to the remaining indexes

     var arr = [1, 2, 3, 4, 5];
     arr.length = 7;
     arr // [1, 2, 3, 4, 5, empty Γ— 2]
    

    Note: The behavior need not be the same in all browsers as it depends if the browsers permit redefining the length of the array. Also do not use this in production πŸ˜‰

    You can always check if the browser allows it

     var arr = [1, 2, 3, 4, 5];
     Object.getOwnPropertyDescriptor(arr, 'length')
      // {value: 7, writable: true, enumerable: false, configurable: false} in chrome
    

    This means you can update the length attribute but cannot delete or enumerate.

  3. Pass or Fail? 🀞🏻

     var myObj = { marks: 60 };
     myObj = Object.defineProperty(myObj, "result", {
     get: () => {
       if (myObj.marks < 50) return "Fail";
       else return "Pass";
       },
     });
    console.log(myObj.result);
    myObj.result = "modified result";
    console.log(myObj.result);
    

    Explanation

    The output will be πŸ‘‡πŸ»

     console.log(myObj.result); // Pass
    

    Since the myObj.marks is > 50 so the get method of myObj.result will return "Pass"😎.

     console.log(myObj.result);// Pass
    

    Since myObj.result doesn't have a set method so setting the value using assignment operator is not possible. Additionally even set method is added still when accessing the value returned will be Pass or Fail as per theget method's implementation πŸ™‚

  4. Playing with console 😎

     Object.defineProperties(console, { happy: { value: () => "πŸ™‚" } });
     console.happy();
     delete console.happy;
     delete console.log;
     console.happy();
     console.log("hello");
    

    Explanation

     console.happy(); // "πŸ™‚"
    
     //since it's not `configurable` hence it cannot be `deleted`.
    
     console.log("hello");
      //Throws error since the `log` property is `configurable` :D
    
  5. Trying out with Functions 🧐

     function Dev() {
       Object.defineProperties(this, {
         name: {
           set: (name) => {
             this.devName = name;
           },
           get: () => {
             return this.devName;
           },
         },
         type: {
           set: (devType) => {
             type = devType;
           },
           get: () => {
             return type;
           },
         },
       });
     }
     dev1 = new Dev();
     dev1.name = "Maria";
     dev1.type = "backend";
     dev2 = new Dev();
     dev2.name = "June";
     dev2.type = "frontend";
     console.log(`${dev1.name} is a ${dev1.type} developer`);
     console.log(`${dev2.name} is a ${dev2.type} developer`);
    

    Explanation

    Functions are objects as any method or property can be attached to the function just like regular objects. The set and get methods are defined for both the name and type properties of the function. However, there is a slight difference, for name the property points to the object which is accessing the property hence every object has its own name, whereas for type the property is shared by all objects. Hence the output will be πŸ‘‡πŸ»

     Maria is a frontend developer
     June is a frontend developer
    

    Closing Thoughts

    If you have used some of the methods like Object.freeze, Object.sealed, now you know what it does behind the scenes πŸ™‚, it modifies the descriptors for the properties of the object. Most of the time these utilities do help but there might be cases where you want more control over the properties and that's where you will need to use it. Hope you enjoyed the post 😍.

Did you find this article valuable?

Support Aakansha Doshi by becoming a sponsor. Any amount is appreciated!

Learn more about Hashnode Sponsors
Β 
Share this