up
|
@ -0,0 +1,285 @@
|
|||
|
||||
# Property flags and descriptors [todo move to objects?]
|
||||
|
||||
Now as we know how to work with primitives and several concrete object-based types, let's return to objects in general.
|
||||
|
||||
As we know, objects can store properties.
|
||||
|
||||
But an object property is actually more complex thing than just a "key-value" mapping.
|
||||
|
||||
[cut]
|
||||
|
||||
## Property flags
|
||||
|
||||
Object properties, besides a **`value`**, have three special attributes (so-called "flags"):
|
||||
|
||||
- **`writable`** -- if `true`, can be changed, otherwise it's read-only.
|
||||
- **`enumerable`** -- if `true`, then listed in loops, otherwise not listed.
|
||||
- **`configurable`** -- if `true`, the property can be deleted and these attributes can be modified, otherwise not.
|
||||
|
||||
We didn't see them yet, because by default they are concealed. When we create a property "the usual way", all of them are `true`. But we also can change them any time.
|
||||
|
||||
First, let's see how to read the flags.
|
||||
|
||||
The method [Object.getOwnPropertyDescriptor](mdn:js/Object/getOwnPropertyDescriptor) allows to query the information about a property.
|
||||
|
||||
The syntax is:
|
||||
```js
|
||||
let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
|
||||
```
|
||||
|
||||
`obj`
|
||||
: The object to get information about.
|
||||
|
||||
`propertyName`
|
||||
: The name of the property of interest.
|
||||
|
||||
The returned value is a so-called "property descriptor" object: it contains the value and all the flags.
|
||||
|
||||
For instance:
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
name: "John"
|
||||
};
|
||||
|
||||
let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
|
||||
|
||||
alert( JSON.stringify(descriptor, null, 2 ) );
|
||||
/* descriptor:
|
||||
{
|
||||
"value": "John",
|
||||
"writable": true,
|
||||
"enumerable": true,
|
||||
"configurable": true
|
||||
}
|
||||
*/
|
||||
```
|
||||
|
||||
Now, we'll use [Object.defineProperty](mdn:js/Object/defineProperty) to change flags.
|
||||
|
||||
The syntax is:
|
||||
|
||||
```js
|
||||
Object.defineProperty(obj, propertyName, descriptor)
|
||||
```
|
||||
|
||||
`obj`, `propertyName`
|
||||
: The object and property to work on.
|
||||
|
||||
`descriptor`
|
||||
: Property descriptor to apply.
|
||||
|
||||
If the property exist, it update its flags.
|
||||
|
||||
Otherwise, it creates the property with the provided value and flags. Please note, that if a flag is not supplied, it is assumed `false`.
|
||||
|
||||
For instance, here a property `name` is created with all falsy flags:
|
||||
|
||||
```js run
|
||||
let user = {};
|
||||
|
||||
Object.defineProperty(user, "name", {
|
||||
value: "John"
|
||||
});
|
||||
|
||||
let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
|
||||
|
||||
alert( JSON.stringify(descriptor, null, 2 ) );
|
||||
/* compare it with "normally created" user.name above:
|
||||
{
|
||||
"value": "John",
|
||||
*!*
|
||||
"writable": false,
|
||||
"enumerable": false,
|
||||
"configurable": false
|
||||
*/!*
|
||||
}
|
||||
*/
|
||||
```
|
||||
|
||||
Now let's see effects of the flags by example.
|
||||
|
||||
## Read-only
|
||||
|
||||
Let's make `user.name` read-only by changing `writable` flag:
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
name: "John"
|
||||
};
|
||||
|
||||
Object.defineProperty(user, "name", {
|
||||
*!*
|
||||
writable: false
|
||||
*/!*
|
||||
});
|
||||
|
||||
*!*
|
||||
user.name = "Pete"; // Error: Cannot assign to read only property 'name'...
|
||||
*/!*
|
||||
```
|
||||
|
||||
Now no one can change the name of our user, unless he applies his own `defineProperty` to override ours.
|
||||
|
||||
Here's the same operation, but for the case when a property doesn't exist:
|
||||
|
||||
```js run
|
||||
let user = { };
|
||||
|
||||
Object.defineProperty(user, "name", {
|
||||
*!*
|
||||
value: "Pete",
|
||||
// for new properties need to explicitly list what's true
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
*/!*
|
||||
});
|
||||
|
||||
alert(user.name); // Pete
|
||||
user.name = "Alice"; // Error
|
||||
```
|
||||
|
||||
|
||||
## Non-enumerable
|
||||
|
||||
Now let's a custom `toString` to `user`.
|
||||
|
||||
Normally, a built-in `toString` for objects is non-enumerable, it does not show up in `for..in`. So we'll make ours behave the same.
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
name: "John",
|
||||
toString() {
|
||||
return this.name;
|
||||
}
|
||||
};
|
||||
|
||||
// By default, both our properties are listed:
|
||||
for(let key in user) alert(key); // name, toString
|
||||
|
||||
Object.defineProperty(user, "toString", {
|
||||
*!*
|
||||
enumerable: false
|
||||
*/!*
|
||||
});
|
||||
|
||||
*!*
|
||||
// Now toString disappears:
|
||||
*/!*
|
||||
for(let key in user) alert(key); // name
|
||||
```
|
||||
|
||||
Non-enumerable properties are also excluded from `Object.keys`.
|
||||
|
||||
## Non-configurable
|
||||
|
||||
Non-configurable flag is often preset for built-in objects and properties.
|
||||
|
||||
A non-configurable property can not be deleted or altered with `defineProperty`.
|
||||
|
||||
For instance, `Math.PI` is both read-only, non-enumerable and non-configurable:
|
||||
|
||||
```js run
|
||||
let descriptor = Object.getOwnPropertyDescriptor(Math, 'PI');
|
||||
|
||||
alert( JSON.stringify(descriptor, null, 2 ) );
|
||||
/*
|
||||
{
|
||||
"value": 3.141592653589793,
|
||||
"writable": false,
|
||||
"enumerable": false,
|
||||
"configurable": false
|
||||
}
|
||||
*/
|
||||
```
|
||||
So, a programmer is unable to change the value of that built-in constant or overwrite it.
|
||||
|
||||
```js run
|
||||
Math.PI = 3; // Error
|
||||
|
||||
// delete Math.PI won't work either
|
||||
```
|
||||
|
||||
Making non-configurable is one-way road. We cannot change it back, because `defineProperty` doesn't work on non-configurable properties.
|
||||
|
||||
Here `user.name` is a forever sealed constant:
|
||||
|
||||
```js run
|
||||
let user = { };
|
||||
|
||||
Object.defineProperty(user, "name", {
|
||||
value: "John",
|
||||
writable: false,
|
||||
configurable: false
|
||||
});
|
||||
|
||||
*!*
|
||||
// can't change it or its flags
|
||||
// user.name = "Pete" won't work
|
||||
// delete user.name won't work
|
||||
// defineProperty won't work either:
|
||||
Object.defineProperty(user, "name", {writable: true}); // Error
|
||||
*/!*
|
||||
```
|
||||
|
||||
```smart header="Errors appear only in use strict"
|
||||
In non-strict mode, there are no errors for writing to read-only properties and such, flag-violating actions are silently ignored.
|
||||
```
|
||||
|
||||
## Many properties at once
|
||||
|
||||
There's a method [Object.defineProperties(obj, descriptors)](mdn:js/Object/defineProperties) that allows to define many properties at once:
|
||||
|
||||
```js
|
||||
Object.defineProperties(user, {
|
||||
name: { writable: false },
|
||||
surname: { ... },
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
And, to get all descriptors, use [Object.getOwnPropertyDescriptors(obj)](mdn:js/Object/getOwnPropertyDescriptors).
|
||||
|
||||
Together they can be used as an "property flags-aware" way of cloning an object:
|
||||
|
||||
```js
|
||||
let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));
|
||||
```
|
||||
|
||||
(For the clone to be fully identical, we need to care about one more thing: "prototype", we'll see into it soon in the chapter [todo])
|
||||
|
||||
## Sealing an object globally
|
||||
|
||||
Property descriptors allow to forbid modifications of individual properties.
|
||||
|
||||
There are also methods that limit access to the whole object:
|
||||
|
||||
[Object.preventExtensions(obj)](mdn:js/Object/preventExtensions)
|
||||
: Forbids to add properties to the object.
|
||||
|
||||
[Object.seal(obj)](mdn:js/Object/seal)
|
||||
: Forbids to add/remove properties, sets for all existing properties `configurable: false`.
|
||||
|
||||
[Object.freeze(obj)](mdn:js/Object/freeze)
|
||||
: Forbids to add/remove/change properties, sets for all existing properties `configurable: false, writable: false`.
|
||||
|
||||
And the tests for them:
|
||||
|
||||
[Object.isExtensible(obj)](mdn:js/Object/isExtensible)
|
||||
: Returns `false` if adding properties is forbidden, otherwise `true`.
|
||||
|
||||
[Object.isSealed(obj)](mdn:js/Object/isSealed)
|
||||
: Returns `true` if adding/removing properties is forbidden, and all existing properties have `configurable: false`.
|
||||
|
||||
[Object.isFrozen(obj)](mdn:js/Object/isFrozen)
|
||||
: Returns `true` if adding/removing/changing properties is forbidden, and all current properties are `configurable: false, writable: false`.
|
||||
|
||||
These methods are rarely used in practice.
|
||||
|
||||
|
||||
|
||||
|
||||
## Tasks
|
||||
|
||||
Check: new property has all non-enum, test
|
221
1-js/9-object-inheritance/02-property-accessors/article.md
Normal file
|
@ -0,0 +1,221 @@
|
|||
|
||||
# Property getters and setters [todo move to objects?]
|
||||
|
||||
There are two kinds of properties.
|
||||
|
||||
The first kind is *data properties*. We already know how to work with them, actually, all properties that we've been using yet are data properties.
|
||||
|
||||
The second type of properties is something new. It's *accessor properties*. They are essentially functions that work on getting and setting a value, but look like regular properties.
|
||||
|
||||
[cut]
|
||||
|
||||
## Getters and setters
|
||||
|
||||
Accessor properties are represented by "getter" and "setter" methods. In an object literal they are preprended with words `get` and `set`:
|
||||
|
||||
```js
|
||||
let obj = {
|
||||
*!*get propName()*/!* {
|
||||
// getter, the code executed on getting obj.propName
|
||||
},
|
||||
|
||||
*!*set propName(value)*/!* {
|
||||
// setter, the code executed on setting obj.propName = value
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
The getter works when `obj.propName` is read, the setter -- when it is assigned.
|
||||
|
||||
For instance, we have a `user` object with `name` and `surname`:
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
name: "John",
|
||||
surname: "Smith"
|
||||
};
|
||||
```
|
||||
|
||||
Now we want to add a "fullName" property, that should be "John Smith". Of course, we don't want to copy-paste existing information, so we can implement it as an accessor:
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
name: "John",
|
||||
surname: "Smith",
|
||||
|
||||
*!*
|
||||
get fullName() {
|
||||
return `${this.name} ${this.surname}`;
|
||||
}
|
||||
*/!*
|
||||
};
|
||||
|
||||
*!*
|
||||
alert(user.fullName); // John Smith
|
||||
*/!*
|
||||
```
|
||||
|
||||
From outside, an accessor property looks like a regular one. That's the idea of accessor properties. We don't call `user.fullName` as a function, we read it normally, and it runs behind the scenes.
|
||||
|
||||
As of now, `fullName` has only a getter. If we attempt to assign `user.fullName=`, there will be an error.
|
||||
|
||||
Let's fix it by adding a setter for `user.fullName`:
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
name: "John",
|
||||
surname: "Smith",
|
||||
|
||||
get fullName() {
|
||||
return `${this.name} ${this.surname}`;
|
||||
},
|
||||
|
||||
*!*
|
||||
set fullName(value) {
|
||||
[this.name, this.surname] = value.split(" ");
|
||||
}
|
||||
*/!*
|
||||
};
|
||||
|
||||
// set fullName is executed with the given value.
|
||||
user.fullName = "Alice Cooper";
|
||||
|
||||
alert(user.name); // Alice
|
||||
alert(user.surname); // Cooper
|
||||
```
|
||||
|
||||
Now we have a "virtual" property. It is readable and writable, but in fact does not exist.
|
||||
|
||||
```smart
|
||||
We can either work with a property as a "data property" or as an "accessor property", these two never mix.
|
||||
|
||||
Once a property as defined with `get prop()`, it can't be assigned with `obj.prop=`, unless there's a setter too.
|
||||
|
||||
And if a property is defined with `set prop()`, then it can't be read unless there's also a getter.
|
||||
```
|
||||
|
||||
|
||||
## Accessor descriptors
|
||||
|
||||
Differences between data properties and accessors are also reflected in their descriptors.
|
||||
|
||||
For accessor properties, there is no `value` and `writable`, but instead there are `get` and `set` functions.
|
||||
|
||||
So an accessor descriptor may have:
|
||||
|
||||
- **`get`** -- a function without arguments, that works when a property is read,
|
||||
- **`set`** -- a function with one argument, that is called when the property is set,
|
||||
- **`enumerable`** -- same as for data properties,
|
||||
- **`configurable`** -- same as for data properties.
|
||||
|
||||
For instance, to create an accessor `fullName` with `defineProperty`, we can pass a descriptor with `get` and `set`:
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
name: "John",
|
||||
surname: "Smith"
|
||||
};
|
||||
|
||||
*!*
|
||||
Object.defineProperty(user, 'fullName', {
|
||||
get() {
|
||||
return `${this.name} ${this.surname}`;
|
||||
},
|
||||
|
||||
set(value) {
|
||||
[this.name, this.surname] = value.split(" ");
|
||||
}
|
||||
*/!*
|
||||
});
|
||||
|
||||
alert(user.fullName); // John Smith
|
||||
|
||||
for(let key in user) alert(key);
|
||||
```
|
||||
|
||||
## Smarter getters/setters
|
||||
|
||||
A combination of getter/setter can be used to validate property values at the moment of assignment.
|
||||
|
||||
For instance, if we want to forbid too short names for `user`, we can store `name` in a special property `_name`, at the same time providing smart getter/setter for it:
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
get name() {
|
||||
return this._name;
|
||||
},
|
||||
|
||||
set name(value) {
|
||||
if (value.length < 4) {
|
||||
throw new Error("Name is too short, need at least 4 characters");
|
||||
}
|
||||
this._name = value;
|
||||
}
|
||||
};
|
||||
|
||||
user.name = "Pete";
|
||||
alert(user.name); // Pete
|
||||
|
||||
user.name = ""; // Error
|
||||
```
|
||||
|
||||
Technically, the "real" name is stored in `user._name`, so the outer code may access it. In Javascript there's no way to prevent reading an object property. But there is a widely known agreement that properties starting with an underscore `"_"` are internal and should not be touched from outside.
|
||||
|
||||
|
||||
## For compatibility
|
||||
|
||||
One of great ideas behind getters and setters -- they allow to take control over a property at any moment.
|
||||
|
||||
For instance, we start implementing user objects using data properties `name` and `age`:
|
||||
|
||||
```js
|
||||
function User(name, age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
let john = new User("John", 25);
|
||||
|
||||
alert( john.age ); // 25
|
||||
```
|
||||
|
||||
...But sooner or later, things may change. Instead of `age` we may decide to store `birthday`, because it's more precise and convenient:
|
||||
|
||||
```js
|
||||
function User(name, birthday) {
|
||||
this.name = name;
|
||||
this.birthday = birthday;
|
||||
}
|
||||
|
||||
let john = new User("John", new Date(1992, 6, 1));
|
||||
```
|
||||
|
||||
Now what to do with the old code that still uses `age`?
|
||||
|
||||
We can try to find all such places and fix them, but that takes time and not always possible with 3rd party libraries. And besides, `age` is a nice thing to have in `user`, right? In some places it's just what we want.
|
||||
|
||||
Adding a getter for `age` mitigates the problem:
|
||||
|
||||
```js run no-beautify
|
||||
function User(name, birthday) {
|
||||
this.name = name;
|
||||
this.birthday = birthday;
|
||||
|
||||
*!*
|
||||
// age is calculated from the current date and birthday
|
||||
Object.defineProperty(this, "age", {
|
||||
get() {
|
||||
let todayYear = new Date().getFullYear();
|
||||
return todayYear - this.birthday.getFullYear();
|
||||
}
|
||||
});
|
||||
*/!*
|
||||
}
|
||||
|
||||
let john = new User("John", new Date(1992, 6, 1));
|
||||
|
||||
alert( john.birthday ); // birthday is available
|
||||
alert( john.age ); // ...as well as the age
|
||||
```
|
||||
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 4 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |