Skip to content

Update some chapters to [Jul 17, 2020] Part-5 #794

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion 1-js/08-prototypes/04-prototype-methods/article.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ alert(obj[key]); // [object Object],并不是 "some value"!

我们不应该对此感到惊讶。`__proto__` 属性很特别:它必须是对象或者 `null`。字符串不能成为 prototype。

但是我们不是 **打算** 实现这种行为,对吗?我们想要存储键值对,然而键名为 `"__proto__"` 的键值对没有被正确存储。所以这是一个 bug。
但是我们不是 **打算** 实现这种行为,对吧?我们想要存储键值对,然而键名为 `"__proto__"` 的键值对没有被正确存储。所以这是一个 bug。

在这里,后果并没有很严重。但是在其他情况下,我们可能会对对象进行赋值操作,然后原型可能就被更改了。结果,可能会导致完全意想不到的结果。

Expand Down
95 changes: 44 additions & 51 deletions 1-js/09-classes/01-class/article.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class User {
// class 是函数 function
alert(typeof User); // function

// ...或者,更确切地说,是构造器方法
// ...或者,更确切地说,是 constructor 方法
alert(User === User.prototype.constructor); // true

// 方法在 User.prototype 中,例如:
Expand All @@ -127,7 +127,7 @@ alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi
function User(name) {
this.name = name;
}
// 任何函数原型默认都具有构造器属性
// 函数的原型(prototype)默认具有 "constructor" 属性
// 所以,我们不需要创建它

// 2. 将方法添加到原型
Expand All @@ -146,7 +146,7 @@ user.sayHi();

1. 首先,通过 `class` 创建的函数具有特殊的内部属性标记 `[[FunctionKind]]:"classConstructor"`。因此,它与手动创建并不完全相同。

不像普通函数,调用类构造器时必须要用 `new` 关键词
编程语言会在许多地方检查该属性。例如,与普通函数不同,必须使用 `new` 来调用它

```js run
class User {
Expand All @@ -166,6 +166,7 @@ user.sayHi();

alert(User); // class User { ... }
```
还有其他的不同之处,我们很快就会看到。

2. 类方法不可枚举。
类定义将 `"prototype"` 中的所有方法的 `enumerable` 标志设置为 `false`。
Expand Down Expand Up @@ -209,7 +210,6 @@ new User().sayHi(); // 正常运行,显示 MyClass 中定义的内容
alert(MyClass); // error,MyClass 在外部不可见
```


我们甚至可以动态地“按需”创建类,就像这样:

```js run
Expand All @@ -229,7 +229,7 @@ new User().sayHi(); // Hello
```


## Getters/setters 及其他速记
## Getters/setters

就像对象字面量,类可能包括 getters/setters,计算属性(computed properties)等。

Expand Down Expand Up @@ -267,22 +267,11 @@ alert(user.name); // John
user = new User(""); // Name is too short.
```

类声明在 `User.prototype` 中创建 getters setters,就像这样:
Technically, such class declaration works by creating getters and setters in `User.prototype`.

```js
Object.defineProperties(User.prototype, {
name: {
get() {
return this._name
},
set(name) {
// ...
}
}
});
```
## Computed names [...]

这是一个 `[...]` 中有计算属性名称(computed property name)的例子:
Here's an example with a computed method name using brackets `[...]`:

```js run
class User {
Expand All @@ -298,13 +287,15 @@ class User {
new User().sayHi();
```

Such features are easy to remember, as they resemble that of literal objects.

## Class 字段

```warn header="旧的浏览器可能需要 polyfill"
类字段(field)是最近才添加到语言中的。
```

之前,类仅具有方法
之前,我们的类仅具有方法

“类字段”是一种允许添加任何属性的语法。

Expand All @@ -313,23 +304,46 @@ new User().sayHi();
```js run
class User {
*!*
name = "Anonymous";
name = "John";
*/!*

sayHi() {
alert(`Hello, ${this.name}!`);
}
}

new User().sayHi();
new User().sayHi(); // Hello, John!
```

So, we just write "<property name> = <value>" in the declaration, and that's it.

alert(User.prototype.sayHi); // 被放在 User.prototype 中
alert(User.prototype.name); // undefined,没有被放在 User.prototype 中
The important difference of class fields is that they are set on individual objects, not `User.prototype`:

```js run
class User {
*!*
name = "John";
*/!*
}

let user = new User();
alert(user.name); // John
alert(User.prototype.name); // undefined
```

关于类字段的重要一点是,它们设置在单个对象上的,而不是设置在 `User.prototype` 上的。
We can also assign values using more complex expressions and function calls:

```js run
class User {
*!*
name = prompt("Name, please?", "John");
*/!*
}

let user = new User();
alert(user.name); // John
```

从技术上讲,它们是在 constructor 完成工作后被处理的。

### 使用类字段制作绑定方法

Expand Down Expand Up @@ -362,30 +376,9 @@ setTimeout(button.click, 1000); // undefined
我们在 <info:bind> 一章中讲过,有两种可以修复它的方式:

1. 传递一个包装函数,例如 `setTimeout(() => button.click(), 1000)`。
2. 将方法绑定到对象,例如在 constructor 中:

```js run
class Button {
constructor(value) {
this.value = value;
*!*
this.click = this.click.bind(this);
*/!*
}

click() {
alert(this.value);
}
}

let button = new Button("hello");

*!*
setTimeout(button.click, 1000); // hello
*/!*
```
2. 将方法绑定到对象,例如在 constructor 中。

类字段为后一种解决方案提供了更优雅的语法
类字段提供了另一种非常优雅的语法

```js run
class Button {
Expand All @@ -404,9 +397,9 @@ let button = new Button("hello");
setTimeout(button.click, 1000); // hello
```

类字段 `click = () => {...}` 在每个 `Button` 对象上创建一个独立的函数,并将 `this` 绑定到该对象上。然后,我们可以将 `button.click` 传递到任何地方,并且它会被以正确的 `this` 进行调用。
The class field `click = () => {...}` is created on a per-object basis, there's a separate function for each `Button` object, with `this` inside it referencing that object. We can pass `button.click` around anywhere, and the value of `this` will always be correct.

这在浏览器环境中,当我们需要将一个方法设置为事件监听器时尤其有用
在浏览器环境中,它对于进行事件监听尤为有用

## 总结

Expand Down
102 changes: 100 additions & 2 deletions 1-js/09-classes/02-class-inheritance/article.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,9 @@ let rabbit = new Rabbit("White Rabbit", 10); // Error: this is not defined.

哎呦!我们得到了一个报错。现在我们没法新建 rabbit。是什么地方出错了?

简短的解释是:继承类的 constructor 必须调用 `super(...)`,并且 (!) 一定要在使用 `this` 之前调用。
简短的解释是:

**继承类的 constructor 必须调用 `super(...)`,并且 (!) 一定要在使用 `this` 之前调用。**

……但这是为什么呢?这里发生了什么?确实,这个要求看起来很奇怪。

Expand All @@ -243,7 +245,7 @@ let rabbit = new Rabbit("White Rabbit", 10); // Error: this is not defined.
- 当通过 `new` 执行一个常规函数时,它将创建一个空对象,并将这个空对象赋值给 `this`。
- 但是当继承的 constructor 执行时,它不会执行此操作。它期望父类的 constructor 来完成这项工作。

因此,派生的 constructor 必须调用 `super` 才能执行其父类(非派生的)的 constructor,否则 `this` 指向的那个对象将不会被创建。并且我们会收到一个报错。
因此,派生的 constructor 必须调用 `super` 才能执行其父类(base)的 constructor,否则 `this` 指向的那个对象将不会被创建。并且我们会收到一个报错。

为了让 `Rabbit` 的 constructor 可以工作,它需要在使用 `this` 之前调用 `super()`,就像下面这样:

Expand Down Expand Up @@ -279,6 +281,102 @@ alert(rabbit.earLength); // 10
```



### Overriding class fields: a tricky note

```warn header="Advanced note"
This note assumes you have a certain experience with classes, maybe in other programming languages.

It provides better insight into the language and also explains the behavior that might be a source of bugs (but not very often).

If you find it difficult to understand, just go on, continue reading, then return to it some time later.
```

We can override not only methods, but also class fields.

Although, there's a tricky behavior when we access an overridden field in parent constructor, quite different from most other programming languages.

Consider this example:

```js run
class Animal {
name = 'animal'

constructor() {
alert(this.name); // (*)
}
}

class Rabbit extends Animal {
name = 'rabbit';
}

new Animal(); // animal
*!*
new Rabbit(); // animal
*/!*
```

Here, class `Rabbit` extends `Animal` and overrides `name` field with its own value.

There's no own constructor in `Rabbit`, so `Animal` constructor is called.

What's interesting is that in both cases: `new Animal()` and `new Rabbit()`, the `alert` in the line `(*)` shows `animal`.

**In other words, parent constructor always uses its own field value, not the overridden one.**

What's odd about it?

If it's not clear yet, please compare with methods.

Here's the same code, but instead of `this.name` field we call `this.showName()` method:

```js run
class Animal {
showName() { // instead of this.name = 'animal'
alert('animal');
}

constructor() {
this.showName(); // instead of alert(this.name);
}
}

class Rabbit extends Animal {
showName() {
alert('rabbit');
}
}

new Animal(); // animal
*!*
new Rabbit(); // rabbit
*/!*
```

Please note: now the output is different.

And that's what we naturally expect. When the parent constructor is called in the derived class, it uses the overridden method.

...But for class fields it's not so. As said, the parent constructor always uses the parent field.

Why is there the difference?

Well, the reason is in the field initialization order. The class field is initialized:
- Before constructor for the base class (that doesn't extend anything),
- Imediately after `super()` for the derived class.

In our case, `Rabbit` is the derived class. There's no `constructor()` in it. As said previously, that's the same as if there was an empty constructor with only `super(...args)`.

So, `new Rabbit()` calls `super()`, thus executing the parent constructor, and (per the rule for derived classes) only after that its class fields are initialized. At the time of the parent constructor execution, there are no `Rabbit` class fields yet, that's why `Animal` fields are used.

This subtle difference between fields and methods is specific to JavaScript

Luckily, this behavior only reveals itself if an overridden field is used in the parent constructor. Then it may be difficult to understand what's going on, so we're explaining it here.

If it becomes a problem, one can fix it by using methods or getters/setters instead of fields.


## 深入:内部探究和 [[HomeObject]]

```warn header="进阶内容"
Expand Down
2 changes: 1 addition & 1 deletion 1-js/09-classes/07-mixins/article.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ let eventMixin = {
* menu.off('select', handler)
*/
off(eventName, handler) {
let handlers = this._eventHandlers && this._eventHandlers[eventName];
let handlers = this._eventHandlers?.[eventName];
if (!handlers) return;
for (let i = 0; i < handlers.length; i++) {
if (handlers[i] === handler) {
Expand Down
2 changes: 1 addition & 1 deletion 1-js/09-classes/07-mixins/head.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* menu.off('select', handler)
*/
off(eventName, handler) {
let handlers = this._eventHandlers && this._eventHandlers[eventName];
let handlers = this._eventHandlers?.[eventName];
if (!handlers) return;
for(let i = 0; i < handlers.length; i++) {
if (handlers[i] == handler) {
Expand Down
Loading