从ES6复习javascript继承

近两年,随着ES6的普及,大家都用上了class作为javascript的继承实现,然而在浏览器端大多数情况下ES6代码是要转译到ES5甚至ES3版本的,否则可能你的目标浏览器并不能直接识别你的class语法。class写多了,可能原始的继承实现会有所遗忘,这里复习一下javascript的继承实现演变史。

在ES6之前javascript并没有正式的『类』概念,通过『构造函数』结合原型链来实现继承,我们一个个来看。

ES3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function Animal(color) {
this.color = color
}
Animal.prototype.walk = function () {
console.log('Animal in ' + this.color + ' is walking...')
}
function Dog() {
var args = Array.prototype.slice.call(arguments)
// 调用父类构造函数,继承父类的初始化行为
Animal.apply(this, args)
}
// 通过中间函数获取父类原型,使用中间函数是为了防止通过子类意外篡改父类
// 如果在Nodejs环境中, 此函数可以使用util模块的inherits函数代替,用法一致
function inherits(Sub, Parent) {
function Ctor() {}
Ctor.prototype = Parent.prototype
Sub.prototype = new Ctor()
Sub.prototype.constructor = Sub
}
inherits(Dog, Animal)
var dog = new Dog('white')
dog.walk()
console.log(dog)

输出:

ES5

ES5新增了Object.create方法,可使用给定的对象作为原型去创建一个新的对象,更适合用于继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function Animal(color) {
this.color = color
}
Animal.prototype.walk = function () {
console.log('Animal in ' + this.color + ' is walking...')
}
function Dog() {
var args = Array.prototype.slice.call(arguments)
// 调用父类构造函数,继承父类的初始化行为
Animal.apply(this, args)
}
// 使用ES5新增的Object.create方法
// 如果在Nodejs环境中, 此函数可以使用util模块的inherits函数代替,用法一致
function inherits(Sub, Parent) {
Sub.prototype = Object.create(Parent.prototype)
Sub.prototype.constructor = Sub
}
inherits(Dog, Animal)
var dog = new Dog('white')
dog.walk()
console.log(dog)

输出结果与上述ES3实现方式一致

ES6

使用class实现继承,更加正统,也更简洁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Animal {
constructor(color) {
this.color = color
}
walk() {
console.log(`Animal in ${this.color} is walking...`)
}
}
class Dog extends Animal {
constructor(...args) {
super(...args)
}
}
const dog = new Dog('red')
dog.walk()
console.log(dog)

输出:

这是基于chrome@63的打印结果,其已实现class。然而,如果你的web应用需要兼容ES5, 你可能需要使用babel来转译你的ES6代码。

babel转译class

通过https://babeljs.io/repl/ ,我们来看看上述代码经过babel转译后得到的是什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
'use strict';
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;
};
}();
function _possibleConstructorReturn(self, call) {
if (!self) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return call && (typeof call === "object" || typeof call === "function") ? call : self;
}
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Animal = function () {
function Animal(color) {
_classCallCheck(this, Animal);
this.color = color;
}
_createClass(Animal, [{
key: 'walk',
value: function walk() {
console.log('Animal in ' + this.color + ' is walking...');
}
}]);
return Animal;
}();
var Dog = function (_Animal) {
_inherits(Dog, _Animal);
function Dog() {
var _ref;
_classCallCheck(this, Dog);
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return _possibleConstructorReturn(this, (_ref = Dog.__proto__ || Object.getPrototypeOf(Dog)).call.apply(_ref, [this].concat(args)));
}
return Dog;
}(Animal);
var dog = new Dog('red');
dog.walk();
console.log(dog);

乍一看略吓人,难怪经过babel转译的代码体积那么大。
再仔细看看,使用了很多高阶函数、偏函数,对比之前的实现,其实babel的转译结果是非常谨慎的,处理了多种异常情况,兼容了多种场景,让开发者更容易提早发现错误。
执行此转译后的代码输出:

ES5版本的输出基本一致。

typescript转译class

同样都是转译,我们看一看typescript是如何处理class的。
通过https://www.typescriptlang.org/play ,得到如下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
var __extends = (this && this.__extends) || (function () {
var extendStatics = Object.setPrototypeOf ||
({
__proto__: []
}
instanceof Array && function (d, b) {
d.__proto__ = b;
}) ||
function (d, b) {
for (var p in b)
if (b.hasOwnProperty(p)) d[p] = b[p];
};
return function (d, b) {
extendStatics(d, b);
function __() {
this.constructor = d;
}
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var Animal = /** @class */ (function () {
function Animal(color) {
this.color = color;
}
Animal.prototype.walk = function () {
console.log("Animal in " + this.color + " is walking...");
};
return Animal;
}());
var Dog = /** @class */ (function (_super) {
__extends(Dog, _super);
function Dog() {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
return _super.apply(this, args) || this;
}
return Dog;
}(Animal));
var dog = new Dog('red');
dog.walk();
console.log(dog);

可以看到,typescript的版本更加贴近我们上述ES5的版本,而且似乎还兼容了ES3

总结

javascript是一门很灵活的『面向对象』语言,随着ECMAScript的完善(ES6, ES7, ES8…),这门语言会变得越来越优秀,其自身的缺陷逐渐被规避,甚至屏蔽。
近些年typescript的发展也比较迅速,可以适当关注。