【JavaScript】✨ JavaScript 对象 & 包装对象:魔法世界大冒险!

4 阅读7分钟

✨ JavaScript 对象 & 包装对象:魔法世界大冒险!

上一篇文章我们讲解了 JS 中的数据类型,包括 7种基本数据类型引用数据类型。对象作为引用数据类型的代表,存储在堆空间中,而调用栈里只存放它的“门牌号”(引用地址)。V8 引擎就是靠这个地址找到真正的对象值。

都说在 JS 中“万物皆对象”,这篇文章,我们就来好好掰扯一下 对象 以及它的好搭档 包装对象


🧱 1. 对象:构建程序世界的积木

🎨 1.1 创建对象的三种姿势

想创建一个新对象?JS 给你提供了三种灵活的方式:

  1. 字面量法 ({}):最简单直接!
  2. new Object() :经典构造函数登场。
  3. new + 自定义函数:打造属于你自己的对象工厂。
// 姿势一:字面量
var obj1 = {}; // 一个空荡荡的新对象

// 姿势二:new Object()
var obj2 = new Object(); // 同上,也是空对象

// 姿势三:new + 自定义函数
function MyCustomObject() {
  // 可以在这里定义属性和方法
}
var obj3 = new MyCustomObject(); // 创建自定义类型的对象

console.log(obj1); // {}
console.log(obj2); // {}
console.log(obj3); // MyCustomObject {}

🔧 1.2 构造函数:对象的“生产流水线”

构造函数本质上就是一个普通函数!  🎯 它的特别之处在于调用方式

  • 普通函数:直接调用,执行函数体,返回结果(或 undefined)。
  • 构造函数:用 new 操作符调用,它的使命是创建并初始化一个新对象
// 这是一个构造函数(因为用 new 调用)
function Car(color) {
  this.name = 'bmw'; // this 指向新创建的对象
  this.color = color;
}

// 这是一个普通函数
function add(a, b) {
  return a + b;
}

// 使用构造函数创建对象
var pinkCar = new Car('pink');
var orangeCar = new Car('orange');

console.log(pinkCar); // Car { name: 'bmw', color: 'pink' }
console.log(orangeCar); // Car { name: 'bmw', color: 'orange' }

💡 小贴士:当我们需要批量创建结构相似的对象时,构造函数就是利器!它类似于 class 的功能(class 本质是构造函数的语法糖,底层依然靠构造函数和原型)。


🪄 2. new 操作符:对象诞生的幕后魔法

当一个函数被 new 调用时,它就摇身一变成了构造函数。那么,new 这个魔法棒到底挥舞了哪些咒语呢?🧙‍♂️

先看个例子,感受下 new 和普通调用的区别:

function IceCream(name, price) {
  this.name = name;
  this.price = price;
  this.type = '冰';
}

// 魔法时刻:使用 new
var dreamDragon = new IceCream('梦龙', 10);
console.log(dreamDragon); // IceCream { name: '梦龙', price: 10, type: '冰' } ✅ 得到一个对象!

// 普通调用:没有魔法
var heyTea = IceCream('喜茶', 15);
console.log(heyTea); // undefined ❌ 啥也没得到(函数没返回值)

结论显而易见:new 调用让函数“无中生有”地返回了一个对象!

🔍 揭秘 new 的魔法步骤(终版)

new 操作符在背后默默做了这几件大事:

  1. 🏗️ 创建空屋:创建一个全新的空对象 {}
  2. 📍 绑定主人:将这个新对象的 __proto__ 属性,指向构造函数的 prototype 属性(建立原型链连接,后续细讲)。
  3. 🔑 移交钥匙:将构造函数内部的 this 绑定到第一步创建的新对象上。
  4. 🚧 装修布置:执行构造函数内部的代码(通常用来给 this 对象添加属性和方法)。
  5. 🎁 交房仪式:如果构造函数没有显式返回一个对象,则自动返回这个新创建的对象。
// 手动实现一个简化版 new (mynew)
function mynew(ConstructorFn, ...args) {
  // 1. 创建空对象
  const newObj = {};
  // 2. 连接原型链 (关键一步!)
  newObj.__proto__ = ConstructorFn.prototype;
  // 3 & 4. 绑定this并执行构造函数 (给新对象添加属性)
  ConstructorFn.call(newObj, ...args);
  // 5. 返回新对象
  return newObj;
}

// 使用我们的 mynew 试试看
var myDreamDragon = mynew(IceCream, "梦龙", 10);
console.log(myDreamDragon); // IceCream { name: '梦龙', price: 10, type: '冰' } ✨ 成功!

📌 记住这五步咒语,你就掌握了 new 的核心奥秘!关于 this 和 prototype 的更多细节,我们后续文章会深入探讨。


📦 3. 包装对象:基本类型的“华丽变身”

🤔 3.1 什么是包装对象?

我们知道,JS 中的基本数据类型(如 stringnumberboolean)本身是没有属性和方法的。它们就是简单的值。

但是!  当你试图访问一个基本类型值的属性(比如 'hello'.length)时,JS 引擎在幕后悄悄施展了魔法:

  1. 🎭 临时变装:引擎瞬间创建一个对应的包装对象(如 new String('hello'))。
  2. 🔍 属性查找:在这个临时包装对象上查找你要的属性(如 length)。
  3. 🎉 返回结果:找到属性值后返回给你。
  4. 🧹 立刻消失:用完后,这个临时包装对象立即被销毁(交给垃圾回收),原始的基本类型值保持不变。

简单说:包装对象是 JS 引擎为了让基本类型值能临时借用对象的能力(属性和方法)而创建的一次性替身演员,用完就丢。

⚖️ 3.2 包装对象的两种形态:隐式 vs 显式

包装对象分两种,它们在创建方式生命周期行为上大不相同:

特点隐式封装 (JS引擎自动)显式封装 (程序员手动 new)
创建者JS 引擎 (偷偷地)程序员 (使用 new String()等)
存在时间⏱️ 超短暂!  (属性访问/方法调用后立刻销毁)🕰️ 持久!  (直到不再被引用或垃圾回收)
能否加属性❌ 不行!  (加了也白加,马上销毁)✅ 可以!  (像普通对象一样操作)
类型原始类型 (stringnumberboolean)object
相等比较'abc' === 'abc' → truenew String('abc') === 'abc' → false
值比较new String('abc') == 'abc' → truenew String('abc') == 'abc' → true

代码演示,一目了然:

// 🌈 隐式封装 (引擎自动)
let greeting = 'hello';
console.log(greeting.length); // 5 ✅ 引擎临时创建 String 包装对象获取 length

greeting.customProp = 'I am ephemeral'; // 试图给原始值添加属性
console.log(greeting.customProp); // undefined ❌ 临时对象已销毁,属性丢失!

// 🎁 显式封装 (手动创建)
let greetingObj = new String('world');
console.log(greetingObj.length); // 5 ✅ 对象持续存在

greetingObj.customProp = 'I stick around!'; // 给包装对象添加属性
console.log(greetingObj.customProp); // 'I stick around!' ✅ 属性保留

// 🔬 检查类型
console.log(typeof greeting);    // "string" (原始值)
console.log(typeof greetingObj); // "object" (包装对象)

// ⚖️ 比较一下
console.log(greeting === 'hello');         // true (同类型同值)
console.log(greetingObj === 'world');      // false (对象 vs 原始值,类型不同)
console.log(greetingObj == 'world');       // true (值相等,== 会进行类型转换)

💡 核心点:隐式包装是为了让基本类型能临时使用对象方法(如 str.lengthnum.toFixed()),用完即焚。显式包装创建的是真正的持久对象


🔍 4. typeof:数据类型的“照妖镜”

既然有隐式包装和显式包装,JS 引擎怎么区分它们和原始值呢?答案就是—— typeof 操作符!

🧠 typeof 的工作原理

typeof 是一个一元操作符(不是函数!),专门用来检测原始数据类型。它会返回以下七种字符串之一:"number""string""boolean""undefined""symbol""bigint""object" (注意 null 和函数比较特殊)。

引擎底层小秘密:JS 在存储值时,会将其转为二进制 机器码。这个机器码包含两部分:

  • 类型标签 (低位) :标识数据类型。
  • 实际值 (高位) :存储数据内容。

typeof 就是通过快速读取这个类型标签来判断类型的!

类型标签 (低位)typeof 返回值对应数据类型
000"object"对象 (或 null!)
1"number"数字
001"function"函数 (可调用对象)
010"string"字符串
110"boolean"布尔值
111"undefined"undefined
(特定)"symbol"Symbol (ES6+)
(特定)"bigint"BigInt (ES2020+)
console.log(typeof(100)); // "number"
console.log(typeof('100')); // "string"
console.log(typeof(true)); // "boolean"
console.log(typeof(undefined)); // "undefined"
console.log(typeof({})); // "object"
console.log(typeof(function() {})); // "function"
console.log(typeof(Symbol())); // "symbol"
console.log(typeof(BigInt(100))); // "bigint"

❓ 著名的 typeof null === 'object' 之谜

这就是底层类型标签机制导致的“历史小意外”:

  • null 在 JS 中被表示为全 0 的机器码 (00000000...)。
  • 对象的类型标签位恰好也是 000
  • typeof 只检查低位类型标签,看到 000 就报告 'object'

所以,typeof null 返回 'object' 是一个历史遗留的 Bug,但为了兼容性一直保留至今。记住这个特例就好啦!