dwqs / dwqs/blog

ES6 Class Methods 定义方式的差异

Open
#67 15 comments 20 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
3.8k
Forks
443
PR merge metrics
No merged PRs in 30d

Description

## 引言
在 JavaScript 中有两条不成文的说法:

* 一切皆对象
* 函数是一等公民

因而函数不仅是一等公民,也是具有属性的特殊对象。这一点,也可以从原型链上得到佐证:

```js
function t () {}

t.__proto__ === Function.prototype // true
t.__proto__.__proto__ === Object.prototype // true
```

函数是继承自 `Object` 的,因而函数也具备 `toStirng`、`valueOf` 等方法。因为函数是对象,所以在 ES6 之前,JavaScript 中的 OOP 编程则纯粹是基于函数的,直到 ES6 提供了 `class`、`super` 以及 `extends` 等关键字,不仅精简了语法,也使得 OOP 的编程形式逐渐趋近于 Java/C++ 等语言。

## class 的背后
ES6 虽然提供了 `class` 等关键字,但只是语法糖,JavaScript 的 OOP 编程仍然是基于函数的,继承则是基于原型的。

看一个示例:

```
class A {
print () {
console.log('print a');
}
}
```

上述代码经过 babel 转换之后:

```
var A = function () {
function A() {
_classCallCheck(this, A);
}

_createClass(A, [{
key: 'print',
value: function print() {
console.log('print a');
}
}]);

return A;
}();
```

可以看到,转换后的 class A 就是一个函数,所以理论上就可以把 A 当作函数调用,但 `_classCallCheck` 的作用就是禁止将类作为普通函数调用:

```
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}

A() // throw a error
const a = new A(); // work well
```

然后看下 `_createClass` 都做了什么:

```
var _createClass = function () {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
return function (Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);
if (staticProps) defineProperties(Constructor, staticProps); return Constructor;
};
}();
```

通过上述代码可知,`_createClass` 的功能主要是通过 `Object.defineProperty` 定义了类的普通属性和静态属性。需要注意的是**普通属性是定义在了类的原型对象上,静态属性是定义在了类本身上**。

所以,类 A 的定义就等同于如下代码:

```
function A () {}

A.prototype.print = function () {
console.log('print a');
}
```

## 两种定义 Methods 的方式
ES6 中有两种常见的定义 Methods 的方式:

```js
// 方式一
class A {
print () {
console.log('print a');
}
}

// 方式二
class B {
print = () => {
console.log('print b');
}
}

const a = new A()
a.print(); // print a

const b = new B()
b.print(); // print b
```

咋一看,二者没什么区别。方式一是常规方式,方式二是通过[箭头函数](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions)来定义方法,如果你写过 React 应用,应该接触过这种方式。

### 区别1:this 的绑定
在箭头函数出现之前,每个新定义的函数都有它自己的 `this` 值,但箭头函数不会创建自己的 `this`,它从会从自己的作用域链的上一层继承 `this`。举个粟子:

```
import React, { Component } from 'react';
class Test extends Component {
testClick () {
console.log('testClick', this);
}

render () {
return

Test

}
}
```

当点击 `div` 元素时,会触发 `testClick`,该方法会输出当前的 `this`,而(严格模式下)此时输出的 `this` 值是 `undefined`,显然这不是我们要的结果。怎么修改呢?这里至少有三种修改方式,其中之一就是通过箭头函数来定义方式。

### 区别2:继承
先看方式一的继承:

```js
class A {
print () {
console.log('print a');
}
}

class C extends A {
print () {
super.print();
console.log('print c');
}
}

const c = new C();
c.print();
// print a
// print c
```

对于上述结果的输出应该没有什么疑问,这是符合我们预期的。然后看下另一段代码:

```js
class B {
print = () => {
console.log('print b');
}
}

class D extends B {
print () {
super.print();
console.log('print d');
}
}

const d = new D();
d.print();
// ???
```

上述的输出会是什么呢?按照常规思路,应该是先输出 `print b`,再输出 `print d`,但其实不是的。

上文有提到,类的继承依然是基于原型的。上文也分析过 babel 转换过的代码,常规的写法中,类的非静态属性都是定义在类的原型对象上,而不是类的实例上的。但箭头函数不一样,通过箭头函数定义的方法时绑定在 `this` 上,而 `this` 是指向当前创建的类实例对象,而不是类的原型对象。可以查看类 B 转换后的代码:

```
var B = function B() {
_classCallCheck(this, B);

this.print = function () {
console.log('print b');
};
};
```

可以看到,`print` 方法是定义在 `this` 上的,而不是定义在 `B.prototype` 上。

类 `D` 继承 `B`,不仅会继承类 `B` 原型上的属性和方法,也会继承其实例上的属性和方法。那么,此时类 `D` 等效的伪代码如下:

```
function D () {
// 继承自 B
this.print = function () {
console.log('print b');
}
}

// 通过原型实现继承
D.__proto__ = B;
D.prototype.__proto__ === B.prototype;

D.prototype.print = function () {
// 类 D 自身定义的 print 方法
}
const d = new D();
d.print();
```

综上,当 `d.print()` 执行时,只会输出 `print b`,而不会输出 `print d`。

因为当访问一个对象实例的属性时,会先在实例上进行查找,如果没有,则顺着原型链往上查找,直到原型链的顶端。若在实例上查找到对应属性,则会返回,停止查找。即使原型上定义了同一个属性,该属性也不会被访问到,这种情况称为"属性遮蔽 (property shadowing)"。

<正文完>

## 相关资料
* [继承与原型链](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain)
* [箭头函数](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions)

Contributor guide

No contributing guide indexed for this repository

Research direction

The issue body is the only referenced source; no repository file, test, or concrete edit is identified. Start by reviewing the article and its linked MDN references, then clarify the intended corrections or publication change before editing; done should mean the agreed JavaScript explanations and examples are accurate.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
content, documentation
Issue type
Documentation
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
20/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.