refactor classes, add private, minor fixes

This commit is contained in:
Ilya Kantor 2019-03-05 18:44:28 +03:00
parent a0c07342ad
commit 1373f6158c
270 changed files with 1513 additions and 890 deletions

View file

@ -0,0 +1,320 @@
# Property flags and descriptors
As we know, objects can store properties.
Till now, a property was a simple "key-value" pair to us. But an object property is actually a more flexible and powerful thing.
In this chapter we'll study additional configuration options, and in the next we'll see how to invisibly turn them into getter/setter functions.
## 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 generally they do not show up. When we create a property "the usual way", all of them are `true`. But we also can change them anytime.
First, let's see how to get those flags.
The method [Object.getOwnPropertyDescriptor](mdn:js/Object/getOwnPropertyDescriptor) allows to query the *full* information about a property.
The syntax is:
```js
let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
```
`obj`
: The object to get information from.
`propertyName`
: The name of the property.
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 ) );
/* property descriptor:
{
"value": "John",
"writable": true,
"enumerable": true,
"configurable": true
}
*/
```
To change the flags, we can use [Object.defineProperty](mdn:js/Object/defineProperty).
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 exists, `defineProperty` updates its flags. Otherwise, it creates the property with the given value and flags; in that case, 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 ) );
/*
{
"value": "John",
*!*
"writable": false,
"enumerable": false,
"configurable": false
*/!*
}
*/
```
Compare it with "normally created" `user.name` above: now all flags are falsy. If that's not what we want then we'd better set them to `true` in `descriptor`.
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 they apply their 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 add a custom `toString` to `user`.
Normally, a built-in `toString` for objects is non-enumerable, it does not show up in `for..in`. But if we add `toString` of our own, then by default it shows up in `for..in`, like this:
```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
```
If we don't like it, then we can set `enumerable:false`. Then it won't appear in `for..in` loop, just like the built-in one:
```js run
let user = {
name: "John",
toString() {
return this.name;
}
};
Object.defineProperty(user, "toString", {
*!*
enumerable: false
*/!*
});
*!*
// Now our toString disappears:
*/!*
for (let key in user) alert(key); // name
```
Non-enumerable properties are also excluded from `Object.keys`:
```js
alert(Object.keys(user)); // name
```
## Non-configurable
The non-configurable flag (`configurable:false`) is sometimes preset for built-in objects and properties.
A non-configurable property can not be deleted or altered with `defineProperty`.
For instance, `Math.PI` is 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 `Math.PI` or overwrite it.
```js run
Math.PI = 3; // Error
// delete Math.PI won't work either
```
Making a property non-configurable is a one-way road. We cannot change it back, because `defineProperty` doesn't work on non-configurable properties.
Here we are making `user.name` a "forever sealed" constant:
```js run
let user = { };
Object.defineProperty(user, "name", {
value: "John",
writable: false,
configurable: false
});
*!*
// won't be able to change user.name or its flags
// all this won't work:
// user.name = "Pete"
// delete user.name
// defineProperty(user, "name", ...)
Object.defineProperty(user, "name", {writable: true}); // Error
*/!*
```
```smart header="Errors appear only in use strict"
In the non-strict mode, no errors occur when writing to read-only properties and such. But the operation still won't succeed. Flag-violating actions are just silently ignored in non-strict.
```
## Object.defineProperties
There's a method [Object.defineProperties(obj, descriptors)](mdn:js/Object/defineProperties) that allows to define many properties at once.
The syntax is:
```js
Object.defineProperties(obj, {
prop1: descriptor1,
prop2: descriptor2
// ...
});
```
For instance:
```js
Object.defineProperties(user, {
name: { value: "John", writable: false },
surname: { value: "Smith", writable: false },
// ...
});
```
So, we can set many properties at once.
## Object.getOwnPropertyDescriptors
To get all property descriptors at once, we can use the method [Object.getOwnPropertyDescriptors(obj)](mdn:js/Object/getOwnPropertyDescriptors).
Together with `Object.defineProperties` it can be used as a "flags-aware" way of cloning an object:
```js
let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));
```
Normally when we clone an object, we use an assignment to copy properties, like this:
```js
for (let key in user) {
clone[key] = user[key]
}
```
...But that does not copy flags. So if we want a "better" clone then `Object.defineProperties` is preferred.
Another difference is that `for..in` ignores symbolic properties, but `Object.getOwnPropertyDescriptors` returns *all* property descriptors including symbolic ones.
## Sealing an object globally
Property descriptors work at the level 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 also there are 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.

View file

@ -0,0 +1,237 @@
# Property getters and setters
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 till now were 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 to an external code.
## Getters and setters
Accessor properties are represented by "getter" and "setter" methods. In an object literal they are denoted by `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: the getter 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 header="Accessor properties are only accessible with get/set"
A property can either be a "data property" or an "accessor property", but not both.
Once a property is defined with `get prop()` or `set prop()`, it's an accessor property. So there must be a getter to read it, and must be a setter if we want to assign it.
Sometimes it's normal that there's only a setter or only a getter. But the property won't be readable or writable in that case.
```
## Accessor descriptors
Descriptors for accessor properties are different -- as compared with data properties.
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); // name, surname
```
Please note once again that a property can be either an accessor or a data property, not both.
If we try to supply both `get` and `value` in the same descriptor, there will be an error:
```js run
*!*
// Error: Invalid property descriptor.
*/!*
Object.defineProperty({}, 'prop', {
get() {
return 1
},
value: 2
});
```
## Smarter getters/setters
Getters/setters can be used as wrappers over "real" property values to gain more control over them.
For instance, if we want to forbid too short names for `user`, we can store `name` in a special property `_name`. And filter assignments in the setter:
```js run
let user = {
get name() {
return this._name;
},
set name(value) {
if (value.length < 4) {
alert("Name is too short, need at least 4 characters");
return;
}
this._name = value;
}
};
user.name = "Pete";
alert(user.name); // Pete
user.name = ""; // Name is too short...
```
Technically, the external code may still access the name directly by using `user._name`. But there is a widely known agreement that properties starting with an underscore `"_"` are internal and should not be touched from outside the object.
## Using for compatibility
One of the great ideas behind getters and setters -- they allow to take control over a "normal" data property and tweak it at any moment.
For instance, we started 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` property?
We can try to find all such places and fix them, but that takes time and can be hard to do if that code is written by other people. 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
```
Now the old code works too and we've got a nice additional property.

View file

@ -0,0 +1,3 @@
# Object properties configuration
In this section we return to objects and study their properties even more in-depth.