ES5中的“类”
function Person(name) {
this.name = name
}
Person.prototype.sayName = function() {
console.log(this.name)
}
var p = new Person('wmui')
p.sayName() // wmui
console.log(p instanceof Person) // true
console.log(p instanceof Object) // true
ES5中没有类的概念,要想实现和类相似的功能,通常是创建一个构造函数,然后把类方法赋值给构造函数的原型。许多模拟类的JS库都是基于这个模式进行开发。
类的声明
在ES6中可以用class关键字声明一个类,关键字后面紧跟着类的名字,其他部分语法类似对象字面量,但是各元素之间不需要逗号分隔
class Person {
constructor(name) {
this.name = name
}
sayName() {
console.log(this.name)
}
}
let p = new Person('wmui')
p.sayName()
console.log(p instanceof Person) // true
console.log(p instanceof Object) // true
通过这种方式定义类和前面的直接使用构造函数定义类的过程相似,只不过这里是使用特殊的constructor方法来定义构造函数
两者差异
类声明与自定义类型虽然很相似,但是还是有差异的
- 类声明不会出现函数声明提升,不能被提升到执行语句前
- 类声明中的所有语句自动运行在严格模式下,且无法强行脱离严格模式执行
- 类中所有方法都是不可枚举到的,而自定义类型中,需要通过Object.defineProperty()方法手工指定某个方法为不可枚举
- 每个类都有一个名为[[Construct]]的内部方法,通过关键字new调用那些不含[[Construct]]的方法会导致程序抛出错误
- 使用new关键字以外的其他方式调用类的构造函数,会导致程序报错
- 在类中修改类名会导致程序报错
了解了两种方式的差异,现在可以不用类语法编写和类等价的代码
let Person = (function(){
'use strict';
const Person = function(name) {
// 确认函数是通过new关键字被调用
if(typeof new.target === 'undefined') {
throw new Error("Constructor must be called with new.");
}
this.name = name;
}
Object.defineProperty(Person.prototype, 'sayName', {
value: function() {
// 确认函数被调用时没有使用 new
if (typeof new.target !== 'undefined') {
throw new Error('Method cannot be called with new.');
}
console.log(this.name);
},
enumerable: false,
writable: true,
configurable: true
})
return Person;
}())
let p = new Person('wmui');
p.sayName() // wmui
尽管可以在不使用new语法的前提下实现类的所有功能,但如此一来代码变得极为复杂
常量类名
类的名称只在类中为常量,所以尽管不能在类的方法中修改类名,但可以在外部修改
class Person {
constructor() {
Person = 'People' // 执行时报错
}
}
Person = 'People' // 不报错
内部的Foo就像是通过const声明的,修改它的值会导致程序抛出错误;而外部的Foo就像是通过let声明的,可以随时修改这个绑定值
类表达式
类和函数都有两种存在的形式:声明形式和表达式形式。
声明形式的函数和类都由相应的关键字(函数是function,类是class)进行定义,后面紧跟着一个标识符;表达式形式的函数和类与之类似,只是不需要在关键字后添加标识符。
类表达式的设计初衷是为了声明相应变量或传入函数作为参数
表达式语法
let Person = class {
constructor(name) {
this.name = name
}
sayName() {
console.log(this.name)
}
}
let p = new Person('wmui')
p.sayName()
console.log(p instanceof Person) // true
console.log(p instanceof Object) // true
这个示例等价于前面的声明形式Person类,仅在代码编写方式略有差异。
命名表达式
类与函数一样,都可以定义为命名表达式。声明时,在关键字class后添加一个标识符即可
let Person = class Person2{
constructor(name) {
this.name = name
}
sayName() {
console.log(this.name)
}
}
console.log(typeof Person) // function
console.log(typeof Person2) // undefined
标识符Person2只存在于类的定义中,可以在类内部的方法中使用。而在类的外部,由于不存在Person2标识符,所以typeof Person2的结果是 undefined
类表达式还可以通过立即调用构造函数创建单例。用new调用类表达式,然后通过小括号调用这个表达式
let p = new class {
constructor(name) {
this.name = name
}
sayName() {
console.log(this.name)
}
}('wmui')
p.sayName() // wmui
一等公民
在程序中,一等公民指的是可以传入函数,可以从函数返回,并且可以赋值给变量的值。在JS中,函数和类都是一等公民。
把类作为参数传入函数
function createObj(classDef) {
return new classDef()
}
class Person {
sayName() {
console.log('wmui')
}
}
let obj = createObj(Person)
obj.sayName() // wmui
访问器属性
类也支持访问器属性,创建getter时,需要在关键字get后紧跟一个空格和相应的标识符;创建setter时,只需把关键字get替换为set
class Person {
constructor(name) {
this.name = name
}
get myName() {
return this.name
}
set myName(value) {
this.name = value
}
}
let descriptor = Object.getOwnPropertyDescriptor(Person.prototype, 'myName')
console.log('get' in descriptor) // true
console.log('set' in descriptor) // true
可计算成员名称
类方法和访问器属性都支持使用可计算名称。就像在对象字面量中一样,用方括号包裹一个表达式即可使用可计算名称
let methodName = 'sayName'
class Person{
constructor(name) {
this.name = name
}
[methodName]() {
console.log(this.name)
}
}
let p = new Person('wmui')
p.sayName() // wmui
通过相同的方式可以在访问器属性中应用可计算名称,并且可以像往常一样通过.myName
访问该属性
let propertyName = 'myName'
class Person {
constructor(name) {
this.name = name
}
get [propertyName]() {
return this.name
}
set [propertyName](value) {
this.name = value
}
}
生成器方法
在对象字面量中,可以通过在方法名前附加一个星号(*)的方式来定义生成器,在类中也可以
class MyClass {
*createIterator() {
yield 1;
yield 2;
yield 3;
}
}
let instance = new MyClass();
let iterator = instance.createIterator();
静态成员
在ES5中,通常直接将方法添加到构造函数中来模拟静态成员
function Person(name) {
this.name = name
}
// 静态方法
Person.testMethod = function() {
return 'hello'
}
// 静态属性
Person.testProperty = 'hi'
// 实例方法
Person.prototype.sayName = function() {
console.log(this.name)
}
在ES6中,可以通过添加静态注释来表示静态成员
class Person {
constructor(name) {
this.name = name
}
sayName() {
console.log(this.name)
}
// 静态方法
static testMethod() {
return 'hello'
}
// 模拟静态属性
static get testProperty() {
return 'hi'
}
}
console.log(Person.testMethod()) // hello
console.log(Person.testProperty) // hi
注意: ES6规定,Class内部只有静态方法,没有静态属性。所以这里是通过getter模拟的静态属性
注意: 静态成员是对象自身的属性和方法,在实例身上无法使用
继承与派生类
在ES6之前,实现类的继承要写很多代码
function Rectangle(length, width) {
this.length = length,
this.width = width
}
Rectangle.prototype.getArea = function() {
return this.length * this.width
}
function Square(length) {
Rectangle.call(this, length, length);
}
Square.prototype = Object.create(Rectangle.prototype, {
constructor: {
value: Square,
enumerable: false,
writable: true,
configurable: true
}
})
var square = new Square(3)
console.log(square.getArea()) // 9
console.log(square instanceof Square) // true
console.log(square instanceof Rectangle) // true
Square继承自Rectangle,核心是创建一个基于Rectangle.prototype的新对象重写Square.prototype,并且在Square中调用Rectangle.call()方法改变this指针。
使用ES6实现相同的功能,代码会精简很多
class Rectangle {
constructor(length, width) {
this.length = length,
this.width = width
}
getArea() {
return this.length * this.width
}
}
class Square extends Rectangle {
constructor(length) {
// 想当与Rectangle.call(this, length, length)
super(length, length)
}
}
let square = new Square(3)
console.log(square.getArea()) // 9
console.log(square instanceof Square) // true
console.log(square instanceof Rectangle) // true
通过extends关键字指定要继承的类,子类的原型会自动调整,然后super()方法可以调用基类的构造函数
继承自其它类的类称为派生类,如果在派生类中指定了构造函数则必须调用super()方法,否则会报错。如果不使用构造函数,创建派生类的实例时会自动调用super()方法并传入所有参数。
class Rectangle {
// 没有构造函数
}
// 等价于
class Square extends Rectangle {
constructor(...args) {
super(...args)
}
}
注意事项
-
只能在派生类中使用super(),在非派生类(不是extends声明的类)或函数中使用会报错
-
在构造函数中访问this前一定要调用super(),它负者初始化this。如果在super()前访问this会报错
-
如果不想在派生类的构造函数中调用super(),可以让构造函数返回一个对象
类方法遮蔽
派生类中的方法总会覆盖基类中的同名方法
class Square extends Rectangle {
constructor(length) {
super(length, length)
}
getArea() {
return this.length * this.length;
}
}
派生类Square中的getArea()会覆盖Rectangle中的同名方法。如果Square的实例想调用Rectangle的getArea()方法,可以通过调用super.getArea()方法间接实现
class Square extends Rectangle {
constructor(length) {
super(length, length)
}
getArea() {
return super.getArea()
}
}
静态成员继承
如果基类有静态成员,派生类也可以继承这些静态成员
class Rectangle {
constructor(length, width) {
this.length = length,
this.width = width
}
getArea() {
return this.length * this.width
}
static create(length, width) {
// 该方法返回一个Rectangle实例
return new Rectangle(length, width)
}
}
class Square extends Rectangle {
constructor(length) {
super(length, length)
}
}
let rect = Square.create(3,4)
console.log(rect.getArea()) // 12
console.log(rect instanceof Rectangle) // true
console.log(rect instanceof Square) // false
Square继承了Rectangle的create()静态方法
派生自表达式的类
派生类不一定非要是继承自class的基类,它也可以继承自某个表达式。只要表达式可以被解析为函数并且具有[[Construct]]属性和原型
function Rectangle(length, width) {
this.length = length
this.width = width
}
Rectangle.prototype.getArea = function() {
return this.length * this.width
}
class Square extends Rectangle {
constructor(length) {
super(length, length)
}
}
let square = new Square(3)
console.log(square.getArea()) // 9
console.log(square instanceof Rectangle) // true
Rectangle是一个ES5风格的构造函数,Square是一个类,由于Rectangle具有[[Construct]]属性和原型,因此Square类可以直接继承它
内建对象的继承
在ES5中要想实现内键对象的继承几乎不可能,比如想通过继承的方式创建基于Array的特殊数组。
function MyArray(length) {
Array.apply(this, arguments)
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
enumerable: false,
writable: true,
configurable: true
}
})
var colors = new MyArray()
colors[0] = 'red'
console.log(colors.length) // 0
colors.length的结果不是期望的1而是0,这是因为通过传统JS继承形式实现的数组继承没有从Array.apply()或原型赋值中继承数组相关功能
在ES6中可以轻松实现内建对象的继承
class MyArray extends Array {}
let colors = new MyArray()
colors[0] = 'red'
console.log(colors.length) // 1
new.target
在类的构造函数中也可以通过new.target来确定类是如何被调用的,new.target及它的值根据函数被调用的方式而改变
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
}
}
let rect = new Rectangle(3, 4);
// true
class Rectangle {
constructor(length, width) {
console.log(new.target === Square);
this.length = length;
this.width = width;
}
}
class Square extends Rectangle {
constructor(length) {
super(length, length)
}
}
let square = new Square(3); // true
示例中,创建Rectangle实例时new.target等价于Rectangle,创建Square实例时new.target等价于Square