ES6中Class

587 阅读6分钟

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 调用构造函数后

  1. 创建一个对象
  2. 将该对象的 __prototype__指向构造函数的 prototype
  3. 将构造函数内部的 this 绑定到该对象
  4. 执行构造函数的内部逻辑
  5. 如果构造函数内部主动 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
})