class是ES6中新增的一个概念,叫做“类”,在JavaScript这种万物皆对象的语言中,这里的类可能并不等同于其他面向对象语言中的类,而实际上 class 更多的可以看成是一个语法糖(但并不完全是), 它的功能,很多都可以用ES5去模拟,babel插件再熟悉不过,可以将我们class的写法转换成ES5的写法。
这篇文章,更多的是从ES5 与 ES6 对比的方式,去整理一些内容。学习一个东西的目的还是为了生产应用,ES5的原型、原型链的内容基本已经很熟了,通过这种对比的方式学习 class,对其的理解更纯粹一点,后续写代码时用起来也会更得心应手。
class
先来看一个例子🌰
class A {
constructor(name, favor) {
this.tag = 'A'
this.lowName = 'aaa'
}
sayA() {
console.log(this.name)
}
}
class B extends A {
constructor() {
super()
this.lowName = 'bbb'
}
sayB(){
console.log(this.name)
}
}
const b = new B()
console.log(b)
不做单个的打印了,直接控制台看一下new出的实例
分析生成的实例,如果使用ES5实现上述功能
function A() {
this.tag = 'A'
this.lowName = 'aaa'
}
A.prototype.sayA = function() {
console.log(this.name)
}
function B() {
A.call(this)
this.lowName = 'bbb'
}
B.prototype.sayB = function() {
console.log(this.name)
}
B.prototype.__proto__ = A.prototype
const b = new B()
console.log(b)
B.prototype = A.prototype 是错误写法,这样会导致A的原型对象被重写,也就不存在A与B的继承的关系了
super
class中的子类继承父类, super 须显式地在constructor 中调用一次,否则会报错,并且 只有在调用super之后,才可以使用this关键字,否则会报错。
这就说明,使用new 实例化对象的流程,ES5中构造函数写法与ES6中的class的过程不太一致。
探讨继承前,可以回顾一下 实例化对象时,new 以后发生了什么
new 调用构造函数后
- 创建一个对象
- 将该对象的 __prototype__指向构造函数的 prototype
- 将构造函数内部的 this 绑定到该对象
- 执行构造函数的内部逻辑
- 如果构造函数内部主动 return 一个对象,则返回 return 的对象(无第2步的继承关系,也就是实例的 __proto__并不指向构造函数的prototype,而是取决于return 的对象的__proto__),如果没有return或者return 的不是对象,则返回前几步创建的对象。
第五点,不管是ES5的new 构造函数还是ES6的class,如果在构造函数中 return 对象,他们的表现是一致的。
明确了这个流程后,然后去分析基类派生类之间是如何实现继承的。
上述ES5的写法中 ,我们可以通过强制绑定的方式(apply、call),在子类的构造函数中显示的调用父类的构造函数,来继承独享父类的属性。不管在构造函数的最开始还是最后的位置调用父类的构造函数,都是可以正常执行,我们仅需要根据需求考虑属性覆盖的问题。
ES6的class中,this绑定的实例对象,是在基类中创建好的,然后通过 super()方法,向下层层传递,直到最下层的派生类(即使在派生类中省略了constructor 方法,也会自动添加 constructor方法,执行super())。
因为二者实现方式的不同,最明显的区别在于,在非最下层派生类的构造函数中 return 对象时,上述ES5的写法和 ES6class 二者的表现并不相同
ES5
function A() {
this.tag = 'A'
this.lowName = 'aaa'
}
A.prototype.sayA = function() {
console.log(this.name)
}
function B() {
A.call(this)
this.lowName = 'bbb'
return {
b: 'bbb'
}
}
B.prototype.sayB = function() {
console.log(this.name)
}
B.prototype.__proto__ = A.prototype
function C() {
B.call(this)
this.lowName = 'ccc'
}
C.prototype.sayC = function() {
console.log(this.name)
}
C.prototype.__proto__ = B.prototype
var c = new C()
console.log(c)
ES6
class A {
constructor() {
this.tag = 'A'
this.lowName = 'aaa'
}
sayA() {
console.log(this.name)
}
}
class B extends A {
constructor() {
super()
this.lowName = 'bbb'
return {
b: 'bbb'
}
}
sayB(){
console.log(this.name)
}
}
class C extends B {
constructor() {
super()
this.lowName = 'ccc'
}
sayC() {
console.log(this.name)
}
}
const c = new C()
console.log(c)
因为 B 构造函数中手动更改了由 super 向下传递的实例对象,导致包含继承关系的实例对象被替换,这也说明了 正常 new 调用链中,this绑定的实例对象的 __proto__ 指向的 prototype 所属的类,是由 super 向上传递的。也就是说,在 this 绑定的实例对象通过 super向下传递前,已经完成了 this = Object.create(C.prototype)这一步。ES6引入的 new.target属性,实际上就是这里的 C ,正是这个向上传递的类
class A {
...
}
class B extends A {
constructor() {
super()
console.log(new.target === C )
...
}
}
class C extends B {
...
}
const c = new C()
//true
上述示例代码中 ES5写法和ES6 class 写法表现不一致,是因为模拟class时,仅通过结果去模拟了class,并没有在行为层面进行模拟,对于一些非示例中的行为,就会表现出差异。在babel中二者的表现基本是一致的,且比较完全的模拟了 class 的行为,下面我提炼了一段基本的代码
function _createSuper(Derived) {
var Super = Derived.__proto__
var result
result = Super.apply(this)
return typeof this === "object" ? this : result
}
function A() {
this.tag = 'A'
this.lowName = 'aaa'
}
function B() {
var _this
_this = _createSuper.call(this,B)
_this.lowName = 'bbb'
return _this
}
B.prototype.__proto__ = A.prototype
B.__proto__ = A
function C() {
var _this
_this = _createSuper.call(this,C)
_this.lowName = 'ccc'
return _this
}
C.prototype.__proto__ = B.prototype
C.__proto__ = B
var c = new C()
最终 C 构造函数中 this 绑定的对象取决于递归通过 call 调用父类的构造函数,执行完毕后的返回。
C.__proto__ = B ,也是为了与 class 的行为保持一致,class中子类的__proto__指向父类。
关于super的用法,可以直接看一下MDN上对于super 的介绍
super([arguments]);
// 调用 父对象/父类 的构造函数
super.functionOnParent([arguments]);
// 调用 父对象/父类 上的方法
super不仅用于传递this需要绑定的实例对象,而且可以通过super 访问父类的 prototype对象。
静态方法
相比于ES5的写法,class中新增了 static 关键字,用来定义一个静态方法。
class A {
constructor(name, favor) {
this.tag = 'A'
this.lowName = 'aaa'
}
sayA() {
console.log(this.name)
}
static sayTagName() {
console.log('my tag is A')
}
}
class B extends A {
constructor() {
super()
this.lowName = 'bbb'
}
sayB() {
console.log(this.name)
}
}
const b = new B()
console.log(b)
static 关键字定义的方法不会被实例继承,可以通过类来调用。
可以逐一分析这两点的意义
- 不会被实例继承:不存在于实例以及实例的原型
- 可以通过类调用:可以在类的prototype 的 constructor 属性下找到该方法,并调用。
其实已经很清楚了,可以打印看一下
b.sayTagName() // Uncaught TypeError: b.sayTagNameis not a function
b.__proto__.constructor.sayTagName() // 'my tag is A'
A.prototype.constructor.sayTagName() // 'my tag is A'
A.prototype.constructor.hasOwnProperty('sayTagName') // true
需要注意的是,static 定义的静态方法内部使用 this ,实际上 this 指向的是这个类(可以通过 类的原型对象的 constructor 属性来访问)
class A {
constructor(name, favor) {
this.tag = 'A'
this.lowName = 'aaa'
}
sayA() {
console.log(this.lowName)
}
static sayTagName() {
console.log('my tag is A')
}
}
A.sayTagName() // 'my tag is A'
A.prototype.constructor.sayTagName() // 'my tag is A'
A === A.prototype.constructor // true
所以,class内部通过 static 定义的静态方法,可以使用this在方法内部互相调用。如果需要通过this 访问使用 new 实例后的属性和方法,则需要使用 bind进行绑定作用域。
而 static 部分开始时的示例代码,ES5的写法就很简单了,只需要在原先的基础上,加一行
A.sayTagName = function() {
console.log('my tag is A')
}
/*
或者
A.prototype.constructor.sayTagName = function() {
console.log('my tag is A')
}
*/
babel中是通过 Object.defineProperty() 实现的
Object.defineProperty(A, 'sayTagName', {
value: function sayTagName() { console.log('my tag is A') },
enumerable: false,
writable: true
})