11. JavaScript 的 prototype, [[Prototype]] 和 __proto__ 晕不晕?

121 阅读5分钟

JavaScript prototype, [[Prototype]]__proto__ 晕不晕?

1 prototype, [[Prototype]]__proto__

名称类型适用对象作用是否推荐使用
prototype函数属性构造函数定义实例共享的原型对象
[[Prototype]]内部属性所有对象表示对象的原型,用于继承不直接操作
__proto__对象属性所有对象[[Prototype]] 的访问接口不推荐

__proto__ 是一个早期引入的属性,现代 JavaScript 中,更推荐使用 Object.getPrototypeOfObject.setPrototypeOf 来操作对象的 原型,而非 直接使用 __proto__

2 什么是原型([[Prototype]])?

在 JavaScript 中,原型 是一个 对象,作为 其他对象的 模板,用于实现 属性方法继承

每个对象 都有一个 内部属性 [[Prototype]](通过 __proto__ 访问),指向它的 原型对象。通过这个机制,对象可以 共享继承 原型链 上的 属性方法

2.1 核心特点

// 示例代码 1
function Person(name) {
    this.name = name;
}
Person.prototype.sayHello = function () { // 所有实例共享同一个函数
    console.log("Hello, I'm " + this.name);
};

const person1 = new Person("Alice");
const person2 = new Person("Bob");

console.log(person1.sayHello === person2.sayHello); // true (同一个函数)
// 示例代码 2
function Person(name) {
    this.name = name;
    this.sayHello = function () { // 每个实例都有一份独立的函数
        console.log("Hello, I'm " + this.name);
    };
}

const person1 = new Person("Alice");
const person2 = new Person("Bob");

console.log(person1.sayHello === person2.sayHello); // false (不同的函数)

观察上述代码(示例代码 1 和 示例代码 2),通过 Person.prototype.sayHello 定义方法,而不是直接在 Person 构造函数 中 定义方法,有以下几个 关键原因

1 节约内存

如果 直接在 构造函数 中 定义方法,那么 每次 创建一个实例 都会 创建一份 新的 sayHello 方法。这会导致 内存浪费,特别是当 实例数量 较多时。

2 方法共享

通过 原型 定义的方法 是 所有实例 共享的,这符合 面向对象 的 设计原则。如果某些功能 是 所有实例 都需要的,应该 共享实现,而不是为每个实例 重复定义

3 更方便地更新逻辑

通过 原型 定义方法,如果需要 更改 sayHello逻辑,只需要 修改一次,所有实例 都会 自动继承 新的逻辑。

4 符合 JavaScript 的设计模式

JavaScript 的 原型机制 是为了实现 共享继承 而设计的。如果直接在 构造函数 中 定义方法,实际上 违背了 它的 设计初衷。

3 什么是原型链?

原型链 是 JavaScript 用于实现 对象继承 的 一种 机制

原型链对象原型 连接 形成的 链式结构。你可以把 原型链 想象成一条 “寻找路径”,每次 访问 属性方法 时,JavaScript 都会 沿着 这条路径 逐级向上查找,直到 找到 或 返回 undefined

3.1 查找步骤

访问 一个 对象的 属性 或 方法 时,JavaScript 按以下 步骤查找:

1 检查对象本身

首先,JavaScript 会检查 对象自身 是否有 这个属性或方法(包括通过 this 定义的 属性,以及 直接添加到 对象上的 属性)。

// 示例代码 3
const obj = { name: "Alice" };
console.log(obj.name); // 输出 "Alice" (找到对象本身的属性)
2 查找对象的原型

如果 对象本身 没有 这个属性或方法,JavaScript 会通过 对象的 内部属性(即 __proto__)指向的 原型 继续查找。

// 示例代码 4
const animal = { eats: true };
const dog = Object.create(animal); // 创建一个对象,并设置其原型为 animal

console.log(dog.eats); // 输出 true (从原型 animal 中获取)
console.log(dog.__proto__ === animal); // true
3 沿着原型链继续查找

如果 对象的原型 上也没有这个 属性或方法,JavaScript 会 沿着 原型链 向上查找,直到 找到该属性 或 到达 原型链的 终点。

// 示例代码 5
// dog 的原型
console.log(dog.__proto__); // 输出 { eats: true }
console.log(dog.__proto__ === animal); // true

// animal 的原型
console.log(animal.__proto__); // 输出 Object.prototype
console.log(animal.__proto__ === Object.prototype); // true

// Object.prototype 的原型
console.log(Object.prototype.__proto__); // null
4 到达原型链的终点

原型链的终点 是 null,即 Object.prototype.__proto__null。如果到达这里仍未找到,返回 undefined

// 示例代码 6
console.log(dog.someNonExistentProperty); // 输出 undefined

4 继承

继承 是指 一个对象 可以通过 另一个对象 获得 属性和方法 的 机制。它是实现 代码复用共享 的重要方式。

JavaScript 中 继承核心机制 是通过 原型链 实现的。

4.1 实现方式

1 使用 Object.create 实现继承
// 示例代码 7
const animal = {
    eats: true,
    walk() {
        console.log("Animal walks");
    }
};

// 创建对象 dog, 设置 dog 的原型为 animal
const dog = Object.create(animal); // dog 继承自 animal
dog.barks = true;

console.log(dog.eats); // true, 从 animal 继承
dog.walk(); // 输出 "Animal walks"
2 构造函数和原型的结合
// 示例代码 8
// 定义父类(构造函数 + 原型)
function Animal(name) {
    this.name = name; // 每个 Animal 实例都会有自己的 name 属性
}

// 给 Animal 的原型添加方法(共享方法)
Animal.prototype.walk = function () {
    console.log(this.name + " is walking");
};

// 定义子类
function Dog(name, breed) {
    // 使用 `call` 是为了在 `Dog` 的构造函数中调用 `Animal` 的构造函数,把 `name` 属性赋值到当前实例上。
    Animal.call(this, name); // 调用父类的构造函数,继承 name 属性
    this.breed = breed;      // 给 Dog 添加自己的 breed 属性
}

// 继承父类的原型方法
Dog.prototype = Object.create(Animal.prototype); // 创建一个新的对象,并把它的原型设置为 Animal.prototype
// 因为继承后,`Dog.prototype.constructor` 被改成了父类的构造函数。
Dog.prototype.constructor = Dog;                // 修正 constructor 指向

// 扩展子类的方法
Dog.prototype.bark = function () {
    console.log(this.name + " says Woof!");
};

// 测试
const dog = new Dog("Buddy", "Golden Retriever"); 
dog.walk(); // 输出 "Buddy is walking" (继承自 Animal) 
dog.bark(); // 输出 "Buddy says Woof!" (Dog 自己的方法)
3 使用 ES6 class 语法
// 示例代码 9
class Animal {
    constructor(name) {
        this.name = name;
    }
    walk() {
        console.log(this.name + " walks");
    }
}

class Dog extends Animal {
    constructor(name, breed) {
        super(name); // 调用父类构造函数
        this.breed = breed;
    }
    bark() {
        console.log("Woof! Woof!");
    }
}

const dog = new Dog("Buddy", "Golden Retriever");
dog.walk(); // 输出 "Buddy walks"
dog.bark(); // 输出 "Woof! Woof!"