Skip to content

对象

Object(对象):基础知识

JavaScript 中有八种数据类型:其中原始数据类型和对象

可以使用带有可选属性列表的花括号 {} 来创建对象。一个对象就是一个键值对,其中键 key 是一个字符串,值 value 可以是任意类型值。

创建对象

通过字面量的方式:

js
let user = {
  name: 'John',
  age: 30,
};

通过构造函数的方式:

js
let user = new Object({ 
  name: 'John',
  age: 30,
});

文本和属性

对象中的一对键值对就是对象的一个属性,我们常说属性名和属性值。

点操作符

访问属性的值:

js
console.log(user.age); // 30

添加属性:

js
user.isAdmin = true;
console.log(user.isAdmin); // true

delete 操作符来删除属性:

js
delete user.isAdmin;
console.log(user.isAdmin); // undefined

列表中的最后一个属性应以逗号结尾,这叫做尾随(trailing)或悬挂(hanging)逗号。这样便于我们添加、删除和移动属性,因为所有的行都是相似的。

方括号

访问属性的值:

js
console.log(user['name']); // John

注意,这里的键需要用引号包裹起来。

方括号提供了一种属性名的灵活表达方式,而不需要必须是静态量,如:

js
let preKey = 'name';
console.log(user[preKey]); // John

添加属性与删除属性同理。

计算属性

当创建一个对象的时候,我们可以在对象字面量中使用方括号。这就叫做计算属性,例如:

js
let fruit = prompt('Which fruit to buy?', 'apple');

let bag = {
  [fruit]: 5, 属性名是从 fruit 变量中获得的
};

console.log(bag.apple); // 5

计算属性的含义很简单:[fruit] 含义是属性名应该从 fruit 变量中获取。

本质上,这与先建立一个对象,再用方括号语法添加属性的效果相同,但看起来更清晰。

不仅如此,方括号中还可以使用更加复杂的表达式:

js
let fruit = 'apple';
let bag = {
  [fruit + 'Computers']: 5 // bag.appleComputers = 5
};

方括号点符号更强大。它允许任何属性名和变量,但写起来也更加麻烦。

所以,大部分时间里,当属性名是已知且简单的时候,就使用点符号。如果我们需要一些更复杂的内容,那么就用方括号。

属性存在性测试

相比于其他语言,JavaScript 有一个需要注意的特性:只要对象存在,任何属性都可以被访问,哪怕属性并不存在也不会报错,只会返回 undefined

我们可以很容易地判断一个属性是否存在:

js
console.log(user.school === undefined); // True

这里还有一个特别的,判断属性是否存在的操作符 in,例如:

js
console.log('school' in user); // flase

in 的左边必须是属性名,通常是一个带引号的字符串;如果省略引号,就代表左边是一个变量,这个变量应该存储实际的属性名。

为什么需要这种方法呢?因为存在一种情况:属性值本身就为 undefined。此时用 in 操作符检查能够得到正确的结果。

for...in 循环

为了遍历一个对象所有的键,可以使用 for...in 循环来实现:

js
for (let key in user) { 
  console.log(key);
  console.log(user[key]);
}

对象的引用和复制

与原始数据类型的根本区别之一是:对象是通过引用存储和复制的。

TIP

将对象赋值的变量存储的不是对象本身,而是对象在内存中的地址,换句话说就是该对象的“引用”。

例如:

js
let user = {
  name: "John"
};

在内存中的存储方式为:

该对象被存储在内存中的某个位置(在图片的右侧),而变量 user(在左侧)保存的是对其的“引用”。

我们可以将一个对象变量(例如 user)想象成一张写有对象的地址的纸。

当我们对对象执行操作时,例如获取一个属性 user.name,JavaScript 引擎会查看该地址中的内容,并在实际对象上执行操作。

现在,这就是为什么它很重要。

TIP

当一个对象被复制时——引用被复制了,而该对象本身并没有被复制。

如上面的图片中显示的,两个变量指向了同一个对象所在的内存区域。

对象的比较

TIP

只有当两个变量指向同一个对象的时候,两个对象才相等。

如下面这种情况:

js
let user = new Object({
  name: 'John',
  age: 30,
});

let person = new Object({
  name: 'John',
  age: 30,
});

console.log(user == person); // false

即使内容完全相同,但指向的不是同一个对象,就不相同。

拷贝与合并

就像上面所说的,直接在变量之间赋值只会创造一个新的引用。

那么,如何真正实现拷贝?

浅拷贝

第一种方法是通过遍历已有对象的属性,并在原始类型值的层面来复制它们,以实现对原始对象的复制,如:

js
let user = new Object({
  name: 'John',
  age: 30,
});

let person = {}; // 创建一个新对象

for (let key in user) { // 遍历
  person[key] = user[key]; // 赋值
}

第二种方法是使用 Object.assign() 方法,该方法的语法是:

js
Object.assign(dest, [src2, src2, src3...]);
  • dest: 目标对象;
  • src2, src2, src3...: 源对象;
  • 将所有源对象的属性拷贝到目标对象 dest 中;
  • 调用结果返回 dest
js
let person = Object.assign({}, user);

深拷贝

在浅拷贝中,我们一直假设 user 的属性都是原始类型,但属性可以是其他对象的引用,例如:

js
let user = {
  name: 'John',
  sizes: {
    width: 180,
    height: 80,
  },
}

对于这样的对象使用上面的两种方式就无法实现完全的拷贝了,为了实现有对象嵌套的深拷贝,可以考虑使用以下四个方法:

JSON

js
let user = {
  name: 'John',
  sizes: {
    width: 180,
    height: 80,
  },
  school: undefined, 
};

let person = JSON.parse(JSON.stringify(user));

这种方法简单易用,但也有两个缺点:

  1. 不支持拷贝函数、undefined 和 Symbol 等特殊值。
  2. 对象中存在循环引用时会报错。

递归实现

js
function deepClone(srcObj) {
  if (srcObj === null || typeof srcObj !== 'object') {
    return srcObj;
  }

  const copiedObj = Array.isArray(srcObj) ? [] : {};
  for (let key in srcObj) {
    copiedObj[key] = deepClone(srcObj[key]); // 递归拷贝
  }
  return copiedObj;
}

这种方式可以拷贝函数、undefiend 和 symbol,但依然无法处理循环引用,如果内部有循环引用,递归调用会陷入死循环,最终导致栈溢出。

可以通过 WeakMap 来跟踪已经拷贝过的对象,从而避免循环引用导致的无限递归。

js
function deepClone(srcObj, seen = new WeakMap()) {
  if (srcObj === null || typeof srcObj !== 'object') {
    return srcObj;
  }

  // 检查 weakmap 中是否存在
  if (seen.has(srcObj)) {
    return seen.get(srcObj); // 如果存在,直接返回已拷贝的对象
  }

  const copiedObj = Array.isArray(srcObj) ? [] : {};
  // 将当前对象存储到 weakmap 中
  seen.set(srcObj, copiedObj);
  
  for (let key in srcObj) {
    copiedObj[key] = deepClone(srcObj[key], seen); // 递归拷贝
  }
  return copiedObj;
}

垃圾回收

当一个变量被系统认为是不可达的时候,就会被引擎垃圾回收。

这一般是指没有任何引用指向它,但也有一种例外。通常,当数组、对象之类的数据结构在内存中时,它们的子元素,如对象的属性、数组的元素都会被认为是可达的,哪怕该变量本身被 null 赋值,例如:

js
let john = { name: "John" };

let array = [ john ];

john = null; // 覆盖引用

此时,john 不会被回收,因为存到了数组中。

参考