原型和原型链

在JavaScript中,原型和原型链是理解对象继承和属性查找机制的核心概念。尽管现代JavaScript引入了class语法,但其底层仍然基于原型继承机制。本文将深入探讨原型和原型链的每一个细节,帮助前端开发者彻底掌握这一重要知识点。

1. 什么是原型

1.1 原型对象

在JavaScript中,每个对象都有一个隐藏的[[Prototype]]属性(在ES5中可通过Object.getPrototypeOf()访问),它指向另一个对象或者null。这个被指向的对象就是该对象的"原型"。

javascript 复制代码
// 创建一个对象
let animal = {
  eats: true
};

let rabbit = {
  jumps: true
};

// 设置rabbit的原型为animal
Object.setPrototypeOf(rabbit, animal);

// 现在rabbit可以访问animal的属性
console.log(rabbit.eats); // true
console.log(rabbit.jumps); // true

1.2 函数的prototype属性

每个函数都有一个特殊的prototype属性(箭头函数除外),这个属性只有在函数作为构造函数使用时才有意义。

javascript 复制代码
function Animal(name) {
  this.name = name;
}

// 函数的prototype属性默认是一个包含constructor属性的对象
console.log(Animal.prototype); // { constructor: Animal }

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

let animal = new Animal('Dog');
animal.speak(); // Dog makes a noise.

2. 原型链机制

2.1 属性查找机制

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

  1. 在对象自身属性中查找
  2. 如果没找到,则在对象的原型中查找
  3. 如果还没找到,则在原型的原型中查找,依此类推直到找到属性或到达原型链末端(null)
javascript 复制代码
let animal = {
  eats: true
};

let rabbit = {
  jumps: true,
  __proto__: animal // 设置原型(实际开发中应使用Object.setPrototypeOf)
};

console.log(rabbit.jumps); // true (自身属性)
console.log(rabbit.eats); // true (从原型获取)
console.log(rabbit.toString); // function (从Object.prototype获取)

2.2 原型链图示

复制代码
rabbit -> animal -> Object.prototype -> null

这是一个典型的三级原型链,rabbit从animal继承,animal从Object.prototype继承。

3. 构造函数与原型

3.1 new操作符的工作原理

当使用new操作符调用函数时,会发生以下几步:

  1. 创建一个新的空对象
  2. 将这个新对象的原型设置为函数的prototype属性
  3. 将函数的this绑定到这个新对象
  4. 执行函数体
  5. 如果函数没有返回对象,则返回this(即新创建的对象)
javascript 复制代码
function MyClass(value) {
  this.value = value;
}

// 相当于
function MyClass(value) {
  // 1. let obj = {}; (隐式创建)
  // 2. obj.__proto__ = MyClass.prototype; (隐式设置)
  // 3. this = obj; (隐式绑定)
  this.value = value;
  // 4. return this; (隐式返回)
}

3.2 constructor属性

每个函数的prototype属性都有一个constructor属性,指向函数本身。

javascript 复制代码
function Animal() {}

console.log(Animal.prototype.constructor === Animal); // true

let animal = new Animal();
console.log(animal.constructor === Animal); // true(通过原型链查找)

4. 原型继承的实现方式

4.1 组合继承(经典继承)

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

javascript 复制代码
// 父类
function Animal(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}

Animal.prototype.speak = function() {
  console.log(`${this.name} makes a noise.`);
};

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

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

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

let dog = new Dog('Rex', 'German Shepherd');
dog.colors.push('green');
console.log(dog.colors); // ['red', 'blue', 'green']

let dog2 = new Dog('Max', 'Golden Retriever');
console.log(dog2.colors); // ['red', 'blue'] (不受影响)

4.2 寄生组合式继承

这是目前最理想的继承方式,避免了组合继承中调用两次父类构造函数的问题。

javascript 复制代码
function inheritPrototype(child, parent) {
  let prototype = Object.create(parent.prototype);
  prototype.constructor = child;
  child.prototype = prototype;
}

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

Animal.prototype.speak = function() {
  console.log(`${this.name} makes a noise.`);
};

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

// 使用寄生组合式继承
inheritPrototype(Dog, Animal);

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

5. ES6 class与原型的关系

5.1 class语法糖

ES6的class本质上是原型继承的语法糖,底层仍然基于原型机制。

javascript 复制代码
class Animal {
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }
  
  bark() {
    console.log(`${this.name} barks.`);
  }
}

// 等价于之前的寄生组合式继承

5.2 静态方法和属性

静态方法和属性是直接添加到构造函数上的,而不是实例的原型上。

javascript 复制代码
class Animal {
  static className = 'Animal';
  
  static identify() {
    console.log('I am an Animal class');
  }
}

// 等价于
function Animal() {}
Animal.className = 'Animal';
Animal.identify = function() {
  console.log('I am an Animal class');
};

6. 原型相关的方法和操作

6.1 原型访问和设置

javascript 复制代码
let obj = {};
let parent = { foo: 'bar' };

// 设置原型
Object.setPrototypeOf(obj, parent);

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

// 检查对象是否在原型链上
console.log(parent.isPrototypeOf(obj)); // true

6.2 属性描述符与原型

javascript 复制代码
let animal = {
  eats: true
};

let rabbit = Object.create(animal, {
  jumps: {
    value: true,
    enumerable: true,
    writable: true,
    configurable: true
  }
});

console.log(rabbit.jumps); // true
console.log(rabbit.eats); // true

7. 实际应用与性能考虑

7.1 原型与内存效率

使用原型共享方法可以节省内存,因为所有实例共享同一个方法引用,而不是每个实例都创建方法副本。

javascript 复制代码
// 不推荐:每个实例都有独立的sayHello方法
function Person(name) {
  this.name = name;
  this.sayHello = function() {
    console.log(`Hello, I'm ${this.name}`);
  };
}

// 推荐:所有实例共享原型上的sayHello方法
function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, I'm ${this.name}`);
};

7.2 避免原型污染

修改内置对象的原型(如Array.prototype、Object.prototype)会导致难以调试的问题,应避免这样做。

javascript 复制代码
// 不推荐:污染Array原型
Array.prototype.myCustomMethod = function() {
  // 实现...
};

// 推荐:使用工具函数
function myCustomMethod(array) {
  // 实现...
}

8. 常见面试题解析

8.1 实现instanceof操作符

javascript 复制代码
function myInstanceof(obj, constructor) {
  if (typeof constructor !== 'function') {
    throw new Error('Right-hand side of instanceof is not callable');
  }
  
  let proto = Object.getPrototypeOf(obj);
  while (proto !== null) {
    if (proto === constructor.prototype) {
      return true;
    }
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}

console.log(myInstanceof([], Array)); // true
console.log(myInstanceof([], Object)); // true

8.2 实现new操作符

javascript 复制代码
function myNew(constructor, ...args) {
  // 1. 创建一个新对象,其原型指向constructor的prototype
  let obj = Object.create(constructor.prototype);
  
  // 2. 调用构造函数,将this绑定到新对象
  let result = constructor.apply(obj, args);
  
  // 3. 如果构造函数返回一个对象,则返回该对象,否则返回新对象
  return typeof result === 'object' && result !== null ? result : obj;
}

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

let person = myNew(Person, 'John');
console.log(person.name); // John

结论

原型和原型链是JavaScript面向对象编程的基石,理解这一机制对于掌握JavaScript至关重要。虽然ES6引入了class语法使其更易用,但底层仍然是基于原型的继承。通过深入理解原型链的工作原理、继承的实现方式以及相关API的使用,开发者能够编写出更高效、更健壮的JavaScript代码。

在实际开发中,建议使用现代的class语法进行面向对象编程,但同时要理解其背后的原型机制,这样才能在遇到复杂问题时游刃有余。