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 进阶内容将持续更新。

相关推荐
hui函数44 分钟前
掌握JavaScript函数封装与作用域
前端·javascript
Carlos_sam1 小时前
Opnelayers:ol-wind之Field 类属性和方法详解
前端·javascript
小毛驴8502 小时前
创建 Vue 项目的 4 种主流方式
前端·javascript·vue.js
你这个年龄怎么睡得着的3 小时前
Babel AST 魔法:Vite 插件如何让你的 try...catch 不再“裸奔”?
前端·javascript·vite
Dream耀4 小时前
提升React移动端开发效率:Vant组件库
前端·javascript·前端框架
NUC_Dodamce5 小时前
Cocos3x 解决同时勾选 适配屏幕宽度和 适配屏幕高度导致Widget组件失效的问题
开发语言·javascript·ecmascript
JSON_L5 小时前
Vue 电影导航组件
前端·javascript·vue.js
xptwop7 小时前
05-ES6
前端·javascript·es6
Heo7 小时前
调用通义千问大模型实现流式对话
前端·javascript·后端
前端小巷子8 小时前
深入 npm 模块安装机制
前端·javascript·面试