2019-7-29
第九章 js类
(一)ES5中的仿类结构
自定义类型:创建一个构造器,将原型指派到该构造器上
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function () {
console.log(this.name);
}
let per = new Person('zc');
per.sayName(); //zc
console.log(per instanceof Person); //true
console.log(per instanceof Object); //true
(二)ES6中的类
1. 类声明
class className {}
class PersonClass {
// 等价于Person构造器
constructor(name) {
this.name = name;
}
// 等价于 Person.prototype.sayName
sayName() {
console.log(this.name);
}
}
let person = new PersonClass('zc');
person.sayName(); // zc
console.log(person instanceof PersonClass); // true
console.log(person instanceof Object); // true
console.log(typeof PersonClass); // function
console.log(typeof PersonClass.prototype.sayName); // function
自有属性:实例上的属性,只能在类的构造器或者方法内部进行创建,比如本例中的name,建议在构造器函数内创建所有可能出现的自有属性,有助于代码检查。
注意:类声明仅仅是自定义类型的语法糖。
类声明与自定义类型的区别:
1)与函数定义不同,类声明不会被提升,行为与let相似,在程序执行到声明处之前,类都会位于暂时性死区;
2)类声明中的所有代码会自动运行并锁定在严格模式下;
3)类的所有方法都是不可枚举的,自定义类型必须用Object.defineProperty()才能将方法改变为不可枚举;
4)类的所有方法内部都没有[[Contruct]],不能使用new来调用;
5)调用类的构造器,必须使用new,否则会报错;
6)不能在类的方法内部重写类名,会报错,但可以在外部重写类名
上例中的PersonClass类声明实际上等价于以下未使用类语法的代码:
// 直接等价于PersonClass
// let声明确保类声明不会被提升,立即执行函数来确保类声明中的所有代码会自动运行
let PersonType2 = (function() {
"use strict";
// const确保类名不能在类的方法内被重写
const PersonType2 = function(name) {
// 调用类的构造器必须使用new
if (typeof new.target === "undefined") {
throw new Error("Constructor must be called with new.");
}
this.name = name;
}
Object.defineProperty(PersonType2.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 PersonType2;
}());
// 类名的修改
class Foo {
constructor() {
Foo = "bar"; // 执行时抛出错误
}
}
// 在类声明之后没问题
Foo = "baz";
2. 类表达式
类和函数相似,都有两种形式:声明与表达式。
类表达式被设计用于变量声明,或可作为参数传递给函数。
1)基本类表达式:(匿名类表达式)
let PersonClass = class {
// 等价于Person构造器
constructor(name) {
this.name = name;
}
// 等价于Person.prototype.sayName
sayName() {
console.log(this.name);
}
};
使用类声明还是类表达式,主要是代码风格问题。相对于函数声明和函数表达式之间的区别,类声明和类表达式都不会被提升,因此对代码运行时的行为影响甚微。
2) 具名类表达式
let PersonClass = class PersonClass2 {
// 等价于Person构造器
constructor(name) {
this.name = name;
}
// 等价于Person.prototype.sayName
sayName() {
console.log(this.name);
}
};
console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass2); // "undefined"
PersonClass标识符只在类定义内部存在,因此只能用在类方法内部,如本例中的sayName()内。
以下PersonClass具名类表达式实际上等价于下面未使用类语法的代码;
// 直接等价于PersonClass
// let声明确保类声明不会被提升,立即执行函数来确保类声明中的所有代码会自动运行
let PersonClass = (function() {
"use strict";
// const确保类名不能在类的方法内被重写
const PersonClass2 = function(name) {
// 调用类的构造器必须使用new
if (typeof new.target === "undefined") {
throw new Error("Constructor must be called with new.");
}
this.name = name;
}
Object.defineProperty(PersonClass2.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 PersonClass2;
}());
类声明:用let定义的外部绑定和用const定义的内部绑定同名;
类表达式:可在内部使用const来定义不同的名称。
3. 作为一等公民的类
一等公民:能被当做值来使用,所有可以给变量赋值,作为函数参数,函数返回值。
// 作为函数参数
function createObject(classDef) {
return new classDef();
}
let obj = createObject(class {
sayHi() {
console.log('hi');
}
});
obj.sayHi(); // hi
// 立即执行类,创建单例
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}('Nicholas');
person.sayName();
4. 类与对象字面量的相似点
1)访问器属性
在类上创建访问器属性,所用语法类似于对象字面量。
class CustomHTMLElement{
constructor(element){
this.element = element;
}
get html() {
return this.element.innerHTML;
}
set html(value) {
this.element.innerHTML = value;
}
}
// getter,setter定义在原型上
var descriptor = Object.getOwnPropertyDescriptor(CustomHTMLElement.prototype, 'html');
console.log('get' in descriptor); // true
console.log('set' in descriptor); // true
console.log(descriptor.enumerable); // false
非类的等价表示如下:
let CustomHTMLElement = (function() {
"use strict";
const CustomHTMLElement = function(element) {
// make sure the function was called with new
if (typeof new.target === "undefined") {
throw new Error("Constructor must be called with new.");
}
this.element = element;
}
Object.defineProperty(CustomHTMLElement.prototype, "html", {
enumerable: false,
configurable: true,
get: function() {
return this.element.innerHTML;
},
set: function(value) {
this.element.innerHTML = value;
}
});
return CustomHTMLElement;
}());
let descriptor = Object.getOwnPropertyDescriptor(CustomHTMLElement.prototype,'html')
console.log('get' in descriptor); // true
console.log('set' in descriptor); // true
console.log(descriptor.enumerable); // false
2)可计算的成员名
类方法和类的访问器属性都能使用可计算的名称,语法与对象字面量相同,用[]来包裹表达式。
let methodName = "sayName";
class PersonClass {
constructor(name) {
this.name = name;
}
// 类方法使用可计算的名称
[methodName]() {
console.log(this.name);
}
}
let me = new PersonClass('zc');
me.sayName(); // zc
<div>hello, August!</div>
<script>
let propertyName = 'html';
class CustomHTMLElement {
constructor(element) {
this.element = element;
}
// 访问器使用可计算名称
get [propertyName]() {
return this.element.innerHTML;
}
set [propertyName](value) {
this.element.innerHTML = value;
}
}
let div = document.getElementsByTagName('div')[0];
let cus = new CustomHTMLElement(div);
console.log(cus.html); // hello, August!
</script>
3)生成器方法
跟对象字面量相似,在类的方法名称前附上*,就能将对类的方法变为一个生成器。
class MyClass {
*createIterator() {
yield 1;
yield 2;
yield 3;
}
}
let instance = new MyClass();
let iterator = instance.createIterator();
console.log(iterator.next()); // {value: 1, done: false}
// 为表示集合的自定义类定义默认迭代器
class Collection{
constructor() {
this.items = [];
}
// 定义默认的迭代器
*[Symbol.iterator](){
yield *this.items.values();
}
}
var collection = new Collection();
collection.items.push(1);
collection.items.push(2);
collection.items.push(3);
for(let x of collection.items){
console.log(x);
}
任意管理集合的类都包含一个默认迭代器,因为一些集合专用的操作都要求目标集合具有迭代器。
5. 静态成员
如果想让方法与访问器属性在对象实例上出现,应当把他们添加到类的原型上,而
如果将方法与访问器属性绑定到类自身,就需要使用静态成员。
静态成员不能用实例来访问,必须直接用类自身来访问它们。
ES5直接在构造器上添加方法来模拟静态成员:
function PersonType(name) {
this.name = name;
}
// static method
PersonType.create = function(name) {
return new PersonType(name);
};
// instance method
PersonType.prototype.sayName = function() {
console.log(this.name);
};
var person = PersonType.create("Nicholas");
ES6的类简化了静态成员的创建,只要在方法和访问器属性的名称前添加static 标注:
class PersonClass{
// 等价于PersonType构造器
constructor(name) {
this.name = name;
}
// 等价于PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
// 等价于PersonType.create
static create(name) {
return new PersonClass(name);
}
}
let person = PersonClass.create('zc');
person.sayName(); // zc
可以在类的任何方法和访问器属性上使用static关键字,但不能用于constructor方法的定义。
(三) 类继承
1. ES5的继承
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
2. ES6让继承变得更轻松
extents — 指定当前类所需要继承的函数
super — 访问基类的构造器
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);
}
}
- 派生类:即子类,派生类如果指定了构造器,则必须使用super(),否则会报错。若没有指定构造器,super()方法会被自动调用,且会使用创建新实例时提供的所有参数。
class Square extends Rectangle {
// 没有构造器
}
// 等价于:
class Square extends Rectangle {
constructor(...args) {
super(...args);
}
}
使用super需注意:
1)只能在派生类中使用super();
2)在构造器中,必须在访问this之前调用super(),由于super()负责初始化this,因此试图先访问this会造成错误;
3)如果在类的构造器中不调用``super()`,唯一避免出错的办法是在构造器中返回一个对象。
3. 屏蔽基类的方法
派生类中的方法总是会屏蔽基类的同名方法。
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
// 重写并屏蔽 Rectangle.prototype.getArea()
getArea() {
return this.length * this.length;
}
}
4. 继承静态成员
class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
static create(length, width) {
return new Rectangle(length, width);
}
}
class Square extends Rectangle {
constructor(length) {
// same as Rectangle.call(this, length, length)
super(length, length);
}
}
// 与Rectangle.create(3,4)一致
var rect = Square.create(3, 4);
console.log(rect instanceof Rectangle); // true
console.log(rect instanceof Square); // false
5. 从表达式中派生类
只要一个表达式能够返回一个具有[[Construct]]属性以及原型的函数,就可以对其使用extends。
let Rectangle = function(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function () {
return this.length * this.width;
}
// Rectangle具有[[Construct]]属性以及原型
class Square extends Rectangle {
constructor(length) {
// same as Rectangle.call(this, length, length)
super(length, length);
}
}
var square = new Square(2);
console.log(square.getArea()); // 4
因为extends后面能接受任意类型的表达式,所有还可以动态地决定所要继承的类:
let Rectangle = function(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function () {
return this.length * this.width;
}
function getBase() {
return Rectangle;
}
class Square extends getBase() {
constructor(length) {
// same as Rectangle.call(this, length, length)
super(length, length);
}
}
var square = new Square(2);
console.log(square.getArea());
console.log(square instanceof Rectangle);
还可以构建混入:
let SerializableMixin = {
serialize() {
return JSON.stringify(this);
}
};
let AreaMixin = {
getArea() {
return this.length * this.width;
}
};
function mixin(...mixins) {
let base = function () {}
// 若多个混入对象拥有相同的属性,只有最后添加的属性会保留
Object.assign(base.prototype, ...mixins);
return base;
}
function getBase() {
return Rectangle;
}
class Square extends mixin(SerializableMixin, AreaMixin) {
constructor(length) {
super();
this.length = length;
this.width = length;
}
}
var square = new Square(2);
console.log(square.getArea()); // 4
console.log(square.serialize()); // {"length":2,"width":2}
注意:null和生成器函数不存在[[Construct]],如果用在extends后会报错。
6. 继承内置对象
在ES5尝试继承数组,会出现length属性与数值属性的行为和内置数组不一致。
function MyArray() {
Array.apply(this,arguments);
}
MyArray.prototype = Object.assign(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
}
});
var colors = new MyArray();
colors[0] = 'red';
console.log(colors.length); // 0
colors.length = 0;
console.log(colors[0]); //red
ES6中的类设计目的之一就是允许从内置对象上进行继承。为了达成此目的,ES6的继承模式和ES5或更早版本的传统继承模式有轻微差异:
ES5:this的值先被派生类(如:MyArray)创建,随后基类构造器(如:Array.apply()方法;)才被调用。这意味着this一开始就是MyArray的实例,之后才使用Array的附加属性对其进行装饰。
ES6: this的值先被基类(如:Array)创建,随后才被派生类的构造器(如:MyArray)修改。这意味着this一开始就拥有作为基类的内置对象的所有功能,并能正确接受与之关联的所有功能。
class MyArray extends Array{
}
var colors = new MyArray();
colors[0] = 'red';
console.log(colors.length); // 1
colors.length = 0;
console.log(colors[0]); //undefined
7. Symbol.species属性
继承内置对象会带来一个有趣的特性,任意能返回内置对象实例的方法,在派生类上都会自动返回派生类的实例,比如,继承了Array的派生类MyArray,诸如slice()之类的方法都会返回MyArray的实例。
class MyArray extends Array{
}
var items = new MyArray(1,2,3,4,5);
// slice()方法是从Array上继承的,原本应该返回的是Array的实例,但这里却返回了MyArray的实例
var sliceItems = items.slice(1,3);
console.log(items instanceof MyArray); // true
console.log(sliceItems instanceof MyArray); // true
Symbol.species:定义了一个可以返回function的静态访问器属性,当不是使用类本身的构造器来创建实例时,这个function会被当做构造器来创造实例。
以下内置类型都定义了Symbol.species:
- Array
- ArrayBuffer
- Map
- Promise
- RegExp
- Set
- 类型化数组
以上每个类型都拥有默认的Symbol.species属性,其返回值为this,意味着该属性总是会返回自身的构造函数。
// 几个内置类型使用species的方式类似于此
class MyClass{
static get [Symbol.species] (){
return this;
}
constructor(value) {
this.value = value;
}
clone() {
return new this.constructor[Symbol.species](this.value);
}
}
class MyDerivedClass1 extends MyClass {
}
class MyDerivedClass2 extends MyClass {
// 重写了Symbol.species,让其返回MyClass
static get [Symbol.species]() {
return MyClass;
}
}
let instance1 = new MyDerivedClass1('foo'),
clone1 = instance1.clone();
let instance2 = new MyDerivedClass2('bar'),
clone2 = instance2.clone();
console.log(clone1 instanceof MyDerivedClass1); //true
console.log(clone1 instanceof MyClass); //true
console.log(clone2 instanceof MyDerivedClass2); //false
console.log(clone2 instanceof MyClass); //true
使用Symbol.species,任意派生类在调用应当返回实例的方法时,都可以判断出需要返回什么类型的值。
class MyArray extends Array{
// 重写了Symbol.species,让其返回Array
static get [Symbol.species]() {
return Array;
}
}
var items = new MyArray(1,2,3,4,5);
var sliceItems = items.slice(1,3);
console.log(items instanceof MyArray); // true
console.log(sliceItems instanceof MyArray); // false
console.log(sliceItems instanceof Array); // true
8. 在类的构造器中使用new.target
在简单情况下,new.target就等于本类的构造器函数;
在有继承的情况下,基类的new.target有可能会等于派生类的构造器函数
class Rectangle{
constructor() {
console.log(new.target);
}
}
var obj = new Rectangle(); // 输出Rectangle
class Square extends Rectangle{
}
// new.target是Square
var squ = new Square(); // 输出Square
创建抽象基类(不能被实例化的类,但仍能被其他类所继承):
class Shape{
constructor(){
// Shape不能被实例化
if(new.target === Shape){
throw new Error('this class cannot be instantiate directly');
}
}
}
class Rectangle extends Shape{
constructor(lenght, width){
super();
this.lenght = lenght;
this.width = width;
}
}
var x = new Shape(); // 抛出错误
var y = new Rectangle(3, 4);
console.log(y instanceof Shape); // true