在 JS 中为什么 class 只是一层糖衣?🍬

111 阅读5分钟

说 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 ✅ 同一个函数,节省内存

🔍 原型三角关系(重点!)

image.png

**核心公式**:
cat1.__proto__ === Cat.prototype       // true
Cat.prototype.constructor === Cat      // true
cat1.constructor === Cat               // true(通过原型链找到)

🔍 属性查找机制

当访问 cat1.type 时:

  1. 先在 cat1 自身属性中查找 → 没找到
  2. 沿着 __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.prototypeObject.prototypenull

五、血脉传承: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() 时

  1. 在 cat 自身找 → 没有
  2. 在 cat.__proto__(Animal实例)找 → 没有
  3. 在 Animal.prototype 找 → ✅ 找到了!

六、知识图谱:一张图总结所有关系

image.png

七、速查表:核心概念对照

概念含义判断方法
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 对象系统的全部秘密。