Javascript ES6 class

ES6的 class 大部分功能都可以通过ES5来实现,所以也就有ES6的 class 只是语法糖的说法,但也有一些认为并不是所有 class 的特性都可以被es5实现,也就认为并不是是语法糖了。

这里不纠结语法糖的概念,只是用es5去实现ES6 class 的一些特性,主要参考 babelBublé 的实现。

寄生组合式继承

先回忆下 寄生组合式继承 ,这个是红皮书里面在介绍OOP继承时最后最完美的继承方法,其它的继承方法名字也不记得了。

抄来的代码如下:

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
// 继承原型的方法
function inheritPrototype(SubType, SuperType){
// 设置派生类的原型为基类原型的副本
// 并修正原型的 constructor 属性指向到派生类
SubType.prototype= Object.create(SuperType.prototype);
SubType.prototype.constructor=SubType;
}

// 基类
function Person(name){
this.name=name;
}
Person.prototype.sayName= function() {
console.log(this.name + ' ' + this.gender +' '+this.age);
}

// 派生类
function Child(name, gender, age){
Person.call(this, name); // 执行基类的构造函数,获取到基类构造函数中的 实例方法/属性
this.age=age;
this.gender=gender;
}

// 继承
inheritPrototype(Child, Person);
Child.prototype.sayAge=function() {
console.log(this.name + ' '+this.age);
}

// 实例化派生类 男
var child = new Child('Liu.Jun','male', 18);
child.sayName(); // Liu.Jun male 19
child.sayAge(); // Liu.Jun 19

原型链和继承

聊到ES5的继承感觉绕不开这里,那在顺带回忆下。

  • 原型链这个只做一个简单的图。
flowchart LR

subgraph 派生类
Fn(Fn - 派生类)-- prototype --> prototype(Fn.prototype - 派生类原型)
prototype -- constructor --> Fn
end

subgraph 基类
BaseFn(BaseFn - 基类) -- prototype --> basePrototype(BaseFn.prototype - 基类原型)
basePrototype -- constructor --> BaseFn
end

subgraph Object类
Object(Object) -- prototype --> objectPrototype(Object.prototype)
objectPrototype -- constructor --> Object
end

prototype -- __proto__ --> basePrototype
basePrototype -- __proto__ --> objectPrototype
objectPrototype -- __proto__ --> null

instance(instance - new Fn) -- __proto__ --> prototype

一些描述:

  • 构造函数 prototype 构造函数原型属性
  • prototype constructor 指向它的构造函数
  • 实例 __proto__ 原型,指向其构造函数的 prototype 原型属性
  • 函数 prototype __proto__ 属性指向到 Object (箭头函数除外)
  • Object prototype __proto__ 属性指向到 null

取值:

  • 读取一个实例的属性,如果找不到,就会查找其原型 (__proto__) 中的属性,找不到再继续查找原型的原型。
  • 一个实例的原型也是另一个类的实例那就构成了上图中的模型,也就是 原型链 的基本概念。

再回去看 寄生组合式继承 的代码实现就是这里的图

New发生了啥?

  1. 创建一个空对象 {}
  2. 对象 __proto__ 属性指向构造函数的原型
  3. 执行构造函数,传入对象作为执行上下文
  4. 如果执行结果是简单类型就直接对象,否则返回执行结果

参见如下方法:newObjectFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Person(name) {
this.name=name;
}

Person.prototype.sayName= function() {
console.log(this.name + ' ' + this.gender +' '+this.age);
}

// new
function newObjectFactory() {
const obj = {};
const Constructor = [].shift.call(arguments);
obj.__proto__ = Constructor.prototype;

const res = Constructor.apply(obj, arguments);

return typeof res === 'object' ? res : obj;
}

console.log(newObjectFactory(Person, 'name'));
console.log(new Person('name'));

ES6 class

还是如上的例子,ES6 class 继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}

class Child extends Person {
constructor(name, gender, age) {
super(name);
this.name = name;
this.age = age;
this.gender = gender;
}
sayAge() {
console.log(this.name + ' ' + this.age);
}
}

const child = new Child('Liu.Jun','male', 18);
child.sayName(); // Liu.Jun male 19
child.sayAge(); // Liu.Jun 19

Bublé 转换后:

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
var Person = function Person(name) {
this.name = name;
};

Person.prototype.sayName = function sayName() {
console.log(this.name);
};

var Child = (function (Person) {
function Child(name, gender, age) {
Person.call(this, name);
this.name = name;
this.age = age;
this.gender = gender;
}

if (Person) Child.__proto__ = Person;
Child.prototype = Object.create(Person && Person.prototype);
Child.prototype.constructor = Child;
Child.prototype.sayAge = function sayAge() {
console.log(this.name + ' ' + this.age);
};

return Child;
}(Person));

var child = new Child('Liu.Jun', 'male', 18);
child.sayName(); // Liu.Jun male 19
child.sayAge(); // Liu.Jun 19

这里转换也是使用的 寄生组合式继承 ,区别就是会让派生类的 __proto__ 指向到基类保持和 class 一致, Child.__proto__ = Person,类似于 babelloose 模式转换。

这里是因为Class的继承同时存在两条继承链

  1. 派生类的 __proto__ 属性,表示构造函数的继承,总是指向基类。
  2. 派生类prototype属性的 __proto__ 属性,表示方法的继承,总是指向基类的 prototype 属性。
    也就是如下代码
    1
    2
    Child.__proto__ = Person;
    Child.prototype = Object.create(Person && Person.prototype);

区别

ES5实现和ES6 Class 有一个本质的区别

  • ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面(Parent.call(this))。
  • ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到 this 上面(所以必须先调用 super 方法),然后再用子类的构造函数修改 this
  • 这里也就是常说的 ES6子类this

参考