标题:JavaScript 面向对象编程:从对象字面量到原型链继承,全链路彻底讲透
JavaScript 是基于对象(object-based)的语言,几乎一切皆对象,但它又不是传统意义上的面向对象语言(class-based OOP)。在 ES6 之前甚至连 class 关键字都没有,真正的核心是原型(prototype)和原型链。
今天我们抛开花里胡哨的框架,直接回到语言本质,用最原始、最经典的方式,一层层把 JS 是怎么实现封装、实例化、共享方法、继承的。
1. 最原始的"面向对象":对象字面量
JavaScript
javascript
var cat1 = {
name: '大橘',
color: '橘色',
sayMeow: function() {
console.log('喵喵喵');
}
};
var cat2 = {
name: '小白',
color: '白色',
sayMeow: function() {
console.log('喵喵喵');
}
};
这样写当然能用,但问题显而易见:
- 每创建一个猫,都要把 sayMeow 方法复制一份,内存浪费严重
- 所有猫之间毫无"血缘"关系,无法体现"同类"概念
我们需要一种能批量生产同类对象的方式 → 构造函数。
2. 构造函数模式:批量生产实例化
JavaScript
ini
function Cat(name, color) {
this.name = name;
this.color = color;
}
const cat1 = new Cat('大橘', '橘色');
const cat2 = new Cat('小白', '白色');
console.log(cat1.name); // 大橘
console.log(cat1 instanceof Cat); // true
new 到底干了什么?V8 底层实际做了四件事:
- 创建一个空对象 {}
- 将这个空对象的 proto 指向构造函数的 prototype
- 将这个空对象绑定为构造函数的 this,执行构造函数体
- 自动返回这个对象(如果构造函数没手动 return 对象或返回基本类型)
这就是"构造函数"名字的由来------它本身只是普通函数,但配合 new 就成了制造实例的"工厂"。
此时 cat1 和 cat2 都有自己独立的 name、color,但如果我们想加一个共有方法呢?
JavaScript
ini
function Cat(name, color) {
this.name = name;
this.color = color;
this.sayMeow = function() { // 千万别这样写!
console.log('喵喵喵');
};
}
这样写虽然能跑,但每个实例的 sayMeow 都是独立函数对象,内存浪费巨大。正确做法是放 prototype 上。
3. Prototype:真正实现方法共享的地方
JavaScript
ini
function Cat(name, color) {
this.name = name;
this.color = color;
}
Cat.prototype.type = '猫科动物';
Cat.prototype.sayMeow = function() {
console.log('喵喵喵~');
};
const cat1 = new Cat('大橘', '橘色');
const cat2 = new Cat('小白', '白色');
console.log(cat1.sayMeow === cat2.sayMeow); // true!完全同一个函数
这才叫真正的"共享"。所有通过 new Cat() 创建的实例,它们的 proto 都指向 Cat.prototype,所以能共享上面的属性和方法。
注意三者关系(前端面试必背):
- cat1.proto === Cat.prototype
- Cat.prototype.constructor === Cat
- Cat.proto === Function.prototype
4. 原型链:属性查找的终极规则
当你访问 cat1.type 时,JS 会这样查找:
- cat1 自身有没有 type?没有
- 去 cat1.proto(即 Cat.prototype)找,有!返回 '猫科动物'
如果 Cat.prototype 也没有,就会继续往上走:
Cat.prototype.proto → Object.prototype → null
这就是原型链(prototype chain)。
JavaScript
arduino
console.log(cat1.toString()); // [object Object]
// cat1 自己没有 toString
// Cat.prototype 也没有
// Object.prototype 有,所以能调用
所有对象最终都继承自 Object.prototype,这就是为什么所有对象都有 toString()、valueOf() 这些方法。
5. 经典检查方式总结
JavaScript
ruby
cat1.hasOwnProperty('name'); // true (自身属性)
cat1.hasOwnProperty('type'); // false(原型上的)
'type' in cat1; // true (原型上也有
Cat.prototype.isPrototypeOf(cat1); // true
cat1 instanceof Cat; // true
cat1 instanceof Object; // true!因为原型链上最终有 Object.prototype
6. 继承:让 Cat 继承 Animal
最早期的继承方式(已淘汰,我们直接看几种主流方式。
方式一:借用构造函数继承(偷属性)
JavaScript
ini
function Animal() {
this.species = '动物';
this.colors = ['black', 'white'];
}
function Cat(name, color) {
Animal.call(this); // 借用父构造函数,偷属性
this.name = name;
this.color = color;
}
const cat1 = new Cat('大橘', '橘色');
console.log(cat1.species); // 动物
优点:能继承父类实例属性 缺点:无法继承父类原型上的方法
方式二:原型链继承(偷方法)
JavaScript
ini
Cat.prototype = new Animal(); // 关键一句!
const cat1 = new Cat('大橘');
cat1.colors.push('yellow');
const cat2 = new Cat('小白');
console.log(cat2.colors); // ['black', 'white', 'yellow'] 污染了!
缺点:所有实例共享父类实例的引用属性,会相互影响。
方式三:组合继承(最常用但有小缺陷)
JavaScript
ini
function Animal() {
this.species = '动物';
this.colors = ['black', 'white'];
}
Animal.prototype.say = function() {
console.log('我是动物');
};
function Cat(name, color) {
Animal.call(this); // 第二次调用 Animal()
this.name = name;
this.color = color;
}
Cat.prototype = new Animal(); // 第二次调用 Animal()
Cat.prototype.constructor = Cat; // 手动修复 constructor
const cat1 = new Cat('大橘', '橘色');
cat1.say(); // 我是动物 成功!
这是 2015 年前最流行的方式,但问题在于 Animal 构造函数被调用了两次,略浪费。
方式四:寄生组合继承(圣杯模式,公认最优)
JavaScript
ini
function inherit(Child, Parent) {
const prototype = Object.create(Parent.prototype); // 创建干净的原型对象
prototype.constructor = Child;
Child.prototype = prototype;
}
function Animal() {
this.species = '动物';
this.colors = ['black', 'white'];
}
Animal.prototype.say = function() {
console.log('我是动物');
};
function Cat(name, color) {
Animal.call(this);
this.name = name;
this.color = color;
}
inherit(Cat, Animal); // 只调用一次 Animal()
const cat1 = new Cat('大橘');
cat1.colors.push('yellow');
const cat2 = new Cat('小白');
console.log(cat2.colors); // ['black', 'white'] 未被污染
cat1.say(); // 我是动物
完美!既能继承实例属性,又能继承原型方法,且无副作用。YUI 库、jQuery 早期都用过类似方式。
7. ES6 class:只是语法糖
JavaScript
javascript
class Animal {
constructor() {
this.species = '动物';
}
say() {
console.log('我是动物');
}
}
class Cat extends Animal {
constructor(name, color) {
super(); // 必须先调用 super()
this.name = name;
this.color = color;
}
sayMeow() {
console.log('喵');
}
}
const cat = new Cat('大橘', '橘色');
cat.say(); // 我是动物
cat.sayMeow(); // 喵
写起来多爽!但请永远记住:
- class 只是语法糖
- 底层仍然是原型继承
- Cat.prototype.proto === Animal.prototype
你可以 console.log(new Cat()) 看它的原型链,和我们上面手写的一模一样。
8. 真正的高级:Object.create 与对象式编程
JavaScript
ini
const animal = {
species: '动物',
say() {
console.log('我是' + this.species);
}
};
const cat = Object.create(animal);
cat.name = '大橘';
cat.color = '橘色';
cat.say(); // 我是动物
这就是《JavaScript 高级程序设计》推崇的"对象指定原型"方式,更贴近 JS "基于对象"的本质。
总结:JS 面向对象的本质只有一句话
所有"类"的概念,都是通过原型链模拟的。
- 实例的 proto 指向 "类"的 prototype
- "类"的 prototype.proto 指向父 "类"的 prototype
- 属性查找沿着 proto 一路向上,直到 Object.prototype
ES6 的 class 只是让我们写得更像 Java、Java,但骨子里 JS 永远是原型式的、灵活的、函数式与面向对象融合的语言。
理解了原型链,你就理解了 JavaScript 面向对象的全部。
不再被"继承"这个词语迷惑,不再分不清 [[Prototype]] 和 prototype,下次面试被问"JS 如何实现继承",你可以直接甩出寄生组合继承 + Object.create,面试官当场拍案叫绝。
这才是真正属于 JavaScript 的面向对象。