ECMAScript 6 入门笔记(六)Class

时间:2022-08-27 13:33:12

ECMAScript 6 入门原文 – 阮一峰
ECMAScript 6 入门笔记(一)let,const,解构
ECMAScript 6 入门笔记(二)String,RegExp
ECMAScript 6 入门笔记(三)数值,Array
ECMAScript 6 入门笔记(四)函数,对象

ECMAScript 6 入门笔记(五)异步promise,Generator,async
ECMAScript 6 入门笔记(六)Class
ECMAScript 6 入门笔记(七)Symbol,set和map
ECMAScript 6 入门笔记(八)Proxy,Reflect

Class

class基本用法

class Point{
constructor(x,y){
this.x = x;
this.y = y;
}

toString(){
return "("+this.x+","+this.y+")";
}

doStuff() {
console.log('stuff');
}
}

var b = new Bar();
b.doStuff()

类的内部所有定义的方法,都是不可枚举的(non-enumerable)。

class Point {
constructor(x, y) {
// ...
}

toString() {
// ...
}
}

Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]

类的实例对象

生成类的实例对象的写法,与ES5完全一样,也是使用new命令。如果忘记加上new,像函数那样调用Class,将会报错。

// 报错
var point = Point(2, 3);

// 正确
var point = new Point(2, 3);

var p1 = new Point(2,3);
var p2 = new Point(3,2);

p1.proto === p2.proto
//true
上面代码中,p1和p2都是Point的实例,它们的原型都是Point.prototype,所以proto属性是相等的。

这也意味着,可以通过实例的proto属性为Class添加方法。

var p1 = new Point(2,3);
var p2 = new Point(3,2);

p1.proto.printName = function () { return ‘Oops’ };

p1.printName() // “Oops”
p2.printName() // “Oops”

var p3 = new Point(4,2);
p3.printName() // “Oops”

Class表达式

与函数一样,类也可以使用表达式的形式定义。

const MyClass = class Me {
getClassName() {
return Me.name;
}
};
上面代码使用表达式定义了一个类。需要注意的是,这个类的名字是MyClass而不是Me,Me只在Class的内部代码可用,指代当前类。

let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined

上面代码表示,Me只在Class内部有定义。

如果类的内部没用到的话,可以省略Me,也就是可以写成下面的形式。

class立即执行

let person = new class {
constructor(name){
this.name = name;
}
sayName(){
console.log(this.name);
}
}("张三");

person.sayName(); //张三

this的指向
类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。

class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`);
}

print(text) {
console.log(text);
}
}

const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined

一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到print方法了。

class Logger {
constructor() {
this.printName = this.printName.bind(this);
}
}

class继承
ColorPoint通过extends关键字,继承了Point类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Point类。

class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}

toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}

上面代码中,constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。

class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}

class ColorPoint extends Point {
constructor(x, y, color) {
this.color = color; // ReferenceError
super(x, y);
this.color = color; // 正确
}
}

上面代码中,子类的constructor方法没有调用super之前,就使用this关键字,结果报错,而放在super方法之后就是正确的。

下面是生成子类实例的代码。

let cp = new ColorPoint(25, 8, 'green');

cp instanceof ColorPoint // true
cp instanceof Point // true

上面代码中,实例对象cp同时是ColorPoint和Point两个类的实例。

类的prototype属性和_proto_属性
大多数浏览器的ES5实现之中,每一个对象都有proto属性,指向对应的构造函数的prototype属性。Class作为构造函数的语法糖,同时有prototype属性和proto属性,因此同时存在两条继承链。

(1)子类的proto属性,表示构造函数的继承,总是指向父类。

(2)子类prototype属性的proto属性,表示方法的继承,总是指向父类的prototype属性。

class A {
}
class B extends A {
}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

上面代码中,子类B的proto属性指向父类A,子类B的prototype属性的proto属性指向父类A的prototype属性。

这样的结果是因为,类的继承是按照下面的模式实现的。

class A {
}

class B {
}

// B的实例继承A的实例
Object.setPrototypeOf(B.prototype, A.prototype);
const b = new B();

// B的实例继承A的静态属性
Object.setPrototypeOf(B, A);
const b = new B();
《对象的扩展》一章给出过Object.setPrototypeOf方法的实现。

Object.setPrototypeOf = function (obj, proto) {
obj.__proto__ = proto;
return obj;
}

因此,就得到了上面的结果。

Object.setPrototypeOf(B.prototype, A.prototype);
// 等同于
B.prototype.proto = A.prototype;

Object.setPrototypeOf(B, A);
// 等同于
B.proto = A;
这两条继承链,可以这样理解:作为一个对象,子类(B)的原型(proto属性)是父类(A);作为一个构造函数,子类(B)的原型(prototype属性)是父类的实例。

Object.create(A.prototype);
// 等同于
B.prototype.proto = A.prototype;

Extends 的继承目标
extends关键字后面可以跟多种类型的值。
第一种特殊情况,子类继承Object类。

class A extends Object {
}

A.proto === Object // true
A.prototype.proto === Object.prototype // true
这种情况下,A其实就是构造函数Object的复制,A的实例就是Object的实例。

第二种特殊情况,不存在任何继承。

class A {
}

A.proto === Function.prototype // true
A.prototype.proto === Object.prototype // true
这种情况下,A作为一个基类(即不存在任何继承),就是一个普通函数,所以直接继承Funciton.prototype。但是,A调用后返回一个空对象(即Object实例),所以A.prototype.proto指向构造函数(Object)的prototype属性。

第三种特殊情况,子类继承null。

class A extends null {
}

A.proto === Function.prototype // true
A.prototype.proto === undefined // true

super 关键字
第一种情况,super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。

第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

class A {
p() {
return 2;
}
}
class B extends A {
constructor() {
super();
console.log(super.p()); // 2
}
}
let b = new B();


class A {
constructor() {
this.p = 2;
}
}

class B extends A {
get m() {
return super.p;
}
}

let b = new B();
b.m // undefined

如果属性定义在父类的原型对象上,super就可以取到。

class A {
constructor() {
this.x = 1;
this.y = 2;
}
}

class B extends A {
constructor() {
super();
this.x = 2;
super.x = 3;
console.log(super.y); //undefined
console.log(super.x); // undefined
console.log(this.x); // 3
}
}

let b = new B();

如果super作为对象,用在静态方法之中,这时super将指向父类,而不是父类的原型对象。

class Parent {
static myMethod(msg) {
console.log('static', msg);
}

myMethod(msg) {
console.log('instance', msg);
}
}

class Child extends Parent {
static myMethod(msg) {
super.myMethod(msg);
}

myMethod(msg) {
super.myMethod(msg);
}
}

Child.myMethod(1); // static 1

var child = new Child();
child.myMethod(2); // instance 2

上面代码中,super在静态方法之中指向父类,在普通方法之中指向父类的原型对象。

注意,使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。

class A {}

class B extends A {
constructor() {
super();
console.log(super); // 报错
}
}

上面代码中,console.log(super)当中的super,无法看出是作为函数使用,还是作为对象使用,所以 JavaScript 引擎解析代码的时候就会报错。这时,如果能清晰地表明super的数据类型,就不会报错。

class A {}

class B extends A {
constructor() {
super();
console.log(super.valueOf() instanceof B); // true
}
}

let b = new B();

ES6允许继承原生构造函数定义子类,因为ES6是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。下面是一个继承Array的例子。

class MyArray extends Array {
constructor(...args) {
super(...args);
}
}

var arr = new MyArray();
arr[0] = 12;
arr.length // 1

arr.length = 0;
arr[0] // undefined

Class的取值函数(getter)和存值函数(setter)
与ES5一样,在Class内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

class MyClass {
constructor() {
// ...
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}

let inst = new MyClass();

inst.prop = 123;
// setter: 123

inst.prop
// 'getter'

Class 的 Generator 方法
如果某个方法之前加上星号(*),就表示该方法是一个 Generator 函数。

class Foo {
constructor(...args) {
this.args = args;
}
* [Symbol.iterator]() {
for (let arg of this.args) {
yield arg;
}
}
}

for (let x of new Foo('hello', 'world')) {
console.log(x);
}
// hello
// world

上面代码中,Foo类的Symbol.iterator方法前有一个星号,表示该方法是一个 Generator 函数。Symbol.iterator方法返回一个Foo类的默认遍历器,for…of循环会自动调用这个遍历器。

Class 的静态方法

class Foo {
static classMethod() {
return 'hello';
}
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()

class Foo {
static classMethod() {
return 'hello';
}
}

class Bar extends Foo {
}

Bar.classMethod(); // 'hello'
上面代码中,父类Foo有一个静态方法,子类Bar可以调用这个方法。

静态方法也是可以从super对象上调用的。

class Foo {
static classMethod() {
return 'hello';
}
}

class Bar extends Foo {
static classMethod() {
return super.classMethod() + ', too';
}
}

Bar.classMethod();