ES6基础系列 —— class的糖怎么吃

背景

原型前前后后大概分享过也不止3次了,面试的时候也经常在提,虽然老话常谈,也没什么好讲的东西

不过,还是要重点强调下,原型和闭包是JS中核心中的核心,这两个东西会玩,基本上日常类的需求、bug改造起来会驾轻就熟很多

先来几个栗子

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
const A = function () {};
A.prototype = {
num: 1,
text: 'aaa'
};

// 第一题
const x = new A();
console.log(x.num);
console.log(x.text);


const y = new A();
A.prototype = {
num: 2
};
// 第二题
console.log(y.num);
console.log(y.text);


y.num = 3;
const z = new A();
// 第三题
console.log(z.num);
console.log(z.text);

一张图描述ES 5 prototype与继承

引用值更改

1
2
3
4
5
6
7
8
9
10
function A () {}
A.prototype = {
obj: {
str: 'aaa'
}
}
var x = new A();
var z = new A();
x.obj.str = 'bbb';
console.log(z.obj.str);

会发现:obj.str 属性共享了,混乱了

一般推荐的正常做法是,属性放在实例上也就是构造函数上,每个实例各自创建,而方法放在原型上,用于继承与共享

ES 6 class 小例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A {
constructor() {
this.obj = {
str: 'aaa'
};
}
alert() {
alert(this.obj.str);
}
};
y = new A();
h = new A();
y.obj.str = 'bbb';

y.alert();
h.alert();

这里的obj.str 没有共享了,为什么?

ES 6 语法糖 extends、super

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
class B extends A {
constructor(props) {
super(props);
// super 必须在最开始
// 一方面的原因从下面这行也可以看出来
this.str2 = this.obj.str + 'bbb';
}

alertAndLog() {
super.alert();
console.log(this.str2);
}
}

// ------ 等价于 ------

// 构造函数
function B (props) {
A.call(this, props);
this.str2 = this.obj.str + 'bbb';
}
B.prototype = Object.create(A.prototype);
B.prototype.alertAndLog = function () {
A.prototype.alert();
console.log(this.str2);
};
  1. 上面class的写法是不是更一目了然?这大概就是ES 6 class 语法糖的意义
  2. 注意上面的 super 的区别,在constructor 中,它代表父类型的构造函数,在原型方法中,则代表父类型的原型
  3. ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。因此,如果有constructor 就必须有 super()

7. static 关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
class B extends A {
static console () {
console.log(this.str2);
}
}

var t = new B();
t.console(); // 是什么?

等价于
B.console = function () {
console.log(this.num);
};

8. 静态方法里使用 super 是怎么样的?

1
2
3
4
5
6
7
8
9
10
11
12
13
class C extends B {
constructor(props) {
super(props);
},
static consoleAndAlert() {
// 跟 constructor 功效一致
super.console();
alert(this.str2);
}
}

C.str2 = 'ccc';
C.consoleAndAlert();

9. 缺陷

没有 private 关键字

a. 使用ES6 Symbol模拟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const bar = Symbol('bar');
const snaf = Symbol('snaf');

export default class myClass{
// 公有方法
foo(baz) {
return this[bar](baz);
}

// 私有方法
[bar](baz) {
return this[snaf] = baz;
}
};

b. 使用闭包模拟

1
2
3
4
5
6
7
8
myClass = (()=> {
let num = 1;
return class A {
foo(num2) {
num += num2;
}
};
})();

c. ES7 提案 —— 用 # 号

1
2
3
4
5
6
class Point {
#x = 0;
constructor() {
#x; // 0
}
}

静态属性、属性没法在class外层中定义,ES7 提案

1
2
3
4
class A {
a = 10;
static a = 20;
}

10. 与Java的类继承区别

  1. JS 始终还是基于原型做的继承实现,基于的属性、闭包变量搜索
  2. 而像JAVA,更多是通过重新构建的形式完成继承,类和实例是不同的东西
  3. 类在定义完成后,运行时是不可动态添加属性
  4. 至于特性方面,JAVA有非常多的关键字,也是基于关键字的作用域实现各种各样的功能,抽象、多态、重写、重载
  5. 例如:public、protected、private、abstract、final

参考链接

ECMAScript 6 入门

JavaScript 原型系统的变迁,以及 ES6 class

Java 继承详解

JavaScript 原型和原型链的理解