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 的面向对象。

相关推荐
前端不太难21 小时前
从 Navigation State 反推架构腐化
前端·架构·react
前端程序猿之路1 天前
Next.js 入门指南 - 从 Vue 角度的理解
前端·vue.js·语言模型·ai编程·入门·next.js·deepseek
大布布将军1 天前
⚡️ 深入数据之海:SQL 基础与 ORM 的应用
前端·数据库·经验分享·sql·程序人生·面试·改行学it
川贝枇杷膏cbppg1 天前
Redis 的 RDB 持久化
前端·redis·bootstrap
D_C_tyu1 天前
Vue3 + Element Plus | el-table 表格获取排序后的数据
javascript·vue.js·elementui
JIngJaneIL1 天前
基于java+ vue农产投入线上管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot
东东的脑洞1 天前
【面试突击二】JAVA基础知识-volatile、synchronized与ReentrantLock深度对比
java·面试
LYFlied1 天前
【每日算法】LeetCode 153. 寻找旋转排序数组中的最小值
数据结构·算法·leetcode·面试·职场和发展
天外天-亮1 天前
v-if、v-show、display: none、visibility: hidden区别
前端·javascript·html
jump_jump1 天前
手写一个 Askama 模板压缩工具
前端·性能优化·rust