FORMA

原型与面向对象

JavaScript 以原型链实现对象共享与「继承」;ES6 class 是语法糖。见 语言基础函数进阶

一、原型链

1. [[Prototype]] 内部槽位

每个 JavaScript 对象都有一个内部属性 [[Prototype]],指向另一个对象(或 null)。当访问对象的一个属性时,如果对象自身没有,就会沿着 [[Prototype]] 链条向上查找,直到找到或到达 null(原型链的终点)。

2. prototype 属性

函数(尤其是构造函数)拥有 prototype 属性。当通过 new 调用该函数创建实例时,实例的 [[Prototype]] 会指向构造函数的 prototype 对象。

js
function Person(name) {
    this.name = name;
}
Person.prototype.sayHi = function () {
    console.log(`Hi, I'm ${this.name}`);
};
const p = new Person("Alice");
// p.[[Prototype]] === Person.prototype

3. __proto__ 访问器

__proto__ 是大多数 JavaScript 环境提供的非标准但广泛支持的访问器,用于读写对象的 [[Prototype]]。标准方法推荐使用 Object.getPrototypeOf()Object.setPrototypeOf()

js
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true

4. Object.create(proto, [propertiesObject])

创建一个新对象,并将其 [[Prototype]] 显式指定为第一个参数。

js
const animal = {
    eat() {
        console.log("Eating");
    },
};
const dog = Object.create(animal);
dog.bark = function () {
    console.log("Woof");
};
dog.eat(); // 'Eating' (来自原型)

Object.create(null) 会创建一个没有原型的纯字典对象(适合存储数据,避免属性名冲突)。

原型链示例

js
function Parent() {}
Parent.prototype.method = function () {};

function Child() {}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

const c = new Child();
// 原型链: c -> Child.prototype -> Parent.prototype -> Object.prototype -> null

二、继承方式

1. 原型链继承

子构造函数的 prototype 指向父构造函数的实例。
缺点:所有子实例共享父实例的引用类型属性(如数组);无法向父构造函数传参。

js
function Parent() {
    this.colors = ["red"];
}
function Child() {}
Child.prototype = new Parent();
const c1 = new Child();
c1.colors.push("blue"); // 影响所有实例

2. 构造函数盗用(经典盗用 / call/apply)

在子构造函数内部调用父构造函数,并绑定 this
缺点:不能复用父原型上的方法,每个实例都会创建一份方法副本。

js
function Parent(name) {
    this.name = name;
}
function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}

3. 组合继承(原型链 + 构造函数盗用)

优点:父构造函数执行两次(一次用于 Child.prototype = new Parent(),一次在 Child 内部调用),但解决了传参与属性共享问题。
缺点:效率稍低(父实例的无关属性存在于原型上)。

js
function Parent(name) {
    this.name = name;
}
Parent.prototype.say = function () {};
function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;

4. 寄生组合继承(最优)

避免调用两次父构造函数,使用 Object.create 创建父原型的副本,然后增强该对象。

js
function inheritPrototype(child, parent) {
    const prototype = Object.create(parent.prototype);
    prototype.constructor = child;
    child.prototype = prototype;
}
function Parent(name) {
    this.name = name;
}
function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}
inheritPrototype(Child, Parent);

优点:只调用一次父构造函数,原型链干净,是 ES5 环境下最理想的继承模式。

5. ES6 class 的本质(语法糖)

ES6 的 classextends 底层仍是基于原型链的寄生组合继承。

js
class Parent {
    constructor(name) {
        this.name = name;
    }
}
class Child extends Parent {
    constructor(name, age) {
        super(name);
        this.age = age;
    }
}

class 内部实现了:

  • 构造函数的 prototype 关联
  • 自动调用 super()Parent.call(this)
  • 静态方法挂载在类本身上
  • class 方法定义在 prototype 上且默认不可枚举(与手写 Person.prototype.say = function(){} 默认可枚举不同)

三、类的进阶特性

1. 静态方法(static)

使用 static 关键字定义的方法属于类本身,而非实例。它们通常用于工具函数。

js
class MathUtils {
    static add(a, b) {
        return a + b;
    }
}
console.log(MathUtils.add(2, 3)); // 5
// 实例无法访问静态方法: new MathUtils().add -> TypeError

静态方法内部 this 指向类(而非实例),也可以被 extends 子类继承。

2. 私有字段(#

ES2022 正式引入私有字段,只能在类内部访问,真正实现封装。

js
class Person {
    #age;
    constructor(name, age) {
        this.name = name;
        this.#age = age;
    }
    #privateMethod() {
        return this.#age;
    }
    getAge() {
        return this.#privateMethod();
    }
}
const p = new Person("Bob", 30);
console.log(p.name); // 'Bob'
console.log(p.#age); // SyntaxError
console.log(p.getAge()); // 30

私有字段在编译时静态绑定,不会因为 Object.getOwnPropertyNames 等操作暴露。

3. instanceofSymbol.hasInstance

instanceof 运算符检查对象的原型链中是否包含构造函数的 prototype 属性。

js
[] instanceof Array; // true
[] instanceof Object; // true (因为 Array.prototype 继承自 Object.prototype)

可以通过 Symbol.hasInstance 自定义 instanceof 行为:

js
class MyArray {
    static [Symbol.hasInstance](instance) {
        return Array.isArray(instance);
    }
}
console.log([] instanceof MyArray); // true

四、new 操作符内部步骤

使用 new 调用一个构造函数时,引擎会执行以下 4 个步骤:

  1. 创建一个新的空对象const obj = {};
  2. 设置原型:将新对象的 [[Prototype]] 指向构造函数的 prototype 属性:obj.__proto__ = Constructor.prototype;(内部操作是 Object.setPrototypeOf(obj, Constructor.prototype)
  3. 绑定 this 并执行构造函数:调用 Constructor 并将 this 绑定到新对象上,传递参数:const result = Constructor.apply(obj, args);
  4. 返回对象
    • 如果构造函数返回一个对象(非 null),则返回该对象;
    • 否则返回第 1 步创建的新对象 obj
js
function Person(name) {
    this.name = name;
    return { custom: true }; // 如果返回对象,new 的结果就是这个对象
}
const p = new Person("Alice");
console.log(p); // { custom: true },不是 Person 实例

正常情况下,构造函数应避免显式返回对象,以确保 new 行为符合预期。

总结

  • 原型链是 JavaScript 实现继承的底层机制,通过 [[Prototype]] 串联对象。
  • 各种继承模式从混乱到优雅,最终 ES6 class 提供了清晰的语法,其本质仍然是原型链。
  • 现代 JavaScript 支持静态方法、私有字段等,进一步增强了类的封装性和表达能力。
  • 理解 new 的执行步骤有助于掌握构造函数和对象创建的过程。

掌握这些内容,就能深入理解 JavaScript 的对象系统,写出更健壮、优雅的代码。

参考文献

以下链接在编写时均可正常访问:

资料说明
MDN:继承与原型链原型链
MDN:classES6 类语法
MDN:newnew 运算符
MDN:instanceof类型检测
ECMA-262:对象规范(英文)

Series

js

11 / 14