在 JavaScript 中,原型 (Prototype) 是其面向对象模型的核心。与许多传统编程语言采用基于类 (Class-based) 的继承不同,JavaScript 采用的是基于原型 (Prototype-based) 的继承。理解原型是掌握 JavaScript 对象工作原理、实现继承和构建高效应用程序的关键。

在 ES6 (ECMAScript 2015) 引入 class 语法糖之前,原型是 JavaScript 实现面向对象编程(OOP)和代码复用的主要机制。即使在现代 JavaScript 中,class 语法也只是在底层对原型机制进行了一层封装,其本质依然是原型链。

本教程将深入探讨 JavaScript 原型的概念,包括显式原型 (prototype 属性) 和隐式原型 ([[Prototype]]),它们如何协同工作形成原型链 (Prototype Chain),从而实现属性和方法的继承。通过掌握原型,您将能够更深刻地理解 JavaScript 的对象模型,并能够编写出更优雅、更高效的代码。


前置知识

在开始本教程之前,建议您具备以下 JavaScript 基础知识:

  • 变量与数据类型: 了解 JavaScript 的基本数据类型和对象类型。
  • 函数: 熟悉函数的声明、调用、参数以及 return 语句。
  • 对象字面量: 了解 const obj = { key: 'value' }; 这种创建对象的方式。
  • 构造函数 (Constructor Function): 对如何使用函数创建对象有初步了解。
  • this 关键字: 理解 this 在函数调用时的动态绑定。

目录

  1. 原型 (Prototype) 简介
    • 1.1 什么是原型?
    • 1.2 为什么需要原型?
    • 1.3 JavaScript 的原型继承 (Prototype-based Inheritance)
  2. prototype 属性 (显式原型)
    • 2.1 构造函数上的 prototype 属性
    • 2.2 向 prototype 添加属性和方法
    • 2.3 constructor 属性的默认存在
  3. [[Prototype]] (隐式原型)
    • 3.1 实例对象的 [[Prototype]] 链接
    • 3.2 访问 [[Prototype]] 的方式 (__proto__Object.getPrototypeOf())
  4. 原型链 (Prototype Chain)
    • 4.1 什么是原型链?
    • 4.2 属性和方法的查找机制
    • 4.3 原型链的终点:Object.prototype
    • 4.4 如何通过原型链实现继承
  5. Object.create() 方法
    • 5.1 创建一个指定原型的对象
    • 5.2 与 new 关键字的区别
  6. class 语法与原型
    • 6.1 class 只是原型链的语法糖
    • 6.2 extends 关键字如何构建原型链
  7. hasOwnProperty() 方法
    • 7.1 区分自身属性和原型属性
  8. 原型污染 (Prototype Pollution) 及其防范
    • 8.1 什么是原型污染?
    • 8.2 潜在风险
    • 8.3 防范措施
  9. 最佳实践
  10. 总结
  11. 附录:术语表

1. 原型 (Prototype) 简介

1.1 什么是原型?

在 JavaScript 中,每个对象都有一个与之关联的原型对象 (prototype object)。当您尝试访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript 引擎就会沿着其原型链向上查找,直到找到该属性或方法,或者查找到原型链的末端。这个机制就是原型。

可以把原型理解为一个备用仓库模板。当对象没有所需的成员时,它会去原型那里“借用”或“继承”。

1.2 为什么需要原型?

原型机制解决了以下几个关键问题:

  • 代码复用: 允许多个对象共享同一个方法或属性,避免了在每个对象实例中重复创建,从而节省内存。
  • 继承: 提供了一种强大的机制,使一个对象可以从另一个对象继承属性和方法。
  • 动态扩展: 可以在运行时向原型对象添加新的属性和方法,这些新的成员会立即对所有继承该原型的实例生效。
1.3 JavaScript 的原型继承 (Prototype-based Inheritance)

与传统的类继承不同,JavaScript 采用的是原型继承。这意味着:

  • 没有“类”的概念 (ES6 class 只是语法糖): 在 ES6 之前,JavaScript 没有 class 关键字,所有的对象都直接从其他对象继承。class 只是提供了一种更熟悉、更简洁的方式来组织基于原型的代码。
  • 对象直接继承自对象: 每个对象都有一个指向其原型的链接,原型本身也是一个对象。

2. prototype 属性 (显式原型)

prototype 属性是函数才有的一个特殊属性。当一个函数被用作构造函数来创建对象时,所有由该构造函数创建的实例都会共享这个 prototype 对象上的属性和方法。

2.1 构造函数上的 prototype 属性

每个函数在创建时都会自动获得一个 prototype 属性,这个属性是一个对象。这个 prototype 对象将作为所有由该函数创建的实例的隐式原型

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

console.log(typeof Animal.prototype); // object
console.log(Animal.prototype);        // { constructor: f Animal() }

默认情况下,Animal.prototype 对象上只有一个 constructor 属性,指向 Animal 构造函数本身。

2.2 向 prototype 添加属性和方法

通常,我们会在构造函数的 prototype 上定义方法,这样所有由该构造函数创建的实例都可以共享这些方法,而不是每个实例都拥有一个独立的副本,从而节省内存。

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

// 在 Person.prototype 上定义方法
Person.prototype.greet = function() {
  console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};

Person.prototype.sayGoodbye = function() {
  console.log(`Goodbye from ${this.name}.`);
};

const person1 = new Person('Alice', 30);
const person2 = new Person('Bob', 25);

person1.greet();      // Hello, my name is Alice and I am 30 years old.
person2.sayGoodbye(); // Goodbye from Bob.

// 验证两个实例是否共享同一个方法
console.log(person1.greet === person2.greet);          // true
console.log(person1.greet === Person.prototype.greet); // true

这种方式是 JavaScript 实现方法共享和内存优化的标准模式。

2.3 constructor 属性的默认存在

每个 prototype 对象都有一个 constructor 属性,它指向拥有该 prototype 对象的构造函数。

function Car() {}
console.log(Car.prototype.constructor === Car); // true

const myCar = new Car();
console.log(myCar.constructor === Car); // true

constructor 属性在判断对象的类型或创建类似对象时非常有用。


3. [[Prototype]] (隐式原型)

[[Prototype]] 是每个 JavaScript 对象(除了少数例外,如 Object.prototype 的原型为 null)都具备的一个内部属性。它指向该对象的原型对象

3.1 实例对象的 [[Prototype]] 链接

当使用 new 关键字和一个构造函数创建对象实例时,这个新创建的实例的 [[Prototype]] 就会被设置为构造函数的 prototype 属性所指向的对象。

function Dog(name) {
  this.name = name;
}
Dog.prototype.bark = function() {
  console.log(`${this.name} says Woof!`);
};

const myDog = new Dog('Buddy');

// myDog 的隐式原型 (内部 [[Prototype]]) 指向 Dog.prototype
console.log(Object.getPrototypeOf(myDog) === Dog.prototype); // true
3.2 访问 [[Prototype]] 的方式 (__proto__Object.getPrototypeOf())
  • __proto__ 访问器:
    这是一个非标准的、历史遗留的属性,但被大多数现代浏览器实现。它允许你直接访问或设置一个对象的 [[Prototype]]。尽管方便,但由于其非标准性,不建议在生产代码中直接使用它来修改原型。

    console.log(myDog.__proto__ === Dog.prototype); // true (浏览器环境)
    
  • Object.getPrototypeOf(obj)
    这是访问对象 [[Prototype]]标准且推荐的方式。

    console.log(Object.getPrototypeOf(myDog)); // 输出 Dog.prototype 对象
    
  • Object.setPrototypeOf(obj, prototype)
    这是修改对象 [[Prototype]]标准且推荐的方式。但频繁修改对象的原型会影响性能,应谨慎使用。


4. 原型链 (Prototype Chain)

4.1 什么是原型链?

当一个对象需要查找某个属性或方法时,如果它自身没有,它会沿着其 [[Prototype]] 链接向它的原型对象查找。如果原型对象也没有,就继续沿着原型对象的 [[Prototype]] 向上查找,这样层层递进,就形成了一条链条,这就是原型链 (Prototype Chain)

4.2 属性和方法的查找机制
  1. 首先查找对象自身: 当访问 myObject.property 时,JavaScript 引擎首先检查 myObject 自身是否拥有 property
  2. 沿原型链向上查找: 如果 myObject 自身没有 property,引擎会沿着 myObject[[Prototype]] 链接到它的原型对象。
  3. 重复步骤2: 如果原型对象也没有 property,引擎会继续沿着原型对象的 [[Prototype]] 向上查找。
  4. 直到原型链末端: 这个过程会一直持续,直到找到 property 或到达原型链的末端 (null)。如果到达末端仍未找到,则返回 undefined
function Gadget(name) {
  this.name = name;
  this.price = 100;
}

Gadget.prototype.getDetails = function() {
  console.log(`Name: ${this.name}, Price: ${this.price}`);
};

const phone = new Gadget('Smartphone');
phone.material = 'Plastic'; // 自身属性

console.log(phone.name);       // 'Smartphone' (自身属性)
console.log(phone.price);      // 100 (自身属性)
console.log(phone.getDetails); // [Function: getDetails] (从原型链上找到)
phone.getDetails();            // Name: Smartphone, Price: 100

console.log(phone.toString);   // [Function: toString] (从更上层原型链 Object.prototype 找到)
console.log(phone.nonExistent); // undefined (原型链末端也未找到)
4.3 原型链的终点:Object.prototype

所有 JavaScript 对象最终都继承自 Object.prototypeObject.prototype 位于大多数原型链的顶端,它提供了许多常用的方法,如 toString(), hasOwnProperty(), valueOf() 等。Object.prototype[[Prototype]]null,标志着原型链的终点。

console.log(Object.getPrototypeOf(phone) === Gadget.prototype);         // true
console.log(Object.getPrototypeOf(Gadget.prototype) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype));                    // null
4.4 如何通过原型链实现继承

原型链是 JavaScript 实现继承的基础。一个对象可以通过其原型链访问其所有祖先原型对象上的属性和方法。

function Animal(name) {
  this.name = name;
}
Animal.prototype.eat = function() {
  console.log(`${this.name} is eating.`);
};

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

// 关键一步:让 Dog.prototype 继承 Animal.prototype
// Object.create() 是推荐的方式,因为它创建了一个新对象,并将其原型设置为指定的对象
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修复 constructor 指向

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

const myDog = new Dog('Max', 'Labrador');
myDog.eat();  // Max is eating. (从 Animal.prototype 继承)
myDog.bark(); // Max (Labrador) barks. (自身原型上的方法)

在这个例子中,myDog[[Prototype]] 指向 Dog.prototype,而 Dog.prototype[[Prototype]] 又指向 Animal.prototype,从而形成了原型链继承。


5. Object.create() 方法

Object.create() 方法用于创建一个新对象,并将其 [[Prototype]] 设置为指定的对象。这是一个非常有用的方法,尤其是在实现原型继承时,它比直接赋值 prototype 更干净。

5.1 创建一个指定原型的对象
const animalPrototype = {
  speak: function() {
    console.log(`${this.name} makes a sound.`);
  },
  isAlive: true
};

const cat = Object.create(animalPrototype);
cat.name = 'Whiskers';

cat.speak();     // Whiskers makes a sound.
console.log(cat.isAlive); // true

console.log(Object.getPrototypeOf(cat) === animalPrototype); // true
5.2 与 new 关键字的区别
  • new 关键字: 用于调用构造函数,它会创建一个新对象,将其 [[Prototype]] 链接到构造函数的 prototype,然后执行构造函数来初始化实例属性。它本质上是围绕构造函数和原型链进行操作。
  • Object.create() 仅仅创建一个新对象,并指定其原型,它不会调用任何构造函数来初始化属性。你需要手动添加属性。
function MyConstructor(value) {
  this.value = value;
}
MyConstructor.prototype.getValue = function() { return this.value; };

// 使用 new
const instanceNew = new MyConstructor(10);
console.log(instanceNew.value);    // 10
console.log(instanceNew.getValue()); // 10

// 使用 Object.create
const instanceCreate = Object.create(MyConstructor.prototype);
// instanceCreate.value; // undefined (没有调用 MyConstructor 构造函数)
// 需要手动初始化
instanceCreate.value = 20;
console.log(instanceCreate.value);    // 20
console.log(instanceCreate.getValue()); // 20

6. class 语法与原型

ES6 引入的 class 语法是 JavaScript 原型机制的语法糖 (Syntactic Sugar)。它并没有改变 JavaScript 基于原型的本质,只是提供了一个更简洁、更符合传统面向对象语言习惯的语法来定义构造函数和原型方法。

6.1 class 只是原型链的语法糖

当您定义一个 class 时:

  • constructor 方法内部定义的属性(this.prop = value;)成为实例自身的属性。
  • class 内部直接定义的方法(methodName() { ... })会自动添加到类的 prototype 对象上,成为共享的原型方法。
class PersonClass {
  constructor(name) {
    this.name = name; // 实例属性
  }

  greet() { // 原型方法
    console.log(`Hello from ${this.name}`);
  }
}

const p = new PersonClass('Charlie');
p.greet(); // Hello from Charlie

console.log(p.hasOwnProperty('name'));  // true
console.log(p.hasOwnProperty('greet')); // false
console.log(Object.getPrototypeOf(p).greet === PersonClass.prototype.greet); // true
6.2 extends 关键字如何构建原型链

extends 关键字用于在 class 之间建立继承关系。它在底层做了两件事来构建原型链:

  1. 设置子类的 prototype 的原型: ChildClass.prototype[[Prototype]] 会被设置为 ParentClass.prototype
  2. 设置子类的 [[Prototype]] ChildClass 本身(作为一个函数)的 [[Prototype]] 会被设置为 ParentClass(作为函数)。这使得子类可以继承父类的静态方法。
class Vehicle {
  constructor(brand) {
    this.brand = brand;
  }
  start() { console.log(`${this.brand} is starting.`); }
  static getType() { return 'Generic Vehicle'; }
}

class Car extends Vehicle {
  constructor(brand, model) {
    super(brand); // 调用父类 constructor,确保 this 初始化
    this.model = model;
  }
  drive() { console.log(`${this.brand} ${this.model} is driving.`); }
}

const myCar = new Car('Tesla', 'Model 3');
myCar.start(); // Tesla is starting. (从 Vehicle.prototype 继承)
myCar.drive(); // Tesla Model 3 is driving. (自身原型上的方法)
console.log(Car.getType()); // Generic Vehicle (从 Vehicle 静态继承)

// 验证原型链
console.log(Object.getPrototypeOf(myCar) === Car.prototype);           // true
console.log(Object.getPrototypeOf(Car.prototype) === Vehicle.prototype); // true
console.log(Object.getPrototypeOf(Vehicle.prototype) === Object.prototype); // true

// 验证类本身的继承 (用于静态成员)
console.log(Object.getPrototypeOf(Car) === Vehicle);                   // true

7. hasOwnProperty() 方法

hasOwnProperty() 方法是 Object.prototype 上的一个方法,因此所有对象都可以调用它。它用于检查一个对象是否拥有自身 (own) 的某个属性(而不是从原型链上继承的)。

function Book(title, author) {
  this.title = title;
  this.author = author;
}
Book.prototype.getDetails = function() {
  return `${this.title} by ${this.author}`;
};

const book1 = new Book('The Hobbit', 'J.R.R. Tolkien');

console.log(book1.hasOwnProperty('title'));      // true (自身属性)
console.log(book1.hasOwnProperty('author'));     // true (自身属性)
console.log(book1.hasOwnProperty('getDetails')); // false (原型上的方法)
console.log(book1.hasOwnProperty('toString'));   // false (原型链上更远的 Object.prototype 上的方法)

这个方法在遍历对象属性时非常有用,可以确保只处理对象自身的属性。


8. 原型污染 (Prototype Pollution) 及其防范

8.1 什么是原型污染?

原型污染是一种安全漏洞,攻击者通过向 JavaScript 应用程序注入恶意代码,从而修改 Object.prototype 或其他原型对象。由于所有对象都继承自 Object.prototype,一旦它被污染,应用程序中的所有对象都可能受到影响。

8.2 潜在风险
  • 拒绝服务 (DoS): 注入无限循环或计算量大的属性,导致应用程序崩溃。
  • 远程代码执行 (RCE): 在某些 Node.js 环境或特定框架中,通过原型污染修改关键配置或执行路径,可能导致代码执行。
  • 数据篡改: 修改 Object.prototype 上的默认值,可能影响应用程序的逻辑或数据完整性。
// 假设攻击者可以通过某种方式执行了以下代码
// 这是原型污染的核心:向 Object.prototype 添加或修改属性
Object.prototype.isAdmin = true;

const user = {}; // 任何普通对象
console.log(user.isAdmin); // true (Oops! 任何对象都变成了管理员)
8.3 防范措施
  1. 输入验证: 严格验证和清理所有用户输入,特别是涉及到键名或路径的操作。

  2. 避免深合并 (Deep Merge) 库中的安全漏洞: 许多库提供了深合并对象的功能。如果这些库没有正确处理原型属性,攻击者可能通过注入 __proto__constructor.prototype 来进行污染。使用已知安全的深合并库,并确保更新到最新版本。

  3. 使用 Object.create(null) 创建纯净对象: 当你需要一个完全不继承任何属性(包括 Object.prototype 上的属性)的空对象时,可以使用 Object.create(null)

    const pureObject = Object.create(null);
    console.log(pureObject.toString); // undefined
    // console.log(pureObject.hasOwnProperty); // undefined
    
  4. 冻结原型: 在某些情况下,可以使用 Object.freeze()Object.seal() 来防止原型对象被修改,但这可能影响到其他需要修改原型的库。


9. 最佳实践

  • 将方法添加到 prototype (或使用 class 语法): 这是实现方法共享和内存优化的标准做法。
  • 不要直接修改 Object.prototype 除非你确切知道自己在做什么,否则永远不要向 Object.prototype 添加属性或方法,因为它会影响所有对象。
  • 使用 Object.getPrototypeOf()Object.setPrototypeOf() 代替非标准的 __proto__ 来访问和修改原型。
  • 优先使用 class 语法进行面向对象编程: 它提供了更清晰、更现代的语法来封装原型机制,同时保留了其所有优点。
  • 理解 this 的绑定: 在原型方法中,this 仍然指向调用该方法的实例。
  • 警惕原型污染: 尤其是在处理来自不可信源的数据时,要对对象创建和合并操作保持警惕。

10. 总结

JavaScript 的原型机制是其面向对象模型的核心,也是其独特魅力的来源。通过本教程,我们深入探讨了:

  • 原型作为对象共享属性和实现继承的基础。
  • prototype 属性 (显式原型) 是构造函数上的一个对象,用于存储所有实例共享的方法和属性。
  • [[Prototype]] (隐式原型) 是每个对象内部的链接,指向其原型对象。
  • 原型链是对象属性查找的机制,它沿着 [[Prototype]] 链接向上查找,直到找到属性或到达链的末端。
  • Object.create() 如何精确控制对象的原型。
  • ES6 class 语法糖如何优雅地封装了原型和原型链。
  • hasOwnProperty() 用于区分自身属性和继承属性。
  • 原型污染的安全风险及防范措施。

深入理解原型是成为一名高级 JavaScript 开发者不可或缺的一部分,它能让您更有效地利用语言的强大功能,编写出结构更清晰、性能更优越的应用程序。


11. 附录:术语表

  • 原型 (Prototype): JavaScript 中对象共享属性和实现继承的机制。
  • 原型对象 (Prototype Object): 被其他对象作为原型继承属性和方法的对象。
  • prototype 属性 (显式原型): 构造函数独有的属性,指向其原型对象,所有由该构造函数创建的实例都会继承这个原型对象上的属性和方法。
  • [[Prototype]] (隐式原型): 每个 JavaScript 对象(除了少数例外)都具备的内部属性,指向该对象的原型对象。
  • __proto__ 非标准但广泛实现的访问器属性,用于访问或设置对象的 [[Prototype]]
  • Object.getPrototypeOf(obj) 标准且推荐的获取对象 [[Prototype]] 的方法。
  • Object.setPrototypeOf(obj, prototype) 标准且推荐的设置对象 [[Prototype]] 的方法。
  • 原型链 (Prototype Chain): 当访问一个对象的属性时,JavaScript 沿着该对象的 [[Prototype]] 链接向上查找的序列。
  • Object.prototype 大多数原型链的顶端,提供许多基本方法(如 toString),其 [[Prototype]]null
  • 构造函数 (Constructor Function): 用于创建和初始化对象的函数,通常与 new 关键字一起使用。
  • Object.create(proto) 创建一个新对象,并指定其 [[Prototype]]proto
  • class 关键字 (ES6): JavaScript 中用于定义类的语法糖,底层仍然是构造函数和原型继承。
  • extends 关键字:class 之间建立继承关系。
  • hasOwnProperty(prop) Object.prototype 上的方法,检查对象是否拥有自身的指定属性。
  • 原型污染 (Prototype Pollution): 一种安全漏洞,攻击者通过修改 Object.prototype 或其他原型对象来影响所有对象。
  • 语法糖 (Syntactic Sugar): 一种编程语言的特性,提供更简洁、更易读的语法,但其底层功能与现有、更复杂的语法相同。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐