满头的问号❓原来包装类也有这么多的细节!

316 阅读6分钟

在JavaScript的世界中,或者说许多的编程语言中,对象占据着核心地位,它们是构建复杂数据结构的基础。本文我将逐步揭开对象的神秘面纱,从最基础的对象创建方式出发,逐步深入到原始值与包装类的概念,通过一些恰当的例子,让你理解这些概念是如何相互关联以构建出JavaScript的丰富生态。⚔️

一、基础对象创建

1. 对象字面量

对象字面量是最直观也恰恰是我们最常用最实用的创建对象方式,它直接以键值对的形式定义属性。例如,创建一个表示人物信息的对象:

var person = {
    name: "Tom",
    age: 18,
    hobby: "draw"
};

在这个例子中,person对象包含三个属性:nameagehobby,分别对应字符串和数字值。

2. new Object()构造函数

另一种创建对象的方式是使用new关键字来接一个Object构造函数,但是需要手动添加属性:

var person = new Object();
person.name = "Jerry";
person.age = 5;

这种方式相比字面量要更加繁琐,但在需要动态创建多个属性时较为实用。

3. 自定义构造函数

使用构造函数可以创建特定类型的对象,请看下面的代码:

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

var tom = new Person("Tom", 20);

通过new操作符和自定义的Person构造函数,我们不仅创建了新的对象,还初始化了它的属性。

二、new关键字的工作原理

明白了创建对象的几个方式后,我们看看为什么使用new关键字时会返回一个对象。当我们使用new调用构造函数时,其实背后主要发生了四件事,用下面的例子来详解。我们创建一个表示汽车的构造函数,并通过new操作符实例化它。

1. 构造函数定义

首先,搭建一个Car构造函数,它有颜色、品牌和车型作为参数,并为新创建的汽车对象设置这些属性:

function Car(color, brand, model) {
    // 使用this关键字给新创建的对象设置属性
    this.color = color;
    this.brand = brand;
    this.model = model;
}

在这个构造函数中,this关键字指向新创建的实例对象。通过this,我们可以为这个新对象添加属性和方法。

2. 使用new关键字

现在,使用new关键字来创建一个Car实例:

var myCar = new Car("Red", "SU7", "Max");

在上面的代码中,new实现以下四个步骤:

    1. 创建一个空对象new首先在内存中创建一个空的对象,这个对象将成为构造函数中的this上下文。
    1. 绑定原型链:新创建的对象会自动链接到构造函数的prototype属性所指向的对象。当构造函数有原型方法,新对象都可以继承这些方法。平时我们写的this.name = name,其实就是构造函数内的this被绑定到新创建的对象上,允许我们通过this访问和修改该对象的属性。
    1. 执行构造函数:接下来,new操作符会调用指定的构造函数,使用新创建的对象作为this的上下文。在我们的例子中,Car构造函数会被执行,给myCar对象设置colorbrandmodel属性。
    1. 返回新对象:如果构造函数没有显式返回一个对象,new操作符会自动返回新创建的对象实例。

3. 输出结果

我们可以通过访问myCar对象的属性来验证new操作的结果:

console.log(myCar.color,myCar.brand,myCar.model);    // Red SU7 Max

4. 深入理解new

除了使用官方的new关键字,我们甚至可以自己手动写出new的操作过程来平替new


function Instance(constructor, ...args) {
    // 创建一个空对象
    const instance = Object.create(constructor.prototype);
    // 调用构造函数,使用新对象作为this
    const result = constructor.apply(instance, args);
    // 确保构造函数返回的是一个对象,如果不是,则返回新创建的实例
    return typeof result === 'object' && result !== null ? result : instance;
}

const anotherCar = Instance(Car, "Black", "benz", "Supercar");
console.log(anotherCar.brand); // benz

三、原始值与对象

原始值如字符串、数字、布尔值等,本身不具备属性和方法,但JavaScript提供了包装类,使原始值在特定情境下能像对象一样拥有属性或方法。

1. 字符串的包装

考虑字符串长度的访问:

var str = "Hello";
console.log(str.length);     // 5

按理说作为原始值,字符串是没有任何属性的,包括length,但通过.length访问时,JavaScript会临时将字符串包装成String对象,让str可以像对象一样拥有属性。

2. 数字的包装

同理,数字也可以通过包装类临时获得“不应该拥有”的方法:

var num = 123.456;
console.log(num.toFixed(2));        // 123.46

这里,toFixed(2)方法是由Number对象提供的,v8引擎会自动将num包装成Number对象,执行完方法后,移除包装。

3. 数组

修改数组的length会直接影响其内容:

var array = [1, 2, 3, 4, 5];
array.length = 3;
console.log(array);      // [1, 2, 3]

人为的减短length会截断数组,而增加length则会在数组末尾添加undefined。

4. 字符串

字符串的length属性是只读的,当我们修改它时并不能改变字符串的内容:

var str = "Hello";
str.length = 7;      // 并没有效果
console.log(str);      // Hello
console.log(str.length);       // 5

四、包装类

所以到底什么是包装类呢,能够让原始值拥有不该出现的属性和方法。包装类主要服务于将基本数据类型转换为引用类型。

原始值对象,v8执行到包装类时,会通过valueOf试探该包装类是不是原始值,如果是,则秉承原始值不具有属性和方法这一原则,再移除掉给包装类添加的属性。

面试题示例

考虑下面的代码片段:

var str = 'abc'
str += 1
var test = typeof(str)
if (test.length === 6) {
    test.sign = 'typeOf的返回结果可能是String'
}
console.log(test.sign); // undefined

一个简单的例子,声明了字符串str值为abc,加上1得到的是abc1。这是因为字符串是一种特殊的类型,通常在与其他类型结合时,会将非字符串类型的数据转换为字符串,然后执行字符串拼接。所以test接收到的是String,也就进入了if中。给test添加一个属性sign值为'typeOf的返回结果可能是String',其实这里的值无论是什么关系都不大,v8运行到这一行时,会给test对象上添加没有出现过的sign,但是通过valueOf方法查看后,发现test应该是一个字符串,不应该拥有属性才对,也就移除了sign这个属性。当最后要输出test.sign时,首先会添加上sign属性,但是它的值为空,也就是没有定义,所以最后的输出结果为undefined,最后还是会移除这个属性。

五、结语

从简单的对象字面量到复杂的包装类机制,通过本文的逐步解析,我们解释了对象的创建方式、new的工作原理、原始值与包装类的关系,以及如何通过包装类赋予原始值临时的属性和方法访问能力。希望看完对你稍微有点帮助,一起进步。✌️✨