引言
在JavaScript 中,万物皆对象。理解对象的创建、属性操作、构造函数的使用,以及内存管理机制,对于编写高效、可靠的代码至关重要。这些核心概念不仅影响着代码的结构和性能,还决定了程序的行为和资源的利用效率。本文将深入探讨这些关键知识点,帮助开发者掌握 JavaScript 对象的本质和内存管理的奥秘。
创建对象的方式
- 字面量 ,示例如下
js
var obj = {
name: "fz",
age: 18,
}
- new Object() ,示例如下
js
var obj = new Object()
obj.name = name
obj.age = age
- new 自定义的构造函数 ,示例如下
js
var obj2 = new Person()
function Person(name,age){
this.name = name
this.age = age
}
访问对象属性和方法
- 点符号(
.
) :obj.name
- 方括号符号(
[]
) :obj['age']
需要注意的是,使用方括号符号时,括号内的内容是字符串(表示属性名)或者是一个变量(该变量存储着属性名的字符串)。 如果直接使用obj[age]
,则没有将age
属性名作为字符串,而是作为变量。由于在全局作用域中并没有定义名为age
的变量,因此会抛出age is not defined
的错误。正确的方式是使用字符串'age'
,即obj['age']
;
如果要使用变量,应先定义一个变量,并将属性名字符串赋值给它。
js
var propertyName = 'age';
console.log(obj[propertyName]); // 输出: 18
对象属性操作的基本行为
在对象属性操作中,新增、修改和删除是三种基本行为。对象的新增 是为对象添加原本不存在的属性和值,修改 是更改对象中已存在属性的值,删除 是通过使用delete
运算符从对象中移除指定属性及对应的值。
js
obj.sex = 'boy' // 新增
obj['age'] = 19 // 修改
delete obj.age // 删除
因此,我们可以总结对象中的键是唯一的,每个键对应一个值,不会重复,即:
- 同一对象中,不存在两个相同名称(key)的属性;
- 当我们给一个已经存在的属性名赋值时,实际上是修改操作,而不是新增;
- 如果使用一个已经存在的属性名进行新增操作,实际上会覆盖原有的值(即修改)。
构造函数
- 当需要批量化创建对象时,使用构造函数。
- 当一个函数被
new
调用时,我们就称之为构造函数。
示例
function Car(color){
this.name = 'su7-Ultra'
this.height = '1400'
this.lang = '4800'
this.weight = 1500
this.color = color
}
var car1 = new Car('orange'); // 实例化一个对象
var car2 = new Car('pink');
car1.name = '劳斯莱斯';
通过构造函数来创建的多个(实例化)对象之间是相互独立的。
示例1
function Person(name,age,sex){
this.name = name;
this.age = age;
this.sex = sex;
}
var p1 = new Person('zs',18,'男');
示例2
function Person(name,age,sex){
var _this = {}
_this.name = name;
_this.age = age;
_this.sex = sex;
return _this;
}
var p1 = Person('zs',18,'男');
对比上述两份代码,我们能够清晰了解 new 做了什么
- 创建了一个
this
对象; - 执行构造函数中的代码,给
this
对象添加属性和方法; - 返回
this
对象。
接着,我们思考一下下述代码是否合理?
dart
var num = 123 // new Number(123)
num.a = 'aaa'
console.log(num + 1); // 输出:124
console.log(num.a); // 输出:undefined
var str = 'hello' // new String('hello')
console.log(str.length); // 输出 :5 | 没有人为在对象增加属性,length是字符串独有的属性(继承)
由于原始类型一定不能添加属性和方法,属性和方法是对象独有的 ,那么为什么上述代码没有报错?是因为V8引擎在执行代码赋值给变量时,会自动推断出其类型,并将其默认执行成一个对象。但由于用户定义的是一个原始类型,违背了用户设定的初衷,v8引擎就会想尽一切办法将其还原成原始类型,即移除对象身上添加的属性delete num.a
。
包装类
- 用户定义的字面量,会被包装成对象 (比如: new Number());
- 一个包装类的实例对象在参与运算时,会被自动拆箱成原始类型即读取内部的原始值(比如: number);
- 因为 js 是弱类型的语言,所以只有在赋值语句时才会判断值的类型,当值被判定为原始类型时,就会自动将包装对象上添加的属性移除。
考点
ini
var arr = [1,2,3,4,5] // new Array(1,2,3,4,5)
arr.length = 2
console.log(arr); // 输出:[1,2]
var str = 'abcd' // new String('abcd')
str.length = 2
console.log(str.length); // 输出:4
思考一下上述代码运行为什么有差异?
在 JavaScript 中,数组本质上是一种特殊对象,其长度(length
)属性与数组元素的索引紧密相关,直接给数组的length
属性赋值,会改变数组的长度。
字符串在 JavaScript 中是原始类型,但它也可以通过new String('abcd')
被包装成对象,字符串对象的length
属性是只读的,并且是从字符串的原型对象继承而来的。因此当尝试给字符串的length
属性赋值时,不会改变字符串的实际长度。
拓展小知识
思考一下下述代码是否合理?
css
const a = {
b : 1
}
a.b = 2
答案是合理的。
a
是一个变量,位于调用栈 的变量环境中,存储的是对象的引用地址 ;{ b: 1 }
作为引用类型,存储在堆内存 中。const
保证的是变量绑定的不可变性 ,对于原始类型(如 number
),值直接保存在栈中,不可修改;对于引用类型,变量存储的是引用地址 ,const
保证的是这个地址不变,而非对象内部状态不变。
V8引擎的执行操作
原始类型数据存储在栈中,引用类型数据存储在堆中。使用const
声明的变量,其值不能被重新赋值,但如果是引用类型,则可通过修改引用地址指向的内容来更新数据。

代码 | 引擎行为 | 内存区域 |
---|---|---|
const a = ... |
在栈中创建 a ,存储对象的引用地址 |
调用栈 |
{ b: 1 } |
在堆中分配对象空间,存储属性值 | 堆内存 |
a.b = 2 |
通过引用地址找到堆中对象,修改属性值 | 堆内存更新 |
显然,变量 a
存储的引用地址 #001
从未改变 ,因此不违反 const
约束。
V8 引擎的内存结构
在 JavaScript 执行过程中,V8 引擎将内存分为栈 和堆两个主要区域。
内存区域 | 存储内容 | 特点 |
---|---|---|
调用栈 | 原始类型值(number, string, boolean 等)和引用地址 | 大小固定,访问快速 |
堆内存 | 引用类型值(对象、数组等) | 大小动态可变,访问稍慢 |
对象存储在堆内存的原因
- 对象属性数量不确定,内存需求动态变化
- 栈内存空间有限且要求固定大小数据
- 堆内存适合存储较大的动态数据结构
- 堆内对象不受作用域限制,可灵活管理