大多数面向对象的编程语言都支持类和类继承的特性,而 JavaScript
却不支持这些特性,只能通过其他方法定义并关联多个相似的对象。
在 Es5
中,我们会利用原型来实现类似的类特性。
而在 Es6
中,正式定义了 JS
的 class
语法。
9-1.ES5中的类
在 Es5
中,我们可以通过构造函数和原型来模拟实现类的语法:
// 构造函数
function Factory (name) {
// 实例属性
this.name = name
}
// 原型方法
Factory.prototype.getName = function() {
return this.name
}
// 静态属性
Factory.time = new Date('1994-10-19')
// 静态方法
Factory.getTime = function() {
return this.time
}
// 定义实例
var p1 = new Factory('Tom')
var p2 = new Factory('Jerry')
console.log(p1.getName())
console.log(p2.getName())
// 调用静态方法
Factory.getTime()
9-2.Es6中的类
在 Es6
中新增了 class
语法。
Es6
中的 class
只是构造函数的一种语法糖。
在上一节中的构造函数,转化为 class
写法的话,如下:
class Factory {
constructor(name) {
// 实例属性
this.name = name
}
// 原型方法
getName() {
return this.name
}
// 静态属性
static time = 'time'
// 静态方法
static getTime() {
return this.time
}
}
9-3.类的声明与表达式
其实,Es6
中的类本身是一种语法糖。它的实现依然借助于构造函数。
那么,如同构造函数有声明和表达式之分,类也能够使用声明或者表达式形式。
9-3-1.类的声明
class Factory {
constructor (name) {
// 实例属性
this.name = name
}
// 原型方法
getName () {
return this.name
}
}
// class其实是function
console.log(typeof Factory) // function
constructor
指代的就是构造函数。它内部通常用来定义实例属性。
另外在 constructor
内部是可以接收到 arguments
的。
类的声明形式有一下特点:
- 不会存在声明作用域提升。这一点要与函数声明区分开来,因为函数声明在
JS
中会提升到文档顶部。 - 内部代码执行严格模式。即代码中存在
use strict
。 - 原型上的属性和方法,不可枚举。
- 每个类都有一个
[[Construct]]
的内部方法。通过关键字new
调用那些不含[[construct]]
的方法会导致程序抛出错误。 - 类只能使用
new
进行调用,否则会抛出错误。因为内部已经利用new.target
锁定。 - 在类中修改类名会导致程序报错。在外部可以修改类名。
根据上述特点,我们借助 Es5
来实现下 class
:
let Factory = (function () {
'use strict'
const Factory = function (name) {
if (new.target === undefined) {
throw new Error('Factory must be called by new')
}
this.name = name
}
Object.defineProperty(Factory.prototype, 'getName', {
value: function (name) {
return this.name
},
writable: true,
configable: true,
enumerable: false
})
return Factory
})()
9-3-2.类的表达式
类的表达式形式与函数的表达式形式很类似。
// 普通类表达式
let Factory = class {
constructor (name) {
this.name = name
}
getName () {
return this.name
}
}
console.log(typeof Factory) // function
// 命名类表达式
let Factory = class Factory2 {
constructor (name) {
this.name = name
}
getName () {
return this.name
}
}
console.log(typeof Factory) // function
console.log(typeof Factory2) // undefined
9-4.原型属性
我们通常在 constructor
里定义实例属性。
在原型上定义原型属性
class Factory {
constructor (name) {
this.name = name
}
getName () {
return this.name
}
}
此外,原型上也可以定义访问器属性。
class Factory {
constructor (name) {
this.name = name
}
getName () {
return this.name
}
get alias () {
return 'alias name'
}
set alias (value) {
this.name = value
}
}
var f = new Factory('Tom')
console.log(f.getName()) // Tom
console.log(f.alias) // alias name
f.alias = 'Jerry'
console.log(f.name) // Jerry
console.log(f.getName()) // Jerry
console.log(f.alias) // alias name
9-5.静态属性
静态属性可以用来挂载构造函数的属性/方法。
在 Es5
中实现静态属性的方式是:
function Factory () {
}
Factory.id = '1019'
Factory.getId = function () {
console.log(this.id)
}
console.log(Factory.getId()) // '1019'
而在 Es6
的 class
中提供了 static
关键字,能够更方便的定义静态属性:
class Factory {
constructor () {
}
static id = '1019'
static getId () {
return this.id
}
}
console.log(Factory.getId()) // '1019'
9-6.私有属性
我们一般声明私有属性时通常会以 _
符号为前缀,但这种形式其实只是一种隐性约定。并不能完全阻止外部对于私有属性的访问。
在 class
中提供了 #
符号。
以 #
为前缀的属性视为私有属性。外部无法访问。
class Factory {
// 私有属性其实也是挂载原型prototype上
#id = '1019'
#getId () {
return this.#id
}
getId () {
return this.#getId()
}
}
var f = new Factory()
console.log(f.getId()) // '1019'
console(f.#id) // Uncaught SyntaxError: Private field '#id' must be declared in an enclosing class
9-7.类的继承
关于 Js
的继承,这里有一章专门的详细介绍
本章大致记录下重点内容。
9-7-1.extends
在 Es5
中,实现继承最理想的方式是采用寄生组合式:
// Factory
function Factory (name) {
this.name = name
}
Factory.prototype.getName = function () {
return this.name
}
// People
function People (name) {
Factory.call(this, name)
}
People.prototype = Object.create(Factory.prototype, {
constructor: {
value: People,
writable: true,
enumerable: true,
configable: true
}
})
var p = new People('Tom')
console.log(p.name) // Tom
console.log(p.getName()) // Tom
console.log(p.__proto__.constructor) // People
在 Es6
中提供了,更加优雅的方式,利用 extends
关键字:
// Factory
class Factory {
constructor (name) {
this.name = name
}
getName () {
return this.name
}
}
// People
class People extends Factory {
constructor (name) {
// 这里必须调用super 用以继承父类的实例属性和静态属性
super(name)
}
}
var p = new People('Tom')
console.log(p.name) // Tom
console.log(p.getName()) // Tom
console.log(p.__proto__.constructor) // People
TIP
如果在 extends
中不调用 super
的话,会报错:
Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
9-7-2.super
super
关键字主要用于 class
的继承中。而 class
的继承依赖于 extends
关键字。
super
既能作为函数调用,又能作为对象使用。
super
的指向要根据实际情况判断,其内部绑定的 this
也是需要根据实际情况判断。
根据使用场景的不同,super
的指向也不同:
// class的继承,核心就在于 super 这个关键字
class SuperClass {
constructor(name = 'super-name') {
this.name = name
}
getName() {
return this.name
}
static time = 'super-time'
static getTime() {
return this.time
}
}
class SubClass extends SuperClass{
constructor(name = 'sub-name', age = 19, job = 'programmer') {
// ① super指向父类构造函数
super(name)
this.age = age
// ② super指向子类实例
super.job = job
}
getSubName() {
// ③ super指向父类原型,this会绑定当前子类实例 而不是父类实例
return super.getName() // 应该是 'sub-name' 而不是 'super-name'
}
static time = 'sub-time'
static getSubTime() {
// ④ super指向父类构造函数,this会绑定当前子类 而不是父类
return super.getTime()
}
}
const sub = new SubClass()
console.log(sub.job) // programmer
console.log(sub.getSubName()) // 'sub-name'
console.log(SubClass.getSubTime()) // 'sub-time'
// 子类的构造函数必须执行一次super函数
// super绑定子类的this
// Object.getPrototypeOf() 方法返回指定对象的原型(内部[[Prototype]]属性的值)