深入解析 JavaScript 对象操作

283 阅读6分钟

引言

在JavaScript 中,万物皆对象。理解对象的创建、属性操作、构造函数的使用,以及内存管理机制,对于编写高效、可靠的代码至关重要。这些核心概念不仅影响着代码的结构和性能,还决定了程序的行为和资源的利用效率。本文将深入探讨这些关键知识点,帮助开发者掌握 JavaScript 对象的本质和内存管理的奥秘。


创建对象的方式

  1. 字面量,示例如下
var obj = { 
    name: "fz",
    age: 18,
}
  1. new Object(),示例如下
var obj = new Object()
obj.name = name
obj.age  = age
  1. new 自定义的构造函数,示例如下
var obj2 = new Person()
function Person(name,age){
this.name = name
this.age  = age
}

访问对象属性和方法

  1. 点符号(.) :obj.name
  2. 方括号符号([]) :obj['age']
    需要注意的是,使用方括号符号时,括号内的内容是字符串(表示属性名)或者是一个变量(该变量存储着属性名的字符串)。 如果直接使用obj[age],则没有将age属性名作为字符串,而是作为变量。由于在全局作用域中并没有定义名为age的变量,因此会抛出age is not defined的错误。正确的方式是使用字符串'age',即obj['age']
    如果要使用变量,应先定义一个变量,并将属性名字符串赋值给它。
var propertyName = 'age';
console.log(obj[propertyName]); // 输出: 18

对象属性操作的基本行为

在对象属性操作中,新增、修改和删除是三种基本行为。对象的新增是为对象添加原本不存在的属性和值,修改是更改对象中已存在属性的值,删除是通过使用delete运算符从对象中移除指定属性及对应的值。

 obj.sex = 'boy' // 新增
 obj['age'] = 19 // 修改
 delete obj.age // 删除

因此,我们可以总结对象中的键是唯一的,每个键对应一个值,不会重复,即:

  1. 同一对象中,不存在两个相同名称(key)的属性;
  2. 当我们给一个已经存在的属性名赋值时,实际上是修改操作,而不是新增;
  3. 如果使用一个已经存在的属性名进行新增操作,实际上会覆盖原有的值(即修改)。

构造函数

  • 当需要批量化创建对象时,使用构造函数。
  • 当一个函数被 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 = '劳斯莱斯';

通过构造函数来创建的多个(实例化)对象之间是相互独立的。

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

var p1 = new Person('zs',18,'男');
function Person(name,age,sex){
    var _this = {}
    _this.name = name;
    _this.age = age;
    _this.sex = sex;
    return _this;
}

var p1 = Person('zs',18,'男');

对比上述两份代码,我们能够清晰了解 new 做了什么

  1. 创建一个空对象
  2. 让构造函数的 this 指向这个空对象
  3. 执行构造函数中的代码
  4. 将这个空对象的隐式原型__proto__赋值成 构造函数的显式原型
  5. 返回这个对象

接着,我们思考一下下述代码是否合理?

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 是弱类型的语言,所以只有在赋值语句时才会判断值的类型,当值被判定为原始类型时,就会自动将包装对象上添加的属性移除。

考点

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属性赋值时,不会改变字符串的实际长度。


拓展小知识

思考一下下述代码是否合理?

const a = {
    b : 1
}
a.b = 2

答案是合理的。
a是一个变量,位于调用栈的变量环境中,存储的是对象的引用地址{ b: 1 } 作为引用类型,存储在堆内存中。const 保证的是变量绑定的不可变性,对于原始类型(如 number),值直接保存在栈中,不可修改;对于引用类型,变量存储的是引用地址const 保证的是这个地址不变,而非对象内部状态不变。

V8引擎的执行操作

原始类型数据存储在栈中,引用类型数据存储在堆中。使用const声明的变量,其值不能被重新赋值,但如果是引用类型,则可通过修改引用地址指向的内容来更新数据。

b4b00373edc6d6ceda0d3a2fa9f7bb0.png

代码引擎行为内存区域
const a = ...在栈中创建 a,存储对象的引用地址调用栈
{ b: 1 }在堆中分配对象空间,存储属性值堆内存
a.b = 2通过引用地址找到堆中对象,修改属性值堆内存更新

显然,变量 a 存储的引用地址 #001 从未改变,因此不违反 const 约束。

V8 引擎的内存结构

在 JavaScript 执行过程中,V8 引擎将内存分为两个主要区域。

内存区域存储内容特点
调用栈原始类型值(number, string, boolean 等)和引用地址大小固定,访问快速
堆内存引用类型值(对象、数组等)大小动态可变,访问稍慢

对象存储在堆内存的原因

  1. 对象属性数量不确定,内存需求动态变化
  2. 栈内存空间有限且要求固定大小数据
  3. 堆内存适合存储较大的动态数据结构
  4. 内对象不受作用域限制,可灵活管理