Class与Class继承 🌔

ES5中,通过构造函数和原型对象实现了「类」,通过原型链实现了「类」的继承。在ES6中,新增了Class的语法,提供了更接近传统语言的写法。

如果你对构造函数、原型对象、原型链、继承等还有问题,可以去看我之前的文章📒 JavaScript高级程序设计第三版 💫
文章是对《JavaScript高级程序设计第三版》的详细批注,其中第6章详细说明了这些概念。

Class

和大多数面向对象的语言不同,JavaScript 在诞生之初并不支持类,也没有把类继承作为创建相似或关联的对象的主要的定义方式。所以从ES1ES5这段时期,很多库都创建了一些工具,让JavaScript看起来也能支持类。尽管一些JavaScript开发者仍强烈主张该语言不需要类,但在流行库中实现类已成趋势,ES6也顺势将其引入。但ES6 中的类和其他语言相比并不是完全等同的,目的是为了和JavaScript的动态特性相配合。

定义

通过class关键字,可以定义类。可以把class看做一个语法糖,一个在ES5上必须非常复杂才能完成的实现的封装。它使得定义一个类更加清晰明了,更符合面向对象编程的语法。

对比ES5

我们来对比一下:

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
// es5
function Person5 (name) {
this.age = 12
this.name = name
this.sayAge = function () {
return this.age
}
}
Person5.prototype.sayName = function () {
return this.name
}
let p1 = new Person5('zhu')
p1.age // 12
p1.sayName() // 'zhu'

// es6
class Person6 {
constructor (name) {
this.age = 12
this.name = name
this.sayAge = function () {
return this.age
}
}
sayName () {
return this.name
}
}

let p2 = new Person6('zhu')
p2.age // 12
p2.sayName() // 'zhu'

类的原型对象的方法(sayName),直接定义在类上即可。类的自有属性(name)在constructor方法里面定义。

两者相比,ES5更能说请ECMAScript通过prototype实现类的原理,ES6写法更加清晰规范。
而生成实例的方法还是一致的:通过new命令。因为,class只是类定义的语法糖。

原型对象的属性

至于类的原型对象的属性的定义,目前还在提案阶段

1
2
3
4
5
6
7
8
9
10
11
// es5
function Person5 () {}
Person5.prototype.shareSex = 'man'
let p1 = new Person5()
p1.shareSex // 'man'
// es6
class Person6 {
shareSex = 'man'
}
let p2 = new Person6()
p2.shareSex // 'man'

constructor

类的constructor方法的行为模式完全与ES5的构造函数一样(关于构造函数可以参考📒 JavaScript高级程序设计第三版 💫 第6.2.2章节)。如果未定义,会默认添加。以下两个定义是等效的。

1
2
3
4
5
6
class Person {}
class Person {
constructor () {
return this
}
}

表达式

上面的例子中,类的定义方式是声明式定义。与函数相似,类也有表达式定义的形式。

1
let Person = class {}

虽然使用了声明变量,但是类表达式并不会提升。所以,声明式声明和表达式式声明除了写法不同,完全等价。

如果两种形式同时使用,声明式定义的名称可作为内部名称使用,指向类本身。但不能在外部使用,会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let PersonMe = class Me {
constructor () {
Me.age = 12
}
sayAge () {
return Me.age
}
}
let p2 = new PersonMe()
p2.age // undefined
PersonMe.age // 12
p2.sayAge() // 12
Me.name // Uncaught ReferenceError: Me is not defined
PersonMe.name // Me

我们看到PersonMe.name的值是Me,而不是PersonMe。由此可知,变量PersonMe只是存储了一个执行Me这个类的指针。

而类名之所以可以在内部使用,是因为具名表达式实际是这样的:

1
2
3
4
5
6
7
8
9
let PersonMe = (function() {
const Me = function() {
Me.age = 12
}
Me.prototype.sayAge = function () {
return Me.age
}
return Me
})()

也可以使用类表达式立即调用,以创建单例。

1
2
3
4
5
6
7
8
let p1 = new class {
constructor (name) {
this.name = name
}
sayName () {
return this.name
}
}('zhu')

一级公民

在编程中,能被当做值来使用的就称为一级公民(first-class citizen)。这意味着它能做函数的参数、返回值、给变量赋值等。在ECMAScript中,函数是一级公民;在ES6中,类同样也是一级公民。

不可在内部重写类名

在类的内部,类名是使用const声明的,所以不能在类的内部重写类名。但是在类的外部可以,因为无论是声明还是表达式的形式,在定义类的上下文中,函数名都只是存储指向类对象的指针。

区别

new.target

new是从构造函数生成实例对象的命令。类必须使用new调用,否则会报错,这点与ES5中的构造函数不同。

1
2
3
4
5
6
7
8
9
10
11
12
// es5
function Person5 (name) {
return name
}
Person5('zhu') // zhu
// es6
class Person6 {
constructor (name) {
return name
}
}
Person6('zhu') // Uncaught TypeError: Class constructor Person6 cannot be invoked without 'new'

而这正是通过new命令在ES6中新增的target属性实现的。该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令调用的,new.target会返回undefined,反之会返回作用的类。

1
2
3
4
5
6
7
8
class Person6 {
constructor () {
console.log(new.target)
}
}

Person6() // undefined
new Person6() // Person6

值得注意的是,子类继承父类时,new.target会返回子类。

1
2
3
4
5
6
7
class Father {
constructor () {
console.log(new.target)
}
}
class Son extends Father {}
new Son() // Son

最后,我们使用new.targetES5中模拟一下ES6class的行为。

1
2
3
4
5
6
7
8
9
function Person5 () {
if(new.target === undefined) {
throw new TypeError("Class constructor Person6 cannot be invoked without 'new'")
}
console.log('success,', new.target === Person5)
}

Person5() // Uncaught TypeError: Class constructor Person6 cannot be invoked without 'new'
new Person5() // success, true

类的方法不可枚举

ES6中,在类上定义的方法,都是不可枚举的(non-enumerable)。在ES5中是可以的。

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
// es5
function Person5 (name) {
this.age = 12
this.name = name
}
Person5.prototype.sayName = function () {
return this.name
}

// es6
class Person6 {
constructor (name) {
this.age = 12
this.name = name
}
sayName () {
return this.name
}
}

Object.getOwnPropertyDescriptor(Person5.prototype, 'sayName').enumerable // true
Object.getOwnPropertyDescriptor(Person6.prototype, 'sayName').enumerable // false
Object.keys(Person5.prototype) // ['sayName']
Object.keys(Person6.prototype) // []
Object.getOwnPropertyNames(Person5.prototype) // ["constructor", "sayName"]
Object.getOwnPropertyNames(Person6.prototype) // ["constructor", "sayName"]

不存在变量提升

函数可以在当前作用域的任意位置定义,在任意位置调用。类不是函数,不存在变量提升。

1
2
3
4
5
6
// es5
new Person5()
function Person5 () {}
// es6
new Person6() // Uncaught ReferenceError: Person6 is not defined
class Person6 {}

内部方法不是构造函数

类的静态方法、实例的方法内部都没有[[Construct]]属性,也没有原型对象(没有prototype属性)。因此使用new来调用它们会抛出错误。

1
2
3
4
5
6
class Person6 {
sayHi () {
return 'hi'
}
}
new Person6.sayHi // Uncaught TypeError: Person6.sayHi is not a constructor

同样的,箭头函数(() => {}})也一样。

1
2
let Foo = () => {}
new Foo // Uncaught TypeError: Person6.sayHi is not a constructor

改进

严格模式

在类和模块的内部,默认开启了严格模式,也就是默认使用了use strict

动态方法名

ES6中,方法名可以动态命名。访问器属性也可以使用动态命名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let methodName1 = 'sayName'
let methodName2 = 'sayAge'
class Person {
constructor (name) {
this.name = name
}
[methodName1] () {
return this.name
}
get [methodName2] () {
return 24
}
}
let p1 = new Person('zhu')
p1.sayName() // zhu

访问器属性

ES5中,如果要将构造函数的自有属性设置成访问器属性,你要这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person5 () {
this._age = 12
Object.defineProperty(this, 'age', {
get: function () {
console.log('get')
return this._age
},
set: function (val) {
console.log('set')
this._age = val
}
})
}
let p1 = new Person5()
p1.age // get 12
p1.age = 15 // set
p1.age // get 15

ES6中我们有了更方便的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person6 {
constructor () {
this._age = 12
}
get age () {
console.log('get')
return this._age
}
set age (val) {
console.log('set')
this._age = val
}
}
let p2 = new Person6()
p2.age // get 12
p2.age = 15 // set
p2.age // get 15

静态属性和静态方法

类的静态属性和静态方法是定义在类上的,也可以说是定义在构造函数的。它们不能被实例对象继承,但是可以被子类继承。需要注意的是,静态属性如果是引用类型,子类继承的是指针。
ES6中,除了constructor方法,在类的其他方法名前面加上static关键字,就表示这是一个静态方法。

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
// es5
function Person5 () {}
Person5.age = 12
Person5.sayAge = function () {
return this.age
}
Person5.age // 12
Person5.sayAge() // 12
let p1 = new Person5()
p1.age // undefined
p1.sayAge // undefined
// 继承
Sub5.__proto__ = Person5
Sub5.age // 12
Sub5.sayAge() // 12
// es6
class Person6 {
static sayAge () {
return this.age
}
}
Person6.age = 12
Person6.age // 12
Person6.sayAge() // 12
let p2 = new Person5()
p2.age // undefined
p2.sayAge // undefined
// 继承
class Sub6 extends Person6 {}
Sub6.age // 12
Sub6.sayAge() // 12

需要注意的是,静态方法里面的this关键字,指向的是类,而不是实例。所以为了避免混淆,建议在静态方法中,直接使用类名。

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
class Person1 {
constructor (name) {
this.name = name
}
static getName () {
return this.name
}
getName () {
return this.name
}
}

let p1 = new Person1('zhu')
p1.getName() // 'zhu'
Person1.getName() // 'Person1'
class Person2 {
constructor (name) {
this.name = name
}
static getName () {
return Person2.name
}
getName () {
return this.name
}
}
let p2 = new Person2('zhu')
p2.getName() // 'zhu'
Person2.getName() // 'Person2'

从上面的实例中我们可以看到,静态方法与非静态方法是可以重名的。

ES6 明确规定,Class 内部只有静态方法,没有静态属性。所以,目前只能在Class外部定义(Person6.age = 12)。
但是,现在已经有了相应的提案

1
2
3
4
5
6
class Person6 {
static age = 12
static sayAge () {
return this.age
}
}

私有方法和私有属性

TODO

其他

this 的指向

Class上实例方法中的this,默认指向实例本身。但是使用解构赋值后,在函数指向时,作用域指向发生了改变,就有可能引起报错。虽说有解决的方法,但是还是尽量避免使用这种方式吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person6 {
constructor () {
this.name = 'zhu'
}
sayName () {
return this.name
}
}
let p1 = new Person6()
p1.sayName() // zhu
let { sayName } = p1
sayName() // Uncaught TypeError: Cannot read property 'name' of undefined
sayName.call(p1) // zhu

babel

最后,我们看一下classbabel中如何转换成ES6的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let methodName = 'sayName'
class Person {
constructor (name) {
this.name = name
this.age = 46
}
static create (name) {
return new Person(name)
}
sayAge () {
return this.age
}
[methodName] () {
return this.name
}
}

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
"use strict";

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

var methodName = "sayName";
var Person = (function() {
function Person(name) {
_classCallCheck(this, Person);
this.name = name;
this.age = 46;
}
Person.create = function create(name) {
return new Person(name);
};
Person.prototype.sayAge = function sayAge() {
return this.age;
};
Person.prototype[methodName] = function() {
return this.name;
};
return Person;
})();

_classCallCheck方法算是new.target的polyfill。

class继承

class继承主要就是添加了extends关键字,相比与class,extends不仅仅是语法糖,还实现了许多ES5无法实现的功能。也就是说,extends是无法完全降级到ES5的。比如,内置对象的继承

extends

Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
我们先来看下ES5的实现:

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
function Father5 (name) {
this.name = name
this.age = 46
}
Father5.prototype.sayName = function () {
return this.name
}
Father5.prototype.sayAge = function () {
return this.age
}

Father5.create = function (name) {
return new this(name)
}

function Son5 (name) {
Father5.call(this, name)
}
Son5.prototype = Object.create(Father5.prototype, {
constructor: {
value: Son5,
enumerable: true,
writable: true,
configurable: true
}
})
Son5.__proto__ = Father5

Son5.prototype.setAge = function (age) {
this.age = age
}

var s1 = Son5.create('zhu')
s1.constructor // Son5
s1.sayName() // 'zhu'
s1.sayAge() // 46
s1.setAge(12)
s1.sayAge() // 12

我们使用classextends 来实现:

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
let Father6 = class Me {
constructor (name) {
this.name = name
this.age = 46
}
static create (name) {
return new Me(name)
}
sayName () {
return this.name
}
sayAge () {
return this.age
}
}

let Son6 = class Me extends Father6 {
constructor (name) {
super(name)
}
setAge (age) {
this.age = age
}
}

let s2 = Son6.create('sang')
s2.constructor // Son6
s2.sayName() // 'sang'
s2.sayAge() // 46
s2.setAge(13)
s2.sayAge() // 13

我们看到extendssuper(name)做了三件事:实例属性继承,原型对象继承,自有属性继承。接下来,我们就来说说super

super

在子类中,如果定义了constructor,则必须在第一行调用super。因为super对子类的this进行了封装,使之继承了父类的属性和方法。
如果在super调用之前使用this,会报错。

1
2
3
4
5
6
7
class Son extends Father {
constructor (name) {
this.name = name // Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
super(name)
this.name = name // 正常执行
}
}

如果没有定义constructor,则会默认添加。

1
2
3
4
5
6
7
class Son extends Father {}
// 等同于
class Son extends Father {
constructor (..arg) {
super(..arg)
}
}

super关键字必须作为一个函数或者一个对象使用,如果作为值使用会报错。

1
2
3
4
5
6
class Son extends Father{
constructor (name) {
super(name)
console.log(super) // Uncaught SyntaxError: 'super' keyword unexpected here
}
}

作为函数调用时,只能在子类的constructor函数中,否则也会报错。

作为对象使用时,在普通方法中,指向的是原父类的原型对象;在静态方法中,指向的是父类本身。

最后,由于对象总是继承其他对象的,所以可以在任意一个对象中,使用super关键字

1
2
3
4
5
6
7
var obj = {
toString() {
return "MyObject: " + super.toString();
}
};

obj.toString(); // MyObject: [object Object]

唯一在constructor中可以不调用super的情况是,constructor显式的返回了一个对象。
不过,这种写法好像没什么意义。

__proto__

__proto__的指向

1
2
3
class Father extends Function {}
class Son extends Father {}
let s1 = new Son()

先说几个定义:

  1. 实例对象的__proto__属性指向类的原型对象。
  2. 类的__proto__属性指向它的父类, prototype指向它的原型对象。
  3. 子类的原型对象的__proto__指向父类的原型对象。
  4. 对象一定是实例,实例不一定是对象。

我们开始验证:

  • s1Son 的实例对象。
  • SonFather 的子类。
1
2
3
s1.__proto__ === Son.prototype // true
Son.__proto__ === Father // ture
Son.prototype.__proto__ === Father.prototype // true

第1,2,3条都得到了验证。

我们继续顺着原型链往下走:

  • FatherFunction 的子类
1
2
Father.__proto__ === Function // true
Father.prototype.__proto__ === Function.prototype // true

第2,3条都得到了验证。

我们知道所有的函数或者类都是原先构造函数Function的实例。所以:

1
2
Function.__proto__ === Function.prototype // true
typeof Function.prototype // 'function'

第1,4条得到了印证。同时,Function.prototype是函数,我们也可以说Function.prototype是所有函数的父类。

我们知道所有对象都是原先构造函数Object的实例,所以:

1
Function.prototype.__proto__ === Object.prototype // true

所有的原型对象都继承自Object.prototype。所以:

1
Object.prototype.__proto__ === null // true

鸡生蛋,蛋生鸡

1
2
Object instanceof Function // true
Function instanceof Object // true

我们看一下instanceof的定义:instanceof运算符用于测试构造函数的prototype属性是否出现在对象的原型链中的任何位置

Object本身是构造函数,继承了Function.prototype;

1
Object.__proto__ === Function.prototype

Function也是对象,继承了Object.prototype

1
Function.__proto__.__proto__ === Object.prototype

所以谁先存在的呢?

1
2
3
4
5
6
7
8
9
10
11
// 确定Object.prototype是原型链的顶端
Object.prototype.__proto__ === null // true
// 确定Function.prototype继承自Object.prototype
Function.prototype.__proto__ === Object.prototype // true
// 确定所有的原生构造函数继承自Function.prototype
Function.__proto__ === Function.prototype // true
Object.__proto__ === Function.prototype // true
Array.__proto__ === Function.prototype // true
String.__proto__ === Function.prototype // true
Number.__proto__ === Function.prototype // true
Boolean.__proto__ === Function.prototype // true

Object.prototype只是一个指针,它指向一个对象(就叫它protoObj吧)。protoObj是浏览器最先创建的对象,这个时候Object.prototype还没有指向它,因为Object还没有被创建。然后根据protoObj创建了另一个即使函数又是对象的funProConstructor,也就是Function.prototype指向的内存地址(是的,Function.prototype也是一个指针),但是现在它们还没有建立关系,Function.prototype还没有指向funProConstructor。再然后,浏览器使用funProConstructor构造函数,创建出了我们熟悉的原生构造函数ObjectFunction等等,所以这些原生构造函数的__proto__属性指向了它们的父类Function.prototype,而这时候,创建出来的ObjectFunction上面的Object.prototypeFunction.prototype也分别指向了protoObjfunProConstructor。自此,浏览器内部原型相关的内容初始化完毕。

我们将上面的描述整理如下:

解开所有疑惑的关键都在这么一句话:Function.prototype是个不同于一般函数(对象)的函数(对象)。

gettersetter

ES6中,读取和修改原型推荐使用:Object.getPrototypeOfObject.setPrototypeOf

没有继承

ECMAScript中,我们会经常使用字面量去「构造」一个基本类型数据。这其实是使用new命令构造一个实例的语法糖。这往往让我们误以为在ECMAScript中,一切函数都是构造函数,而一切对象都是这些构造函数的实例,而ECMAScript也是一门面向对象的语言。

1
2
3
4
5
// 引用类型
var obj = {} // var obj = new Object()
var arr = [] // var arr = new Array()
// 值类型
var str = "" // var strObj = new String();var str = strObj.valueOf()

ECMAScript并不是纯粹的面向对象语言,它里面也有函数式编程的东西。所以,并不是每个函数都有原型对象,都有constructor

比如原生构造函数的原型对象上面的方法(如Array.prototype.concatNumber.prototype.toFixed)都是没有prototype属性的。还有,箭头函数也是没有prototype属性的。所以,这些函数是不能是用new命令的,如果用了会抛错。

1
new Array.prototype.concat() // Uncaught TypeError: Array.prototype.concat is not a constructor

这些没有prototype属性的方法,是函数式编程的实现,看起来也更纯粹。使用这些方法时,也建议使用lambda的链式语法。

表达式继承

extends后面能接受任意类型的表达式,这带来了巨大的可能性。例如,动态的决定父类。

1
2
3
4
5
6
7
8
9
10
11
12
class FatherA {}
class FatherB {}
const type = 'A'
function select (type) {
return type === 'A' ? FatherA : FatehrB
}
class Son extends select('A') {
constructor () {
super()
}
}
Object.getPrototypeOf(Son) === FatherA // true

如果,想要一个子类同时继承多个对象的方法呢?我们也可以使用mixin

Mixin

Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。

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
const objA = {
sayA() {
return 'A'
}
}
const objB = {
sayB() {
return 'B'
}
}
const objC = {
sayC() {
return 'C'
}
}
function mixin (...args) {
const base = function () {}
Object.assign(base.prototype, ...args)
return base
}
class Son extends mixin(objA, objB, objC) {}
let s1 = new Son()
s1.sayA() // 'A'
s1.sayB() // 'B'
s1.sayC() // 'C'

当然,我们也可以让一个子类继承多个父类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function mix(...mixins) {
class Mix {}

for (let mixin of mixins) {
copyProperties(Mix.prototype, mixin); // 拷贝实例属性
copyProperties(Mix.prototype, Reflect.getPrototypeOf(mixin)); // 拷贝原型属性
}

return Mix;
}

function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if ( key !== "constructor"
&& key !== "prototype"
&& key !== "name"
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}

内置对象的继承

ES5及之前,无法通过继承机制来继承内置对象的某些特性。我们以试图创建一个特殊数组为例:

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
// es5
// Array 的特性
var colors = []
colors[0] = 'red'
// length 跟着改变
colors.length // 1
// 改变数组的length
colors.length = 0
colors[0] // undefined

// 试图使用ES5的方式继承
function MyArray () {
Array.apply(this)
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
}
})

var colors = new MyArray()
colors[0] = 'red'
// length 没有跟着改变
colors.length // 0
// 改变数组的length
colors.length = 0
colors[0] // 'red'

结果并不尽如人意,我们继续使用ES6的继承:

1
2
3
4
5
6
class MyArray extends Array {}
let colors = new MyArray()
colors[0] = 'red'
colors.length // 1
colors.length = 0
colors[0] // undefined

与我们的预期完全一致。所以,ES5ES6中对内置对象的继承还是有区别的。

ES5中,this的值是被MyArray函数创建的,也就是说this的值其实是MyArray的实例,然后Array.apply(this)被调用,this上面又被添加了Array上面一些附加的方法和属性,而内置的属性和方法并没有被添加到this上。

而在ES6中,this的值会先被Array创建(super()),然后才会把MyArray的上面的附加属性和方法添加上去。

基于此,我们可以通过继承内置对象实现更多更利于我们自己使用的「超级内置对象」。

注意

继承Object的子类,有一个行为差异

1
2
3
4
5
6
7
class NewObj extends Object{
constructor(){
super(...arguments);
}
}
var o = new NewObj({attr: true});
o.attr // undefined

上面代码中,NewObj继承了Object,但是无法通过super方法向父类Object传参。这是因为 ES6 改变了Object构造函数的行为,一旦发现Object方法不是通过new Object()这种形式调用,ES6 规定Object构造函数会忽略参数。

Symbol.species

TODO

babel

我们将以下ES6的代码,在babel中转换为ES5的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let Father6 = class Me {
constructor (name) {
this.name = name
this.age = 46
}
static create (name) {
return new Me(name)
}
sayName () {
return this.name
}
sayAge () {
return this.age
}
}
let Son6 = class Me extends Father6 {
constructor (name) {
super(name)
}
setAge (age) {
this.age = age
}
}

转换后的代码:

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
"use strict";

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 Father6 = (function() {
function Me(name) {
_classCallCheck(this, Me);

this.name = name;
this.age = 46;
}

Me.create = function create(name) {
return new Me(name);
};

Me.prototype.sayName = function sayName() {
return this.name;
};

Me.prototype.sayAge = function sayAge() {
return this.age;
};

return Me;
})();

var Son6 = (function(_Father) {
_inherits(Me, _Father);

function Me(name) {
_classCallCheck(this, Me);

return _possibleConstructorReturn(this, _Father.call(this, name));
}

Me.prototype.setAge = function setAge(age) {
this.age = age;
};

return Me;
})(Father6);

babel定义了三个有趣的方法:

  1. _classCallCheck用于判断类是否被new命令符调用,是new.target的polyfill;
  2. _inherits用于子类继承父类的原型对象和静态方法,
  3. _possibleConstructorReturn用于继承自有属性。这个方法里面有个很有意思的判断,如果构造函数的返回是object或者function,就把这个返回值作为子类的实例,反之,返回子类的实例。这是为了降级解决ES5中无法继承内置对象的问题,因为内置对象默认都会返回对应的实例,而我们自定义的构造函数一般是不会写返回值的。
    这样我们在ES5中如果要继承内置对象,就不能给子类添加自定义的方法和属性了,因为返回的是内置对象的实例。

总结

ES6的类让JS中的继承变得更简单,因此对于你已从其他语言学习到的类知识,你无须将其丢弃。ES6的类起初是作为ES5传统继承模型的语法糖,但添加了许多特性来減少错误。

ES6的类配合原型继承来工作,在类的原型上定义了非静态的方法,而静态的方法最终则被绑定在类构造器自身上。类的所有方法初始都是不可枚举的,这更契合了内置对象的行为, 后者的方法默认情况下通常都不可枚举。此外,类构造器被调用时不能缺少new ,确保了不能意外地将类作为函数来调用。

基于类的继承允许你从另一个类、函数或表达式上派生新的类。这种能力意味着你可以调用一个函数来判断需要继承的正确基类,也允许你使用混入或其他不同的组合模式来创建一个新类。新的继承方式让继承内置对象(例如数组)也变为可能,并且其工作符合预期。

你可以在类构造器内部使用new.target ,以便根据类如何被调用来做出不同的行为。最常用的就是创建一个抽象基类,直接实例化它会抛出错误,但它仍然允许被其他类所继承。

总之,类是JS的一项新特性,它提供了更简洁的语法与更好的功能,通过安全一致的方式来自定义一个对象类型。

参考

  1. Class 的基本语法
  2. Class 的继承
  3. understandinges6
  4. proto和prototype来深入理解JS对象和原型链
  5. javascript-functions-without-prototype