深入理解 JavaScript 中的面向对象编程(OOP):从构造函数到原型继承

引言

JavaScript 是一门"披着函数式外衣"的基于对象的语言。它没有传统 OOP 语言(如 Java、C++)那样严格的类系统,但通过构造函数 + 原型链的方式,实现了灵活而强大的面向对象能力。本文将结合你提供的代码和文档,带你一步步揭开 JavaScript 面向对象的神秘面纱。

JavaScript 的面向对象机制不同于传统语言。它没有"类"这一概念(早期),而是通过对象原型 来组织代码。随着 ES6 的到来,class 被引入,但它只是语法糖------底层依然是基于原型的机制。掌握原型链,是真正理解 JavaScript OOP 的关键。


一、JavaScript 是"基于对象"的语言

这意味着,在 JavaScript 中,一切皆可视为对象 (或至少可以被当作对象使用)。但早期的 JavaScript 并没有 class 关键字,因此开发者只能通过构造函数来模拟"类"的行为。

它又不是一种真正的面向对象(OOP),早期连 class 关键字都没有,只能通过构造函数来模拟类的行为。(哪怕 ES6 有了 class,仍然是原型式的面向对象)

这说明:ES6 的 class 只是语法糖,底层依然是基于原型(prototype)的机制


二、最原始的"类":对象字面量

我们先看最原始的方式:

javascript 复制代码
// Cat大写,开发时约定是类
// name color 模板 ,抽象,封装的特性在显现
var Cat = {
    name: '',
    color: '',
}

接着手动创建实例:

javascript 复制代码
var cat1 = {}; //空对象
cat1.name = '加菲猫';
cat1.color = '橘色';

var cat2 = {};
cat2.name = '黑猫警长';
cat2.color = '黑色';

这种方式的问题很明显:

  • 重复代码多
  • 实例之间毫无关联
  • 无法复用方法

于是,构造函数登场!


三、构造函数:模拟"类"的诞生

javascript 复制代码
// 解决了重复代码的问题
// 封装了实例化的过程
function Cat(name, color) {
    // this 指向实例对象,由运行的时候决定的
    // 实例对象有name color属性
    // 实例对象的name color属性值由参数传递
    this.name = name; //对象的实例化
    this.color = color;
    console.log(this); //空对象(实际此时已赋值)
}

构造函数的本质:一个可复用的"蓝图"

构造函数并不是魔法,它只是一个普通的函数。它的特殊之处在于:当使用 new 关键字调用时,JavaScript 引擎会自动执行一套标准化的初始化流程

当你执行 new Cat('Tom', 'black') 时,引擎做了什么?
  1. 创建一个全新的空对象{}
  2. 将这个空对象的内部属性 [[Prototype]](即 __proto__)指向 Cat.prototype
    → 这一步建立了原型链的起点
  3. 将函数内部的 this 绑定到这个新对象上
  4. 执行函数体中的代码 :给 this 添加属性(如 name, color
  5. 如果函数没有显式返回一个对象,则自动返回 this

这个过程是隐式的,但极其重要。它使得每个实例都"继承"了构造函数的模板结构。

错误调用:不使用 new
javascript 复制代码
// 普通函数调用的时候,this指向window
Cat('黑猫警长', '黑色');

在非严格模式下,this 默认指向全局对象(浏览器中是 window)。于是:

  • window.name = '黑猫警长'
  • window.color = '黑色'

这不仅污染了全局命名空间,还可能导致难以调试的 bug。在严格模式('use strict')下,这种调用会直接报错,因为 thisundefined

因此,构造函数必须配合 new 使用,这是 JavaScript OOP 的基本礼仪。

正确调用:使用 new
javascript 复制代码
const cat1 = new Cat('黑猫警长', '黑色');
const cat2 = new Cat('加菲猫', '橘色');

现在:

  • cat1cat2 是两个完全独立的对象
  • 它们各自拥有自己的 namecolor 属性
  • 它们的 __proto__ 都指向 Cat.prototype

你可以验证:

javascript 复制代码
console.log(cat1 !== cat2); // true
console.log(cat1.__proto__ === Cat.prototype); // true

这就是"封装"的雏形:数据被隔离在各自的实例中,互不干扰。


四、实例之间的关系:constructorinstanceof

javascript 复制代码
console.log(cat1.constructor === cat2.constructor); // true

为什么为 true?因为:

  • 每个函数在创建时,JavaScript 自动为其分配一个 prototype 对象
  • 这个 prototype 对象默认有一个 constructor 属性,指向函数本身
  • 所有通过 new 创建的实例,其 __proto__ 指向该 prototype
  • 因此,cat1.constructor 实际上是 cat1.__proto__.constructor,即 Cat

这是一种"回溯"机制,让你能知道某个对象是由哪个构造函数创建的。

再看:

javascript 复制代码
console.log(cat1 instanceof Cat); // true
console.log(cat2 instanceof Cat); // true

instanceof 的工作原理是:

检查右边构造函数的 prototype 是否出现在左边对象的原型链上。

换句话说,它会沿着 cat1.__proto__ → Cat.prototype → Object.prototype → null 这条链向上查找,直到找到匹配项或到达终点。

你可以手动模拟:

javascript 复制代码
function myInstanceOf(obj, Constructor) {
    let proto = Object.getPrototypeOf(obj);
    while (proto) {
        if (proto === Constructor.prototype) return true;
        proto = Object.getPrototypeOf(proto);
    }
    return false;
}

console.log(myInstanceOf(cat1, Cat)); // true

instanceof 是类型判断的重要工具,尤其在处理继承关系时非常有用。


五、Prototype 原型模式:共享与效率

问题:方法重复定义

javascript 复制代码
function Cat(name, color) {
    this.name = name;
    this.color = color;
    this.meow = function() {
        console.log(this.name + ' says meow!');
    };
}

每创建一个 Cat 实例,就会新建一个 meow 函数。即使内容完全相同,它们在内存中也是不同的函数对象:

javascript 复制代码
const c1 = new Cat('A', 'red');
const c2 = new Cat('B', 'blue');
console.log(c1.meow === c2.meow); // false

这不仅浪费内存,还违背了"方法应被共享"的 OOP 原则。

解决方案:将共享成员放入 prototype

javascript 复制代码
<script>
    function Cat(name, color) {
        this.name = name
        this.color = color
        // 浪费内存,方法和共有属性可放在函数的原型对象上
        // this.type = '猫科动物'
        // this.eat = function () {console.log('喜欢杰瑞')}
    }
    Cat.prototype.type = '猫科动物'
    Cat.prototype.eat = function () {console.log('喜欢杰瑞')}
    var cat1 = new Cat('tom', '黑色');
    var cat2 = new Cat('jerry', '白色')
    console.log(cat1.type, cat2.type); // 猫科动物 猫科动物
    cat1.type = '猫'
    console.log(cat1.type, cat2.type); // 猫 猫科动物
</script>
方法共享(Method Sharing)

当执行 Cat.prototype.eat = function () { console.log('喜欢杰瑞') } 时:

  • 并不会在每个 Cat 实例上创建独立的 eat 函数
  • 而是将 eat 方法定义在 Cat.prototype 这个共享对象上
  • 此后所有通过 new Cat() 创建的实例(如 cat1, cat2)在调用 .eat() 时,都会沿着原型链找到并执行同一个函数

这实现了方法的高效复用 ,避免了内存浪费,并体现了 JavaScript 原型机制的核心优势:一处定义,多处共享

属性遮蔽(Property Shadowing)

当执行 cat1.type = '猫' 时:

  • 并不会修改 Cat.prototype.type
  • 而是在 cat1 实例上新增一个自有属性 type
  • 此后访问 cat1.type 时,引擎优先返回实例自身的值,屏蔽了原型上的同名属性

这就是原型链查找规则

从实例自身开始查找 → 若未找到,则沿 __proto__ 向上查找 → 直到 Object.prototypenull
这种机制既保证了共享,又允许个性化覆盖,非常灵活。


六、如何判断属性属于实例还是原型?

javascript 复制代码
console.log(cat1.hasOwnProperty('name')); // true
console.log(cat1.hasOwnProperty('type')); // false
  • hasOwnPropertyObject.prototype 上的方法
  • 只检查对象自身的属性,不包括原型链上的属性

in 操作符则不同:

javascript 复制代码
console.log("name" in cat1); // true
console.log("type" in cat1); // true
  • in 会检查整个原型链
  • 只要能找到,就返回 true
遍历属性的正确姿势
javascript 复制代码
// 遍历所有可枚举属性(包括原型)
for (var prop in cat1) {
    console.log(prop, cat1[prop]);
}

// 仅遍历实例自身属性
for (var prop in cat1) {
    if (cat1.hasOwnProperty(prop)) {
        console.log(prop, cat1[prop]);
    }
}

在开发库或框架时,通常只关心实例自身的属性,避免意外遍历到原型上的方法(如 toString)。

此外:

javascript 复制代码
console.log(Cat.prototype.isPrototypeOf(cat1)); // true

isPrototypeOf 是判断某个对象是否在另一个对象的原型链上的权威方法,比手动遍历更可靠。


七、继承:如何实现"子类"?

仅用 apply/call 的局限性

javascript 复制代码
function Animal() {
  this.species = '动物';
}
function Cat(name, color) {
  Animal.apply(this); // 继承属性
  this.name = name;
  this.color = color;
}
const cat1 = new Cat('kitty', '粉色');
console.log(cat1); // { species: '动物', name: 'kitty', color: '粉色' }

✅ 成功复制了 species 属性。

❌ 但如果 Animal 有原型方法,cat1 无法调用!

完整继承:组合模式(Combination Inheritance)

javascript 复制代码
function Animal() {
  this.species = '动物';
}
Animal.prototype.sayHi = function() {
  console.log('啦啦啦啦啦啦');
};

function Cat(name, color) {
  Animal.apply(this); // 继承实例属性
  this.name = name;
  this.color = color;
}

// 关键:设置原型链
Cat.prototype = new Animal(); // 让 Cat.prototype 继承 Animal 的实例
Cat.prototype.constructor = Cat; // 修复 constructor 指向

Cat.prototype.meow = function() {
  console.log(this.name + ' says meow!');
};

const cat1 = new Cat('kitty', '粉色');
cat1.sayHi(); // 啦啦啦啦啦啦 
cat1.meow();  // kitty says meow! 
为什么需要 Cat.prototype.constructor = Cat

因为 Cat.prototype = new Animal() 后:

  • Cat.prototype.constructor 指向 Animal
  • 导致 cat1.constructor === Animal(错误!)

修复后,才能保证:

javascript 复制代码
console.log(cat1.constructor === Cat); // true

这是经典继承模式的"标配"操作。


八、ES6 class:语法糖的真相

javascript 复制代码
class Cat {
    constructor(name, color) {
        this.name = name;
        this.color = color;
    }
    eat() {
        console.log('喜欢杰瑞');
    }
}

这段代码等价于:

javascript 复制代码
function Cat(name, color) {
    this.name = name;
    this.color = color;
}
Cat.prototype.eat = function() {
    console.log('喜欢杰瑞');
};

验证原型链:

javascript 复制代码
const cat1 = new Cat('kitty', 'pink');
console.log(
    cat1.__proto__ === Cat.prototype,         // true
    Cat.prototype.constructor === Cat,        // true
    Cat.prototype.__proto__ === Object.prototype, // true
    Object.prototype.__proto__ === null       // true
);

class 只是让代码更像 Java/C++,但一切仍是原型驱动

class 的优势
  • 语法简洁,可读性强
  • 支持 extendssuper 等关键字
  • 自动绑定 constructor
class 的限制
  • 不能像函数那样被提升(hoisting)
  • 方法不可枚举(enumerable: false
  • 必须用 new 调用,否则报错

所以,理解 class 背后的原型机制,才能写出健壮的代码。


九、总结:JavaScript OOP 的核心要点

概念 说明 实战意义
构造函数 function 模拟类,配合 new 创建实例 是 OOP 的起点
this new 调用时指向新实例 决定属性归属
prototype 所有实例共享的方法/属性仓库 节省内存,实现复用
__proto__ 实例的隐式原型,指向构造函数的 prototype 构成原型链
instanceof 检查原型链中是否存在某 prototype 类型判断
hasOwnProperty 判断属性是否属于实例自身 避免原型污染
继承 call/apply + 原型链实现完整继承 实现代码复用与扩展
class 语法糖,底层仍是原型 提升开发体验,但需知其本质

十、结语:JavaScript 的 OOP 是"动态的哲学"

JavaScript 的面向对象不像 Java 那样"刻在石头上",而是像水流一样灵活。你可以随时给原型添加方法,甚至修改已有实例的行为。这种动态性既是优势,也是挑战。

但只要理解了:

  • 构造函数是模板
  • 原型是共享仓库
  • new 是魔法开关
  • 原型链是继承的桥梁

你就能驾驭这门语言的面向对象之力!

正如代码所示:
"实例化的对象就可以调用类模板的方法了" ------ 这,就是面向对象的魔法开始的地方 🪄

Happy Coding!🐱

相关推荐
汝生淮南吾在北1 小时前
SpringBoot+Vue游戏攻略网站
前端·vue.js·spring boot·后端·游戏·毕业设计·毕设
电子_咸鱼1 小时前
【QT SDK 下载安装步骤详解 + QT Creator 导航栏使用教程】
服务器·开发语言·网络·windows·vscode·qt·visual studio code
2301_797312261 小时前
学习Java22天
java·开发语言
jllllyuz1 小时前
MATLAB雷达系统设计与仿真
开发语言·matlab
cos1 小时前
React RCE 漏洞影响自建 Umami 服务 —— 记 CVE-2025-55182
前端·安全·react.js
IMPYLH1 小时前
Lua 的 type 函数
开发语言·笔记·后端·junit·lua
ConardLi1 小时前
分析了 100 万亿 Token 后,得出的几个关于 AI 的真相
前端·人工智能·后端
老华带你飞1 小时前
英语学习|基于Java英语学习系统(源码+数据库+文档)
java·开发语言·数据库·vue.js·spring boot·后端·学习
qq_479875431 小时前
C++ 模板元编程
java·开发语言·c++