JavaScript知识点(五)——原型和原型链

0 阅读3分钟

引言 💭

在之前的高频面试题整合中有简单介绍原型和原型链,这篇文章再来深入探究一下。

JavaScript 与传统面向对象语言(如 Java 或 C++)不同,它采用基于原型的继承机制,而非基于类的继承。虽然 ES6 引入了 class 语法,但本质上仍是对原型继承的语法糖。

1. 什么是原型(Prototype)

1.1 原型的定义

在 JavaScript 中,每个对象都有一个内部属性 [[Prototype]],这是语言规范中定义的隐藏属性,用于指向该对象的原型。虽然我们无法直接访问 [[Prototype]],但大多数浏览器实现提供了一个非标准的访问接口 __proto__ 来操作它。

这个被 [[Prototype]] 指向的对象就是当前对象的原型。通过原型,对象可以访问它本身未定义但其原型中存在的属性和方法。

⚠️ 注意:虽然 __proto__ 可以访问或修改原型,但它是早期实现遗留下来的方式,不推荐在现代代码中使用。


1.2 原型的访问方式

方法说明
obj.__proto__非标准属性,现代浏览器支持,但不推荐使用
Object.getPrototypeOf(obj)标准方法,用于获取对象的原型
Object.setPrototypeOf(obj, proto)标准方法,用于设置对象的原型(慎用,性能较差)

1.3 原型查找机制

当访问对象的某个属性时,JavaScript 会按照如下步骤查找:

  1. 检查对象本身是否拥有该属性;
  2. 若没有,则查找其原型(即 [[Prototype]] 指向的对象);
  3. 若仍未找到,则继续沿原型链向上查找;
  4. 一直查找到原型链末端 null 为止,若仍未找到,则返回 undefined

1.4 示例代码

let obj = { name: "Alice" };

console.log(obj.hasOwnProperty("name"));      // true
console.log(obj.hasOwnProperty("toString"));  // false
console.log(obj.toString());                  // Object.prototype.toString

此例中:

  • nameobj 自身的属性;
  • toString 不是 obj 的属性,但它的原型是 Object.prototype,其中定义了 toString 方法。

2. 原型链(Prototype Chain)

2.1 定义

原型链是多个对象通过 [[Prototype]] 属性串联起来的引用链。这条链从对象本身开始,一直延伸到 null

通过这条链,JavaScript 实现了类似“继承”的效果。一个对象可以通过其原型访问另一个对象的属性和方法。

2.2 结构

let obj = {};obj.__proto__ === Object.prototype
        ↓
Object.prototype.__proto__ === null

2.3 原型链中的属性查找规则

let a = {};
console.log(a.toString()); // 从 Object.prototype 继承

当访问 a.toString()

  1. a 没有 toString
  2. 查找 a.__proto__(即 Object.prototype);
  3. 找到 toString 方法并调用;
  4. 若仍未找到,则查找 Object.prototype.__proto__,即 null,此时终止查找。

3. 构造函数与 prototype 属性

3.1 构造函数的定义

JavaScript 中的构造函数是普通函数,只要通过 new 关键字调用,它就会生成一个新对象并将该对象的 [[Prototype]] 设置为构造函数的 prototype 属性。

3.2 构造函数示例

function Person(name) {
  this.name = name;
}

Person.prototype.greet = function() {
  console.log(`Hello, my name is ${this.name}`);
};

let p1 = new Person("Alice");
p1.greet(); // Hello, my name is Alice

流程说明:

  1. 执行 new Person("Alice") 时,会创建一个新对象 p1
  2. p1.[[Prototype]] 被自动设置为 Person.prototype
  3. p1.greet() 会查找 p1Person.prototype,找到 greet 并调用。

3.3 prototype[[Prototype]] 的区别

属性名所属对象类型作用
prototype构造函数对象用于设置由该构造函数创建的实例对象的原型
[[Prototype]](或 __proto__实例对象对象引用其原型对象

总结:prototype 是构造函数自带的属性,而 [[Prototype]] 是对象的内部属性。通过构造函数创建的对象,其 [[Prototype]] 会自动指向构造函数的 prototype

4. 原型链继承的实现方式

JavaScript 的继承并非基于类,而是通过原型链实现。你可以使用构造函数 + Object.create() 等方式来模拟传统语言的继承结构。

4.1 示例:Animal → Dog

function Animal(name) {
  this.name = name;
}
Animal.prototype.speak = function() {
  console.log(`${this.name} makes a sound.`);
};

function Dog(name, breed) {
  Animal.call(this, name); // 继承属性
  this.breed = breed;
}

// 继承方法
Dog.prototype = Object.create(Animal.prototype);

// 修正 constructor 指向
Dog.prototype.constructor = Dog;

Dog.prototype.speak = function() {
  console.log(`${this.name} barks.`);
};

const dog1 = new Dog("Buddy", "Golden Retriever");
dog1.speak(); // Buddy barks.

4.2 原型链查找路径图

dog1
  ↓ [[Prototype]]
Dog.prototype
  ↓ [[Prototype]]
Animal.prototype
  ↓ [[Prototype]]
Object.prototype
  ↓
null

5. ES6 class 与原型的关系

虽然 ES6 引入了 class 语法,但它只是对原型链继承的语法糖封装,底层依然依赖 [[Prototype]]

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

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

const c = new Cat("Milo");
c.speak(); // Milo meows.

等价于:

function Cat(name) {
  this.name = name;
}
Cat.prototype.speak = function() {
  console.log(`${this.name} meows.`);
};
console.log(Object.getPrototypeOf(c) === Cat.prototype); // true

6. Object.create() 的继承方式

Object.create() 是另一种创建对象并设置原型的方式,尤其适用于纯粹对象之间的继承关系。

const animal = {
  speak() {
    console.log(`${this.name} makes a sound.`);
  }
};

const dog = Object.create(animal);
dog.name = "Buddy";
dog.speak(); // Buddy makes a sound.

结构图:

dog
  ↓ [[Prototype]]
animal
  ↓ [[Prototype]]
Object.prototype

7. 常见误区 ⚠️

  • ❌ 所有对象都有 prototype 属性

实际上,只有函数对象(即构造函数)才具有 prototype 属性。普通对象如 {} 没有该属性,但它们有内部的 [[Prototype]]

  • ❌ 使用 __proto__ 是标准做法

__proto__ 是历史遗留的非标准属性,虽然现代浏览器普遍支持,但不推荐使用。应使用 Object.getPrototypeOf()Object.setPrototypeOf() 来获取和设置原型。


8. 性能与实践建议

  • 避免频繁使用 Object.setPrototypeOf() :它会阻碍引擎优化,应尽量在对象创建时设置原型。
  • 继承链不要太深:原型链越长,属性查找成本越高,影响性能。
  • 方法共享建议写在原型上:可节省内存空间,避免重复绑定。

结语 ✒️

欢迎补充或纠正✍🏻,持续更新中……🚀🚀🚀

猫抓爱心.gif