Skip to content

基本语法

在面向对象的编程中,class 是用于创建对象的可扩展的程序代码模版,它为对象提供了状态(成员变量)的初始值和行为(成员函数或方法)的实现。

基本语法:

javascript
class MyClass {
  constructor() { ... }
  method1() { ... }
  method2() { ... }
  method3() { ... }
  ...
}

然后使用 new MyClass() 来创建具有上述列出的所有方法的新对象。new 会自动调用 constructor() 方法,因此我们可以在 constructor() 中初始化对象。

例如

javascript
class User {
  constructor(name) {
	this.name = name;
  }
  
  sayHi() { 
	  alert(this.name); 
  }
}

// class 是一个函数
alert(typeof User); // function

// ...或者,更确切地说,是 constructor 方法
alert(User === User.prototype.constructor); // true

// 方法在 User.prototype 中,例如:
alert(User.prototype.sayHi); // sayHi 方法的代码

// 在原型中实际上有两个方法
alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi

Pasted image 20250626231602.png

人们常说 class 是一个语法糖(旨在使内容更易阅读,但不引入任何新内容的语法),因为我们实际上可以在不使用 class 的情况下声明相同的内容:

javascript
// 用纯函数重写 class User

// 1. 创建构造器函数
function User(name) {
  this.name = name;
}
// 函数的原型(prototype)默认具有 "constructor" 属性,
// 所以,我们不需要创建它

// 2. 将方法添加到原型
User.prototype.sayHi = function() {
  alert(this.name);
};

// 用法:
let user = new User("John");
user.sayHi();

这个定义的结果与使用类得到的结果基本相同。因此,这确实是将 class 视为一种定义构造器及其原型方法的语法糖的理由。

尽管,它们之间存在着重大差异:

  1. 首先,通过 class 创建的函数具有特殊的内部属性标记 [IsClassConstructor](IsClassConstructor.md): true。因此,它与手动创建并不完全相同。
  2. 类方法不可枚举。 类定义将 "prototype" 中的所有方法的 enumerable 标志设置为 false。这很好,因为如果我们对一个对象调用 for..in 方法,我们通常不希望 class 方法出现。
  3. 类总是使用 use strict。 在类构造中的所有代码都将自动进入严格模式。

就我的个人理解来说,就是在语法糖的基础上加入了一些新的特性,基本上就差不多


类表达式

就和函数表式一样:

javascript
let User = class {
  sayHi() {
    alert("Hello");
  }
};

类似于命名函数表达式(Named Function Expressions),类表达式可能也应该有一个名字。 如果类表达式有名字,那么该名字仅在类内部可见:

javascript
// “命名类表达式(Named Class Expression)”
// (规范中没有这样的术语,但是它和命名函数表达式类似)
let User = class MyClass {
  sayHi() {
    alert(MyClass); // MyClass 这个名字仅在类内部可见
  }
};

new User().sayHi(); // 正常运行,显示 MyClass 中定义的内容

alert(MyClass); // error,MyClass 在外部不可见

Getters/setters

就像对象字面量,类可能包括 getters/setters,计算属性(computed properties)等。 这是一个使用 get/set 实现 user.name 的示例:

javascript
class User {

  constructor(name) {
    // 调用 setter
    this.name = name;
  }

  get name() {
    return this._name;
  }

  set name(value) {
    if (value.length < 4) {
      alert("Name is too short.");
      return;
    }
    this._name = value;
  }

}

let user = new User("John");
alert(user.name); // John

user = new User(""); // Name is too short.

从技术上来讲,这样的类声明可以通过在 User.prototype 中创建 getters 和 setters 来实现。

计算属性名称

javascript
class User {

  ['say' + 'Hi']() {
    alert("Hello");
  }

}

new User().sayHi();

“类字段”

是一种允许添加任何属性的语法。 例如,让我们在 class User 中添加一个 name 属性:

javascript
class User {
  name = "John";

  sayHi() {
    alert(`Hello, ${this.name}!`);
  }
}

new User().sayHi(); // Hello, John!

类字段的重要区别在于,它们会被挂在实例对象上,而非 User.prototype 上

正如 函数绑定 一章中所讲的,JavaScript 中的函数具有动态的 this。它取决于调用上下文。 因此,如果一个对象方法被传递到某处,或者在另一个上下文中被调用,则 this 将不再是对其对象的引用。 类字段提供了一种很优雅的写法:

javascript
class Button {
  constructor(value) {
    this.value = value;
  }
  click = () => {
    alert(this.value);
  }
}

let button = new Button("hello");

setTimeout(button.click, 1000); // hello
javascript
class Button {
    constructor(value) {
        this.value = value;
    }

    click() {
        console.log(this.value);
    }
}

let button = new Button("hello");

setTimeout(button.click, 1000); // undefined
setTimeout(() => button.click(), 1000);//hello

类继承

继承另一个类的语法是:class Child extends ParentPasted image 20250626234203.png 在内部,关键字 extends 使用了很好的旧的原型机制进行工作。它将 Rabbit.prototype.[Prototype](Prototype.md) 设置为 Animal.prototype。所以,如果在 Rabbit.prototype 中找不到一个方法,JavaScript 就会从 Animal.prototype 中获取该方法。

类语法不仅允许指定一个类,在 extends 后可以指定任意表达式

重写方法

"super" 关键字。

  • 执行 super.method(...) 来调用一个父类方法。
  • 执行 super(...) 来调用一个父类 constructor(只能在我们的 constructor 中)。
javascript
class Animal {
    constructor(name) {
        this.name = name;
    }

    run() {
        console.log('animal run');
    }

}

class Rabbit extends Animal {
    run() {
        super.run();//animal run
        console.log('rabbit run');//rabbit run
    }
}

let rabbit = new Rabbit("White Rabbit");

rabbit.run();

箭头函数没有 super。如果被访问,它会从外部函数获取。

如果一个类扩展了另一个类并且没有 constructor,那么将生成下面这样的“空” constructor

javascript
class Rabbit extends Animal {
  // 为没有自己的 constructor 的扩展类生成的
  constructor(...args) {
    super(...args);
  }
}

继承类的 constructor 必须调用 super(...),并且 (!) 一定要在使用 this 之前调用。

在 JavaScript 中,继承类(所谓的“派生构造器”,英文为 “derived constructor”)的构造函数与其他函数之间是有区别的。派生构造器具有特殊的内部属性 [ConstructorKind](ConstructorKind.md):"derived"。这是一个特殊的内部标签。

该标签会影响它的 new 行为:

  • 当通过 new 执行一个常规函数时,它将创建一个空对象,并将这个空对象赋值给 this
  • 但是当继承的 constructor 执行时,它不会执行此操作。它期望父类的 constructor 来完成这项工作。

因此,派生的 constructor 必须调用 super 才能执行其父类(base)的 constructor,否则 this 指向的那个对象将不会被创建。并且我们会收到一个报错。

为了让 Rabbit 的 constructor 可以工作,它需要在使用 this 之前调用 super(),就像下面这样:重写constructor

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

    run() {
        console.log('animal run');
    }

}

class Rabbit extends Animal {
    constructor(name,age) {
        super(name);
        this.age =age;
}

    run() {
        super.run();//animal run
        console.log('rabbit run');//rabbit run
    }
}

let rabbit = new Rabbit("White Rabbit",21);

console.log(rabbit);//Rabbit { name: 'White Rabbit', age: 21 }

我们不仅可以重写方法,还可以重写类字段。 不过,当我们在父类构造器中访问一个被重写的字段时,有一个诡异的行为,这与绝大多数其他编程语言都很不一样。

当父类构造器在派生的类中被调用时,它会使用被重写的方法。但是父类构造器总是会使用它自己字段的值,而不是被重写的那一个。

javascript

没看完。


静态属性和静态方法

它们以 static 关键字开头,静态方法可以在类上调用,而不是在单个对象上。

javascript
class User {
  static staticMethod() {
    alert(this === User);
  }
}

User.staticMethod(); // true

他们本质是一样的

javascript
class User { }

User.staticMethod = function() {
  alert(this === User);
};

User.staticMethod(); // true

它不是对象的方法,而是整个 class 的方法。 静态的属性是一样的。静态属性和方法是可被继承的。


受保护的、私有的属性和方法

受保护的属性通常以下划线 _ 作为前缀。

javascript
class CoffeeMachine {
  _waterAmount = 0;

  set waterAmount(value) {
    if (value < 0) {
      value = 0;
    }
    this._waterAmount = value;
  }

  get waterAmount() {
    return this._waterAmount;
  }

  constructor(power) {
    this._power = power;
  }

}

// 创建咖啡机
let coffeeMachine = new CoffeeMachine(100);

// 加水
coffeeMachine.waterAmount = -10; // _waterAmount 将变为 0,而不是 -10

如果属性不想被修改,不设置setter即可

推荐使用者种:

javascript
class CoffeeMachine {
  _waterAmount = 0;

  setWaterAmount(value) {
    if (value < 0) value = 0;
    this._waterAmount = value;
  }

  getWaterAmount() {
    return this._waterAmount;
  }
}

new CoffeeMachine().setWaterAmount(100);

受保护的字段是可以继承的; 如果我们继承 class MegaMachine extends CoffeeMachine,那么什么都无法阻止我们从新的类中的方法访问 this._waterAmount 或 this._power


私有的属性

这是最近加入的新特性。 私有属性和方法应该以 # 开头。它们只在类的内部可被访问。 在语言级别,# 是该字段为私有的特殊标志。我们无法从外部或从继承的类中访问它。

私有字段与公共字段不会发生冲突。我们可以同时拥有私有的 #waterAmount 和公共的 waterAmount 字段。

如果我们继承自 CoffeeMachine,那么我们将无法直接访问 #waterAmount。我们需要依靠 waterAmount getter/setter


扩展内建类(使用内建类做父类)

内建类却是一个例外。它们相互间不继承静态方法。


类检查

用于返回值
typeof原始数据类型
{}.toString原始数据类型,内建对象,包含 Symbol.toStringTag 属性的对象
instanceof对象
正如我们所看到的,从技术上讲,{}.toString 是一种“更高级的” typeof

当我们使用类的层次结构(hierarchy),并想要对该类进行检查,同时还要考虑继承时,这种场景下 instanceof 操作符确实很出色。


Mixin模式

没看