可以认为其为一个语法糖,本质为 new Function 的语法。

旨在使内容更易阅读,但不引入任何新内容的语法

Class 基本语法

语法

class User {

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

  sayHi() {
    console.info(this.name);
  }

}

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

new User("John") 意味着:

  • 一个新对象被创建
  • constructor 使用给定参数运行,完成赋值

注意!
类方法之间不需要添加逗号

Class 本质

JavaScript 中,其本质为函数 Function

class User {
  constructor(name) { this.name = name; }
  sayHi() { console.info(this.name); }
}

// 佐证:User 是一个函数
console.info(typeof User); // function

做了两件事情:

  1. 创建名为 User 的函数。成为类声明结果
  2. 将类中方法全部挂载到函数的 prototype

因此调用方法时,会从原型上获取这些函数。以下与使用 Class 效果一致:

// 用纯函数重写 class User

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

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

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

原型链检验:

class User {
  constructor(name) { this.name = name; }
  sayHi() { console.info(this.name); }
}

// class 是一个函数
console.info(typeof User); // function

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

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

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

ClassFunction 区别

  1. Class 创建函数具有内部标记;[[IsClassConstructor]]: true
    JavaScript 会对该属性进行检查,使用 class 必须使用 new 创建

    class User {
      constructor() {}
    }
    
    console.info(typeof User); // function
    User(); // Error: Class constructor User cannot be invoked without 'new'
    
    // 进行字符串显示时,也是展示 class
    console.info(`${User}`); // class User { ... }
    
  2. 类方法不可枚举:prototype 中的所有方法都是不可枚举的。

  3. 类中总是使用 use strict 严格模式。

类表达式(类似于命名函数表达式 NFE

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

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

console.info(MyClass); // error,MyClass 在外部不可见

Getters/Setters 与 计算属性

class User {

  constructor(name) {
    // 调用 setter
    this.name = name;
  }
  // 允许设置访问器属性
  get name() {
    return this._name;
  }

  set name(value) {
    if (value.length < 4) {
      console.info("Name is too short.");
      return;
    }
    this._name = value;
  }
  // 计算属性
  ['say' + 'Hi']() {
    console.info("Hello");
  }
}

let user = new User("John");
console.info(user.name); // John
// 调用计算属性
user.sayHi(); // Hello
user = new User(""); // Name is too short.

类字段

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

class User {
  name = "John";
}

let user = new User();
console.info(user.name); // John
console.info(User.prototype.name); // undefined

类字段实现绑定:

正常情况下会丢失 this

class Button {
  constructor(value) {
    this.value = value;
  }

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

let button = new Button("hello");

setTimeout(button.click, 1000); // undefined

使用类字段进行避免

class Button {
  constructor(value) {
    this.value = value;
  }
  click = () => {
    console.info(this.value);
  }
}

let button = new Button("hello");

setTimeout(button.click, 1000); // hello

类继承

抽取共性成为基类,子类基于基类进行编写,用于子类能力扩展。

继承关键字 extends

扩展语法:class Child extends Parent

子类继承基类后,可以调用基类方法:

// 基类
class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  run(speed) {
    this.speed = speed;
    console.info(`${this.name} runs with speed ${this.speed}.`);
  }
  stop() {
    this.speed = 0;
    console.info(`${this.name} stands still.`);
  }
}

// 子类
class Rabbit extends Animal {
  hide() {
    console.info(`${this.name} hides!`);
  }
}

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

rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.hide(); // White Rabbit hides!

调用规则:

  1. 查找当前对象中是否存在此方法,有则调用
  2. 否则进入当前对象原型中递归查找,直到查找到对应方法进行调用
  3. 递归至原型链最顶端后依旧未查询到,进行异常报错

子类中调用基类方法

重写方法:

在子类中实现 stop 后自动 hide

// 子类
class Rabbit extends Animal {
  hide() {
    console.info(`${this.name} hides!`);
  }
  stop() {
    super.stop(); // 调用父类的 stop (*)
    this.hide(); // 然后 hide
  }
}

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

rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.stop(); // White Rabbit stands still. White Rabbit hides!

箭头函数没有 super,会自动从外部环境查找

// 正常调用,外部 stop 与 super.stop 一致
class Rabbit extends Animal {
  stop() {
    setTimeout(() => super.stop(), 1000); // 1 秒后调用父类的 stop
  }
}

使用普通函数就会错误

// 未知的 super 环境
setTimeout(function() { super.stop() }, 1000);

高级编程:继承表达式

运行依据条件实现继承不同的基类

function makeClass(condition) {
  if (condition) {
    return class {
      sayHi() { console.info(`This is ${condition} class`); }
    };
  }
  else {
    return class {
      sayHi() { console.info(`This is another ${condition} class`); }
    };
  }
  
}
// 可以根据许多条件使用函数生成类,并继承它们
class ClassByCondition extends makeClass(true) {}

new ClassByCondition().sayHi();

重写构造函数

默认会在子类中生成空构造函数

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

当需要自己定义构造函数时,必须最先调用基类构造函数
派生构造器具有特殊的内部属性 [[ConstructorKind]]:"derived"

此标签会影响子类的 new 行为:

  • 当通过 new 执行一个常规函数时,它将创建一个空对象,并将这个空对象赋值给 this
  • 当继承的 constructor 执行时,它不会执行此操作。它期望父类的 constructor 来完成这项工作。
class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
}

class Rabbit extends Animal {
  constructor(name, earLength) {
    // 调用基类构造函数
    super(name);
    this.earLength = earLength;
  }
}

let rabbit = new Rabbit("White Rabbit", 10);
console.info(rabbit.name); // White Rabbit
console.info(rabbit.earLength); // 10

重写类字段

当在子类中对基类字段进行重写时,不能直接在子类中使用类字段进行覆盖,如下:

class Animal {
  name = 'animal';

  constructor() {
    console.info(this.name); // (*)
  }
}

class Rabbit extends Animal {
  name = 'rabbit';
}

let animal = new Animal();
let rabbit = new Rabbit();

console.innfo(animal.name); // animal
console.innfo(rabbit.name); // animal

看到,即使重写了类字段,但依旧使用了基类的字段数据,原因在于类字段的初始化顺序:

  • 对于基类,构造函数调用前完成初始化
  • 对于子类/派生类,在 super 后即可初始化

因此,子类无自己的构造器时,相当于执行了基类的构造器 super 方法,
因此基类的字段会被最开始初始化,从而导致重名字段已被使用,
最好的字段初始化是在子类的构造器中完成

深入:内部探究与 [[HomeObject]]

本章属进阶内容。
揭示 super 背后的真实机制与常见陷阱。

super 为什么不能靠 this.__proto__

1. 朴素思路(行不通)
let animal = { eat() { console.log(`${this.name} eats.`) } };
let rabbit = {
  __proto__: animal,
  name: 'Rabbit',
  eat() { this.__proto__.eat.call(this) }  // 想象成"super"
};
rabbit.eat(); // Rabbit eats.   ← 一层继承时看似正常

再嵌套一层就崩:

let longEar = { __proto__: rabbit, name: 'Long Ear', eat() { this.__proto__.eat.call(this) } };
longEar.eat(); // RangeError: Maximum call stack size exceeded

原因

  • 方法内部的 this 始终是最终调用者longEar)。
  • 于是 this.__proto__ 永远指向 rabbitrabbit.eat 又回调自己 → 无限递归。
  • 仅靠 this 无法沿着原型链"向上"走一步

[[HomeObject]] —— 真正的钥匙

1. 定义

当函数以方法语法method() {} 或类方法)创建时,引擎为其写入不可变的内部属性 [[HomeObject]],值为所属对象本身

因此函数是自由的,但方法是被限制的,它与对象进行了绑定,不能进行传递。

普通函数方法
function f() {}[[HomeObject]] 未定义
const obj = { m() {} }m.[[HomeObject]] === obj
2. super 的执行步骤
  1. 取出当前方法的 [[HomeObject]](记为 home)。
  2. 沿 home.[[Prototype]] 查找同名方法 → 即为父级方法。
  3. 用当前 this 调用父方法。

因此 super 与运行时 this 无关,只与"定义时所属对象"有关

3. 正确示范(三层继承)
let animal = { eat() { console.log(`${this.name} eats.`) } };
let rabbit   = { __proto__: animal, name: 'Rabbit',   eat() { super.eat() } };
let longEar  = { __proto__: rabbit, name: 'Long Ear', eat() { super.eat() } };

longEar.eat(); // Long Ear eats.   ← 无栈溢出
4. 复制方法会"带错家"
let plant = { sayHi() { console.log("I'm a plant") } };
let tree  = { __proto__: plant, sayHi: rabbit.sayHi }; // 复制自 rabbit
tree.sayHi(); // I'm an animal (?!)

解释rabbit.sayHi.[[HomeObject]] 固定为 rabbitsuper 仍从 rabbit 向上找 → 找到 animal.sayHi,与 plant 无关。

方法 vs 函数属性 —— 有无 [[HomeObject]] 的分界线

1. 方法语法(有 [[HomeObject]]
const obj = {
  m() { super.x() }        // ← OK
};
2. 函数属性语法(无 [[HomeObject]]
const obj = {
  m: function() { super.x() }  // ← SyntaxError: 'super' outside of method
};

简记:只要写成 method() {} 或类方法,就被视为方法;写成 key: function 或箭头函数,都不是方法。

小结要点

特性说明
super 不靠 this[[HomeObject]] 定位原型链上级。
[[HomeObject]] 不可改方法一旦创建,终身"记住"所属对象。
复制/赋值方法时要小心带回家对象,可能使 super 指向错误原型。
只用方法语法method() {} 或类方法,才能使用 super

一句话

super 的"上级"取决于方法定义时所在的对象[[HomeObject]]),与最终谁来调用、运行时 this 是谁都无关。

静态属性与静态方法

在方法和属性前添加 static 即可将其变为静态属性/方法。
静态属性/方法是属于整个类,并非某一个实例对象
在实例对象上无法调用这些方法和属性。

静态属性和方法都允许进行继承,在子类中进行调用,

  • 子类 [[Prototype]] 指向基类函数
  • 子类 prototype 继承自基类的 prototype

因此基类原型中存在的方法都可在子类中调用

静态方法

class Article {
  constructor(title, date) {
    this.title = title;
    this.date = date;
  }
  // 静态比较方法
  static compare(articleA, articleB) {
    return articleA.date - articleB.date;
  }
}

// 用法
let articles = [
  new Article("HTML", new Date(2019, 1, 1)),
  new Article("CSS", new Date(2019, 0, 1)),
  new Article("JavaScript", new Date(2019, 11, 1))
];

// 方法传入
articles.sort(Article.compare);

console.info( articles[0].title ); // CSS

有点相当于直接在类上增加一个属性,其功能一致

Article.compare =  function(articleA, articleB) {
  return articleA.date - articleB.date;
}

或者是类中内建工厂方法

class Article {
  constructor(title, date) {
    this.title = title;
    this.date = date;
  }

  static createTodays() {
    // 记住 this = Article
    return new this("Today's digest", new Date());
  }
}
// 直接从类中调用工厂方法快速创建对象
let article = Article.createTodays();

console.info( article.title ); // Today's digest

静态属性

class Article {
  static publisher = "Levi Ding";
}

console.info( Article.publisher ); // Levi Ding

相当于

Article.publisher = "Levi Ding";

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

在面向对象 OOP 的编程中,属性和方法分为两组,被称为封装

  • 内部接口:可以通过该类的其他方法访问,但不能从外部访问的方法和属性。
  • 外部接口:可以从类的外部访问的方法和属性。

JavaScript 中,有两种类型的对象字段(属性和方法):

  • 公共的:可从任何地方访问。它们构成了外部接口。到目前为止,我们只使用了公共的属性和方法。
  • 私有的:只能从类的内部访问。这些用于内部接口。

受保护属性

还有一种受保护的属性,但在 JavaScript 中并没有语言层面的限制,而是一种社区规范:
采用下划线 _ 作为前缀表示受保护属性,这是一个众所周知的约定,即不应该从外部访问此类型的属性和方法。

class CoffeeMachine {
  _waterAmount = 0;
  // 大多数情况下应该提供
  // getWaterAmount() 和 setWaterAmount()
  // 这种方法,而不是使用 set/get
  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

对于只读的受保护属性,可以在构造函数中初始化,只提供 get 方法

class CoffeeMachine {
  constructor(power) {
    this._power = power;
  }
  // 大多数情况下应该提供
  // getPower()
  // 这种方法,而不是使用 set/get
  get power() {
    return this._power;
  }
}

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

console.info(`Power is: ${coffeeMachine.power}W`); // 功率是:100W

coffeeMachine.power = 25; // Error(没有 setter)

私有属性

使用 # 作为前缀,只在类的内部通过 this. 进行访问,无法通过 this[key] 进行访问。

私有属性在子类中也无法被直接访问。
也只能依靠提供的访问器接口。

class MegaCoffeeMachine extends CoffeeMachine {
  method() {
    // Error: can only access from CoffeeMachine 
    console.info( this.#waterAmount ); 
  }
}
class CoffeeMachine {
  #waterLimit = 200;

  #fixWaterAmount(value) {
    if (value < 0) return 0;
    if (value > this.#waterLimit) return this.#waterLimit;
  }
  // 因此需要给外部提供一个访问器接口
  setWaterAmount(value) {
    this.#waterLimit = this.#fixWaterAmount(value);
  }
}

let coffeeMachine = new CoffeeMachine();

// 不能从类的外部访问类的私有属性和方法
coffeeMachine.#fixWaterAmount(123); // Error
coffeeMachine.#waterLimit = 1000; // Error

扩展内建类

扩展能力

允许对 ArrayMap 等内建类进行扩展。

// 给 PowerArray 新增了一个方法(可以增加更多)
class PowerArray extends Array {
  isEmpty() {
    return this.length === 0;
  }
}

let arr = new PowerArray(1, 2, 5, 10, 50);
console.info(arr.isEmpty()); // false

let filteredArr = arr.filter(item => item >= 10);
console.info(filteredArr); // 10, 50
console.info(filteredArr.isEmpty()); // false

可以发现,内建类的方法 filtermap 等返回的正是子类 PowerArray 的新对象。
这是借助对象的 constructor 来实现的。

arr.filter() 被调用时,内部使用的是 arr.constructor 来创建新的结果数组,而不是使用原生的 Array,因此新对象可以依旧使用扩展的方法。

控制内建类方法返回的对象类型

特殊的静态 getter Symbol.species:它会返回 JavaScript 在内部用来在 mapfilter 等方法中创建新实体的 constructor

class PowerArray extends Array {
  isEmpty() {
    return this.length === 0;
  }

  // 内建方法将使用这个作为 constructor
  static get [Symbol.species]() {
    return Array;
  }
}

let arr = new PowerArray(1, 2, 5, 10, 50);
console.info(arr.isEmpty()); // false

// filter 使用 arr.constructor[Symbol.species] 作为 constructor 创建新数组
let filteredArr = arr.filter(item => item >= 10);

// filteredArr 不是 PowerArray,而是 Array
console.info(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function

内建类无静态方法继承

已知:

  • 内建对象有自己的静态方法 - Object.keysArray.isArray
  • 原生类互相扩展 - Array 扩展自 Object

通常类之间继承扩展时会继承静态方法和非静态方法,但内建类除外。

原因为:

  • ArrayDate 都继承自 Object,因此实例都具有来自 Object.prototype 方法
  • Array.[[Prototype]] 并不指向 Object 不指向 Object,因此不存在 Array.keys 等方法。

ArrayObject 之间无直接连接,是独立的,
只是 Array.prototype 继承自 Object.prototype

这是内建对象继承与 extends 继承的一个重要区别。

类检查:"instanceof"

instanceof 操作符

语法

obj instanceof Class

作用
判断 obj 是否隶属于 Class 或其衍生类(沿原型链逐级比对)。

示例

class Rabbit {}
console.log(new Rabbit() instanceof Rabbit); // true

function Animal() {}
console.log(new Animal() instanceof Animal); // true

const arr = [1, 2, 3];
console.log(arr instanceof Array);  // true
console.log(arr instanceof Object); // true (Array 继承 Object)

1.1 算法步骤(简化版)

  1. 若存在静态方法 Class[Symbol.hasInstance],直接调用它并返回结果。
  2. 否则沿原型链查找:
    obj.__proto__ === Class.prototype ?
    obj.__proto__.__proto__ === Class.prototype ?
    任意一步匹配即返回 true,到达链尾返回 false

自定义 Symbol.hasInstance 示例

class Animal {
  static [Symbol.hasInstance](obj) {
    return obj.canEat === true;
  }
}
console.log({ canEat: true } instanceof Animal); // true

1.2 改动 prototype 会导致类别脱离

function Rabbit() {}
const rabbit = new Rabbit();
Rabbit.prototype = {}; // 原型被整体替换
console.log(rabbit instanceof Rabbit); // false

检查只与当前 Class.prototype 有关,构造函数本身不参与比对。

{}.toString —— 强化版 typeof

2.1 基本用法

利用转发 call 完成

const toString = Object.prototype.toString;

toString.call(123);        // [object Number]
toString.call(null);       // [object Null]
toString.call(undefined);  // [object Undefined]
toString.call([1, 2]);     // [object Array]
toString.call({});         // [object Object]
toString.call(console.info);      // [object Function]

2.2 自定义标签 Symbol.toStringTag

class User {
  [Symbol.toStringTag] = 'User';
}
console.log({}.toString.call(new User())); // [object User]

2.3 常见内建标签

console.log(globalThis[Symbol.toStringTag]);              // Window
console.log(XMLHttpRequest.prototype[Symbol.toStringTag]); // XMLHttpRequest

类型检查方法对照表

方法适用场景返回值
typeof原始类型string
{}.toString.call原始类型 + 内建对象 + 自定义标签对象string
instanceof对象层级(含继承)boolean

口诀

  • 只想知道"大体类型" → typeof
  • 需要精准字符串标签(含内建/自定义)→ {}.toString.call
  • 必须考虑继承层级 → instanceof

Mixin

JavaScript 单继承体系下,Mixin 提供了一种"多行为组合"方案:
通用功能打包成纯对象(或函数),合并到目标类/对象中,无需继承即可复用代码。

核心概念

特点说明
无继承通过 Object.assign / 手动拷贝 方法 到目标原型或实例。
可组合一个类可混入多个 Mixin;一个 Mixin 也可被多个类使用。
可嵌套Mixin 内部允许继承其他 Mixin__proto__extends)。
不实例化Mixin 通常不 new,仅作为"工具包"存在。

最简实现:对象级别合并

// 1. 定义 Mixin
const sayHiMixin = {
  sayHi()   { console.log(`Hi ${this.name}`); },
  sayBye()  { console.log(`Bye ${this.name}`); }
};

// 2. 任意类
class User {
  constructor(name) { this.name = name; }
}

// 3. 混入(拷贝到原型)
Object.assign(User.prototype, sayHiMixin);

// 4. 使用
new User('Alice').sayHi();  // Hi Alice

可同时继承另一个类:
class User extends Person { ... }
Object.assign(User.prototype, sayHiMixin);

Mixin 内部继承(嵌套 Mixin

const sayMixin = {
  say(phrase) { console.log(phrase); }
};

const sayHiMixin = {
  __proto__: sayMixin,          // 继承 sayMixin
  sayHi()   { super.say(`Hi ${this.name}`); },
  sayBye()  { super.say(`Bye ${this.name}`); }
};

Object.assign(User.prototype, sayHiMixin);

关键点

  • sayHiMixin 的方法 [[HomeObject]] 固定为 sayHiMixin
  • super.say 沿 sayHiMixin.[[Prototype]] 查找,与最终类无关

实战:EventMixin —— 让任何对象都能发布/订阅事件

const eventMixin = {
  on(name, handler) {
    if (!this._eh) this._eh = {};
    (this._eh[name] ||= []).push(handler);
  },

  off(name, handler) {
    const h = this._eh?.[name];
    if (h) h.splice(h.indexOf(handler), 1);
  },

  trigger(name, ...args) {
    this._eh?.[name]?.forEach(fn => fn.apply(this, args));
  }
};

用法

class Menu {
  choose(value) { this.trigger('select', value); }
}
Object.assign(Menu.prototype, eventMixin);

const menu = new Menu();
menu.on('select', v => console.log('Selected:', v));
menu.choose('123'); // Selected: 123

优点

  • 零继承入侵:原继承链保持不变。
  • 多态:任何类/对象都可混入,复用同一套事件逻辑。

最佳实践与注意点

建议原因
优先使用 Object.assign 拷贝方法避免共享引用,支持多类独立状态。
Mixin 方法内慎用 super确保 [[HomeObject]] 指向预期 Mixin,避免复制后 super 找错原型。
状态字段加前缀(如 _eh防止与用户字段冲突。
复杂状态用 Map/Set 替代对象避开 __proto__ 特殊键问题。
不要把 Mixin 当类用new Mixin(),保持"工具包"语义。

总结

Mixin = 行为乐高
把可复用的"能力块"通过简单合并插到任何类/对象上,
让单继承的 JavaScript 也能享受"多行为组合"的灵活与简洁。