包装类的详细解析原理实现与引擎分析——装箱机制
前言
在JavaScript的世界里,数据类型分为两大阵营:简单数据类型(原始类型)与复杂数据类型(对象类型)。简单数据类型包括number
、string
、boolean
、undefined
、null
、symbol
和bigint
,而复杂数据类型则涵盖了所有其他类型,如对象、数组、函数等。尽管简单数据类型在内存中以值的形式存储,不支持直接的属性或方法添加,JavaScript引擎却巧妙地引入了包装类的概念,使简单数据类型在需要时表现出对象的特性。
包装类概述
包装类是JavaScript中一种特殊的对象类型,用于“包裹”简单数据类型,使其在需要时能以对象的形式存在。每当对简单数据类型执行方法调用或属性访问时,JavaScript引擎会自动创建一个对应的包装类实例,执行相关操作后立即将其销毁。这一过程对开发者是透明的,但却是实现简单数据类型方法调用的关键。
正文
对于这段代码你是怎么想它的运行过程的是怎么样的?报错?undefined?还是什么?
let num=1
num.a=2
console.log(num.a)
这个问题我们放在文章后面进行分析,我们先来复习一下类的引用操作
类的引用操作
var mrPeng={
name:'彭于晏',
age:18,
sex:'男',
}
我们创建了一个mrPeng的类,在这里我们怎么去对键值来应用呢,给出下面三个方法:
- Object.sex=? 正常查找键名为sex属性
- Object[sex]=? 查找变量sex代表值与Object键名相同的属性
- Object['sex']=? 正常查找键名为sex属性
我们常用的是第一种查找方式,但是对于使用类似于数组查找的这种查找方式确是很多人所模糊的,这里我们记住如果在[]内部是内没加''的,那么在执行这句语句时,内部的名称是会被看作是一个变量名从而去查找它的值。选择哪种方式取决于你是否需要动态确定键名,或者是否希望键名包含特殊字符。直接属性访问适用于静态已知的属性名,而计算属性访问则适用于动态或含有特殊字符的键名。
- 直接属性访问 (
Object.sex
) : 尝试访问预定义或自定义在Object
原型链上的属性,如果属性不存在则返回undefined
。 - 计算属性访问 (
Object[sex]
) : 首先解析sex
变量,使用其值作为键名来访问Object
对象。如果sex
变量未定义,则抛出引用错误;如果sex
变量的值不是有效的键名(例如,包含空格或其他非合法字符),则将无法找到相应的属性(undefined)。 - 字符串键名访问 (
Object['sex']
) : 直接指定键名来访问属性,不会解析额外的变量,即使属性不存在也安全返回undefined
。
类的创建方式
- var Obj1={} //创建对象字面量
- var Obj2=new Object(); //使用
new Object()
构造函数 - new XXX() //XXX为构造函数 三种不同的类的创建方式
这里面对象字面量是创建对象最简洁、最常用的方式。通过一对大括号{}
,可以直接定义对象及其属性和方法。而通过new
关键字调用Object
构造函数。这种方式创建的对象与通过对象字面量创建的对象本质上是一样的,但在实际开发中较少使用,因为它比对象字面量冗长且不易读。使用自己的构造函数,用于创建具有特定属性和方法的对象。这非常适用于创建多个具有相同结构的对象。
记住在实际开发中单个类的创建我们一般都是才有var obj={}
这种方式来创建的。
包装类
讲完了类的几个属性,我们开始回到主体,看下面这段在浏览器中的执行代码:
怎么会这样呢?因为在JavaScript中,Number
既是一个内置的构造函数,也是一个类型。当你使用new Number(10)
创建变量a
时,你实际上是在使用Number
构造函数来创建一个对象,这个对象封装了一个数值。因此,a
是一个Number
类型的对象,所以typeof(a)
返回的是'object'
。
另一方面,当你简单地声明let b = 10
时,你创建的是一个原始数值(即简单数据类型),在这种情况下,b
就是一个数值(number
类型),因此typeof(b)
返回的是'number'
。
这里的关键区别在于使用new
关键字创建的是一个对象,而直接赋值一个数值则是创建一个简单的数值类型。
更详细的解释
当你使用new Number()
构造函数时,JavaScript创建了一个Number
对象,这个对象包含了你传递给构造函数的数值。这种对象在内部存储了数值,但是它是一个完整的对象,具有对象的所有特性,包括方法和属性。例如,a
可以调用Number
对象的内置方法,如toFixed()
,这是因为a
实际上是一个对象。
那为什么b可以调用tofixed()呢,而且却也是10.0呢?不是说只有对象身上才可以携带属性和方法吗?
装箱机制
在JavaScript中,装箱机制是一种运行时的自动转换过程,它允许原始数据类型(如number
、string
、boolean
)在需要时被转换成对应的对象类型(如Number
、String
、Boolean
对象)。这种机制使得原始数据类型可以像对象一样被调用方法或访问属性。
装箱机制的工作原理
当JavaScript引擎检测到对一个原始数据类型值进行方法调用或属性访问时,它会自动创建一个对应的包装对象,然后调用该对象上的方法或属性。一旦方法调用完成或属性访问结束,这个临时创建的包装对象就会被销毁,以防止不必要的内存占用。
因此我们在对b进行.toFixed操作时没有进行报错
这里我们注重分析这一句:“一旦方法调用完成或属性访问结束,这个临时创建的包装对象就会被销毁”什么叫做会被销毁,也就是当我们在原始数据类型身上挂载属性和方法时没问题的,例如分析
console.log(b.toString(1));
由于装箱机制的存在,V8引擎是不会报错的,引擎做了以下事情:
- 装箱:引擎自动创建了一个
Number
对象,将b
的值(10)作为参数传递给Number
构造函数,这样b
就被临时封装成了一个Number
对象。 - 方法调用:然后,
toString
方法在这个临时的Number
对象上被调用,该方法将数值转换为字符串。 - 解包:方法调用完成后,临时创建的
Number
对象被销毁,b
变量仍然保持其原始的数值类型。
总结
现在再回过头来看这道题,相信你已经有结果了,答案是undefined。
let num=1
num.a=2
console.log(num.a)
因为在你给num添加a属性为2时,引擎自动创建了一个Number
对象,将num
的值(2)作为参数传递给Number
构造函数,这样num
就被临时封装成了一个Number
对象。然后,添加a
属性等于2在这个临时的Number
对象方法调用完成后,临时创建的Number
对象被销毁,num
变量仍然保持其原始的数值类型。
当你再去打印num.a时,又会自动创建了一个Number
对象,这时这个Number
对象中去查找a
属性是一个没有的属性,返回的就是undefined。
虽然装箱机制使得在原始数据类型上使用对象方法变得方便,但也有一些需要注意的地方:
- 性能开销:每次装箱和解包都会产生一定的性能开销,特别是在循环或频繁调用的情况下。
- 内存消耗:虽然装箱的对象是临时的,但如果频繁进行装箱操作,可能会增加垃圾回收的压力。
- 避免修改:不要尝试在原始数据类型上添加持久性的属性或方法,因为这些更改不会保留,一旦装箱操作结束,这些属性或方法就会丢失。
转载自:https://juejin.cn/post/7376186315643584521