在JavaScript的世界中,或者说许多的编程语言中,对象占据着核心地位,它们是构建复杂数据结构的基础。本文我将逐步揭开对象的神秘面纱,从最基础的对象创建方式出发,逐步深入到原始值与包装类的概念,通过一些恰当的例子,让你理解这些概念是如何相互关联以构建出JavaScript的丰富生态。⚔️
一、基础对象创建
1. 对象字面量
对象字面量是最直观也恰恰是我们最常用最实用的创建对象方式,它直接以键值对的形式定义属性。例如,创建一个表示人物信息的对象:
var person = {
name: "Tom",
age: 18,
hobby: "draw"
};
在这个例子中,
person对象包含三个属性:name、age和hobby,分别对应字符串和数字值。
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实现以下四个步骤:
-
- 创建一个空对象:
new首先在内存中创建一个空的对象,这个对象将成为构造函数中的this上下文。
- 创建一个空对象:
-
- 绑定原型链:新创建的对象会自动链接到构造函数的
prototype属性所指向的对象。当构造函数有原型方法,新对象都可以继承这些方法。平时我们写的this.name = name,其实就是构造函数内的this被绑定到新创建的对象上,允许我们通过this访问和修改该对象的属性。
- 绑定原型链:新创建的对象会自动链接到构造函数的
-
- 执行构造函数:接下来,
new操作符会调用指定的构造函数,使用新创建的对象作为this的上下文。在我们的例子中,Car构造函数会被执行,给myCar对象设置color、brand和model属性。
- 执行构造函数:接下来,
-
- 返回新对象:如果构造函数没有显式返回一个对象,
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的工作原理、原始值与包装类的关系,以及如何通过包装类赋予原始值临时的属性和方法访问能力。希望看完对你稍微有点帮助,一起进步。✌️✨