✨ JavaScript 对象 & 包装对象:魔法世界大冒险!
上一篇文章我们讲解了 JS 中的数据类型,包括 7种基本数据类型和引用数据类型。对象作为引用数据类型的代表,存储在堆空间中,而调用栈里只存放它的“门牌号”(引用地址)。V8 引擎就是靠这个地址找到真正的对象值。
都说在 JS 中“万物皆对象”,这篇文章,我们就来好好掰扯一下 对象 以及它的好搭档 包装对象!
🧱 1. 对象:构建程序世界的积木
🎨 1.1 创建对象的三种姿势
想创建一个新对象?JS 给你提供了三种灵活的方式:
- 字面量法 (
{}
):最简单直接! new Object()
:经典构造函数登场。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
操作符在背后默默做了这几件大事:
- 🏗️ 创建空屋:创建一个全新的空对象
{}
。 - 📍 绑定主人:将这个新对象的
__proto__
属性,指向构造函数的prototype
属性(建立原型链连接,后续细讲)。 - 🔑 移交钥匙:将构造函数内部的
this
绑定到第一步创建的新对象上。 - 🚧 装修布置:执行构造函数内部的代码(通常用来给
this
对象添加属性和方法)。 - 🎁 交房仪式:如果构造函数没有显式返回一个对象,则自动返回这个新创建的对象。
// 手动实现一个简化版 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 中的基本数据类型(如 string
, number
, boolean
)本身是没有属性和方法的。它们就是简单的值。
但是! 当你试图访问一个基本类型值的属性(比如 'hello'.length
)时,JS 引擎在幕后悄悄施展了魔法:
- 🎭 临时变装:引擎瞬间创建一个对应的包装对象(如
new String('hello')
)。 - 🔍 属性查找:在这个临时包装对象上查找你要的属性(如
length
)。 - 🎉 返回结果:找到属性值后返回给你。
- 🧹 立刻消失:用完后,这个临时包装对象立即被销毁(交给垃圾回收),原始的基本类型值保持不变。
简单说:包装对象是 JS 引擎为了让基本类型值能临时借用对象的能力(属性和方法)而创建的一次性替身演员,用完就丢。
⚖️ 3.2 包装对象的两种形态:隐式 vs 显式
包装对象分两种,它们在创建方式、生命周期和行为上大不相同:
特点 | 隐式封装 (JS引擎自动) | 显式封装 (程序员手动 new ) |
---|---|---|
创建者 | JS 引擎 (偷偷地) | 程序员 (使用 new String() 等) |
存在时间 | ⏱️ 超短暂! (属性访问/方法调用后立刻销毁) | 🕰️ 持久! (直到不再被引用或垃圾回收) |
能否加属性 | ❌ 不行! (加了也白加,马上销毁) | ✅ 可以! (像普通对象一样操作) |
类型 | 原始类型 (string , number , boolean ) | object |
相等比较 | 'abc' === 'abc' → true | new String('abc') === 'abc' → false |
值比较 | new String('abc') == 'abc' → true | new 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.length
,num.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,但为了兼容性一直保留至今。记住这个特例就好啦!