JS 原型和原型链

253 阅读7分钟

在JavaScript中,原型和原型链是理解对象继承和方法共享的关键概念。以下是对它们的详细介绍:

原型(Prototype)

每个JavaScript对象(除null外)都有一个与之关联的对象,这个对象称为原型。原型对象可以包含其他属性和方法,这些属性和方法可以通过继承被其他对象共享。原型机制是JavaScript实现继承的主要方式。

构造函数

构造函数是一种特殊的函数,用于创建和初始化对象。构造函数通常与new关键字一起使用,以便创建新的对象实例。构造函数的主要作用是为新对象设置初始属性和方法。JS中没有类(Class)这个概念,ES6中的class只是语法糖,本质上仍然是通过构造函数和原型实现的。

构造函数的特点

  1. 命名约定:  构造函数的名称通常以大写字母开头,以区别于普通函数。
  2. 使用new关键字:  当使用new关键字调用构造函数时,会创建一个新的对象,并将这个对象的原型设置为构造函数的prototype属性。
  3. this关键字:  在构造函数内部,this指向新创建的对象。
  4. 自动返回对象:  构造函数会自动返回新创建的对象,你不需要显式地返回它。

示例

// 定义一个构造函数
function Person(name, age) {
  this.name = name;
  this.age = age;

  // 可以在构造函数内部定义方法
  this.sayHello = function() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
  };
}

// 使用new关键字创建Person的实例
var person1 = new Person('Alice', 30);
var person2 = new Person('Bob', 25);

// 调用实例方法
person1.sayHello(); // 输出: Hello, my name is Alice and I am 30 years old.
person2.sayHello(); // 输出: Hello, my name is Bob and I am 25 years old.

在这个例子中,Person是一个构造函数,它接受两个参数nameage,并将它们赋值给新对象的属性。sayHello方法在构造函数内部定义,每个实例都会有自己独立的sayHello方法。

使用原型来定义方法

虽然可以在构造函数内部定义方法,但这样做会在每个实例上创建一个新的函数副本。为了节省内存,通常推荐将方法添加到构造函数的prototype属性上,这样所有实例都可以共享同一个方法副本。

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

// 将方法添加到Person的prototype上
Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};

// 使用new关键字创建Person的实例
var person1 = new Person('Alice', 30);
var person2 = new Person('Bob', 25);

// 调用共享的原型方法
person1.sayHello(); // 输出: Hello, my name is Alice and I am 30 years old.
person2.sayHello(); // 输出: Hello, my name is Bob and I am 25 years old.

在这个例子中,sayHello方法被添加到Person.prototype上,因此所有Person实例都共享同一个sayHello方法。这种方式更加内存高效。

原型对象

当你创建一个函数时,JavaScript会自动为这个函数创建一个prototype属性,该属性是一个对象,包含一个constructor属性指向该函数本身。任何由这个函数创建的实例对象都会共享这个原型对象。

每个函数都有一个prototype属性,这个属性指向一个对象,这个对象包含了通过该构造函数创建的实例共享的属性和方法。通过原型对象,可以实现对象之间的继承和方法共享。

原型对象的特点

  1. prototype属性:  每个函数(包括构造函数)都有一个prototype属性,这个属性指向一个对象,这个对象称为原型对象。
  2. 共享属性和方法:  通过构造函数创建的所有实例对象都可以访问原型对象上的属性和方法。
  3. 原型链:  当访问一个对象的属性或方法时,JavaScript引擎会首先查找对象自身的属性或方法,如果没有找到,则会沿着原型链向上查找,直到找到该属性或方法或到达原型链的顶端(即Object.prototype)。

示例

// 定义一个构造函数
function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 将方法添加到Person的prototype上
Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};

// 使用new关键字创建Person的实例
var person1 = new Person('Alice', 30);
var person2 = new Person('Bob', 25);

// 调用共享的原型方法
person1.sayHello(); // 输出: Hello, my name is Alice and I am 30 years old.
person2.sayHello(); // 输出: Hello, my name is Bob and I am 25 years old.

在这个例子中,sayHello方法被添加到Person.prototype上,因此所有Person实例都共享同一个sayHello方法。

原型链(Prototype Chain)

原型链(Prototype Chain)是JavaScript中实现继承和属性查找机制的核心概念。通过原型链,JavaScript对象可以继承其他对象的属性和方法,从而实现代码的复用和扩展。

原型链的基本概念

  1. 对象的原型:  每个JavaScript对象都有一个内部链接到另一个对象,这个对象称为原型(prototype)。这个链接通过__proto__属性(非标准,但大多数浏览器支持)或Object.getPrototypeOf方法来访问。
  2. 构造函数的prototype属性:  每个构造函数都有一个prototype属性,这个属性指向一个对象,这个对象包含了通过该构造函数创建的实例共享的属性和方法。
  3. 原型链:  当访问一个对象的属性或方法时,JavaScript引擎会首先查找对象自身的属性或方法,如果没有找到,则会沿着原型链向上查找,直到找到该属性或方法或到达原型链的顶端(即Object.prototype)。
  4. js之父在设计js原型、原型链的时候遵从以下两个准则
1. Person.prototype.constructor == Person // 原型对象(即Person.prototype)的constructor指向构造函数本身
2. person1.__proto__ == Person.prototype // 实例(即person1)的__proto__和原型对象指向同一个地方

2.png

原型链的示例

// 定义一个构造函数
function Animal(name) {
  this.name = name;
}

// 将方法添加到Animal的prototype上
Animal.prototype.speak = function() {
  console.log(`${this.name} makes a noise.`);
};

// 定义另一个构造函数
function Dog(name, breed) {
  Animal.call(this, name); // 继承属性
  this.breed = breed;
}

// 设置Dog的prototype为一个新的Animal实例
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// 将方法添加到Dog的prototype上
Dog.prototype.bark = function() {
  console.log(`${this.name} barks.`);
};

// 使用new关键字创建Dog的实例
var dog1 = new Dog('Rex', 'German Shepherd');

// 调用共享的原型方法
dog1.speak(); // 输出: Rex makes a noise.
dog1.bark();  // 输出: Rex barks.
dog1.__proto__ == Dog.prototype; // true
Dog.prototype.__proto__ === Animal.prototype; // true
Animal.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true

总结关系

  1. 属性继承: Dog 通过 Animal.call(this, name) 继承了 Animal 的属性。

  2. 方法继承: Dog 的原型被设置为一个以 Animal.prototype 为原型的对象,因此 Dog 的实例可以访问 Animal.prototype 上的方法。

  3. 原型链:

  • dog1.__proto__ === Dog.prototype 表明 dog1 是通过 Dog 构造函数创建的实例。
  • Dog.prototype.__proto__ === Animal.prototype 表明 Dog.prototype 继承自 Animal.prototype
  • Animal.prototype.__proto__ === Object.prototype 表明 Animal.prototype 继承自 Object.prototype
  • Object.prototype.__proto__ === null 表明 Object.prototype 是原型链的顶端。

通过这种方式,DogAnimal 之间建立了一个完整的继承关系,使得 Dog 的实例不仅拥有自己的属性和方法,还能访问和共享 Animal 原型上的方法

原型链的查找过程

  1. 对象自身:  当你访问dog1.speak()时,JavaScript引擎首先查找dog1对象自身是否有speak方法。
  2. 原型对象:  如果dog1对象自身没有speak方法,则查找dog1的原型对象(即Dog.prototype)。
  3. 继续查找:  如果在Dog.prototype上仍然没有找到speak方法,则继续沿着原型链向上查找Dog.prototype的原型对象(即Animal.prototype)。
  4. 找到方法:  在Animal.prototype上找到了speak方法,因此调用该方法。

原型链的顶端

所有对象的原型链最终都会指向Object.prototype,这是所有对象的原型的原型。如果在Object.prototype上仍然没有找到属性或方法,则返回undefined

console.log(Object.prototype); // 输出: {}
console.log(Object.getPrototypeOf(Object.prototype)); // 输出: null

原型链的可视化

可以通过以下代码来可视化原型链:

function printPrototypeChain(obj) {
  let proto = Object.getPrototypeOf(obj);
  while (proto) {
    console.log(proto);
    proto = Object.getPrototypeOf(proto);
  }
}

// 打印dog1的原型链
printPrototypeChain(dog1);

最后,奉上神图一张,祝大家理解JS的原型链

1.jpg

总结

  • 原型:  每个对象都有一个原型对象,原型对象可以包含其他属性和方法,这些属性和方法可以被其他对象共享。
  • 构造函数和原型:  构造函数用于创建和初始化对象,其prototype属性指向一个对象,这个对象包含了通过该构造函数创建的实例共享的属性和方法。
  • 原型链:  通过原型链,JavaScript对象可以继承其他对象的属性和方法,实现代码的复用和扩展。