引言
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') 时,引擎做了什么?
- 创建一个全新的空对象 :
{} - 将这个空对象的内部属性
[[Prototype]](即__proto__)指向Cat.prototype
→ 这一步建立了原型链的起点 - 将函数内部的
this绑定到这个新对象上 - 执行函数体中的代码 :给
this添加属性(如name,color) - 如果函数没有显式返回一个对象,则自动返回
this
这个过程是隐式的,但极其重要。它使得每个实例都"继承"了构造函数的模板结构。
错误调用:不使用 new
javascript
// 普通函数调用的时候,this指向window
Cat('黑猫警长', '黑色');
在非严格模式下,this 默认指向全局对象(浏览器中是 window)。于是:
window.name = '黑猫警长'window.color = '黑色'
这不仅污染了全局命名空间,还可能导致难以调试的 bug。在严格模式('use strict')下,这种调用会直接报错,因为 this 为 undefined。
因此,构造函数必须配合
new使用,这是 JavaScript OOP 的基本礼仪。
正确调用:使用 new
javascript
const cat1 = new Cat('黑猫警长', '黑色');
const cat2 = new Cat('加菲猫', '橘色');
现在:
cat1和cat2是两个完全独立的对象- 它们各自拥有自己的
name和color属性 - 它们的
__proto__都指向Cat.prototype
你可以验证:
javascript
console.log(cat1 !== cat2); // true
console.log(cat1.__proto__ === Cat.prototype); // true
这就是"封装"的雏形:数据被隔离在各自的实例中,互不干扰。
四、实例之间的关系:constructor 与 instanceof
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.prototype或null
这种机制既保证了共享,又允许个性化覆盖,非常灵活。
六、如何判断属性属于实例还是原型?
javascript
console.log(cat1.hasOwnProperty('name')); // true
console.log(cat1.hasOwnProperty('type')); // false
hasOwnProperty是Object.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 的优势
- 语法简洁,可读性强
- 支持
extends、super等关键字 - 自动绑定
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!🐱