JavaScript 原型与原型链详解

小飞兽 JavaScript 270 次阅读 2026-06-13

JavaScript 原型与原型链详解

Introduction

JavaScript 是一门基于原型的(prototype-based)面向对象语言,与传统的类继承(class-based)语言(如 Java、C++)有着本质区别。在 JavaScript 中,几乎所有对象都有一个内部引用指向另一个对象,这个被引用的对象就是该对象的原型(Prototype)。原型链(Prototype Chain)则是由一系列原型对象组成的链式结构,它决定了对象的属性查找机制,也是 JavaScript 实现继承的核心机制。

理解原型与原型链,是深入掌握 JavaScript 语言特性的必经之路。无论是理解 this 的指向、继承的实现,还是排查框架底层源码,都离不开原型知识。本篇文章将系统讲解原型的概念、原型链的查找机制、构造函数与原型对象的关系,以及实际开发中的常见问题和最佳实践。

基础语法

1. prototype 属性

每个函数(除箭头函数)都有一个 prototype 属性,它是一个对象,包含一个 constructor 属性指向该函数本身。当使用 new 关键字调用构造函数创建实例时,实例对象内部会拥有一个 [[Prototype]] 内部槽,指向构造函数的 prototype 对象。

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

Person.prototype.greet = function() {
return 你好,我叫${this.name},今年${this.age}岁。;
};

console.log(Person.prototype);
// { constructor: ƒ Person, greet: ƒ }
console.log(Person.prototype.constructor === Person); // true

2. __proto__ 与 Object.getPrototypeOf()

每个对象都有一个 __proto__ 属性(现已不推荐直接访问),它指向该对象的原型。在现代 JavaScript 中,推荐使用 Object.getPrototypeOf(obj) 来获取对象的原型,使用 Object.setPrototypeOf(obj, prototype) 来设置原型。

const obj = { x: 1 };
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true

// 构造函数实例
const alice = new Person("Alice", 25);
console.log(Object.getPrototypeOf(alice) === Person.prototype); // true
console.log(alice.__proto__ === Person.prototype); // true(不推荐直接访问)

3. constructor 属性

每个原型对象都有一个 constructor 属性,指向创建实例的构造函数。通过实例的 constructor 属性,可以判断实例是由哪个构造函数创建的。

function Animal(type) {
    this.type = type;
}

const dog = new Animal("Dog");
console.log(dog.constructor === Animal); // true
console.log(dog instanceof Animal); // true

4. 原型链的尽头

所有对象的原型链尽头是 Object.prototype,它的原型是 null。这意味着当查找属性时,如果当前对象找不到,会沿着原型链一直向上查找,直到 null 为止。

console.log(Object.getPrototypeOf(Object.prototype)); // null

const arr = [1, 2, 3];
// arr -> Array.prototype -> Object.prototype -> null
console.log(Object.getPrototypeOf(arr) === Array.prototype); // true
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype); // true

代码示例

示例一:实现原型链继承

// 父类
function Animal(name) {
    this.name = name;
}

Animal.prototype.speak = function() {
return ${this.name} 发出了声音。;
};

// 子类
function Dog(name, breed) {
Animal.call(this, name); // 调用父类构造函数
this.breed = breed;
}

// 建立原型链:Dog.prototype -> Animal.prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修复 constructor

Dog.prototype.bark = function() {
return ${this.name} 汪汪叫!;
};

const dog = new Dog("旺财", "中华田园犬");
console.log(dog.name); // 旺财
console.log(dog.speak()); // 旺财 发出了声音。
console.log(dog.bark()); // 旺财 汪汪叫!
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true

示例二:原型链属性查找过程

const grandfather = { hobby: "钓鱼" };
const father = Object.create(grandfather);
father.job = "工程师";
const son = Object.create(father);
son.name = "小明";

console.log(son.name); // 小明 —— 自身属性
console.log(son.job); // 工程师 —— 来自 father
console.log(son.hobby); // 钓鱼 —— 来自 grandfather
console.log(son.toString()); // [object Object] —— 来自 Object.prototype
console.log(son.hasOwnProperty("name")); // true
console.log(son.hasOwnProperty("hobby")); // false
console.log("hobby" in son); // true(包含原型链)

示例三:hasOwnProperty 与 for...in 的区别

const parent = { a: 1 };
const child = Object.create(parent);
child.b = 2;

console.log(child.hasOwnProperty("a")); // false,原型上的属性
console.log(child.hasOwnProperty("b")); // true,自身属性

// for...in 会遍历自身和原型链上的可枚举属性
for (const key in child) {
console.log(key); // b, a
}

// 只获取自身属性
console.log(Object.keys(child)); // ["b"]

运行效果

运行上述所有示例代码,控制台输出如下:

// 示例一
旺财
旺财 发出了声音。
旺财 汪汪叫!
true
true

// 示例二
小明
工程师
钓鱼
[object Object]

// 示例三
false
true
b
a
["b"]

常见问题

Q1:__proto__ 和 prototype 有什么区别?

prototype函数独有的属性,用于构造函数创建实例时建立原型关系。__proto__所有对象都有的属性,是对象内部 [[Prototype]] 的浏览器暴露形式。理解这一点至关重要:只有函数才有 prototype,而所有对象都有 __proto__

Q2:原型链继承和 class 继承有什么区别?

ES6 的 class 本质上是语法糖,其底层实现仍然是原型链。class 让继承的写法更接近传统面向对象语言,但不会改变 JavaScript 基于原型的本质。class 中的方法仍然定义在 prototype 上。

class Animal {
    constructor(name) {
        this.name = name;
    }
    speak() {
        return ${this.name} 发出了声音。;
    }
}

class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
}

const dog = new Dog("旺财", "哈士奇");
console.log(dog.speak()); // 旺财 发出了声音。
console.log(Object.getPrototypeOf(dog) === Dog.prototype); // false(实际是 Animal.prototype)

Q3:Object.create(null) 创建的对象有什么特点?

使用 Object.create(null) 创建的对象没有原型链,其 __proto__null,因此不会有 toStringhasOwnProperty 等继承来的方法。这种对象常用于需要纯净键值映射的场景(如实现字典或映射表),可以避免原型属性干扰。

const pureObj = Object.create(null);
pureObj.key = "value";
console.log(pureObj.toString); // undefined
console.log(Object.keys(pureObj)); // ["key"]

Q4:修改构造函数 prototype 后,已创建的实例会受影响吗?

会。因为实例的原型引用是在创建时就确定的,指向当时的 prototype 对象。只要不重新赋值构造函数的 prototype 属性,而是在现有 prototype 上添加或修改属性,已创建实例就能看到这些变化。

function A() { this.x = 1; }
const a1 = new A();

// 在现有 prototype 上添加
A.prototype.addMethod = function() { return this.x + 1; };
console.log(a1.addMethod()); // 2 —— 实例可以访问新方法

// 重新赋值 prototype(危险!)
A.prototype = { y: 2 };
const a2 = new A();
console.log(a1.addMethod()); // 2 —— a1 仍用旧 prototype
console.log(a2.y); // 2 —— a2 用新 prototype
console.log(a1.constructor === A); // true

Q5:原型链过长会有什么性能问题?

原型链越长,属性查找的路径就越深,查找速度越慢。在极端情况下,如果一个对象有几十层原型链,每次访问属性都会产生明显延迟。实践中应避免深层继承,尽量使用组合替代继承,或使用 Object.create() 直接关联对象层级。

延伸阅读

掌握原型与原型链,是从 JavaScript 初学者迈向高级开发者的关键一步。建议结合浏览器调试工具,在控制台中亲自打印和探索各种对象的原型关系,做到理论与实践相结合。