Skip to content

原型和原型链

原型(Prototype)和原型链(Prototype Chain)是JavaScript中的核心概念,它们是实现对象继承的基础。理解原型和原型链对于掌握JavaScript的面向对象编程至关重要。本文将详细介绍JavaScript中的原型和原型链,以及它们的应用。

1. 原型的概念

在JavaScript中,每个对象都有一个原型对象,这个原型对象是该对象的父对象。对象可以从其原型继承属性和方法。

1.1 构造函数、原型对象和实例对象

在JavaScript中,有三个重要的概念:构造函数、原型对象和实例对象。

  • 构造函数:用于创建对象的函数,使用new关键字调用。
  • 原型对象:构造函数的prototype属性指向的对象,包含了所有实例对象共享的属性和方法。
  • 实例对象:通过构造函数创建的对象,有一个内部属性[[Prototype]]指向原型对象。

使用方式

javascript
// 构造函数
function Person(name, age) {
  this.name = name;
  this.age = age;
  this.sayHello = function() {
    console.log(`Hello, my name is ${this.name}!`);
  };
}

// 原型对象
Person.prototype.sayAge = function() {
  console.log(`I am ${this.age} years old!`);
};

// 实例对象
const person1 = new Person('John', 30);
const person2 = new Person('Jane', 25);

// 访问实例属性和方法
console.log(person1.name); // John
person1.sayHello(); // Hello, my name is John!

// 访问原型方法
person1.sayAge(); // I am 30 years old!
person2.sayAge(); // I am 25 years old!

// 检查原型关系
console.log(person1.__proto__ === Person.prototype); // true
console.log(person2.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true

1.2 原型的作用

原型的主要作用是实现对象的继承和属性共享。通过将方法定义在原型对象上,可以让所有实例对象共享这些方法,减少内存使用。

使用方式

javascript
// 不使用原型
function Person(name) {
  this.name = name;
  this.sayHello = function() {
    console.log(`Hello, my name is ${this.name}!`);
  };
}

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

console.log(person1.sayHello === person2.sayHello); // false(每个实例都有自己的sayHello方法)

// 使用原型
function PersonWithPrototype(name) {
  this.name = name;
}

PersonWithPrototype.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}!`);
};

const person3 = new PersonWithPrototype('John');
const person4 = new PersonWithPrototype('Jane');

console.log(person3.sayHello === person4.sayHello); // true(所有实例共享sayHello方法)

2. 原型链

原型链是指当访问对象的属性或方法时,JavaScript会先在对象自身查找,如果找不到,就会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的末端(null)。

2.1 原型链的结构

在JavaScript中,每个对象都有一个原型对象,而原型对象也有自己的原型对象,这样就形成了一条原型链。原型链的末端是null

使用方式

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

Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}!`);
};

// 实例对象
const person = new Person('John');

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

// 访问原型链上的方法
person.sayHello(); // Hello, my name is John!(来自Person.prototype)
console.log(person.toString()); // [object Object](来自Object.prototype)

2.2 原型链的查找机制

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

  1. 在对象自身查找该属性或方法。
  2. 如果找不到,就到对象的原型对象中查找。
  3. 如果还是找不到,就到原型对象的原型对象中查找,以此类推。
  4. 直到找到该属性或方法,或者到达原型链的末端(null),此时返回undefined

使用方式

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

Person.prototype.age = 30;
Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}!`);
};

const person = new Person('John');
person.age = 25; // 实例属性,覆盖原型属性

// 查找属性
console.log(person.name); // John(自身属性)
console.log(person.age); // 25(自身属性,覆盖了原型属性)
console.log(person.gender); // undefined(原型链上找不到)

// 查找方法
person.sayHello(); // Hello, my name is ${this.name}!(来自原型)
console.log(person.toString()); // [object Object](来自Object.prototype)

2.3 原型链的注意事项

  • 属性遮蔽:当实例对象有一个与原型对象同名的属性时,实例对象的属性会遮蔽原型对象的属性。
  • 原型链长度:原型链过长会影响性能,因为查找属性时需要遍历更长的链。
  • 修改原型:修改原型对象会影响所有实例对象,即使是修改前创建的实例。

3. 继承

JavaScript中的继承主要通过原型链实现,以下是几种常见的继承方式:

3.1 原型链继承

原型链继承是最基本的继承方式,通过将子类的原型设置为父类的实例来实现继承。

使用方式

javascript
// 父类
function Animal(name) {
  this.name = name;
  this.eat = function() {
    console.log(`${this.name} is eating!`);
  };
}

Animal.prototype.sleep = function() {
  console.log(`${this.name} is sleeping!`);
};

// 子类
function Dog(name, breed) {
  this.breed = breed;
}

// 原型链继承
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog; // 修复constructor指向

// 添加子类方法
Dog.prototype.bark = function() {
  console.log(`${this.name} is barking!`);
};

// 创建实例
const dog = new Dog('Buddy', 'Golden Retriever');
dog.name = 'Buddy'; // 需要手动设置父类属性

// 访问父类方法
dog.eat(); // Buddy is eating!
dog.sleep(); // Buddy is sleeping!

// 访问子类方法
dog.bark(); // Buddy is barking!

缺点

  • 父类的构造函数会被调用多次。
  • 子类实例无法向父类构造函数传递参数。
  • 所有子类实例共享父类的引用类型属性。

3.2 构造函数继承

构造函数继承通过在子类构造函数中调用父类构造函数来实现继承。

使用方式

javascript
// 父类
function Animal(name) {
  this.name = name;
  this.eat = function() {
    console.log(`${this.name} is eating!`);
  };
  this.friends = ['Cat', 'Bird'];
}

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

// 添加子类方法
Dog.prototype.bark = function() {
  console.log(`${this.name} is barking!`);
};

// 创建实例
const dog1 = new Dog('Buddy', 'Golden Retriever');
const dog2 = new Dog('Max', 'German Shepherd');

// 访问父类属性和方法
console.log(dog1.name); // Buddy
dog1.eat(); // Buddy is eating!

// 访问子类方法
dog1.bark(); // Buddy is barking!

// 引用类型属性不共享
dog1.friends.push('Mouse');
console.log(dog1.friends); // ['Cat', 'Bird', 'Mouse']
console.log(dog2.friends); // ['Cat', 'Bird']

缺点

  • 父类的原型方法不会被继承。
  • 每个实例都有父类方法的副本,浪费内存。

3.3 组合继承

组合继承结合了原型链继承和构造函数继承的优点,是最常用的继承方式。

使用方式

javascript
// 父类
function Animal(name) {
  this.name = name;
  this.friends = ['Cat', 'Bird'];
}

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

Animal.prototype.sleep = function() {
  console.log(`${this.name} is sleeping!`);
};

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

// 原型链继承,继承原型方法
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog; // 修复constructor指向

// 添加子类方法
Dog.prototype.bark = function() {
  console.log(`${this.name} is barking!`);
};

// 创建实例
const dog1 = new Dog('Buddy', 'Golden Retriever');
const dog2 = new Dog('Max', 'German Shepherd');

// 访问父类方法
dog1.eat(); // Buddy is eating!
dog1.sleep(); // Buddy is sleeping!

// 访问子类方法
dog1.bark(); // Buddy is barking!

// 引用类型属性不共享
dog1.friends.push('Mouse');
console.log(dog1.friends); // ['Cat', 'Bird', 'Mouse']
console.log(dog2.friends); // ['Cat', 'Bird']

优点

  • 既继承了父类的实例属性,又继承了父类的原型方法。
  • 子类实例可以向父类构造函数传递参数。
  • 引用类型属性不会被所有实例共享。

缺点

  • 父类构造函数会被调用两次(一次在创建子类原型时,一次在子类构造函数中)。

3.4 原型式继承

原型式继承通过Object.create()方法创建一个新对象,该对象的原型指向指定的对象。

使用方式

javascript
// 原型对象
const animal = {
  name: 'Animal',
  eat: function() {
    console.log(`${this.name} is eating!`);
  }
};

// 创建新对象
const dog = Object.create(animal);
dog.name = 'Dog';
dog.bark = function() {
  console.log(`${this.name} is barking!`);
};

// 访问属性和方法
console.log(dog.name); // Dog
dog.eat(); // Dog is eating!
dog.bark(); // Dog is barking!

// 检查原型关系
console.log(dog.__proto__ === animal); // true

优点

  • 简单易用,适合创建基于现有对象的新对象。

缺点

  • 所有实例共享原型对象的引用类型属性。

3.5 寄生式继承

寄生式继承是在原型式继承的基础上,增强新对象的功能。

使用方式

javascript
function createDog(original) {
  const dog = Object.create(original);
  dog.bark = function() {
    console.log(`${this.name} is barking!`);
  };
  return dog;
}

// 原型对象
const animal = {
  name: 'Animal',
  eat: function() {
    console.log(`${this.name} is eating!`);
  }
};

// 创建实例
const dog = createDog(animal);
dog.name = 'Buddy';

// 访问方法
dog.eat(); // Buddy is eating!
dog.bark(); // Buddy is barking!

优点

  • 可以增强新对象的功能。

缺点

  • 每个实例都有增强方法的副本,浪费内存。

3.6 寄生组合式继承

寄生组合式继承是组合继承的改进版,通过Object.create()方法创建子类原型,避免了父类构造函数被调用两次的问题。

使用方式

javascript
// 父类
function Animal(name) {
  this.name = name;
  this.friends = ['Cat', 'Bird'];
}

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

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

// 寄生组合式继承
function inheritPrototype(subType, superType) {
  const prototype = Object.create(superType.prototype);
  prototype.constructor = subType;
  subType.prototype = prototype;
}

inheritPrototype(Dog, Animal);

// 添加子类方法
Dog.prototype.bark = function() {
  console.log(`${this.name} is barking!`);
};

// 创建实例
const dog = new Dog('Buddy', 'Golden Retriever');

// 访问方法
dog.eat(); // Buddy is eating!
dog.bark(); // Buddy is barking!

优点

  • 既继承了父类的实例属性,又继承了父类的原型方法。
  • 避免了父类构造函数被调用两次的问题。
  • 是最理想的继承方式。

3.7 ES6类继承

ES6引入了class关键字,使JavaScript的继承更加简洁和清晰。

使用方式

javascript
// 父类
class Animal {
  constructor(name) {
    this.name = name;
    this.friends = ['Cat', 'Bird'];
  }

  eat() {
    console.log(`${this.name} is eating!`);
  }

  sleep() {
    console.log(`${this.name} is sleeping!`);
  }
}

// 子类
class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 调用父类构造函数
    this.breed = breed;
  }

  bark() {
    console.log(`${this.name} is barking!`);
  }
}

// 创建实例
const dog = new Dog('Buddy', 'Golden Retriever');

// 访问父类方法
dog.eat(); // Buddy is eating!
dog.sleep(); // Buddy is sleeping!

// 访问子类方法
dog.bark(); // Buddy is barking!

// 检查原型关系
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true

优点

  • 语法简洁清晰,类似于传统面向对象语言。
  • 自动处理原型链,避免了手动设置原型的复杂性。
  • 支持super关键字,方便调用父类方法。

4. 原型的相关方法

4.1 Object.getPrototypeOf()

Object.getPrototypeOf()方法返回指定对象的原型对象。

使用方式

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

const person = new Person('John');
const prototype = Object.getPrototypeOf(person);

console.log(prototype === Person.prototype); // true

4.2 Object.setPrototypeOf()

Object.setPrototypeOf()方法设置指定对象的原型对象。

使用方式

javascript
const animal = {
  eat: function() {
    console.log(`${this.name} is eating!`);
  }
};

const dog = {
  name: 'Buddy'
};

// 设置原型
Object.setPrototypeOf(dog, animal);

// 访问原型方法
dog.eat(); // Buddy is eating!

4.3 Object.create()

Object.create()方法创建一个新对象,该对象的原型指向指定的对象。

使用方式

javascript
const animal = {
  eat: function() {
    console.log(`${this.name} is eating!`);
  }
};

// 创建新对象
const dog = Object.create(animal);
dog.name = 'Buddy';

dog.eat(); // Buddy is eating!

4.4 hasOwnProperty()

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(person.hasOwnProperty('eat')); // false(不存在的属性)

4.5 in操作符

in操作符检查对象是否具有指定的属性(包括原型链上的属性)。

使用方式

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

Person.prototype.age = 30;

const person = new Person('John');

console.log('name' in person); // true(自身属性)
console.log('age' in person); // true(原型属性)
console.log('eat' in person); // false(不存在的属性)

4.6 Object.keys()

Object.keys()方法返回对象自身的可枚举属性的数组(不包括原型链上的属性)。

使用方式

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

Person.prototype.age = 30;
Person.prototype.eat = function() {};

const person = new Person('John');
person.gender = 'male';

console.log(Object.keys(person)); // ['name', 'gender']

4.7 Object.getOwnPropertyNames()

Object.getOwnPropertyNames()方法返回对象自身的所有属性的数组(包括不可枚举属性,不包括原型链上的属性)。

使用方式

javascript
function Person(name) {
  this.name = name;
  Object.defineProperty(this, 'age', {
    value: 30,
    enumerable: false
  });
}

const person = new Person('John');

console.log(Object.getOwnPropertyNames(person)); // ['name', 'age']

5. 原型链的应用

5.1 实现方法共享

通过将方法定义在原型对象上,可以让所有实例对象共享这些方法,减少内存使用。

使用方式

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

// 方法定义在原型上,所有实例共享
Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}!`);
};

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

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

5.2 实现继承

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

使用方式

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 = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// 添加子类方法
Dog.prototype.bark = function() {
  console.log(`${this.name} is barking!`);
};

const dog = new Dog('Buddy', 'Golden Retriever');
dog.eat(); // 继承自Animal
dog.bark(); // 子类自己的方法

5.3 扩展内置对象

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

使用方式

javascript
// 扩展Array原型
Array.prototype.sum = function() {
  return this.reduce((total, current) => total + current, 0);
};

const numbers = [1, 2, 3, 4, 5];
console.log(numbers.sum()); // 15

// 扩展String原型
String.prototype.reverse = function() {
  return this.split('').reverse().join('');
};

const str = 'Hello';
console.log(str.reverse()); // olleH

注意:扩展内置对象的原型可能会与其他代码产生冲突,应谨慎使用。

6. 面试常见问题

1. 什么是原型和原型链?

原型是JavaScript中每个对象都具有的一个内部属性,它指向另一个对象,包含了所有实例对象共享的属性和方法。

原型链是指当访问对象的属性或方法时,JavaScript会先在对象自身查找,如果找不到,就会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的末端(null)。

2. 构造函数、原型对象和实例对象之间的关系是什么?

  • 构造函数:用于创建对象的函数,有一个prototype属性指向原型对象。
  • 原型对象:包含了所有实例对象共享的属性和方法,有一个constructor属性指向构造函数。
  • 实例对象:通过构造函数创建的对象,有一个内部属性[[Prototype]]指向原型对象。

3. 如何检查一个对象是否是另一个对象的实例?

可以使用instanceof操作符或Object.prototype.isPrototypeOf()方法。

使用方式

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

const person = new Person('John');

// 使用instanceof
console.log(person instanceof Person); // true
console.log(person instanceof Object); // true

// 使用isPrototypeOf
console.log(Person.prototype.isPrototypeOf(person)); // true
console.log(Object.prototype.isPrototypeOf(person)); // true

4. 什么是原型链继承?它有什么优缺点?

原型链继承是通过将子类的原型设置为父类的实例来实现继承。

优点

  • 简单易用,代码量少。
  • 子类可以继承父类的所有属性和方法。

缺点

  • 父类的构造函数会被调用多次。
  • 子类实例无法向父类构造函数传递参数。
  • 所有子类实例共享父类的引用类型属性。

5. 什么是组合继承?它有什么优缺点?

组合继承是结合了原型链继承和构造函数继承的优点,通过在子类构造函数中调用父类构造函数来继承实例属性,通过将子类的原型设置为父类的实例来继承原型方法。

优点

  • 既继承了父类的实例属性,又继承了父类的原型方法。
  • 子类实例可以向父类构造函数传递参数。
  • 引用类型属性不会被所有实例共享。

缺点

  • 父类构造函数会被调用两次(一次在创建子类原型时,一次在子类构造函数中)。

6. 什么是寄生组合式继承?它为什么是最理想的继承方式?

寄生组合式继承是组合继承的改进版,通过Object.create()方法创建子类原型,避免了父类构造函数被调用两次的问题。

为什么是最理想的继承方式

  • 既继承了父类的实例属性,又继承了父类的原型方法。
  • 避免了父类构造函数被调用两次的问题。
  • 代码简洁,性能良好。

7. ES6的class继承与ES5的原型继承有什么区别?

ES6的class继承

  • 使用classextends关键字,语法简洁清晰。
  • 自动处理原型链,避免了手动设置原型的复杂性。
  • 支持super关键字,方便调用父类方法。
  • 本质上是ES5原型继承的语法糖。

ES5的原型继承

  • 使用构造函数和原型对象,语法较为复杂。
  • 需要手动设置原型链和修复constructor指向。
  • 没有super关键字,调用父类方法较为麻烦。

8. 如何判断一个属性是对象自身的还是来自原型链?

可以使用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(原型属性)

9. 什么是属性遮蔽?

属性遮蔽是指当实例对象有一个与原型对象同名的属性时,实例对象的属性会遮蔽原型对象的属性,即访问该属性时会返回实例对象的属性值,而不是原型对象的属性值。

使用方式

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

Person.prototype.age = 30;

const person = new Person('John');
person.age = 25; // 实例属性,覆盖原型属性

console.log(person.age); // 25(实例属性,遮蔽了原型属性)

10. 如何扩展一个对象的功能而不修改其原型?

可以使用以下方法:

方法1:使用对象组合

javascript
const animal = {
  eat: function() {
    console.log(`${this.name} is eating!`);
  }
};

const dog = {
  ...animal,
  name: 'Dog',
  bark: function() {
    console.log(`${this.name} is barking!`);
  }
};

方法2:使用工厂函数

javascript
function createDog(name) {
  const dog = {
    name: name,
    eat: function() {
      console.log(`${this.name} is eating!`);
    },
    bark: function() {
      console.log(`${this.name} is barking!`);
    }
  };
  return dog;
}

const dog = createDog('Buddy');

方法3:使用类继承

javascript
class Animal {
  constructor(name) {
    this.name = name;
  }

  eat() {
    console.log(`${this.name} is eating!`);
  }
}

class Dog extends Animal {
  constructor(name) {
    super(name);
  }

  bark() {
    console.log(`${this.name} is barking!`);
  }
}

const dog = new Dog('Buddy');

11. 原型链的末端是什么?

原型链的末端是null,它表示没有原型。

使用方式

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

12. 如何避免原型链过长导致的性能问题?

可以通过以下方法避免原型链过长:

  • 合理设计继承层次:尽量减少继承层次,避免创建过深的原型链。
  • 使用组合而非继承:对于复杂的功能,优先使用对象组合而不是继承。
  • 缓存频繁访问的属性:对于需要频繁访问的原型链上的属性,可以将其缓存到实例对象上。
  • 使用ES6的class:ES6的class语法更加简洁清晰,有助于避免原型链相关的错误。

7. 总结

原型和原型链是JavaScript中的核心概念,它们是实现对象继承的基础。

  • 原型:每个对象都有一个原型对象,包含了所有实例对象共享的属性和方法。
  • 原型链:当访问对象的属性或方法时,JavaScript会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的末端(null)。
  • 继承方式
    • 原型链继承
    • 构造函数继承
    • 组合继承
    • 原型式继承
    • 寄生式继承
    • 寄生组合式继承
    • ES6类继承
  • 原型的相关方法
    • Object.getPrototypeOf()
    • Object.setPrototypeOf()
    • Object.create()
    • hasOwnProperty()
    • in操作符
    • Object.keys()
    • Object.getOwnPropertyNames()

通过理解和掌握原型和原型链的概念,你可以更好地理解JavaScript的面向对象编程,创建更加高效、可维护的代码。

好好学习,天天向上