说 JavaScript 中的 class 是 “一层糖衣”(语法糖),核心原因是:ES6 引入的 class 本质没有新增语言底层机制,只是对原有原型链(prototype)继承体系的封装和语法简化—— 它让代码写法更接近传统面向对象(如 Java、C#),但底层依然依赖原型、构造函数、原型链查找等原生逻辑,没有改变 JS 的继承本质。
早期连class关键字都没有 , 哪怕有了class, JS 仍然是原型式的面向对象
那么问题来了:没有 Constructor,没有 class,JavaScript 如何实现面向对象?
一、创世纪:对象字面量的"刀耕火种"
最原始的创建对象方式:
var cat1 = {};
cat1.name = '加菲猫';
cat1.color = '橘色';
var cat2 = {};
cat2.name = '黑猫警长';
cat2.color = '黑色';
🔍 代码解析
这里每只猫都是独立创建的空对象 {},然后逐一添加属性。
❌ 致命问题
| 问题 | 说明 |
|---|---|
| 代码重复 | 每只猫都要写相同的属性赋值逻辑 |
| 毫无关联 | cat1 和 cat2 之间没有任何联系,无法判断它们是否属于同一"类" |
| 无法追溯 | 不知道这个对象是由什么"模板"创建的 |
二、工业革命:构造函数的封装之道
为了解决代码重复问题,我们引入构造函数:
function Cat(name, color) {
console.log(this); // 当使用 new 调用时,这里是一个空对象 {}
this.name = name;
this.color = color;
}
// 普通调用 vs new 调用
Cat('黑猫警长', '黑色'); // this 指向 window(严格模式下是 undefined)
const cat1 = new Cat('加菲猫', '橘色'); // this 指向新创建的实例
🔍 new 关键字的四步魔法
当你写下 new Cat() 时,JavaScript 引擎偷偷做了这些事:
// 伪代码,模拟 new 的内部实现
function myNew(Constructor, ...args) {
// 1️⃣ 创建一个空对象
const obj = {};
// 2️⃣ 将空对象的 __proto__ 指向构造函数的 prototype
obj.__proto__ = Constructor.prototype;
// 3️⃣ 执行构造函数,this 绑定到这个空对象
const result = Constructor.apply(obj, args);
// 4️⃣ 如果构造函数返回对象则用它,否则返回新创建的对象
return result instanceof Object ? result : obj;
}
🔍 实例的身份验证
const cat1 = new Cat('加菲猫', '橘色');
const cat2 = new Cat('黑猫警长', '黑色');
// constructor:指向创建该实例的构造函数
console.log(cat1.constructor === Cat); // true
console.log(cat1.constructor === cat2.constructor); // true ✅ 它们来自同一个"工厂"
// instanceof:检查原型链上是否存在某构造函数的 prototype
console.log(cat1 instanceof Cat); // true
console.log(cat1 instanceof Object); // true(所有对象都继承自 Object)
❌ 新的问题浮现
如果我们想给每只猫都加一个
eat 方法:
function Cat(name, color) {
this.name = name;
this.color = color;
this.type = '猫科动物';
this.eat = function() { console.log('吃鱼'); }
}
const cat1 = new Cat('tom', '黑');
const cat2 = new Cat('咖啡猫', '橘');
console.log(cat1.eat === cat2.eat); // false ❌
每 new 一次,就创建一个新的 eat 函数,100只猫就有100份相同的函数副本——内存浪费!
三、共享经济:Prototype 原型模式
解决方案:把公共属性和方法挂到原型对象上:
function Cat(name, color) {
this.name = name; // 实例独有
this.color = color;
}
// 所有实例共享
Cat.prototype.type = '猫科';
Cat.prototype.eat = function() {
console.log('eat jerry');
}
var cat1 = new Cat('tom', '黑色');
var cat2 = new Cat('咖啡猫', '橘色');
console.log(cat1.eat === cat2.eat); // true ✅ 同一个函数,节省内存
🔍 原型三角关系(重点!)
cat1.__proto__ === Cat.prototype // true
Cat.prototype.constructor === Cat // true
cat1.constructor === Cat // true(通过原型链找到)
🔍 属性查找机制
当访问 cat1.type 时:
- 先在
cat1自身属性中查找 → 没找到 - 沿着
__proto__到Cat.prototype查找 → 找到了!
// 修改实例属性,不影响原型
cat1.type = '我是独特的猫';
console.log(cat1.type); // '我是独特的猫'(实例属性,遮蔽了原型)
console.log(cat2.type); // '猫科'(依然读取原型)
🔍 判断属性来源的工具
var cat1 = new Cat('tom', '黑色');
// hasOwnProperty:是否为实例自身的属性
console.log(cat1.hasOwnProperty('name')); // true ✅ 自身属性
console.log(cat1.hasOwnProperty('type')); // false ❌ 来自原型
// in 操作符:属性是否存在(包括原型链)
console.log('name' in cat1); // true
console.log('type' in cat1); // true(原型上也算)
// isPrototypeOf:判断原型关系
console.log(Cat.prototype.isPrototypeOf(cat1)); // true
// 遍历所有可枚举属性(包括原型链)
for (var prop in cat1) {
console.log(prop, cat1[prop]);
// name tom
// color 黑色
// type 猫科
// eat function...
}
四、语法革命:ES6 Class 的华丽外衣
ES6 终于给了我们熟悉的类式写法:
class Cat {
constructor(name, color) {
this.name = name;
this.color = color;
}
// 方法自动挂到 prototype 上
eat() {
console.log('eat jerry');
}
}
const cat1 = new Cat('tom', '黑色');
cat1.eat();
🔍 揭开语法糖的真面目
// 验证:class 本质还是函数
console.log(typeof Cat); // 'function'
// 验证:原型链依然存在
console.log(cat1.__proto__ === Cat.prototype); // true
console.log(cat1.__proto__.constructor === Cat); // true
console.log(cat1.__proto__.__proto__ === Object.prototype); // true
console.log(cat1.__proto__.__proto__.__proto__); // null(原型链终点)
原型链的终极路径:
cat1 → Cat.prototype → Object.prototype → null
五、血脉传承:JavaScript 的继承实现
方案一:构造函数绑定(借用构造函数)
function Animal() {
console.log(this, '////'); // this 可以被外部指定
this.species = '动物';
}
function Cat(name, color) {
// 🔑 关键:用 apply 改变 Animal 内部的 this 指向
// 让 Animal 的 this 指向 Cat 的实例
Animal.apply(this);
this.name = name;
this.color = color;
}
const cat = new Cat('加菲猫', '橘色');
console.log(cat.species); // '动物' ✅
console.log(cat.name); // '加菲猫'
🔍 apply 的作用解析
javascript
Animal.apply(this);
// 等价于在 Cat 构造函数里"借用"了 Animal 的代码
// 相当于把 Animal 函数体里的代码复制过来执行
// 此时 Animal 内部的 this 就是 Cat 正在创建的实例
❌ 致命缺陷
javascript
Animal.prototype.sayHi = function() {
console.log('Hello!');
}
const cat = new Cat('加菲猫', '橘色');
cat.sayHi(); // ❌ TypeError: cat.sayHi is not a function
apply 只能继承构造函数内的属性,无法继承原型上的方法!
方案二:原型链继承(完整继承)
function Animal() {
this.species = '动物';
}
Animal.prototype.sayHi = function() {
console.log('啦啦啦啦啦');
}
function Cat(name, color) {
Animal.apply(this); // 继承构造函数属性
this.name = name;
this.color = color;
}
// 🔑 关键:让 Cat 的原型指向 Animal 的实例
Cat.prototype = new Animal();
const cat = new Cat('加菲猫', '橘色');
console.log(cat.species); // '动物' ✅
cat.sayHi(); // '啦啦啦啦啦' ✅ 原型方法也继承了!
当调用cat.sayHi() 时:
- 在
cat自身找 → 没有 - 在
cat.__proto__(Animal实例)找 → 没有 - 在
Animal.prototype找 → ✅ 找到了!
六、知识图谱:一张图总结所有关系
七、速查表:核心概念对照
| 概念 | 含义 | 判断方法 |
|---|---|---|
prototype | 构造函数的原型对象,存放共享属性/方法 | Cat.prototype |
__proto__ | 实例指向其原型对象的指针 | cat1.__proto__ |
| constructor | 指向创建该对象的构造函数 | cat1.constructor === Cat |
instanceof | 检查原型链上是否存在某构造函数 | cat1 instanceof Cat |
hasOwnProperty | 判断是否为自身属性 | cat1.hasOwnProperty('name') |
isPrototypeOf | 判断是否在原型链上 | Cat.prototype.isPrototypeOf(cat1) |
结语:拥抱原型,理解本质
JavaScript 的面向对象不是"假的",而是"不同的"。它用原型链代替了传统的类继承,用委托代替了复制。
当你下次写 class Cat extends Animal 时,请记住:你写的是语法糖,JavaScript 执行的是原型链操作。理解原型,才能真正掌握 JavaScript。
那只橘色的加菲猫,正沿着 __proto__ 的阶梯,一步步向上攀登,直到触碰 null 的终点——这就是 JavaScript 对象系统的全部秘密。