JavaScript 面向对象编程:从对象字面量到原型链继承,全链路彻底讲透

标题: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 底层实际做了四件事:

  1. 创建一个空对象 {}
  2. 将这个空对象的 proto 指向构造函数的 prototype
  3. 将这个空对象绑定为构造函数的 this,执行构造函数体
  4. 自动返回这个对象(如果构造函数没手动 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 会这样查找:

  1. cat1 自身有没有 type?没有
  2. 去 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 的面向对象。

相关推荐
想吃火锅10053 小时前
【leetcode】405.数字转换为十六进制数js
开发语言·javascript·ecmascript
程序员二叉4 小时前
【Java】集合面试全套精讲|HashMap/ArrayList高频考点完整版
java·面试·哈希算法
原则猫5 小时前
HOOKS 背后机制
前端
码语智行5 小时前
首页导航跳转功能深度解析-系统内和系统外
前端
不懂数据的小白6 小时前
面试题一:【三】AB实验入门(验证)
面试
阿猫的故乡6 小时前
Vue过渡动画从入门到装X:淡入淡出、滑动、列表动画、第三方库全搞定
前端·javascript·vue.js
我叫黑大帅6 小时前
通过php 中的Route:: 的写法了解什么是静态类调用
后端·面试·php
IManiy6 小时前
总结之Vibe Coding前端骨架
前端
小和尚敲木头6 小时前
vue3 vite动态拼接图片路径
javascript
JS菌7 小时前
AI Agent 沙箱双层防护体系:从权限过滤到内核隔离的完整实现
前端·人工智能·后端