普通视图

发现新文章,点击刷新页面。
今天 — 2025年11月29日首页

当我把 proto 打印出来那一刻,我懂了JS的原型链

作者 栀秋666
2025年11月29日 16:19

💬 前言:我本以为我会面向对象,结果我连“对象”都没搞懂

刚开始学 JavaScript 的时候,我以为:

function Person(name) {
  this.name = name;
}

const p1 = new Person('小明');
console.log(p1.name); // 小明

这不就是面向对象吗?简单!

直到有一天,我在控制台敲下:

console.log(p1.__proto__);

然后——我的世界崩塌了。

满屏的 [[Prototype]]constructor__proto__……我仿佛掉进了一个无限嵌套的俄罗斯套娃里。

“我是谁?”
“我从哪里来?”
“我要到哪里去?”
——来自一个被原型链逼疯的学生的灵魂三问。

今天,就用一个真实学习者的视角,带你从困惑到理解,一步步揭开 prototype 的神秘面纱。没有高深术语,只有大白话 + 可运行代码 + 我踩过的坑。


🚪 一、为什么需要原型?—— 因为我不想每个对象都背一份方法

假设我们要创建多个学生对象:

❌ 错误写法:每个学生都自带“技能包”

function Student(name) {
  this.name = name;
  // 每个学生都独立拥有一个 sayHello 方法
  this.sayHello = function() {
    console.log(`大家好,我是${this.name}`);
  };
}

const s1 = new Student('张三');
const s2 = new Student('李四');

console.log(s1.sayHello === s2.sayHello); // false → 完全不同的两个函数!

问题来了:如果创建 1000 个学生,就会有 1000 个 sayHello 函数,内存直接爆炸 💥。

这就像学校给每个学生发一本《礼仪手册》,其实大家看的都是同一本书,但每人一本——太浪费了!

✅ 正确姿势:把公共方法放进“共享书架”(prototype)

function Student(name) {
  this.name = name; // 每个学生独有的属性
}

// 所有学生共享的方法,统一挂载到 prototype 上
Student.prototype.sayHello = function() {
  console.log(`大家好,我是${this.name}`);
};

const s1 = new Student('张三');
const s2 = new Student('李四');

console.log(s1.sayHello === s2.sayHello); // true → 同一个函数,只存一份!
s1.sayHello(); // 大家好,我是张三
s2.sayHello(); // 大大家好,我是李四

📌 我的理解

  • prototype 就是构造函数的“共享书架”
  • 实例自己没有的方法,会自动去书架上找
  • 既节省内存,又方便统一管理

这就是原型存在的意义:让对象学会“蹭”!


🔗 二、四大核心概念:别再混淆 prototype 和 proto 了!

刚开始我总分不清 prototype__proto__,后来我画了张图,终于懂了。


1️⃣ 构造函数:创建实例的“模板”

function Student(name) {
  this.name = name;
}

它就是一个普通函数,但通常:

  • 首字母大写
  • new 调用

new 的过程可以简化为:

  1. 创建空对象 {};
  2. this 指向它;
  3. 执行函数体;
  4. 返回这个对象。

2️⃣ prototype:构造函数的“共享仓库”

每个函数都有一个 prototype 属性,它是一个对象,用来存放所有实例共享的内容

Student.prototype.species = '人类';
Student.prototype.study = function() {
  console.log(`${this.name}在努力学习`);
};

⚠️ 注意:prototype函数才有的属性


3️⃣ __proto__:实例通往原型的“梯子”

每个对象(包括实例)都有一个 __proto__ 属性(非标准但广泛支持),它指向其构造函数的 prototype

const s1 = new Student('张三');

console.log(s1.__proto__ === Student.prototype); // true

👉 这就是实例能访问到 sayHello 的原因:
s1.sayHello() → 自己没有 → 顺着 __proto__ 找 → 找到 Student.prototype.sayHello

🎯 记住一句话:实例的 __proto__ 指向构造函数的 prototype


4️⃣ constructor:原型的“回老家按钮”

原型对象上有一个 constructor 属性,指向构造函数本身。

console.log(Student.prototype.constructor === Student); // true
console.log(s1.constructor === Student); // true

⚠️ 重要提醒:手动重写 prototype 要修复 constructor!

Student.prototype = {
  sayHello() { console.log('hi') }
};

const s1 = new Student('张三');
console.log(s1.constructor === Student); // false ❌
console.log(s1.constructor === Object); // true → 错了!

// ✅ 修复:
Student.prototype = {
  constructor: Student,
  sayHello() { console.log('hi') }
};

否则后续 instanceof 判断可能出错。


📊 核心关系图(建议收藏)

lQLPJwvG0tJ1vuPNASLNAkSwAikK2ZgDD2YJAF6EAku5AA_580_290.png

📌 再说一遍:实例的 __proto__ 指向构造函数的 prototype,原型的 constructor 指向构造函数

🔍 三、原型查找机制:JS是怎么找到方法的?

当你调用 s1.sayHello() 时,JS 引擎是这样找的:

  1. 先看 s1 自己有没有 sayHello
  2. 没有?那就通过 __proto__Student.prototype 找;
  3. 还没有?继续通过 Student.prototype.__proto__ 找上一级;
  4. 直到找到,或者查到 null

这个链条,就是原型链

🖼️JavaScript 原型链完整关系图

66b94b61f939741c0ca1db2e69984697.png

1. 查找示例

function Student(name) {
  this.name = name;
}

Student.prototype.species = '人类';
Student.prototype.study = function() {
  console.log(`${this.name}在学习`);
};

const s1 = new Student('张三');

console.log(s1.name);        // 张三 → 自身属性
console.log(s1.species);     // 人类 → 来自 prototype
console.log(s1.toString());  // [object Object] → 来自 Object.prototype
console.log(s1.abc);         // undefined → 找不到

2. 原型链终点:null

console.log(Object.prototype.__proto__); // null → 终点!

// 验证整个链:
console.log(s1.__proto__);                 // Student.prototype
console.log(s1.__proto__.__proto__);       // Object.prototype
console.log(s1.__proto__.__proto__.__proto__); // null

3. 实例属性可以“屏蔽”原型属性

function Student(name) {
  this.name = name;
  this.species = '外星人'; // 覆盖原型属性
}

Student.prototype.species = '人类';

const s1 = new Student('张三');
console.log(s1.species); // 外星人

delete s1.species;
console.log(s1.species); // 人类 → 删除后重新查找原型

✅ 应用:为个别实例定制行为,不影响全局。


🧬 四、原型式继承:JS的“继承”到底是什么?

传统语言是“类继承”(血缘关系),而 JS 是“委托继承”——你不会,就去问你爸,你爸不会,就去问爷爷。

1. 经典继承实现

// 父类
function Person(name) {
  this.name = name;
}
Person.prototype.greet = function() {
  console.log(`你好,我是${this.name}`);
};

// 子类
function Student(name, grade) {
  Person.call(this, name); // 继承父类实例属性
  this.grade = grade;
}

// 继承父类原型方法
Student.prototype = new Person();
Student.prototype.constructor = Student;

// 扩展子类方法
Student.prototype.study = function() {
  console.log(`${this.name}在读${this.grade}年级`);
};

const s1 = new Student('张三', 3);
s1.greet(); // 你好,我是张三(继承)
s1.study(); // 张三在读3年级(自有)

2. ES6 class 只是语法糖

class Person {
  constructor(name) { this.name = name; }
  greet() { console.log(`你好,我是${this.name}`); }
}

class Student extends Person {
  constructor(name, grade) {
    super(name);
    this.grade = grade;
  }
  study() { console.log(`${this.name}在读${this.grade}年级`); }
}

底层依然是原型链驱动。class 不是新东西,只是让你写得更爽。


💡 五、原型的实际应用

1. 工具类共享方法

function Utils() {}
Utils.prototype.formatDate = function(date) { /* ... */ };

2. 扩展原生对象(谨慎!)

Array.prototype.unique = function() {
  return [...new Set(this)];
};
[1,2,2,3].unique(); // [1,2,3]

⚠️ 注意:生产环境慎用,避免污染全局。

3. 单例模式

function Singleton() {
  if (Singleton.prototype.instance) {
    return Singleton.prototype.instance;
  }
  this.data = '唯一实例';
  Singleton.prototype.instance = this;
}

4. 框架中的应用(如 Vue)

Vue.prototype.$http = axios; // 所有组件都能用 this.$http

⚠️ 六、常见误区

❌ 误区1:混淆 prototype 和 proto

  • prototype:函数才有,是“仓库”
  • __proto__:对象都有,是“梯子”

❌ 误区2:覆盖 prototype 不修 constructor

会导致 instanceof 失效。

✅ 正确做法:永远记得修 constructor!


🏁 七、总结:原型是JS的灵魂

核心要点 说明
🔹 核心价值 共享方法,节省内存
🔹 核心关系 实例.__proto__ === 构造函数.prototype
🔹 查找机制 自身 → 原型链 → null
🔹 继承本质 委托查找,非类继承
🔹 class 本质 原型的语法糖

🌟 最后感悟
学原型的过程,就像在迷宫中找出口。
一开始觉得混乱,但当你画出那张关系图,执行第一段可运行代码,听到“啊哈!”的那一声——
你就真正理解了 JavaScript 的灵魂。

昨天 — 2025年11月28日首页

JavaScript 中的 简单数据类型:Symbol——是JavaScript成熟的标志

作者 栀秋666
2025年11月28日 17:08

深入理解 JavaScript 中的 Symbol:不只是“唯一值”的哲学

在 JavaScript 的八种数据类型中,Symbol 是 ES6 引入的新成员。它不像 numberstring 那样直观,也不像 object 那样复杂多变。它安静地存在于语言底层,却承载着一种独特的“身份标识”使命。很多人初识 Symbol 时,往往只记住一句话:“它是唯一的值”,然后便止步于此。但真正理解 Symbol,需要我们跳出“唯一性”这个标签,去思考它的设计哲学、使用场景以及它如何悄然改变了 JavaScript 对象模型的运作方式。


一、Symbol 的本质:不是“值”,而是“身份”

从技术层面看,Symbol 是一个原始数据类型(primitive type),通过调用全局函数 Symbol() 创建:

const sym1 = Symbol();
const sym2 = Symbol('description');

每次调用 Symbol() 都会返回一个全新且独一无二的 symbol 值,即使参数相同也是如此:

Symbol('foo') === Symbol('foo'); // false

这说明,Symbol 不是在比较内容,而是在比较“身份”。就像世界上没有两片完全相同的雪花,也没有两个相同的 symbol —— 它们生来就是不同的个体。

这种特性使得 Symbol 天然适合作为对象的“私有键”或“元属性键”。但它真正的价值,并不在于“不可重复”,而在于 “不可预见”“不可枚举”


二、Symbol 与对象:一场关于“命名冲突”的救赎

JavaScript 的对象是动态的,我们可以随时添加属性。但在大型项目或多团队协作中,这种灵活性反而成了隐患:不同模块可能无意中使用了相同的属性名,导致覆盖和 bug。

传统做法是加前缀,比如 _privateProp$$internal,但这只是“约定俗成”,无法真正避免冲突。

Symbol 提供了一种语言级别的解决方案

// 模块 A
const cacheKey = Symbol('cache');
class MyClass {
  [cacheKey] = new Map();

  setCache(key, value) {
    this[cacheKey].set(key, value);
  }

  getCache(key) {
    return this[cacheKey].get(key);
  }
}

// 模块 B 即使也创建了一个同名 Symbol,也不会影响模块 A
const anotherCacheKey = Symbol('cache'); // 完全无关

这里的 cacheKey 是一个 symbol,作为对象的 key 使用时,不会被外部轻易访问或覆盖。更重要的是,其他代码即使知道你用了 'cache' 这个描述,也无法构造出相同的 key —— 因为 symbol 的唯一性不由描述决定。

这就是 Symbol 的核心优势:提供一种机制,让开发者可以安全地向对象注入元信息,而不必担心名字污染。


三、Symbol 的“隐身性”:for...in 看不见它

Symbol 作为对象 key 时,默认不会出现在常规的属性枚举中:

const obj = {
  name: 'Alice'
};

obj[Symbol('secret')] = 'hidden';

for (let key in obj) {
  console.log(key); // 只输出 'name'
}

console.log(Object.keys(obj));        // ['name']
console.log(JSON.stringify(obj));     // {"name":"Alice"}

甚至连 JSON.stringify 都会忽略 symbol 属性!这是有意为之的设计 —— 表明 symbol 更像是“元数据”而非“业务数据”。

但如果你真的想获取这些“隐藏钥匙”,JavaScript 也提供了专门的方法:

Object.getOwnPropertySymbols(obj); 
// 返回 [Symbol(secret)]

这就形成了一种有趣的分层结构:

  • for...inObject.keys():面向公众的属性
  • Object.getOwnPropertySymbols():面向内部或特定上下文的元属性

这种分离让我们可以在不干扰公共 API 的前提下,附加调试信息、缓存、状态标记等。


四、Symbol 的高级用法:不仅仅是 key

除了作为对象 key,Symbol 还有一些内置的“知名符号”(Well-Known Symbols),用于定制 JavaScript 对象的行为。这些以 Symbol.xxx 形式存在的属性,其实是语言内部的钩子(hooks)。

1. Symbol.iterator:让对象可迭代

const myCollection = {
  items: ['a', 'b', 'c'],
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        return index < this.items.length ?
          { value: this.items[index++], done: false } :
          { done: true };
      }
    };
  }
};

for (let item of myCollection) {
  console.log(item); // a, b, c
}

通过实现 Symbol.iterator,普通对象也能被 for...of 遍历。这是 JavaScript 迭代协议的核心。

2. Symbol.toStringTag:自定义 toString 输出

const myObj = {
  [Symbol.toStringTag]: 'MySpecialObject'
};

Object.prototype.toString.call(myObj); 
// "[object MySpecialObject]"

原本所有对象 toString 都是 [object Object],现在你可以让它更具体。

3. Symbol.hasInstance:控制 instanceof 行为

class MyClass {
  static [Symbol.hasInstance](instance) {
    return instance.type === 'myclass';
  }
}

const obj = { type: 'myclass' };
console.log(obj instanceof MyClass); // true!

这打破了 instanceof 必须基于原型链的传统认知,赋予我们更大的控制权。

这些内置 Symbol 表明:Symbol 不只是一个“防重命名工具”,更是 JavaScript 开放其内部机制的一种手段 —— 它把原本封闭的语言行为,变成了可扩展的接口。


五、Symbol 的局限与误解

尽管强大,Symbol 并非银弹。我们需要清醒认识它的边界:

❌ Symbol 不是真正的“私有”

虽然 symbol key 不易被访问,但并非绝对私有:

const sym = Object.getOwnPropertySymbols(obj)[0];
console.log(obj[sym]); // 依然能拿到

如果有人拿到了 symbol 引用,就能访问对应属性。真正的私有应使用 #field(ES2022 私有字段)。

❌ Symbol 不能序列化

如前所述,JSON.stringify 会忽略 symbol 属性。因此不适合用于需要持久化的数据结构。

❌ 全局 symbol?可以用 Symbol.for()

如果确实需要跨文件共享同一个 symbol,可以使用全局注册表:

const s1 = Symbol.for('shared');
const s2 = Symbol.for('shared');
s1 === s2; // true

注意:Symbol.for(key) 是查找或创建,而 Symbol(key) 永远新建。


六、Symbol 的哲学意义:从“命名”到“标识”

回顾编程史,我们一直在与“命名”斗争。变量名、函数名、类名……每一个名字都是一次承诺,也可能是一次妥协。当系统越来越大,命名空间就变得拥挤不堪。

Symbol 的出现,某种程度上是对“命名中心主义”的反叛。它告诉我们:有些东西不需要名字,只需要身份。

就像现实世界中,每个人都有身份证号,但平时我们用名字称呼彼此。Symbol 就是那个身份证号 —— 不常提起,但在关键时候能准确识别“你是谁”。

这也反映了现代编程的一个趋势:从“显式命名”转向“隐式标识”。无论是 React 的 fiber 节点、Vue 的响应式依赖追踪,还是 Redux 的 action type,越来越多的系统开始使用 symbol 来管理内部状态,避免对外暴露过多细节。


七、实战建议:何时该用 Symbol?

结合以上分析,以下是使用 Symbol 的典型场景:

场景 示例
✅ 防止属性名冲突 插件系统中挂载私有状态
✅ 添加元信息 给 DOM 元素附加调试标记
✅ 实现语言协议 让对象支持迭代、转换字符串等
✅ 模拟私有成员 类的内部缓存、配置项
❌ 数据存储 需要 JSON 序列化的字段
❌ 真正的私有 应使用 #private 字段

结语:Symbol 是 JavaScript 成熟的标志

Symbol 看似小众,实则是 JavaScript 走向成熟的重要一步。它不再满足于做一个“脚本语言”,而是开始构建更严谨的抽象能力。

它教会我们:有时候,“看不见”比“看得见”更有力量;“唯一”不仅是技术特性,更是一种设计哲学。

当你下次面对对象属性命名纠结时,不妨问自己一句:

“这个属性,真的需要一个名字吗?”

也许答案是:它只需要一个 Symbol

❌
❌