I want to present to your court a number of mini-articles, which will describe the techniques and basics of metaprogramming. Basically, I will write about the use of certain techniques in JavaScript or in TypeScript
This is the first (and hopefully not the last) article from the series.
It is a programming technique. It can be used to create, analyze, or transform programs, and even modify itself while running. It turns out that in some cases, it will turn out to be a number of lines .
A rather confusing description, but the main benefits of metaprogramming are quite understandable:
... it allows programmers to minimize the number of lines of code to implement a solution, which in turn reduces development time
In fact, metaprogramming has many faces and looks. And you can discuss for a long time about "where metaprogramming ends and programming itself begins"
For myself, I accepted the following rules:
In JavaScript, metaprogramming is a relatively new trend, the base brick of which is the descriptor.
A descriptor is a kind of description (meta information) of a certain property or method in an object.
Understanding and proper manipulation of this object ( descriptor ) allows much more than just creating and changing methods or properties in objects.
Also descriptor will help in understanding the work with decorators (but more on that in the next article).
For clarity, imagine that our object is a description of the apartment.
We describe the object of our apartment:
let apt = { floor: 12, number: '12B', size: 3400, bedRooms: 3.4, bathRooms: 2, price: 400000, amenities: {...} };
Let's determine which of the properties are changeable and which are not.
For example, the floor or the total size of an apartment cannot be changed, but the number of rooms or bathrooms is quite possible.
And so we have the following requirement: in apt objects, it is impossible to change the properties: floor and size .
To solve this problem, we just need the descriptors of each of these properties. To get a descriptor , we use the static getOwnPropertyDescriptor method, which belongs to the Object class.
let descriptor = Object.getOwnPropertyDescriptor(todoObject, 'floor'); console.log(descriptor); // Output { value: 12, writable: true, enumerable: true, configurable: true }
We analyze in order:
value: any - actually the very value that at some point was assigned to the floor property
writable: boolean - determines whether or not it is possible to change the value
enumerable: boolean - determines if the floor property may or may not be enumerated - (more on that later).
configurable: boolean - defines the ability to make changes to the descriptor object.
In order to prevent the possibility of changing the floor property after initialization, it is necessary to change the value writable to false .
To change the descriptor properties, there is a static method defineProperty , which accepts the object itself, the property name and descriptor .
Object.defineProperty(apt, 'floor', {writable: false});
In this example, we are not passing the entire descriptor object, but only one writable property with the value false .
Now let's try changing the value in the floor property:
apt.floor = 44; console.log(apt.floor); // output 12
The value has not changed, and when using 'use strict' we get an error message:
Cannot assign to read only property 'floor' of object ...
And now we can not change the value. However, we can still return the value writable -> true and then change the floor property. To avoid this, it is necessary in the descriptor to change the value of the configurable property to false .
Object.defineProperty(apt, 'floor', {writable: false, configurable: false});
If we now try to change the value of any of the properties of our descriptor ...
Object.defineProperty(apt, 'floor', {writable: true, configurable: true});
In response, we get:
TypeError: Cannot redefine property: floor
In other words, we can neither change the value of the floor nor its descriptor .
To make a property value in an object unchanged, you must set the configuration of this property: {writable: false, configurable: false} .
This can be done both during property initialization:
Object.defineProperty(apt, 'floor', {value: 12, writable: false, configurable: false});
Or after.
Object.defineProperty(apt, 'floor', {writable: false, configurable: false});
Finally, consider an example with a class:
class Apartment { constructor(apt) { this.apt = apt; } getFloor() { return this.apt.floor } } let apt = { floor: 12, number: '12B', size: 3400, bedRooms: 3.4, bathRooms: 2, price: 400000, amenities: {...} };
Change the getFloor method:
Apartment.prototype.getFloor = () => { return 44 }; let myApt = new Apartment(apt); console.log(myApt); // output will be changed. 44
Now change the descriptor of the getFloor () method:
Object.defineProperty(Apartment.prototype, 'getFloor', {writable: false, configurable: false}); Apartment.prototype.getFloor = () => { return 44 }; let myApt = new Apartment(apt); console.log(myApt); // output will be original. 12
I hope this article will shed a little more light on what descriptor is and how it can be used.
Everything written above does not claim to be absolutely true or the only correct one.
Source: https://habr.com/ru/post/450540/
All Articles