Skip to content

JavaScript 原型与继承机制

引言

如果把JavaScript比作一座城市,那么原型系统就是这座城市的地下管网——虽然平时不太引人注目,却是整个城市运转的关键基础设施。JavaScript的原型继承机制与其他主流编程语言的类继承有很大不同,理解它不仅能帮助我们写出更高效、更优雅的代码,还能揭示许多JavaScript内部运作的奥秘。今天,让我们一起深入这个既强大又容易被误解的特性!

原型基础

JavaScript的原型系统是实现对象行为共享和代码复用的核心机制。

什么是原型

javascript
// 每个JavaScript对象都有一个原型
let obj = {};
console.log(Object.getPrototypeOf(obj) === Object.prototype);  // true

原型本质上是一个对象,它作为其他对象的模板,包含可被继承的属性和方法。当我们访问一个对象的属性时,如果对象本身没有这个属性,JavaScript会沿着原型链向上查找。

prototype 属性

javascript
// 函数才有prototype属性
function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function() {
  console.log(`你好,我是${this.name}`);
};

let person1 = new Person("小明");
person1.sayHello();  // 输出: 你好,我是小明

每个函数都有一个prototype属性,指向一个对象。当这个函数被用作构造函数时,新创建的对象会将这个函数的prototype作为自己的原型。

proto 属性

javascript
let person2 = new Person("小红");
console.log(person2.__proto__ === Person.prototype);  // true

每个对象都有一个特殊的属性__proto__(现代浏览器中可以直接访问),它指向该对象的原型。注意这是一个非标准属性,推荐使用Object.getPrototypeOf()方法获取对象的原型。

constructor 属性

javascript
console.log(person1.constructor === Person);  // true
console.log(Person.prototype.constructor === Person);  // true

原型对象有一个constructor属性,指回构造函数本身。这形成了一个从实例到原型再到构造函数的引用循环。

原型链原理

javascript
// 原型链示例
console.log(person1.__proto__ === Person.prototype);  // true
console.log(Person.prototype.__proto__ === Object.prototype);  // true
console.log(Object.prototype.__proto__ === null);  // true

原型链是由一系列对象通过__proto__链接形成的。当访问一个对象的属性时,JavaScript会:

  1. 先检查对象自身是否有该属性
  2. 如果没有,则检查对象的原型
  3. 如果还没有,则继续查找原型的原型
  4. 一直到找到属性或达到原型链的顶端(null

继承方式

JavaScript提供了多种实现继承的方式,每种都有其特点和适用场景。

原型链继承

javascript
function Animal(name) {
  this.name = name;
  this.colors = ["黑", "白"];
}

Animal.prototype.eat = function() {
  console.log(`${this.name}正在吃东西`);
};

function Dog(name) {
  this.type = "狗";
}

// 设置原型链关系
Dog.prototype = new Animal("旺财");

let dog = new Dog();
dog.eat();  // 输出: 旺财正在吃东西
console.log(dog.colors);  // 输出: ["黑", "白"]

优点:简单,子类能访问父类原型上的方法。
缺点:所有实例共享父类实例属性,无法向父类构造函数传参。

构造函数继承

javascript
function Animal(name) {
  this.name = name;
  this.colors = ["黑", "白"];
}

Animal.prototype.eat = function() {
  console.log(`${this.name}正在吃东西`);
};

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

let dog = new Dog("小黑");
console.log(dog.name);  // 输出: 小黑
console.log(dog.colors);  // 输出: ["黑", "白"]
// dog.eat();  // 错误: dog.eat is not a function

优点:可以向父类构造函数传参,实例不共享父类属性。
缺点:无法继承父类原型上的方法,每个实例都会创建父类方法的副本。

组合继承

javascript
function Animal(name) {
  this.name = name;
  this.colors = ["黑", "白"];
}

Animal.prototype.eat = function() {
  console.log(`${this.name}正在吃东西`);
};

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

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

let dog = new Dog("小黑");
dog.eat();  // 输出: 小黑正在吃东西
dog.colors.push("黄");
console.log(dog.colors);  // 输出: ["黑", "白", "黄"]

let dog2 = new Dog("小白");
console.log(dog2.colors);  // 输出: ["黑", "白"], 不受dog影响

优点:结合了上述两种方式的优点。
缺点:父类构造函数被调用两次,效率较低。

原型式继承

javascript
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

let animal = {
  name: "动物",
  colors: ["黑", "白"],
  eat: function() {
    console.log(`${this.name}正在吃东西`);
  }
};

let dog = object(animal);
dog.name = "小黑";
dog.eat();  // 输出: 小黑正在吃东西

// ES5提供了简化方法
let dog2 = Object.create(animal);

优点:简单,不需要构造函数。
缺点:共享引用类型属性,与原型链继承类似。

寄生式继承

javascript
function createDog(original) {
  let clone = Object.create(original);
  clone.bark = function() {
    console.log("汪汪汪!");
  };
  return clone;
}

let animal = {
  name: "动物",
  eat: function() {
    console.log(`${this.name}正在吃东西`);
  }
};

let dog = createDog(animal);
dog.bark();  // 输出: 汪汪汪!
dog.eat();   // 输出: 动物正在吃东西

优点:在原型式继承基础上增强对象。
缺点:函数不能复用,每次创建都会生成新函数。

寄生组合式继承

javascript
function inheritPrototype(subType, superType) {
  let prototype = Object.create(superType.prototype);
  prototype.constructor = subType;
  subType.prototype = prototype;
}

function Animal(name) {
  this.name = name;
  this.colors = ["黑", "白"];
}

Animal.prototype.eat = function() {
  console.log(`${this.name}正在吃东西`);
};

function Dog(name) {
  Animal.call(this, name);
  this.type = "狗";
}

inheritPrototype(Dog, Animal);

Dog.prototype.bark = function() {
  console.log("汪汪汪!");
};

let dog = new Dog("小黑");
dog.eat();  // 输出: 小黑正在吃东西
dog.bark();  // 输出: 汪汪汪!

优点:只调用一次父类构造函数,保持原型链,是引用类型最理想的继承方式。
缺点:实现较复杂。

ES6 中的继承

ES6引入的类语法大大简化了继承的实现,但底层仍是基于原型。

class 关键字

javascript
class Animal {
  constructor(name) {
    this.name = name;
    this.colors = ["黑", "白"];
  }
  
  eat() {
    console.log(`${this.name}正在吃东西`);
  }
}

let animal = new Animal("动物");
animal.eat();  // 输出: 动物正在吃东西

class关键字提供了更清晰的语法来定义类,但实际上是原型继承的语法糖。

extends 与 super

javascript
class Dog extends Animal {
  constructor(name) {
    super(name);  // 调用父类构造函数
    this.type = "狗";
  }
  
  bark() {
    console.log("汪汪汪!");
  }
  
  // 重写父类方法
  eat() {
    super.eat();  // 调用父类方法
    console.log("狗喜欢啃骨头");
  }
}

let dog = new Dog("小黑");
dog.eat();
// 输出:
// 小黑正在吃东西
// 狗喜欢啃骨头

extends关键字设置继承关系,super关键字用于调用父类的构造函数和方法。

静态方法继承

javascript
class Animal {
  static isAnimal(obj) {
    return obj instanceof Animal;
  }
}

class Dog extends Animal {
  static isDog(obj) {
    return obj instanceof Dog;
  }
}

console.log(Dog.isAnimal(new Dog()));  // true,继承了父类的静态方法

子类自动继承父类的静态方法,这在ES5中需要手动实现。

私有属性与方法

javascript
class Animal {
  // 公有属性
  name;
  
  // 私有属性(ES2022标准)
  #age;
  
  constructor(name, age) {
    this.name = name;
    this.#age = age;
  }
  
  // 私有方法
  #calculateLifeExpectancy() {
    return this.#age * 3;
  }
  
  getInfo() {
    console.log(`${this.name}的预期寿命是${this.#calculateLifeExpectancy()}年`);
  }
}

let animal = new Animal("猫", 5);
animal.getInfo();  // 输出: 猫的预期寿命是15年
// console.log(animal.#age);  // 语法错误:私有字段不可访问

私有字段和方法使用#前缀,只能在类内部访问,有助于封装实现细节。

继承内置类型

javascript
// 扩展内置Array类
class MyArray extends Array {
  first() {
    return this[0];
  }
  
  last() {
    return this[this.length - 1];
  }
}

let arr = new MyArray(1, 2, 3, 4);
console.log(arr.first());  // 1
console.log(arr.last());   // 4
console.log(arr.length);   // 4
console.log(arr instanceof Array);  // true

ES6允许继承内置类型,这在ES5中很难实现。继承内置类型可以扩展它们的功能,同时保留原有特性。

原型模式应用

原型模式是JavaScript中的一种重要设计模式,有许多实际应用。

使用原型优化性能

javascript
// 不优化的写法
function Person(name, age) {
  this.name = name;
  this.age = age;
  this.sayHello = function() {
    console.log(`你好,我是${this.name}`);
  };
}

// 优化后的写法
function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayHello = function() {
  console.log(`你好,我是${this.name}`);
};

将方法定义在原型上而非实例中,可以节省内存,提高性能。因为所有实例共享同一个方法,而不是每个实例都创建一个方法副本。

多态实现

javascript
// 基类
function Animal() {}
Animal.prototype.makeSound = function() {
  console.log("动物发出声音");
};

// 子类
function Dog() {}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.makeSound = function() {
  console.log("汪汪汪!");
};

function Cat() {}
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.makeSound = function() {
  console.log("喵喵喵!");
};

// 多态使用
function animalSound(animal) {
  animal.makeSound();
}

animalSound(new Dog());  // 输出: 汪汪汪!
animalSound(new Cat());  // 输出: 喵喵喵!

通过原型继承和方法重写,JavaScript可以实现类似其他语言的多态性。

工厂模式结合

javascript
// 定义基础原型
const carPrototype = {
  init(model, color) {
    this.model = model;
    this.color = color;
    return this;
  },
  getInfo() {
    return `这是一辆${this.color}色的${this.model}`;
  }
};

// 工厂函数
function createCar(model, color) {
  return Object.create(carPrototype).init(model, color);
}

// 使用工厂创建对象
const car1 = createCar("特斯拉", "黑");
const car2 = createCar("宝马", "白");

console.log(car1.getInfo());  // 输出: 这是一辆黑色的特斯拉
console.log(car2.getInfo());  // 输出: 这是一辆白色的宝马

结合工厂模式和原型模式,可以灵活创建共享行为的对象,同时保持代码的组织性。

原型设计模式

javascript
// 原型对象
const personPrototype = {
  greeting: function() {
    console.log(`你好,我是${this.name}`);
  },
  farewell: function() {
    console.log(`再见,${this.name}要走了`);
  }
};

// 创建实例
function createPerson(name) {
  const person = Object.create(personPrototype);
  person.name = name;
  return person;
}

const person1 = createPerson("小明");
person1.greeting();  // 输出: 你好,我是小明

// 扩展原型
const studentPrototype = Object.create(personPrototype);
studentPrototype.study = function() {
  console.log(`${this.name}正在学习`);
};

function createStudent(name, school) {
  const student = Object.create(studentPrototype);
  student.name = name;
  student.school = school;
  return student;
}

const student1 = createStudent("小红", "某大学");
student1.greeting();  // 输出: 你好,我是小红
student1.study();     // 输出: 小红正在学习

原型设计模式允许通过克隆现有对象来创建新对象,避免了显式创建类的过程,适合动态环境。

常见问题与陷阱

理解原型机制的一些常见问题和注意事项。

原型污染

javascript
// 危险操作
Object.prototype.customMethod = function() {
  console.log("我被添加到了所有对象上");
};

let obj = {};
obj.customMethod();  // 输出: 我被添加到了所有对象上

// 更危险的例子
Object.prototype.hasOwnProperty = function() {
  return false;
};  // 这会破坏许多依赖hasOwnProperty的代码

修改内置对象的原型(如Object.prototype)可能导致意外行为。最佳实践是避免修改内置对象原型,如需扩展,可以使用子类化或组合。

继承深度问题

javascript
// 继承链太长
function A() {}
function B() {}
B.prototype = new A();
function C() {}
C.prototype = new B();
function D() {}
D.prototype = new C();
// ...更多层级

const instance = new D();
// 访问属性时需要遍历整个原型链,影响性能

过深的继承链会影响属性查找效率。建议保持继承层次相对扁平,避免过复杂的继承结构。

this 指向问题

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

Person.prototype.sayHello = function() {
  console.log(`你好,我是${this.name}`);
};

const person = new Person("小明");

// 正常调用
person.sayHello();  // 输出: 你好,我是小明

// 问题情况
const greet = person.sayHello;
greet();  // 输出: 你好,我是undefined (因为this指向全局对象或undefined)

// 解决方法
const boundGreet = person.sayHello.bind(person);
boundGreet();  // 输出: 你好,我是小明

方法中的this指向调用该方法的对象,而不是定义该方法的对象。这是JavaScript中常见的混淆点,尤其在事件处理和回调函数中。

属性查找性能

javascript
// 低效属性查找
function findProp(obj, prop) {
  return obj[prop];  // 可能需要遍历整个原型链
}

// 更高效的实现
function findOwnProp(obj, prop) {
  return obj.hasOwnProperty(prop) ? obj[prop] : undefined;
}

属性查找需要遍历原型链,对于频繁访问的属性,可以考虑将其直接定义在实例上,或使用闭包来避免原型查找。

总结与拓展

JavaScript的原型继承机制虽然与传统的类继承不同,但具有极大的灵活性和表现力。从ES5的原型操作到ES6的类语法,JavaScript提供了多种方式来组织和重用代码。理解原型系统不仅能帮助你编写更优雅的代码,还能让你更好地理解现代JavaScript框架和库的内部工作原理。

拓展阅读建议:

原型继承可能初看复杂,但一旦掌握,你会发现它是JavaScript中最强大、最灵活的特性之一。不断实践,慢慢体会,你会越来越欣赏这个独特的系统!

用❤️分享前端技术 fedev.wiki