[学习]es6-类

JavaScript类

类实质上是 JavaScript 现有的基于原型继承的语法糖。类的主体是在大括号中间的部分。

class Animal { 
  constructor(name) {
    this.name = name;
  }
  speak() {
    return this;
  }
}

console.log(typeof Animal); // function
console.log(Animal === Animal.prototype.constructor); // true

上面的代码用es5的方式写:

function Animal(name) {
  this.name = name;
}
Animal.prototype.speak = function()  {
  return this;
}

类声明和类表达式的主体都执行在严格模式下。比如,构造函数,静态方法,原型方法,getter和setter都在严格模式下执行。

类声明

类声明不像函数声明一样会提升,所以要先声明类,然后再使用。

var rectangle = new Rectangle(); // 在类的声明语句之前使用类会报错
class Rectangle {
 
}

类表达式

类表达式可以是具名的,也可以是匿名的。

具名类表达式

一个具名类表达式的名称是类内的一个局部属性,它可以通过类本身(而不是类实例)的name属性来获取。

// 具名类
let Rectangle = class Rectangle2 {
    constructor(height, width) {
        this.height = height;
        this.width = width;
    }
};
console.log(Rectangle.name);
// 输出: "Rectangle2"

匿名类表达式

// 匿名类
let Rectangle = class {
    constructor(height, width) {
        this.height = height;
       this.width = width;
    }
};
console.log(Rectangle.name);
// output: "Rectangle"

类体和方法定义

构造函数 constructor

constructor方法是一个特殊的方法,这种方法用于创建和初始化一个由class创建的对象。
一个类只能拥有一个名为 “constructor”的特殊方法。如果类包含多个constructor的方法,则将抛出 一个SyntaxError 。
constructor方法默认返回实例对象(即this),可以手动改写指定返回另外一个对象。
没有显式定义类的constructor方法,会默认为其添加一个空的constructor方法:constructor() {}。

class Animal {
  // 没有显示定义Animal的constructor方法,会默认为其添加一个空的constructor方法。
  // constructor() {}
  speak() {
    console.log('Hello world!');
  }
}
console.log(Animal === Animal.prototype.constructor); // true
// class Animal {} 实际上对应的是ES5中的 function Animal {} 写法

super

super关键字的规则比较复杂,既可以作为函数使用,也可以作为对象使用。

super([arguments]); 

super.functionOnParent([arguments]); 

在子类中, 在使用'this'之前, 必须先调用super(), 否则, 将导致引用错误。

class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

class Square extends Polygon {
  constructor(length) {
    // 注意: 在子类中, 在你使用'this'之前, 必须先调用super()。否则, 将导致引用错误。
    this.height = lenght; // ReferenceError,super 需要先被调用!
    
    // 这里,调用父类的构造函数, 
    super(length, length); 
  }
}

super在子类中使用的位置不一样,可调用的方法也不一样。

  • 在子类的构造函数中单独使用时,只能作为父类的构造函数调用:super(),此时其内部的this指向子类的实例
  • 在子类的构造函数中或者在子类的原型方法中作为对象使用时,此时super指向父类原型,通过super可以调用父类中的原型方法和访问父类原型上的属性,通过super调用的方法内部的this指向子类的实例(有一点要额外说明在子类构造函数中使用super可以修改子类实例的属性值,可以给子类实例添加属性)
  • 在子类的静态方法或者给子类的静态公有字段赋值时,此时super指向父类,通过super可以调用父类的静态方法和静态公有字段,通过super调用的父类静态方法中的this指向子类本身;

使用super给公有实例字段赋值,super指向父类的原型。将在公有实例字段一节中说明。

验证第一条和第二条的例子:

class Polygon {
  constructor(height, width) {
    this.name = 'Rectangle';
    this.height = height || 0;
    this.width = width || 0;
    return this;
  }
  sayName() {
    console.log(this.name);
  }
  get area() {
    return this.height * this.width;
  }
  set area(value) {
    this._area = value;
  }
}
Polygon.prototype.pp = 100; // 定义在父类原型上的属性

class Square extends Polygon {
  constructor(length) {
    // 这里,调用父类的构造函数, 
    // 但实质上是通过父类的构造函数给子类的实例添加了name,height,width属性
    // 再稍微深入一想,super作为函数使用时,在子类的构造函数中调用,其内部的this指向了子类的实例
    console.log(super(length, length)); // 打印出的是子类的实例
    this.test = 'test';
    this.name = 'Square';
    // 访问的是父类原型上的方法,方法内部的this同样指向了子类的实例
    super.sayName(); // Square
  }
  sayName() {
    super.sayName(); // Square
    console.log(super.pp); // 100 通过super访问父类原型上的属性
    console.log(super.constructor === Polygon); // true
  }
}

验证第三条的例子:

class Animal {
  static test = 'ttt';
  constructor(name) {
    this.name = name;
    return this;
  }
  static printFather(str) {
    console.log('this is father' + '---' + str + '--' + this.name);
  }
  
}

class Dog extends Animal {
  static dd = super.test; // 还可以访问父类的静态公有字段 此时super指向父类自身
  pp = super.test; // 此时super指向父类的原型
  static printSon() {
    super.printFather('son'); // 调用父类的静态方法,此时printFather方法中的this指向了Dog类自身
  }
  constructor(name) {
    super(name);
  }
}

var ss = new Dog('pubby');
Dog.printSon(); // this is father---son--Dog
console.log(Dog.dd); // ttt
console.log(ss.pp); // undefined

对于第二条的额外说明的验证:

class A {
  constructor() {
    this.x = 1;
    this.z = 12;
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
    // 根据结果来看,下面两条语句的super实际上是this
    super.x = 3;
    super.y = 4;
    console.log(super.x); // undefined,这里super指向了父类的原型
    console.log(this.x); // 3
  }
}

let b = new B();
console.log(b.x); // 3
console.log(b.y); // 4

类主体中的严格模式

类主体中的所有的函数、方法、构造函数、getters或setters都在严格模式下执行。

class Animal { 
  speak() {
    return this;
  }
  static eat() {
    return this;
  }
}

let obj = new Animal();
obj.speak(); // Animal {}
let speak = obj.speak;
speak(); // undefined  speak方法是在严格模式下声明的,单独调用时this不会指向全局对象

Animal.eat() // class Animal
let eat = Animal.eat;
eat(); // undefined  eat方法是在严格模式下声明的,单独调用时this不会指向全局对象

实例属性

实例的属性一般定义在this上(也可以使用其他方法定义实例属性,下文会讲到):

class Rectangle {
  constructor(height, width) {    
    this.height = height; // height是实例属性
    this.width = width; // width是实例属性
  }
  test() { // test方法定义在类的原型上(Rectangle.prototype)
    console.log('test');
  }
}

静态的和原型的数据属性:

Rectangle.staticWidth = 20; // 静态属性 --- 相当于定义在类的构造函数上
Rectangle.prototype.prototypeWidth = 25; // 原型属性 --- 定义在原型上

字段声明

  • 公有字段
  • 公共方法
  • 私有字段
  • 私有方法

公有字段

静态公有字段

静态公有字段用关键字static声明。
我们声明的静态公有字段,本质上是使用Object.defineProperty方法添加到类的构造函数上。
在类被声明之后,可以通过类的构造函数访问静态公有字段。

class ClassWithStaticField {
  static staticField = 'static field';
  // 没有设置值的字段将默认被初始化为undefined。
  static staticParam;
}

console.log(ClassWithStaticField.staticField); // 输出值: "static field"​
console.log(ClassWithStaticField.staticParam); // undefined

静态公有字段不会在子类里重复初始化,但我们可以通过原型链访问它们。
下面的代码中子类Dog通过原型链访问到了父类Animal上的静态公有字段test。

class Animal {
  static test = 'ttt';
  constructor(name) {
    this.name = name;
  }
}


class Dog extends Animal {
  static tt = 'pp';
  constructor(name) {
    super(name);
  }
}

console.log(Dog.tt); // pp
console.log(Dog.test); // ttt

当初始化字段时,类主体中的this指向的是类的构造函数(前面验证过静态方法中的this也是指向类的构造函数)。

class Animal {
  static test = 'ttt';
  static testOne = this.test; // 这里this指向Animal
  constructor(name) {
    this.name = name;
    this.test = 'instance';
  }
}

class Dog extends Animal {
  static tt = 'pp';
  constructor(name) {
    super(name);
    console.log(super.test); // undefined 此时super指向的是父类的原型Animal.prototype
  }
}

var dog = new Dog('pubby');
console.log(Dog.testOne); // ttt

公有实例字段

公有实例字段是声明实例属性的另外一种方式。可以在类的构造函数之前声明,也可以在类的构造函数之后声明。
注意,如果在类的构造函数之后声明公有实例字段,必须先在构造函数中调用父类的构造函数 -- super。

class Animal {
  vv = 'vv';
}

class Dog extends Animal {
  oo = 'oo';
}
var animal = new Animal();
var dog = new Dog();
console.log(dog.oo); // oo
console.log(animal.vv); // vv
// 下面这种写法会报错。必须在构造函数中调用super()后才可以正常运行

class Animal {
  vv = 'vv';
}

class Dog extends Animal {
  oo = 'oo'; 
  constructor() {}
}
var dog = new Dog();
console.log(dog.oo);
class Animal {
  vv = 'vv';
}

class Dog extends Animal {
  oo = super.test;
  constructor(name) {
    super();// super此时是父类的构造函数
  }
  xxx = 'xxx'; // 如果不先调用super()就声明xxx,将会报错
}
var dog = new Dog();
console.log(dog.xxx); // xxx

公有实例字段如果只声明而不赋值则会初始化为undefined。

class Animal {
  vv;
}
var animal = new Animal();
console.log(animal.vv); // undefined

公有实例字段也可以由计算得出:

var str = 'Person';
class Person {
  ['the' + str] = 'Jack'; 
}
var person = new Person();
console.log(person.thePerson); // Jack

下面看一个复杂点的例子,注意例子中的this和super:

class Animal {
  fatherClass = 'Animal';
  copyFatherClass = this.fatherClass;
}
Animal.prototype.childClass = 'i am a child';

class Dog extends Animal {
  childClass = super.childClass;
}
var animal = new Animal();
var dog = new Dog();
console.log(animal.copyFatherClass); // Animal
console.log(dog.childClass); // i am a child

使用super给公有实例字段赋值,super指向父类的原型。

公共方法

静态方法

在类中定义的方法都存在于原型上,都会被实例继承。但也有例外。
如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
不能通过一个类的实例调用静态方法。静态方法通常用于为一个应用程序创建工具函数。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  static distance(a, b) {
    return a * b;
  }
}

const p1 = new Point(5, 5);
console.log(Point.distance(10,20)); // 200
console.log(p1.distance(10,20)); // TypeError: p1.distance is not a function

如果静态方法包含this关键字,这个this指的是类本身(构造函数),而不是实例。

class Foo {
  static oneFun() {
    this.baz(); // 在静态方法内,this指向类本身,而不是类的实例
  }
  static baz() {
    console.log('hello'); // 静态方法可以和实例方法重名,而不会覆盖实例方法
  }
  baz() {
    console.log('world');
  }
}

Foo.oneFun() // hello 

父类的静态方法可以被子类继承:

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

class Bar extends Foo {
}

Bar.classMethod() // 'hello'
公共实例方法
class Animal {
  say(str) {
    return str; 
  }
}

上面代码中的say方法就是一个公共实例方法。
公共实例方法是定义在类的原型上的。

getter和setter

getter和setter是和类的属性绑定的特殊方法,分别会在其绑定的属性被取值、赋值时调用。
使用get和set句法定义实例的公共getter和setter。

// MDN上的例子
class ClassWithGetSet {
  #msg = 'hello world'; // 这是一个私有字段
  get msg() {
    return this.#msg;
  }
  set msg(x) {
    this.#msg = `hello ${x}`;
  }
}

const instance = new ClassWithGetSet();
console.log(instance.msg); // hello worl​d

instance.msg = 'cake';
console.log(instance.msg); // hello cake

console.log(instance.hasOwnProperty('msg')); // false

下面的例子是《ECMAScript 6 入门》里的,解释了类中getter和setter的本质:

class CustomHTMLElement {
  constructor(element) {
    this.element = element;
  }

  get html() {
    return this.element.innerHTML;
  }

  set html(value) {
    this.element.innerHTML = value;
  }
}

var descriptor = Object.getOwnPropertyDescriptor(
  CustomHTMLElement.prototype, "html"
);

"get" in descriptor  // true
"set" in descriptor  // true

公共实例方法也可以是一个Generator函数。

私有字段

声明私有字段的时候需要加上#前缀。
例如:
#test是一个私有字段,#与后面的test是一个整体,使用的时候要写完整。
也可就是说#test与test是完全不同的两个字段。

静态私有字段

静态私有字段需要使用static关键字声明;

class Animal {
  static #test = 'doom';

  ppp() {
    return Animal.#test;
  }
}
var animal = new Animal();
console.log(animal.ppp()); // 'doom'

静态私有字段必须在声明他的类的内部使用(在哪个类内部声明,在哪个类的内部使用,不参与继承):

// 虽然子类继承了父类的静态方法,但是并没有继承父类的静态私有属性,也不能使用他
class BaseClassWithPrivateStaticField {
  static #PRIVATE_STATIC_FIELD;

  static basePublicStaticMethod() {
    this.#PRIVATE_STATIC_FIELD = 42;
    return this.#PRIVATE_STATIC_FIELD;
  }
}

class SubClass extends BaseClassWithPrivateStaticField { };
console.log(SubClass.basePublicStaticMethod());// 报错:Cannot write private member #PRIVATE_STATIC_FIELD to an object whose class did not declare it

私有实例字段

声明私有实例字段要使用#前缀,不使用static关键字。
私有实例字段也只能在类的内部使用。

class Animal {
  #test = 'test';
  say() {
    console.log(this.#test);
  }
}

var animal = new Animal();
animal.say();
console.log(animal.#test); // SyntaxError

私有方法

私有方法的内容目前还是提案,在node 12版本中还不支持

静态私有方法

静态私有方法使用static关键字进行声明,需要添加#前缀。
静态私有方法与静态方法一样,只能通过类本身调用,并且只能在类的声明主体中使用:

class Animal {
  static #test() {
    console.log('static #test call');
  }
  ttt() {
    Animal.#test();
  }
}
var animal = new Animal();
animal.ttt();
私有实例方法

私有实例方法在类的实例中可用,它的访问方式的限制和私有实例字段相同。

class ClassWithPrivateMethod {
  #privateMethod() {
    return 'hello world';
  }

  getPrivateMessage() {
      return #privateMethod();
  }
}

const instance = new ClassWithPrivateMethod();
console.log(instance.getPrivateMessage());
// 预期输出值: "hello worl​d"

私有实例方法可以是生成器、异步或者异步生成器函数。私有getter和setter也是可以的:

class ClassWithPrivateAccessor {
  #message;

  get #decoratedMessage() {
    return `✨${this.#message}✨`;
  }
  set #decoratedMessage(msg) {
    this.#message = msg;
  }

  constructor() {
    this.#decoratedMessage = 'hello world';
    console.log(this.#decoratedMessage);
  }
}

new ClassWithPrivateAccessor();
// 预期输出值: "✨hello worl​d✨"

参考资料

  1. ECMAScript 6 入门
posted @ 2020-05-09 16:40  Fogwind  阅读(228)  评论(0编辑  收藏  举报