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

相关推荐
用户44455436542644 分钟前
TooltipBox在Compose里
前端
liberty8881 小时前
dppt如何找到弹框
java·服务器·前端
宁雨桥1 小时前
使用pnpm构建高效Monorepo:从零到一的完整指南
前端·pnpm·项目架构
chéng ௹1 小时前
uniapp vue3 unipush2.0 调用系统通知功能流程
前端·vue.js·uni-app
小菜今天没吃饱1 小时前
DVWA-XSS(DOM)
前端·javascript·xss·dvwa
q***04631 小时前
Spring Cloud Alibaba 组件版本选择
android·前端·后端
李少兄1 小时前
解决 `npm install` 卡在 `idealTree: sill idealTree buildDeps` 的排查与修复
前端·npm·node.js
毕设十刻1 小时前
基于Vue的企业管理系统pk6uy(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js
27669582921 小时前
雷池waf 逆向
java·开发语言·前端·python·wasm·waf·雷池waf