JS 对象与包装类:new 做了什么?字符串为什么有 length?

对象创建三种方式、new 的四步原理、包装类的自动装箱拆箱机制


前言

学 JavaScript 的时候,有几个问题一直困扰我:

  • 为什么字符串明明是原始类型,却能调用 .length.split() 这些方法?
  • new 关键字到底做了什么?为什么函数既能普通调用又能 new 调用?
  • 原始类型和引用类型的区别到底是什么?

后来搞懂了对象包装类 的底层机制,这些问题才全部串起来。这篇文章把对象创建、new 原理、包装类机制一次性讲清楚。

最初我以为字符串明明是原始类型,却能调用 .length.split() 是被设计成这样的,增加了易用性,但并不知道为什么?


一、JavaScript 的类型系统

JavaScript 中一切皆对象(这句话虽然不完全准确,但日常开发中大部分操作都围绕对象展开)。

1.1 两大类型

类型分类 包含类型 特点
原始类型(基本类型) stringnumberbooleannullundefinedsymbolbigint 存储值本身,按值访问
引用类型 objectfunctionarray 存储地址(引用),按引用访问

1.2 核心区别

javascript 复制代码
// 原始类型不能添加属性
let str = 'hello';
str.name = '我的字符串';
console.log(str.name);  // undefined ← 属性加不上去

// 引用类型可以随意添加属性
let obj = { name: '张三' };
obj.age = 18;
console.log(obj.age);  // 18 ← 属性正常添加

这是理解包装类的关键前提:原始类型不能添加属性和方法。


二、创建对象的三种方式

2.1 字面量(最常用)

javascript 复制代码
// 对象字面量
const person = {
    name: '张三',
    age: 18,
    sayHello() {
        console.log('你好,我是' + this.name);
    }
};

// 数组字面量
const list = [1, 2, 3];

// 函数字面量
const fn = function() {
    console.log('hello');
};

优点:简洁直观,日常开发 90% 都用字面量。

2.2 new Object()

javascript 复制代码
const obj = new Object();
obj.name = '张三';
obj.age = 18;

// 等价于
const obj2 = {
    name: '张三',
    age: 18
};

特点:和字面量效果一样,但写法更啰嗦,而且字面量的性能还更好,实际开发中很少用。

2.3 new 构造函数(批量创建对象)

当需要创建多个结构相似的对象时,用构造函数:

javascript 复制代码
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayHello = function() {
        console.log('你好,我是' + this.name);
    };
}

const p1 = new Person('张三', 18);
const p2 = new Person('李四', 22);

p1.sayHello();  // "你好,我是张三"
p2.sayHello();  // "你好,我是李四"

优点:可以批量创建结构相同的对象,是面向对象编程的基础。


三、JS函数的二义性:普通调用 vs new 调用

JavaScript 中的函数很特殊:既可以当普通函数调用,也可以当构造函数用 new 调用

javascript 复制代码
function Foo() {
    this.name = 'bar';
    console.log(this);
}

// 普通调用:this 指向调用者(非严格模式下指向 window)
Foo();  // Window {name: 'bar', ...}

// new 调用:this 指向新创建的对象
const f = new Foo();  // Foo {name: 'bar'}

同一个函数,两种调用方式,this 指向完全不同。

javascript 复制代码
function User(name) {
    this.name = name;
}

// ❌ 普通调用:this 指向 window,污染全局变量
User('张三');
console.log(window.name);  // '张三' ← 意外修改了全局变量

// ✅ new 调用:this 指向新对象,不会污染全局
const user = new User('李四');
console.log(user.name);  // '李四'

⚠️ 注意 :构造函数命名习惯首字母大写(PersonUser),用来和普通函数区分。

传统 Java/TS 是 类→实例 的面向对象; JS 早期只有函数 + 对象 + 原型,用 new 函数() 假装成类。

② new 到底干了 4 件事(必理解)

  1. 创建一个空新对象
  2. 把函数里的 this 绑定到这个新对象
  3. 把新对象的 proto 指向函数的 prototype (原型链)
  4. 函数默认 return 这个新对象

所以函数普通调用就是直接执行,new 调用 经过一系列事情返回了对象


四、new 的工作原理(重点)

new 关键字在调用函数时,背后做了四件事:

4.1 四步原理

arduino 复制代码
new Foo('张三')

第一步:创建一个空对象

javascript 复制代码
// new 内部自动做了这件事
const obj = {};

第二步 :执行函数体,把 this 指向这个空对象

javascript 复制代码
// 相当于
Foo.call(obj, '张三');
// 函数体内的 this.name = '张三' 就变成了 obj.name = '张三'

第三步:设置对象的原型

javascript 复制代码
// 让新对象的原型指向构造函数的 prototype
obj.__proto__ = Foo.prototype;

第四步:返回这个对象

javascript 复制代码
// 如果函数没有手动 return,默认返回 this(即新创建的对象)
return obj;

4.2 用代码模拟 new

javascript 复制代码
function myNew(Constructor, ...args) {
    // 第一步:创建空对象
    const obj = {};
  
    // 第二步:执行函数,this 指向空对象
    const result = Constructor.apply(obj, args);
  
    // 第三步:设置原型
    obj.__proto__ = Constructor.prototype;
  
    // 第四步:返回对象(如果函数手动返回了对象,则返回手动返回的对象)
    return result instanceof Object ? result : obj;
}
javascript 复制代码
// 用模拟的 myNew 测试
function Person(name, age) {
    this.name = name;
    this.age = age;
}

const p = myNew(Person, '张三', 18);
console.log(p.name);  // '张三'
console.log(p.age);   // 18
console.log(p instanceof Person);  // true

4.3 new 的特殊情况

javascript 复制代码
// 如果构造函数手动 return 了一个对象,new 返回的是那个对象
function Foo() {
    this.name = 'bar';
    return { name: 'baz' };  // 手动返回一个对象
}

const f = new Foo();
console.log(f.name);  // 'baz' ← 不是 'bar'!
javascript 复制代码
// 如果 return 的是原始类型,则忽略,仍然返回 this
function Bar() {
    this.name = 'bar';
    return 123;  // 返回原始类型,被忽略
}

const b = new Bar();
console.log(b.name);  // 'bar' ← 正常返回 this

五、包装类:原始类型的"替身"

5.1 一个让人困惑的现象

前面说过,原始类型不能添加属性和方法。那为什么这样能行?

javascript 复制代码
let str = 'hello';
console.log(str.length);    // 5 ← 字符串有 length?
console.log(str.toUpperCase());  // 'HELLO' ← 字符串能调方法?

let num = 3.14159;
console.log(num.toFixed(2));  // '3.14' ← 数字也能调方法?

原始类型明明不是对象,为什么能调用属性和方法?

答案:包装类(Wrapper Class)。

5.2 包装类是什么

JavaScript 有三种包装类构造函数:

包装类 对应原始类型 原型上的属性/方法
String string .length.split().toUpperCase().charAt() ...
Number number .toFixed().toString().valueOf() ...
Boolean boolean .toString().valueOf() ...

当你对原始类型的值进行属性/方法操作时,JavaScript 引擎会自动进行装箱操作

5.3 装箱与拆箱的过程

str.length 为例:

javascript 复制代码
let str = 'hello';
console.log(str.length);  // 5

V8 引擎实际执行的过程

javascript 复制代码
1. 发现 str 是原始类型,但尝试访问 .length 属性
2. 自动执行装箱:new String('hello')
   → 创建一个临时的 String 对象
3. 在这个临时对象上查找 .length
   → 找到了 String.prototype.length,值为 5
4. 立即执行拆箱操作
   → 销毁这个临时对象
5. 返回 5

用代码模拟就是:

javascript 复制代码
let str = 'hello';

// V8 实际做了类似这样的事:
const tempObj = new String('hello');  // 装箱:创建临时对象
const result = tempObj.length;        // 在临时对象上查找属性
// tempObj 被立即销毁                 // 拆箱:销毁临时对象
console.log(result);                   // 5

5.4 为什么原始类型不能添加属性?

因为装箱创建的临时对象用完即销毁,你添加的属性跟着临时对象一起消失了。

javascript 复制代码
let str = 'hello';

// 你以为在给字符串添加属性
str.name = '我的字符串';

// 实际发生的是:
// 1. new String('hello') → 创建临时对象
// 2. 临时对象.name = '我的字符串' → 给临时对象添加属性
// 3. 临时对象被销毁 → 属性跟着消失

// 下次访问时,又创建了一个新的临时对象
console.log(str.name);  // undefined ← 新临时对象上没有 name 属性
javascript 复制代码
// 对比:真正的对象可以持久保存属性
let obj = new String('hello');
obj.name = '我的字符串';
console.log(obj.name);  // '我的字符串' ← 对象没被销毁,属性还在

5.5 原型链查找过程

V8 在对象上查找属性的规则:

markdown 复制代码
1. 先在对象自身查找
2. 找不到?去对象的原型(__proto__)上查找
3. 还找不到?继续沿原型链向上找
4. 直到找到属性,或到达原型链顶端(null)

对于 'hello'.length

javascript 复制代码
临时 String 对象自身 → 没有 length
    ↓
String.prototype → 找到了 length!值为 5
    ↓
返回 5
javascript 复制代码
// 验证:length 确实在 String.prototype 上
console.log(String.prototype.hasOwnProperty('length'));  // false
// 注意:length 不是 String.prototype 自身的属性
// 它是 String 实例的内部属性,通过原型链访问

// 但 toUpperCase 确实在原型上
console.log(String.prototype.hasOwnProperty('toUpperCase'));  // true

六、总结

核心概念回顾

概念 一句话解释
原始类型 存储值本身,不能添加属性和方法
引用类型 存储地址,可以自由添加属性和方法
函数二义性 同一个函数既能普通调用,也能 new 调用
new 四步 创建空对象 → 执行函数(this指向空对象) → 设置原型 → 返回对象
包装类 V8 自动将原始类型临时包装成对象,以便调用属性和方法
装箱 原始类型 → 临时对象(new String/Number/Boolean
拆箱 临时对象用完即销毁,回到原始类型

一张图理解包装类

lua 复制代码
'hello'.toUpperCase()

原始值 'hello'
    ↓ V8 发现原始值调用了方法
    ↓ 自动装箱
临时 String 对象 { [[PrimitiveValue]]: 'hello' }
    ↓ 在对象上查找 toUpperCase
    ↓ 没找到,去原型上找
String.prototype.toUpperCase → 找到了!
    ↓ 执行方法,返回 'HELLO'
    ↓ 自动拆箱
临时 String 对象被销毁
    ↓
返回 'HELLO'

一句话记住

原始类型本身没有属性和方法,能调用是因为 V8 在背后偷偷帮你创建了临时对象(包装类),用完立刻销毁。

相关推荐
还有多久拿退休金5 小时前
LLM应用开发二:让AI学会"翻书"——RAG检索增强从踩坑到跑通
前端·llm
weiggle5 小时前
第二篇:搭建你的第一个 Compose 项目——开发环境与项目结构
android·前端
Simon523145 小时前
Spring AOP 五大通知类型
java·前端·spring
Asmewill6 小时前
LangGraph学习笔记八(SubGraph)
前端
叶落阁主6 小时前
AntV npm 投毒复盘:一次公司私服缓存恶意包引发的账号封禁事件
前端·安全·npm
vaexu6 小时前
Android 定时提醒的终极防线:我是如何用“双保险机制”攻克后台保活的?
前端
小村儿6 小时前
连载11- Claude code 的 Agent Teams——当子 Agent 开始互相说话
前端·后端·ai编程
潍坊老登6 小时前
关于 number类型从vue端传到golang后端是float而不是int的事
前端
茶底世界之下6 小时前
你的 Mac 里,藏着一支 AI 开发团队
前端·javascript