JavaScript 中的原型对象与原型链:从底层深入理解

JavaScript 是一门基于原型(Prototype-based)的面向对象语言,它不像 Java 或 C++ 那样使用类(class)来实现继承和共享行为。相反,它通过 原型对象(prototype object)原型链(prototype chain) 机制来实现对象之间的属性继承和方法共享。

对于刚入门 JavaScript 的开发者来说,"原型"和"原型链"常常是一个难以理解的概念。本文将从底层出发,详细讲解原型对象、原型链的工作原理,并结合代码示例帮助你建立清晰的理解框架。


一、JavaScript 对象的本质

在深入了解原型之前,我们需要先明确一个核心概念:在 JavaScript 中,一切皆为对象(Object) 。函数是对象,数组是对象,甚至 nullundefined 虽然不是对象,但很多操作都围绕对象模型展开。

每个对象都有一个 [[Prototype]]

在 JavaScript 内部,每个对象都有一个隐藏的内部属性 [[Prototype]],它指向另一个对象,这个对象就是该对象的原型。你可以把它理解为对象的"父级"。

当访问一个对象的属性或方法时,如果该对象本身没有这个属性,JavaScript 引擎会自动去它的 [[Prototype]] 上查找,这个过程就构成了所谓的原型链

javascript 复制代码
let obj = {};
console.log(obj.toString()); // [object Object]

虽然我们并没有给 obj 定义 toString() 方法,但它依然可以调用这个方法,原因就在于它继承自 Object.prototype


二、构造函数与 prototype 属性

在 JavaScript 中,函数也是对象 。而所有函数(除了箭头函数)都有一个特殊的属性:prototype,它是一个对象,默认包含一个 constructor 属性,指向该函数本身。

构造函数的 prototype 是实例的原型

当我们使用构造函数创建对象时,新对象的 [[Prototype]] 会被设置为构造函数的 prototype 属性所指向的对象。

例如:

javascript 复制代码
function Person(name) {
    this.name = name;
}

Person.prototype.sayHello = function() {
    console.log('Hello, my name is ' + this.name);
};

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

在这个例子中:

  • Person.prototype 是一个对象。
  • p1.__proto__(即 [[Prototype]])指向 Person.prototype
  • 所以 p1 可以访问到定义在 Person.prototype 上的 sayHello 方法。

⚠️ 注意:__proto__ 是非标准但广泛支持的属性,建议使用 Object.getPrototypeOf(obj) 来获取对象的原型。


三、原型链是如何工作的?

原型链的核心思想是:对象之间可以通过原型链接起来,形成一条链式结构,在访问属性时沿着这条链向上查找

示例说明

让我们来看一个更复杂的例子:

javascript 复制代码
function Animal() {}
Animal.prototype.eat = function() {
    console.log('Animal is eating.');
};

function Dog() {}
Dog.prototype = Object.create(Animal.prototype); // 设置原型链
Dog.prototype.bark = function() {
    console.log('Woof!');
};

let dog = new Dog();
dog.eat();   // Animal is eating.
dog.bark();  // Woof!

在这段代码中:

  • Dog.prototype 被设置为 Animal.prototype 的副本(通过 Object.create() 创建)。
  • 所以 Dog 实例的 [[Prototype]] 指向 Dog.prototype,而 Dog.prototype[[Prototype]] 又指向 Animal.prototype
  • 当调用 dog.eat() 时,JavaScript 引擎发现 dog 自身没有 eat 方法,于是顺着原型链找到 Dog.prototype,也没有,继续找到 Animal.prototype,最终找到了 eat 方法并执行。

这就是原型链的基本工作方式。


四、原型链的终点 ------ null

原型链并不是无限延伸的。它的终点是 null。也就是说,原型链的最顶端通常是 Object.prototype,而 Object.prototype 的原型是 null

我们可以验证一下:

javascript 复制代码
console.log(Object.getPrototypeOf(Object.prototype)); // null

这说明 Object.prototype 是原型链的尽头。一旦到达这里还没有找到对应的属性或方法,就会返回 undefined


五、构造函数、原型、实例之间的关系图解

为了更直观地理解它们的关系,我们可以画出一张图:

lua 复制代码
构造函数: Person
            |
            v
       Person.prototype(原型对象)
            |
            v
        实例 p1, p2, ... 的 [[Prototype]]
  • 构造函数 Person 有一个 prototype 属性,指向其原型对象。
  • 所有通过 new Person() 创建的实例,其 [[Prototype]] 都指向 Person.prototype
  • 原型对象本身也有自己的 [[Prototype]],构成完整的原型链。

六、原型链中的继承机制

JavaScript 的继承本质上就是原型链的构建过程。要实现继承,通常的做法是让子类的原型指向父类的一个实例或者其原型对象。

经典继承方式:借用构造函数 + 原型链

javascript 复制代码
function Parent(name) {
    this.name = name;
}
Parent.prototype.sayName = function() {
    console.log(this.name);
};

function Child(name, age) {
    Parent.call(this, name); // 借用构造函数
    this.age = age;
}

// 设置原型链
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

let child = new Child('Tom', 5);
child.sayName(); // Tom

在这个例子中:

  • 使用 Parent.call(this, name) 在子类中调用父类构造函数,实现属性继承。
  • 使用 Object.create(Parent.prototype) 将子类的原型指向父类原型,从而继承方法。

七、原型链的性能与内存优化

由于原型上的属性和方法是被多个实例共享的,因此非常适合用来存储不依赖于具体实例的方法(如工具函数),这样可以节省内存。

例如:

javascript 复制代码
function Rectangle(width, height) {
    this.width = width;
    this.height = height;
}

Rectangle.prototype.getArea = function() {
    return this.width * this.height;
};

上面的例子中,getArea 方法只在 Rectangle.prototype 上定义一次,所有实例都可以访问,避免了重复定义方法造成的内存浪费。


八、常见误区与注意事项

❌ 误区一:直接修改内置对象的 prototype

虽然技术上可行,但修改 Object.prototypeArray.prototype 等内置对象的原型是非常危险的行为,可能导致与其他库冲突或破坏现有代码。

✅ 正确做法:

如果你希望扩展某些功能,可以通过模块化的方式封装成工具函数,而不是污染原型链。

javascript 复制代码
// 不推荐
Array.prototype.myMethod = function() { ... }

// 推荐
function myArrayMethod(arr) { ... }

❌ 误区二:把所有方法都写在构造函数里

有些开发者习惯在构造函数中定义方法:

javascript 复制代码
function Person(name) {
    this.name = name;
    this.sayHello = function() {
        console.log('Hello, ' + this.name);
    };
}

这种做法会导致每次创建实例时都重新定义一次 sayHello 方法,造成资源浪费。正确做法应是将其定义在原型上。


九、ES6 类语法糖下的原型本质

ES6 引入了 class 语法,使得 JavaScript 的面向对象编程更加接近传统 OOP 语言的风格。但实际上,class 只是原型机制的语法糖。

例如:

javascript 复制代码
class Person {
    constructor(name) {
        this.name = name;
    }

    sayHello() {
        console.log('Hello, ' + this.name);
    }
}

等价于:

javascript 复制代码
function Person(name) {
    this.name = name;
}

Person.prototype.sayHello = function() {
    console.log('Hello, ' + this.name);
};

所以,即使使用 class,原型链依然是 JavaScript 实现继承和共享方法的核心机制。


十、总结

原型对象和原型链是 JavaScript 面向对象编程的基础,也是区别于其他语言的重要特性。它们不仅决定了对象如何继承属性和方法,也影响着程序的性能和可维护性。

通过本文的学习,你应该已经掌握了以下内容:

  • 原型对象(prototype) :每个函数都有一个 prototype 属性,用于存放可以被其实例共享的属性和方法。
  • 原型链(prototype chain) :对象通过 [[Prototype]] 形成的一条链,在属性查找时逐层向上查找。
  • 继承机制:通过设置子类的原型为父类的原型或实例,实现继承。
  • 性能优化:将公共方法放在原型上,避免重复定义。
  • ES6 class 的本质:只是对原型机制的封装,底层依然是原型链。

掌握这些知识,不仅能让你写出更高效、更优雅的代码,也能帮助你在阅读源码或调试复杂问题时游刃有余。


参考资料:


如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发!更多 JavaScript 进阶内容将持续更新。

相关推荐
duanyuehuan17 分钟前
Vue 组件定义方式的区别
前端·javascript·vue.js
搏博1 小时前
基于Vue.js的图书管理系统前端界面设计
前端·javascript·vue.js·前端框架·数据可视化
掘金安东尼2 小时前
前端周刊第419期(2025年6月16日–6月22日)
前端·javascript·面试
Hilaku2 小时前
20MB 的字体文件太大了,我们把 Icon Font 压成了 10KB
前端·javascript·css
fs哆哆2 小时前
在VB.net中,文本插入的几个自定义函数
服务器·前端·javascript·html·.net
慧慧吖@2 小时前
箭头函数的this指向
开发语言·前端·javascript
锈儿海老师2 小时前
关于平凡AI 提示词造就世界最强ast-grep 规则这件事
前端·javascript·人工智能
开开心心就好2 小时前
高效批量转换Word到PDF的方法
javascript·安全·智能手机·pdf·word·objective-c·lisp
汪子熙3 小时前
npm install 输出信息解析与最佳实践
javascript·后端