Skip to content

JavaScript 原型链

什么是原型链

原型链(Prototype Chain)是 JavaScript 中实现继承的核心机制。在 JavaScript 中,每个对象都有一个原型对象(prototype),对象可以从其原型中继承属性和方法。当我们访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript 会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的末端(null)。

原型对象

1. prototype 属性

函数对象有一个 prototype 属性,指向一个对象,这个对象就是该函数的原型对象。当使用该函数作为构造函数创建实例时,实例的原型会指向这个原型对象。

javascript
// 示例
function Person(name) {
  this.name = name;
}

// 向原型对象添加方法
Person.prototype.greet = function () {
  return `Hello, my name is ${this.name}`;
};

// 创建实例
const person = new Person("John");
console.log(person.greet()); // 'Hello, my name is John'

// 检查原型链
console.log(person.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true

2. __proto__ 属性

每个对象都有一个 __proto__ 属性(也称为 [[Prototype]]),指向其原型对象。这是一个非标准但广泛支持的属性,在现代 JavaScript 中,推荐使用 Object.getPrototypeOf()Object.setPrototypeOf() 来操作原型。

javascript
// 示例
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true

const arr = [];
console.log(arr.__proto__ === Array.prototype); // true
console.log(arr.__proto__.__proto__ === Object.prototype); // true

const func = function () {};
console.log(func.__proto__ === Function.prototype); // true
console.log(func.__proto__.__proto__ === Object.prototype); // true

3. Object.getPrototypeOf()Object.setPrototypeOf()

现代 JavaScript 提供了标准方法来操作对象的原型。

javascript
// 示例
const obj = {};
const proto = { greet: () => "Hello" };

// 设置原型
Object.setPrototypeOf(obj, proto);
console.log(obj.greet()); // 'Hello'

// 获取原型
const objProto = Object.getPrototypeOf(obj);
console.log(objProto === proto); // true

原型链的工作原理

1. 属性查找

当访问对象的属性或方法时,JavaScript 会按照以下步骤查找:

  1. 首先在对象本身查找该属性或方法
  2. 如果找不到,就沿着 __proto__ 指向的原型对象查找
  3. 如果还找不到,就沿着原型对象的 __proto__ 继续向上查找
  4. 直到找到该属性或方法,或者到达原型链的末端(null
javascript
// 示例
function Person(name) {
  this.name = name;
}

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

const person = new Person("John");

// 访问对象本身的属性
console.log(person.name); // 'John'

// 访问原型链上的方法
console.log(person.greet()); // 'Hello, my name is John'

// 访问原型链上的属性
console.log(person.toString()); // '[object Object]'(继承自 Object.prototype)

// 访问不存在的属性
console.log(person.age); // undefined

2. 属性赋值

当给对象赋值时,JavaScript 会直接在对象本身创建属性,不会影响原型链。

javascript
// 示例
function Person(name) {
  this.name = name;
}

Person.prototype.age = 30;

const person1 = new Person("John");
const person2 = new Person("Jane");

// 访问原型链上的属性
console.log(person1.age); // 30
console.log(person2.age); // 30

// 给对象赋值,会在对象本身创建属性
person1.age = 25;
console.log(person1.age); // 25(对象本身的属性)
console.log(person2.age); // 30(原型链上的属性)

// 检查属性是否在对象本身
console.log(person1.hasOwnProperty("age")); // true
console.log(person2.hasOwnProperty("age")); // false

3. 原型链的末端

原型链的末端是 Object.prototype,其 __proto__ 指向 null

javascript
// 示例
console.log(Object.prototype.__proto__ === null); // true

const obj = {};
console.log(obj.__proto__ === Object.prototype); // true
console.log(obj.__proto__.__proto__ === null); // true

构造函数和原型

1. 构造函数

构造函数是用于创建对象的函数,使用 new 关键字调用。

javascript
// 示例
function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 创建实例
const person = new Person("John", 30);
console.log(person.name); // 'John'
console.log(person.age); // 30

2. 构造函数的原型

构造函数的 prototype 属性指向一个对象,这个对象是所有通过该构造函数创建的实例的原型。

javascript
// 示例
function Person(name) {
  this.name = name;
}

// 向原型添加方法
Person.prototype.greet = function () {
  return `Hello, my name is ${this.name}`;
};

Person.prototype.sayAge = function () {
  return `I am ${this.age} years old`;
};

// 创建实例
const person = new Person("John");
person.age = 30;

console.log(person.greet()); // 'Hello, my name is John'
console.log(person.sayAge()); // 'I am 30 years old'

// 检查原型
console.log(person.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true

3. 修改原型

可以通过修改构造函数的 prototype 来为所有实例添加或修改方法。

javascript
// 示例
function Person(name) {
  this.name = name;
}

const person1 = new Person("John");

// 修改原型
Person.prototype.greet = function () {
  return `Hello, my name is ${this.name}`;
};

const person2 = new Person("Jane");

// 所有实例都能访问新添加的方法
console.log(person1.greet()); // 'Hello, my name is John'
console.log(person2.greet()); // 'Hello, my name is Jane'

继承

1. 原型继承

通过原型链实现继承。

javascript
// 示例
// 父构造函数
function Person(name) {
  this.name = name;
}

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

// 子构造函数
function Student(name, grade) {
  // 调用父构造函数
  Person.call(this, name);
  this.grade = grade;
}

// 设置原型链,实现继承
Student.prototype = Object.create(Person.prototype);
// 修复 constructor 指向
Student.prototype.constructor = Student;

// 添加子构造函数特有的方法
Student.prototype.study = function () {
  return `${this.name} is studying`;
};

// 创建实例
const student = new Student("John", 9);
console.log(student.name); // 'John'
console.log(student.grade); // 9
console.log(student.greet()); // 'Hello, my name is John'(继承自 Person)
console.log(student.study()); // 'John is studying'(Student 特有的方法)

// 检查原型链
console.log(student.__proto__ === Student.prototype); // true
console.log(Student.prototype.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true

2. 多重继承

JavaScript 不直接支持多重继承,但可以通过混合(mixin)的方式实现类似的功能。

javascript
// 示例
// 第一个混入对象
const canEat = {
  eat: function (food) {
    return `${this.name} is eating ${food}`;
  },
};

// 第二个混入对象
const canSleep = {
  sleep: function () {
    return `${this.name} is sleeping`;
  },
};

// 构造函数
function Person(name) {
  this.name = name;
}

// 混合混入对象
Object.assign(Person.prototype, canEat, canSleep);

// 创建实例
const person = new Person("John");
console.log(person.eat("apple")); // 'John is eating apple'
console.log(person.sleep()); // 'John is sleeping'

原型链的方法

1. hasOwnProperty()

检查对象本身是否具有指定的属性(不包括原型链上的属性)。

javascript
// 示例
function Person(name) {
  this.name = name;
}

Person.prototype.age = 30;

const person = new Person("John");

console.log(person.hasOwnProperty("name")); // true
console.log(person.hasOwnProperty("age")); // false
console.log("age" in person); // true(包括原型链上的属性)

2. isPrototypeOf()

检查一个对象是否是另一个对象的原型。

javascript
// 示例
function Person(name) {
  this.name = name;
}

const person = new Person("John");

console.log(Person.prototype.isPrototypeOf(person)); // true
console.log(Object.prototype.isPrototypeOf(person)); // true
console.log(Array.prototype.isPrototypeOf(person)); // false

3. Object.getPrototypeOf()

获取对象的原型。

javascript
// 示例
function Person(name) {
  this.name = name;
}

const person = new Person("John");

const proto = Object.getPrototypeOf(person);
console.log(proto === Person.prototype); // true

4. Object.setPrototypeOf()

设置对象的原型。

javascript
// 示例
const proto = { greet: () => "Hello" };
const obj = {};

Object.setPrototypeOf(obj, proto);
console.log(obj.greet()); // 'Hello'

5. Object.create()

创建一个新对象,使用指定的对象作为其原型。

javascript
// 示例
const proto = {
  greet: function () {
    return `Hello, my name is ${this.name}`;
  },
};

const person = Object.create(proto);
person.name = "John";
console.log(person.greet()); // 'Hello, my name is John'
console.log(Object.getPrototypeOf(person) === proto); // true

常见的原型链

1. 数组的原型链

javascript
// 示例
const arr = [];

console.log(arr.__proto__ === Array.prototype); // true
console.log(Array.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true

// 数组继承的方法
console.log(arr.push); // 继承自 Array.prototype
console.log(arr.toString); // 继承自 Object.prototype

2. 函数的原型链

javascript
// 示例
function func() {}

console.log(func.__proto__ === Function.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true

// 函数继承的方法
console.log(func.apply); // 继承自 Function.prototype
console.log(func.toString); // 继承自 Object.prototype

3. 日期的原型链

javascript
// 示例
const date = new Date();

console.log(date.__proto__ === Date.prototype); // true
console.log(Date.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true

// 日期继承的方法
console.log(date.getFullYear); // 继承自 Date.prototype
console.log(date.toString); // 继承自 Object.prototype

原型链的应用场景

1. 共享方法

通过原型链共享方法,可以减少内存使用,提高性能。

javascript
// 示例
// 不好的做法:每个实例都有自己的方法
function Person(name) {
  this.name = name;
  this.greet = function () {
    return `Hello, my name is ${this.name}`;
  };
}

// 好的做法:通过原型链共享方法
function Person(name) {
  this.name = name;
}

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

const person1 = new Person("John");
const person2 = new Person("Jane");

console.log(person1.greet === person2.greet); // true(共享同一个方法)

2. 实现继承

通过原型链实现对象之间的继承关系。

javascript
// 示例
// 基础类
function Animal(name) {
  this.name = name;
}

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

// 派生类
function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function () {
  return `${this.name} is barking`;
};

const dog = new Dog("Rex", "German Shepherd");
console.log(dog.name); // 'Rex'
console.log(dog.breed); // 'German Shepherd'
console.log(dog.eat()); // 'Rex is eating'(继承自 Animal)
console.log(dog.bark()); // 'Rex is barking'(Dog 特有的方法)

3. 扩展内置对象

通过修改内置对象的原型,可以扩展内置对象的功能。

javascript
// 示例
// 扩展 Array 原型
Array.prototype.last = function () {
  return this[this.length - 1];
};

const arr = [1, 2, 3];
console.log(arr.last()); // 3

// 扩展 String 原型
String.prototype.capitalize = function () {
  return this.charAt(0).toUpperCase() + this.slice(1);
};

const str = "hello";
console.log(str.capitalize()); // 'Hello'

// 注意:扩展内置对象可能会与未来的 JavaScript 版本冲突,谨慎使用

4. 原型模式

使用原型模式创建对象,可以避免重复初始化对象的开销。

javascript
// 示例
const personPrototype = {
  greet: function () {
    return `Hello, my name is ${this.name}`;
  },
  init: function (name, age) {
    this.name = name;
    this.age = age;
    return this;
  },
};

// 创建实例
const person1 = Object.create(personPrototype).init("John", 30);
const person2 = Object.create(personPrototype).init("Jane", 25);

console.log(person1.greet()); // 'Hello, my name is John'
console.log(person2.greet()); // 'Hello, my name is Jane'

原型链的注意事项

1. 原型链的性能

属性查找的性能与原型链的长度有关,原型链越长,查找速度越慢。因此,应该尽量保持原型链简短。

javascript
// 示例
// 避免深层原型链
function Level1() {}
Level1.prototype.method1 = function () {};

function Level2() {}
Level2.prototype = Object.create(Level1.prototype);
Level2.prototype.method2 = function () {};

function Level3() {}
Level3.prototype = Object.create(Level2.prototype);
Level3.prototype.method3 = function () {};

// 深层原型链会影响性能
const obj = new Level3();
// 查找 method1 需要遍历三层原型链

2. 原型链的修改

修改原型链会影响所有基于该原型的实例,包括修改前创建的实例。

javascript
// 示例
function Person(name) {
  this.name = name;
}

const person1 = new Person("John");

// 修改原型
Person.prototype.greet = function () {
  return `Hello, my name is ${this.name}`;
};

const person2 = new Person("Jane");

// 所有实例都能访问新添加的方法
console.log(person1.greet()); // 'Hello, my name is John'
console.log(person2.greet()); // 'Hello, my name is Jane'

3. 原型链的循环引用

应避免创建循环引用的原型链,这会导致无限递归。

javascript
// 示例
// 避免循环引用
const obj1 = {};
const obj2 = {};

// 循环引用会导致问题
// Object.setPrototypeOf(obj1, obj2);
// Object.setPrototypeOf(obj2, obj1);

// 这样会导致无限递归
// console.log(obj1.someProperty);

4. 原型链与 this

在原型链的方法中,this 指向调用该方法的对象。

javascript
// 示例
function Person(name) {
  this.name = name;
}

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

const person = new Person("John");
console.log(person.greet()); // 'Hello, my name is John'(this 指向 person)

// 注意:箭头函数的 this 不会绑定到调用对象
Person.prototype.greetArrow = () => {
  return `Hello, my name is ${this.name}`;
};

console.log(person.greetArrow()); // 'Hello, my name is undefined'(箭头函数的 this 指向全局对象)

原型链的最佳实践

1. 使用 Object.create() 创建原型

使用 Object.create() 创建原型,可以更清晰地设置原型链。

javascript
// 示例
// 好的做法
function Parent() {}

function Child() {
  Parent.call(this);
}

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

// 不好的做法
function Child() {
  Parent.call(this);
}

Child.prototype = new Parent(); // 会执行 Parent 构造函数,可能产生副作用

2. 避免修改内置对象的原型

修改内置对象的原型可能会与未来的 JavaScript 版本冲突,谨慎使用。

javascript
// 示例
// 避免这样做
Array.prototype.myMethod = function () {
  // 自定义方法
};

// 推荐的做法:使用工具函数
function myMethod(arr) {
  // 操作数组
}

3. 使用 hasOwnProperty() 检查属性

在遍历对象属性时,使用 hasOwnProperty() 检查属性是否属于对象本身,避免遍历原型链上的属性。

javascript
// 示例
function Person(name) {
  this.name = name;
}

Person.prototype.age = 30;

const person = new Person("John");

// 好的做法:使用 hasOwnProperty
for (const key in person) {
  if (person.hasOwnProperty(key)) {
    console.log(key, person[key]); // 只遍历对象本身的属性
  }
}

// 或者使用 Object.keys()
Object.keys(person).forEach((key) => {
  console.log(key, person[key]); // 只遍历对象本身的属性
});

4. 合理使用原型链长度

保持原型链简短,避免深层继承,以提高属性查找性能。

javascript
// 示例
// 好的做法:保持原型链简短
function Animal() {}

function Dog() {
  Animal.call(this);
}

Dog.prototype = Object.create(Animal.prototype);

// 不好的做法:深层继承
function Animal() {}

function Mammal() {
  Animal.call(this);
}
Mammal.prototype = Object.create(Animal.prototype);

function Carnivore() {
  Mammal.call(this);
}
Carnivore.prototype = Object.create(Mammal.prototype);

function Dog() {
  Carnivore.call(this);
}
Dog.prototype = Object.create(Carnivore.prototype);

5. 使用 ES6 类

ES6 引入了 class 语法,提供了更清晰的方式来定义类和实现继承,底层仍然使用原型链。

javascript
// 示例
// 使用 ES6 类
class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    return `Hello, my name is ${this.name}`;
  }
}

class Student extends Person {
  constructor(name, grade) {
    super(name);
    this.grade = grade;
  }

  study() {
    return `${this.name} is studying`;
  }
}

const student = new Student("John", 9);
console.log(student.greet()); // 'Hello, my name is John'
console.log(student.study()); // 'John is studying'

// 底层仍然使用原型链
console.log(student.__proto__ === Student.prototype); // true
console.log(Student.prototype.__proto__ === Person.prototype); // true

原型链的兼容性

1. 浏览器兼容性

原型链是 JavaScript 的核心特性,所有现代浏览器都支持。在旧版本浏览器中,可能存在一些差异:

  • IE8 及以下版本不支持 Object.getPrototypeOf()Object.setPrototypeOf()
  • IE8 及以下版本的 __proto__ 属性实现不一致

2. 兼容性解决方案

对于不支持现代方法的浏览器,可以使用 polyfill 或替代方案。

javascript
// 示例
// 兼容 Object.getPrototypeOf()
if (!Object.getPrototypeOf) {
  Object.getPrototypeOf = function (obj) {
    return (
      obj.__proto__ ||
      (obj.constructor ? obj.constructor.prototype : Object.prototype)
    );
  };
}

// 兼容 Object.create()
if (!Object.create) {
  Object.create = function (proto) {
    function F() {}
    F.prototype = proto;
    return new F();
  };
}

总结

原型链是 JavaScript 中实现继承的核心机制,它允许对象从其原型中继承属性和方法。原型链的主要特点包括:

  1. 属性查找:当访问对象的属性或方法时,JavaScript 会沿着原型链向上查找
  2. 原型对象:每个对象都有一个原型对象,通过 __proto__ 属性指向
  3. 构造函数的 prototype:函数对象的 prototype 属性指向其原型对象
  4. 继承:通过设置原型链,可以实现对象之间的继承关系
  5. 原型链的末端:原型链的末端是 Object.prototype,其 __proto__ 指向 null

原型链的应用场景非常广泛,包括:

  • 共享方法,减少内存使用
  • 实现继承,构建对象层次结构
  • 扩展内置对象的功能
  • 使用原型模式创建对象

通过理解原型链的工作原理,我们可以更好地掌握 JavaScript 的面向对象编程特性,编写更加高效、可维护的代码。在现代 JavaScript 中,虽然引入了 class 语法,但底层仍然使用原型链实现继承,因此理解原型链对于深入掌握 JavaScript 仍然非常重要。