JavaScript 系列 - JavaScript 中的类

90 阅读5分钟

类是用于创建对象的模板。他们用代码封装数据以处理该数据。JS 中的类建立在原型上,但也具有某些语法和语义未与 ES5 类相似语义共享。

实际上,类是“特殊的函数”,就像你能够定义的函数表达式和函数声明一样,类语法有两个组成部分:类表达式类声明

定义类

类声明

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

类表达式

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

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

类体和方法定义

构造函数

一个构造函数可以使用 super 关键字来调用一个父类的构造函数

原型属性和原型方法

class Rectangle {
    // constructor
    constructor(height, width) {
        this.height = height;
        this.width = width;
    }
    // Getter
    get area() {
        return this.calcArea()
    }
    // Method
    calcArea() {
        return this.height * this.width;
    }
}
Rectangle.prototype.prototypeWidth = 25;
const square = new Rectangle(10, 10);

console.log(square.area);
// 100

静态属性和静态方法

static 关键字用来定义一个类的一个静态方法。调用静态方法不需要实例化该类,但不能通过一个类实例调用静态方法。静态方法通常用于为一个应用程序创建工具函数。

静态方法是可编辑、不可枚举和可配置的。

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

    static displayName = "Point";

    static distance(a, b) {
        const dx = a.x - b.x;
        const dy = a.y - b.y;
        return Math.hypot(dx, dy);
    }
}

const p1 = new Point(5, 5);
const p2 = new Point(10,10);
p1.displayName;
// undefined
p1.distance;
// undefined

console.log(Point.displayName);
// "Point"
console.log(Point.distance(p1, p2));
// 7.0710678118654755

this 指向

当调用静态或原型方法时没有指定 this 的值,那么方法内的 this 值将被置为 undefined

  • 构造方法中绑定 this
  • 使用箭头函数
  • Proxy
class Animal {
  speak() {
    return this;
  }
  static eat() {
    return this;
  }
}

let obj = new Animal();
obj.speak(); // Animal {}
let speak = obj.speak;
speak(); // undefined

Animal.eat() // class Animal
let eat = Animal.eat;
eat(); // undefined

实例属性

实例的属性必须定义在类的方法里

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

字段声明

公有字段声明

公有静态字段和公有实例字段都是可编辑、可枚举和可配置的属性。

使用 JavaScript 字段声明语法,上面的示例可以写成:

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

私有字段声明

  • 内部调用 call
class Widget {
  foo(baz) {
    bar.call(this, baz);
  }
}
function bar(baz) {
  return (this.snaf = baz);
}
  • Symbol
const bar = Symbol("bar");
const snaf = Symbol("snaf");
class myClass {
  // 公有方法
  foo(baz) {
    this[bar](baz);
  }
  // 私有方法
  [bar](baz) {
    return (this[snaf] = baz);
  }
}
  • #
class Rectangle {
  #height = 0;
  #width;
  constructor(height, width) {
    this.#height = height;
    this.#width = width;
  }
}
  • in 运算符
    • 指定的属性在指定的对象或其原型链上面
    • 对于Object.create()、Object.setPrototypeOf形成的继承,不会传递私有属性

从作用域之外引用 # 名称、内部在未声明的情况下引用私有字段、或尝试使用 delete 移除声明的字段都会抛出语法错误。它们只能在类里面中读取或写入。通过定义在类外部不可见的内容,可以确保类的用户不会依赖于内部,因为内部可能在不同版本之间发生变化。

特点

  • 方法不可枚举

  • constructor() 方法默认返回实例对象

  • 类必须使用 new 调用

  • 存值函数和取值函数是设置在属性的 Descriptor 对象

    let descriptor = Object.getOwnPropertyDescriptor(PointClass.prototype, "prop");
    "get" in descriptor; // true
    "set" in descriptor; // true
    
  • 属性表达式方法名可以使用表达式

  • 类名不存在提升

使用 extends 扩展子类

extends 关键字在 类声明 或 类表达式 中用于创建一个类作为另一个类的一个子类。

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

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Dog extends Animal {
  constructor(name) {
    super(name); // 调用超类构造函数并传入 name 参数
  }

  speak() {
    console.log(`${this.name} barks.`);
  }
}

var d = new Dog('Mitzie');
d.speak();// 'Mitzie barks.'

如果子类中定义了构造函数,那么它必须先调用 super() 才能使用 this 。

也可以继承传统的基于函数的“类”:

function Animal (name) {
  this.name = name;
}
Animal.prototype.speak = function () {
  console.log(this.name + ' makes a noise.');
}

class Dog extends Animal {
  speak() {
    super.speak();
    console.log(this.name + ' barks.');
  }
}

var d = new Dog('Mitzie');
d.speak();//Mitzie makes a noise.  Mitzie barks.

如果要继承常规对象,可以改用 `Object.setPrototypeOf()

var Animal = {
  speak() {
    console.log(this.name + ' makes a noise.');
  }
};

class Dog {
  constructor(name) {
    this.name = name;
  }
}

Object.setPrototypeOf(Dog.prototype, Animal);// 如果不这样做,在调用 speak 时会返回 TypeError

var d = new Dog('Mitzie');
d.speak(); // Mitzie makes a noise.

使用 super 调用超类

super 关键字用于调用对象的父对象上的函数。

class Cat {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(this.name + ' makes a noise.');
  }
}

class Lion extends Cat {
  speak() {
    super.speak();
    console.log(this.name + ' roars.');
  }
}

Mix-ins / 混入

抽象子类。一个 ECMAScript 类只能有一个单超类,所以想要从工具类来多重继承的行为是不可能的。子类继承的只能是父类提供的功能性。因此,例如,从工具类的多重继承是不可能的。该功能必须由超类提供。

一个以超类作为输入的函数和一个继承该超类的子类作为输出可以用于在 ECMAScript 中实现混合

var calculatorMixin = Base => class extends Base {
  calc() { }
};

var randomizerMixin = Base => class extends Base {
  randomize() { }
};
class Foo { }
class Bar extends calculatorMixin(randomizerMixin(Foo)) { }

new.target 属性

  • 返回 new 命令作用于的那个构造函数
  • 不能独立使用、必须继承后才能使用的类
function Person(name) {
  if (new.target !== undefined) {
    this.name = name;
  } else {
    throw new Error("必须使用 new 命令生成实例");
  }
}
// 另一种写法
function Person(name) {
  if (new.target === Person) {
    this.name = name;
  } else {
    throw new Error("必须使用 new 命令生成实例");
  }
}

var person = new Person("张三"); // 正确
var notAPerson = Person.call(person, "张三"); // 报错

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);
}

Species

你可能希望在派生数组类 MyArray 中返回 Array 对象。这种 species 方式允许你覆盖默认的构造函数。

class MyArray extends Array {
  // Overwrite species to the parent Array constructor
  static get [Symbol.species]() { return Array; }
}
var a = new MyArray(1,2,3);
var mapped = a.map(x => x * x);

console.log(mapped instanceof MyArray);
// false
console.log(mapped instanceof Array);
// true