JavaScript 基础:原型及原型链
JavaScript 基础:原型及原型链
什么是原型?

在 JavaScript 中,每个对象都有一个内部链接指向另一个对象,我们称之为“原型”(Prototype)。该原型对象也是另一个对象,而这个链接最终会指向 null。通过这个机制,一个对象可以继承另一个对象的属性和方法。

当我们访问一个对象的属性时,JavaScript 引擎首先会检查该对象自身是否有这个属性,如果没有,它会沿着原型链查找该属性,直到找到为止,或直到链的末尾(null)。

原型的核心概念
  • 原型对象(Prototype Object): 每个 JavaScript 对象都有一个与之关联的原型对象。这个原型对象可以包含该对象可以访问的属性和方法。
  • __proto__ 属性: 虽然 JavaScript 并不推荐直接使用 __proto__,但它是访问对象原型的方式之一。它指向了对象的原型,即对象的内部原型链接。
const obj = {};
console.log(obj.__proto__); // 输出对象的原型
  • constructor 属性: 每个函数都有一个 prototype 属性,这个 prototype 指向的是一个对象,作为所有由该构造函数创建的实例的原型。原型对象又有一个 constructor 属性,指向该构造函数。
function Person(name) {
  this.name = name;
}
const person1 = new Person('Alice');
console.log(person1.constructor === Person); // true
console.log(Person.prototype.constructor === Person); // true
什么是原型链?

原型链是多个对象通过原型关联在一起形成的链式结构。JavaScript 中的对象通过原型链可以继承其他对象的属性和方法。

当我们访问一个对象的属性时,JavaScript 引擎首先会在对象自身的属性中查找,如果没有找到,它会沿着该对象的原型链依次查找原型对象,直到找到该属性或到达原型链的终点——null

原型链的结构

原型链的终点是 null,即 Object.prototype.__proto__null

const obj = {};
console.log(Object.getPrototypeOf(obj)); // 输出 Object.prototype
console.log(Object.getPrototypeOf(Object.prototype)); // 输出 null
原型链:例子
function Animal(name) {
  this.name = name;
}

Animal.prototype.sayName = function() {
  console.log(this.name);
};

function Dog(name, breed) {
  Animal.call(this, name); // 继承属性
  this.breed = breed;
}

// 继承方法
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.sayName(); // 输出 'Buddy'

// 原型链结构
console.log(myDog.__proto__); // Dog.prototype
console.log(myDog.__proto__.__proto__); // Animal.prototype
console.log(myDog.__proto__.__proto__.__proto__); // Object.prototype
console.log(myDog.__proto__.__proto__.__proto__.__proto__); // null

在上面的例子中,myDogDog 的实例,Dog.prototypeAnimal.prototype 的一个子对象,继承了 Animal 的方法。这就是原型链继承的典型例子。

原型链中的属性查找机制

当访问对象属性时,JavaScript 引擎按照以下顺序查找:

  1. 先检查对象自身是否拥有该属性。
  2. 如果对象没有这个属性,则沿着 __proto__ 查找它的原型。
  3. 继续沿着原型链查找,直到找到该属性或到达原型链的终点(null)。
属性查找:例子说明
function Car(brand) {
  this.brand = brand;
}

Car.prototype.getBrand = function() {
  return this.brand;
};

const myCar = new Car('Tesla');
console.log(myCar.brand); // 'Tesla',找到实例自身的属性
console.log(myCar.getBrand()); // 'Tesla',找到原型链上的方法

如果我们尝试访问一个不存在的属性,例如:

console.log(myCar.color); // undefined

此时,JavaScript 引擎会沿着原型链查找 color 属性,但最终没有找到,因此返回 undefined

原型链的继承机制

JavaScript 的继承是基于原型链实现的。通过构造函数和原型的结合,可以实现对象之间的继承。

  1. 构造函数继承属性: 子类构造函数调用父类构造函数,继承父类的属性。

    function Parent() {
      this.name = 'Parent';
    }
    
    function Child() {
      Parent.call(this); // 继承父类属性
    }
    
    const child = new Child();
    console.log(child.name); // 'Parent'
    
  2. 原型链继承方法: 子类通过 Object.create 或直接设置其 prototype 指向父类的原型,实现对父类方法的继承。

    function Parent() {}
    Parent.prototype.sayHello = function() {
      console.log('Hello');
    };
    
    function Child() {}
    Child.prototype = Object.create(Parent.prototype); // 继承父类方法
    Child.prototype.constructor = Child; // 修正 constructor 指向
    
    const child = new Child();
    child.sayHello(); // 输出 'Hello'
    
原型链的常见问题
  1. 原型污染: 修改原型对象会影响所有共享该原型的实例。如果不小心在原型上修改了属性或方法,所有实例都会受到影响。

    function Person(name) {
      this.name = name;
    }
    
    Person.prototype.sayName = function() {
      console.log(this.name);
    };
    
    const person1 = new Person('Alice');
    const person2 = new Person('Bob');
    
    person1.sayName(); // 'Alice'
    
    Person.prototype.sayName = function() {
      console.log('Modified');
    };
    
    person2.sayName(); // 'Modified',所有实例都受到了影响
    
  2. 无法枚举原型链上的属性: 原型链上的属性默认是不可枚举的,即 for...inObject.keys() 只会遍历对象自身的属性,不包括从原型链继承的属性。

    const obj = Object.create({ a: 1 });
    obj.b = 2;
    
    console.log(Object.keys(obj)); // ['b'],原型链上的 'a' 不会被列出
    
应用场景
  1. 继承: 原型链的最大应用场景是继承,通过构造函数和原型链可以创建继承链,实现父类和子类的关系,复用代码。

  2. 属性查找: 原型链机制保证了对象在查找属性时可以自动向上查找,实现了简单的属性继承和扩展。