Hacking Javascript Objects - I
An Introduction to Object.defineProperty
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ππ»
The
literal
notation withcurly braces {}
. This is the most commonly used syntax.const myObj = { id: 1, name: 'Aakansha'}; console.log(myObj);// { id: 1, name: 'Aakansha'}
Using the
Object
constructor.Object
class represents one of the data structures inJavascript
. 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 orsquare brackets[]
.const obj = { id: 1} obj.id // 1 obj['id'] // 1
Most of the time
dot
notation should work, but for the cases, wherekey
containsspaces
orkey
is computed at runtimedot
notation will not work andsquare brackets
notation will be needed.const obj = { id: 1, "first name": "Aakansha" }; obj["first name"] // Aakansha const key = "id"; obj[key] // 1
- 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
: Theobject
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 usingName
.descriptor
: Anobject
denoting theattributes
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 asdescriptors
.
There are two types of descriptors
Data Descriptors
- The property descriptor has avalue
and may or may not be modifiable.Accessor Descriptors
- The property descriptor hasget
andset
methods with which the value of the property can be retrieved and updated respectively.
Data descriptors
Name | Type | Default | Description |
value | Any valid javascript type | undefined | This denotes the value of the property . Defaults to undefined . |
writeable | boolean | true | Implies 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
Name | Type | Default | Description |
get | function | undefined | A function which returns the value of the property. This function gets called when the property is accessed. |
set | function | undefined | A 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.
Name | Type | Default | Description |
configurable | boolean | false | Implies whether the property can be deleted from the object. |
enumerable | boolean | false | Implies 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 ππ»
Name | Value |
value | The value which is set when creating / updating the object. |
writeable | true , hence update is possible |
enumerable | true , hence the keys are enumerable |
configurable | true , 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
: Theobject
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 usingName
.
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 π§.
An attempt to "Hack"
console.log
π±Object.defineProperty(console, "log", { value: () => "console.log is hacked π±", }); console.log("Hello world!");
What will be the value of
arr
and why π€ ?var arr = [1, 2, 3, 4, 5]; arr.length = 2; console.log(arr);
Pass
orFail
? π€π»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);
Playing with
console
πObject.defineProperties(console, { happy: { value: () => "π" } }); console.happy(); delete console.happy; delete console.log; console.happy(); console.log("hello");
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
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 thelog
property in theconsole
object iswriteable
.Object.getOwnPropertyDescriptor(console, 'log').writable // true
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-likeobjects
, andlength
property iswriteable
hence it reduces the length of the array when updatinglength
. Hence the value ofarr
will be ππ»[1,2]
If you increase the
length
then it will addempty slots
to the remainingindexes
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 cannotdelete
orenumerate
.Pass
orFail
? π€π»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 theget
method ofmyObj.result
will return "Pass"π.console.log(myObj.result);// Pass
Since
myObj.result
doesn't have aset
method so setting the value usingassignment operator
is not possible. Additionally evenset
method is added still when accessing the value returned will bePass
orFail
as per theget
method's implementation π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
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
areobjects
as anymethod
orproperty
can be attached to the function just like regularobjects
. Theset
andget
methods are defined for both thename
andtype
properties of the function. However, there is a slight difference, forname
the property points to the object which is accessing the property hence every object has its own name, whereas fortype
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!