当然可以!你提供的文章已经非常系统、清晰地讲解了 JavaScript 面向对象的核心机制。为了进一步完善内容 ,我们将把上一个对话中关于 Animal.apply(this) 的详细解析有机融入到"第五部分:继承"中,并补充一些关键细节(如传参、组合继承的局限性、现代替代方案等),使整篇文章逻辑更严密、教学更完整。
JavaScript 面向对象编程:从构造函数到原型继承的完整指南
本文将带你深入理解 JavaScript 中面向对象(OOP)的核心机制 ------ 构造函数、原型(prototype)、继承,以及 ES6 的 class 语法糖背后的真相。
JavaScript 是一门基于对象的语言,但它的 OOP 实现方式与其他主流语言(如 Java、C++)截然不同。它没有传统意义上的"类",而是通过**原型链(Prototype Chain)**实现对象之间的继承与共享。
即使在 ES6 引入了 class 关键字后,其底层依然是基于原型的。因此,要真正掌握 JS 的面向对象,必须理解 构造函数 + 原型 这一核心模型。
一、原始模式:对象字面量的局限
最简单的创建对象方式:
js
var cat1 = {
name: '加菲猫',
color: '橘色'
};
var cat2 = {
name: '黑猫警长',
color: '黑色'
};
这种方式虽然直观,但存在明显问题:
- 代码重复:每个实例都要手动定义相同结构;
- 无法批量生成:缺乏"模板"概念;
- 无类型标识 :无法判断
cat1是否属于"猫"这个类别。
于是,我们需要一种封装实例化过程的方式。
二、构造函数:封装实例化的第一步
使用函数配合 new 关键字,模拟"类"的行为:
js
function Cat(name, color) {
this.name = name;
this.color = color;
}
const cat1 = new Cat('加菲猫', '橘色');
const cat2 = new Cat('黑猫警长', '黑色');
new 调用时发生了什么?
- 创建一个空对象
{}; - 将该对象的内部
[[Prototype]]链接到构造函数的prototype; - 将
this指向该空对象; - 执行构造函数体,为
this添加属性; - 自动返回
this(除非显式返回一个非null对象)。
⚠️ 注意:若直接调用
Cat()(不加new),this指向全局对象(浏览器中是window),造成污染。严格模式下会报错。
类型判断
cat1 instanceof Cat→truecat1.constructor === Cat→true
这表明实例与构造函数建立了明确的归属关系。
三、原型(Prototype):解决方法冗余问题
如果我们在构造函数内定义方法:
js
function Cat(name, color) {
this.name = name;
this.color = color;
this.eat = function() { console.log('eat jerry'); }; // ❌ 每个实例都新建一个函数!
}
这会导致内存浪费------每个实例都持有一份相同的函数副本。
解决方案:使用 prototype
js
Cat.prototype.type = '猫科动物';
Cat.prototype.eat = function() {
console.log('eat jerry');
};
所有通过 new Cat() 创建的实例,都会共享这些原型上的属性和方法。
如何区分"自身属性"和"继承属性"?
cat1.hasOwnProperty('name')→true(自身)cat1.hasOwnProperty('type')→false(来自原型)'type' in cat1→true(无论来源)
遍历实例属性时,建议过滤:
js
for (let prop in cat1) {
if (cat1.hasOwnProperty(prop)) {
console.log(prop, cat1[prop]);
}
}
四、ES6 的 class:语法糖而已
ES6 提供了更清晰的 OOP 语法:
js
class Cat {
constructor(name, color) {
this.name = name;
this.color = color;
}
eat() {
console.log('eat jerry');
}
}
但请注意:class 本质仍是原型机制!
验证如下:
js
const cat1 = new Cat('tom', '黑色');
console.log(cat1.__proto__ === Cat.prototype); // true
console.log(Cat.prototype.constructor === Cat); // true
所以,class 只是让代码更易读,底层逻辑未变。
五、继承:如何让 Cat 继承 Animal?
JavaScript 的继承需要同时处理两部分:
- 实例属性的继承(通过构造函数)
- 原型方法的继承(通过原型链)
步骤 1:借用构造函数(继承实例属性)
js
function Animal(species) {
this.species = species || '动物';
console.log(this, '/////'); // 此时 this 是 Cat 的新实例
}
function Cat(name, color) {
console.log(this); // {} ------ new Cat() 创建的新对象
// 👇 关键:在当前 Cat 实例上下文中执行 Animal
Animal.apply(this, ['哺乳动物']); // 可传递参数!
this.name = name;
this.color = color;
}
🔍 深入解析 Animal.apply(this)
-
apply的第一个参数指定了函数执行时的this上下文。 -
当
new Cat(...)被调用时,JS 引擎先创建一个新对象(设为obj),并将Cat内部的this指向obj。 -
执行
Animal.apply(this)相当于:js// 在 obj 上运行 Animal 函数 obj.species = '哺乳动物'; -
结果:
Cat实例拥有了species属性,且每个实例都有独立副本,避免引用类型共享问题。
✅ 优点:支持传参、属性隔离
❌ 缺点:无法继承
Animal.prototype上的方法
步骤 2:设置原型链(继承原型方法)
为了让 Cat 实例也能访问 Animal 原型上的方法,需建立原型链:
js
// 先给 Animal 添加原型方法
Animal.prototype.sayHi = function() {
console.log(`Hi, I'm a ${this.species}`);
};
// 让 Cat.prototype 指向一个 Animal 实例
Cat.prototype = new Animal(); // 注意:这里没传参,species 为默认值
// 修复 constructor 指向(否则 cat.constructor === Animal)
Cat.prototype.constructor = Cat;
现在测试:
js
const cat = new Cat('加菲猫', '橘色');
console.log(cat);
// { species: '哺乳动物', name: '加菲猫', color: '橘色' }
cat.sayHi(); // ✅ "Hi, I'm a 哺乳动物"
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true
完整继承模型图解
lua
cat (实例)
└─ [[Prototype]] → Cat.prototype (即 new Animal())
└─ [[Prototype]] → Animal.prototype
└─ [[Prototype]] → Object.prototype
└─ [[Prototype]] → null
💡 提示:现代开发中,可使用
Object.create(Animal.prototype)替代new Animal(),避免调用父构造函数带来的副作用(如不需要初始化父类实例属性时)。
更健壮的组合继承写法(推荐)
js
function Animal(species) {
this.species = species || '动物';
}
Animal.prototype.sayHi = function() {
console.log(`Hi, I'm a ${this.species}`);
};
function Cat(name, color) {
// 继承属性(可传参)
Animal.call(this, '猫科动物'); // 使用 call 更常见(参数列表 vs 参数数组)
this.name = name;
this.color = color;
}
// 继承方法(不调用 Animal 构造函数)
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;
// 可继续扩展 Cat 自己的方法
Cat.prototype.meow = function() {
console.log('Meow~');
};
这种模式称为 "组合继承"(Combination Inheritance) ,是 ES5 时代最常用的继承方式。
六、总结:JS OOP 的核心要点
| 概念 | 说明 |
|---|---|
| 构造函数 | 用于创建实例,配合 new 使用 |
prototype |
存放共享属性/方法,避免重复创建 |
__proto__ / [[Prototype]] |
实例指向其构造函数的 prototype |
hasOwnProperty |
判断是否为自身属性 |
instanceof |
检查原型链中是否存在某构造函数的 prototype |
| 继承 | 属性靠 call/apply,方法靠原型链 |
ES6 class |
语法糖,底层仍是原型 |
| 组合继承 | 最常用的传统继承模式(属性 + 原型链) |
七、延伸思考:现代继承的最佳实践
虽然组合继承很强大,但它调用了两次父构造函数 (一次在 new Animal() 设置原型,一次在 Cat 内部),略显冗余。
ES6 之后,直接使用 class extends 是首选:
js
class Animal {
constructor(species = '动物') {
this.species = species;
}
sayHi() {
console.log(`Hi, I'm a ${this.species}`);
}
}
class Cat extends Animal {
constructor(name, color) {
super('猫科动物'); // 等价于 Animal.call(this, ...)
this.name = name;
this.color = color;
}
meow() {
console.log('Meow~');
}
}
- 语义清晰
- 自动处理原型链
- 支持静态方法、getter/setter 等高级特性
但请记住:super() 底层仍然是 Parent.call(this) + 原型链设置。
📌 终极箴言 :
JavaScript 没有类,只有对象;没有继承,只有委托。理解原型链,就掌握了 JS 的灵魂。
标签:#JavaScript #面向对象 #原型链 #前端进阶 #ES6
欢迎点赞、收藏、评论交流!