【 前端三剑客-35 /Lesson58(2025-12-08)】JavaScript 原型继承与对象创建机制详解🧬

6 阅读6分钟

🧬JavaScript 是一门基于原型(Prototype-based)的动态语言,其继承机制与传统的类式(Class-based)语言(如 Java、C++)有本质区别。本文将系统性地讲解 JavaScript 中 原型继承 的核心原理、多种实现方式、对象创建方法、call/apply 的作用、组合继承优化技巧、以及现代 ES6 class 语法的本质,并结合实际代码深入剖析每一个细节。


🔗 一、原型与原型链:JavaScript 继承的基石

📌 构造函数、原型、实例三者关系

在 JavaScript 中,每个函数都有一个 prototype 属性,它指向一个对象(称为原型对象)。当我们使用 new 关键字调用构造函数时,会创建一个新对象,该对象的内部属性 [[Prototype]](可通过 __proto__ 访问)会指向构造函数的 prototype

function Cat() {}
Cat.prototype.species = '猫科动物';

const cat = new Cat();
console.log(cat.__proto__ === Cat.prototype); // true

此时:

  • Cat 是构造函数;
  • Cat.prototype 是所有 Cat 实例共享的原型对象;
  • cat 是实例,其 __proto__ 指向 Cat.prototype

🔍 属性查找机制:遮蔽(Shadowing)

当访问 cat.species 时,JavaScript 引擎按以下顺序查找:

  1. 先查实例自身cat 是否有 species 属性?
  2. 若无,则沿 __proto__ 向上查找:即 Cat.prototype.species
  3. 继续向上Cat.prototype.__proto__Object.prototype
  4. 最终到 null,查找失败。
console.log(cat.species); // '猫科动物'(来自原型)

cat.species = 'hello'; // 在实例上定义同名属性
console.log(cat.species); // 'hello'(遮蔽了原型属性)

delete cat.species;
console.log(cat.species); // '猫科动物'(恢复原型值)

关键点:实例属性优先于原型属性,这就是“属性遮蔽”。

🌐 原型链可视化

cat (实例)
  └─ __proto__ → Cat.prototype
        └─ __proto__ → Object.prototype
              └─ __proto__ → null

验证:

console.log(cat.constructor === Cat); // true
console.log(Cat.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true

⚙️ 二、继承的本质:对象委托(Object Delegation)

JavaScript 并非“类继承”,而是对象直接从其他对象继承——这称为委托
当你调用 cat.say(),而 cat 自身没有 say 方法时,引擎会委托给 Cat.prototype 去执行。

Cat.prototype.say = function() { return '喵喵喵'; };
const cat1 = new Cat(), cat2 = new Cat();
console.log(cat1.say === cat2.say); // true(共享同一个函数)

💡 这就是方法复用的核心优势:节省内存,提高性能。


🧱 三、继承的多种实现方式

1️⃣ 原型链继承(Prototype Chain Inheritance)

function SuperType() {
  this.property = true;
}
SuperType.prototype.getSuperValue = function() { return this.property; };

function SubType() {
  this.subproperty = false;
}
SubType.prototype = new SuperType(); // 关键:子类原型 = 父类实例
SubType.prototype.getSubValue = function() { return this.subproperty; };

✅ 优点:简单直观。
❌ 缺点:

  • 引用类型属性被所有实例共享(如 colors: []);
  • 无法向父构造函数传参。

2️⃣ 构造函数继承(Constructor Stealing / Classical Inheritance)

利用 callapply 在子类中调用父类构造函数:

function SuperType(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}

function SubType(name, age) {
  SuperType.call(this, name); // ✅ 绑定 this,传递参数
  this.age = age;
}

🔁 call vs apply 详解

方法语法参数形式适用场景
callfunc.call(thisArg, arg1, arg2, ...)逐个参数参数数量固定
applyfunc.apply(thisArg, [arg1, arg2, ...])数组或 arguments参数动态、已存在数组
// 示例:借用方法
greet.call(person1, '你好'); 
Math.max.apply(null, [1,2,3,4,5]); // 推荐方式

// 继承中两者等价
SuperType.call(this, name);
SuperType.apply(this, [name]);

✅ 优点:

  • 可传参;
  • 避免引用类型共享(每个实例独立副本)。

❌ 缺点:

  • 方法定义在构造函数内 → 无法复用;
  • 无法继承父类原型上的方法

3️⃣ 组合继承(Combination Inheritance)⭐ 最常用

结合前两种方式:

function SuperType(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}
SuperType.prototype.sayName = function() { return this.name; };

function SubType(name, age) {
  SuperType.call(this, name); // 继承属性(每个实例独立)
  this.age = age;
}

SubType.prototype = new SuperType(); // 继承方法(共享)
SubType.prototype.constructor = SubType; // 修复 constructor
SubType.prototype.sayAge = function() { return this.age; };

✅ 优点:兼具属性独立 + 方法复用。
❌ 缺点:父类构造函数被调用两次(一次 new SuperType(),一次 SuperType.call)。


4️⃣ 利用空对象作为中介(寄生组合继承)✨ 优化版

为避免父构造函数被调用两次,引入空函数中介

function extend(Child, Parent) {
  const F = function() {};       // 空函数
  F.prototype = Parent.prototype; // 中介原型 = 父类原型
  Child.prototype = new F();     // 子类原型 = 空实例
  Child.prototype.constructor = Child; // 修复 constructor
}

// 使用
extend(Cat, Animal);

🧠 原理:new F() 不会执行 Parent 构造函数,只建立原型链接。

这是 最高效的原型链继承方式,也是许多库(如早期 jQuery)采用的模式。


🧪 四、new Object()new function() 创建对象的区别

🔹 new Object()

var obj1 = new Object(); // {}
var obj2 = new Object("hello"); // String 对象
  • 原型:Object.prototype
  • 初始化能力弱
  • 适合创建简单空对象

🔸 new function()

var user = new function(username) {
  if (!username) throw Error("用户名不能为空");
  this.username = username;
  var privateVar = "secret"; // 私有变量(闭包)
  this.check = function(pwd) { return pwd === privateVar; };
};
  • 原型:自定义函数的 prototype
  • 支持复杂初始化、私有变量、方法定义
  • 可用于对象工厂
特性new Object()new function()
原型Object.prototype自定义 prototype
私有属性✅(闭包)
方法复用✅(通过原型)
灵活性

💡 在继承中,new function()(空函数)是实现中介继承的关键。


🧩 五、ES6 class 语法:语法糖,本质仍是原型

class Person {
  constructor(name) {
    this.name = name;
  }
  sayHello() {
    return `Hello, I'm ${this.name}`;
  }
}

class Student extends Person {
  constructor(name, grade) {
    super(name); // 必须调用,等价于 Person.call(this, name)
    this.grade = grade;
  }
  study() {
    return `${this.name} is studying...`;
  }
}

⚠️ 注意:

  • extends 本质仍是基于原型的继承
  • super() 必须在子类构造函数中调用;
  • 静态方法和属性也可继承。

🛠 六、最佳实践与高级技巧

✅ 组合优于继承(Composition over Inheritance)

const canFly = { fly() { console.log('Flying...'); } };
const canSwim = { swim() { console.log('Swimming...'); } };
const duck = Object.assign({}, canFly, canSwim);

更灵活,避免继承层级过深。

✅ 使用标准 API 操作原型

// 获取原型
Object.getPrototypeOf(obj);

// 设置原型(谨慎!影响性能)
Object.setPrototypeOf(obj, newProto);

// 创建对象并指定原型
Object.create(proto);

避免使用非标准的 __proto__

✅ 重写原型时修复 constructor

Person.prototype = {
  constructor: Person, // 手动设置
  method1() {},
  method2() {}
};

否则 instance.constructor 会指向 Object


⚠️ 七、常见陷阱与注意事项

1. 原型上不要放引用类型

// ❌ 错误
Person.prototype.skills = ['JS', 'HTML']; // 所有实例共享!

// ✅ 正确
function Person() {
  this.skills = ['JS', 'HTML']; // 每个实例独立
}

2. 原型链过深影响性能

频繁访问的属性应直接定义在实例上,避免长链查找。

3. instanceof 原理

myDog instanceof Dog; // 检查 Dog.prototype 是否在 myDog 的原型链上

🧪 八、综合示例:完整的动物继承体系

// 父类
function Animal(name) {
  this.name = name;
  this.alive = true;
}
Animal.prototype = {
  constructor: Animal,
  eat() { console.log(`${this.name} 在进食`); },
  sleep() { console.log(`${this.name} 在睡觉`); }
};

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

// 原型链继承(空对象中介)
const F = function() {};
F.prototype = Animal.prototype;
Dog.prototype = new F();
Dog.prototype.constructor = Dog;

// 扩展方法
Dog.prototype.bark = function() {
  console.log(`${this.name} 汪汪叫`);
};

// 测试
const myDog = new Dog('小黑', '拉布拉多');
myDog.eat();  // 继承自动物
myDog.bark(); // 狗特有
console.log(myDog instanceof Animal); // true

🌈 总结

JavaScript 的继承机制围绕 原型链对象委托 展开,核心概念包括:

  • 构造函数:创建对象的模板;
  • prototype:共享属性和方法的容器;
  • proto:实例指向原型的链接;
  • call/apply:实现构造函数继承的关键;
  • 组合继承 + 空对象中介:最优实践;
  • ES6 class:语法糖,底层仍是原型。

理解这些机制,不仅能写出健壮的继承代码,还能深入掌握 JavaScript 的面向对象哲学——万物皆对象,继承即委托

🧠 记住:JavaScript 不是“模拟类”,而是原生支持对象间直接继承。拥抱原型,而非对抗它。