Skip to content

对象

花括号 {…} 来创建对象。一个属性就是一个键值对(“key: value”)。其中键(key)是一个字符串(也叫做属性名),值(value)可以是任何值。

  1. 文本和属性
    1. 移除属性:delete user.age;
  2. 方括号
    1. 对于多词属性,点操作就不能用了,例如:user["likes birds"] = true;
  3. 计算属性
    1. 当创建一个对象时,我们可以在对象字面量中使用方括号。这叫做 计算属性
JavaScript
let fruit = prompt("Which fruit to buy?", "apple");

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

alert( bag.apple ); // 5 如果 fruit="apple"
  1. 属性值简写
JavaScript
	function makeUser(name, age) {
	  return {
	    name, // 与 name: name 相同
	    age,  // 与 age: age 相同
	    // ...
	  };
	}
  1. 属性名称限制
    1. 变量名不能是编程语言的某个保留字,如 “for”、“let”、“return” 等……,但对象的属性名并不受此限制
    2. 一个名为 __proto__ 的属性。我们不能将它设置为一个非对象的
  2. 属性存在性测试,“in” 操作符
    1. 检查这个属性是否存在对象内
    2. "key" in object
    3. in 的左边必须是 属性名
  3. "for..in" 循环
    1. 为了遍历一个对象的所有键(key),可以使用一个特殊形式的循环:for..in。这跟我们在前面学到的 for(;;) 循环是完全不一样的东西。
JavaScript
	for (key in object) {
	  // 对此对象属性中的每个键执行的代码
	}
  1. 像对象一样排序
    1. 对象属性有特别的顺序。

对象引用和复制

对象与原始类型的根本区别之一是,对象是“通过引用”存储和复制的,而原始类型:字符串、数字、布尔值等 —— 总是“作为一个整体”复制。 赋值了对象的变量存储的不是对象本身,而是该对象“在内存中的地址” —— 换句话说就是对该对象的“引用”。当一个对象变量被复制 —— 引用被复制,而该对象自身并没有被复制。 例如:let admin = user; // 复制引用

  1. 通过引用来比较
    1. 两个对象引用相同才相等,不然对象内容相同,两个对象也不相等。
  2. 克隆与合并,Object.assign
JavaScript
let user = {
  name: "John",
  age: 30
};

let clone = {}; // 新的空对象

// 将 user 中所有的属性拷贝到其中
for (let key in user) {
  clone[key] = user[key];
}

// 现在 clone 是带有相同内容的完全独立的对象
clone.name = "Pete"; // 改变了其中的数据

alert( user.name ); // 原来的对象中的 name 属性依然是 John


 Object.assign 方法:
 Object.assign(dest, [src1, src2, src3...])
	 1. 第一个参数 dest 是指目标对象。
	 2. 更后面的参数 src1, ..., srcN(可按需传递多个参数)是源对象。
	 3. 该方法将所有源对象的属性拷贝到目标对象 dest 中。换句话说,从第二个开始的所有         参数的属性都被拷贝到第一个参数的对象中。
	 4. 调用结果返回 dest。


合并多个对象:
	let user = { name: "John" };

	let permissions1 = { canView: true };
	let permissions2 = { canEdit: true };
	
	// 将 permissions1 和 permissions2 中的所有属性都拷贝到 user 中
	Object.assign(user, permissions1, permissions2);
	
	// 现在 user = { name: "John", canView: true, canEdit: true }
		1. 拷贝的属性的属性名已经存在,那么它会被覆盖
		2. 可以用 Object.assign 代替 for..in 循环来进行简单克隆
		3. 还有其他克隆对象的方法,例如使用 spread 语法 clone = {...user}
  1. 深层克隆
    1. 假设 user 的所有属性均为原始类型。但属性可以是对其他对象的引用。
JavaScript
	let user = {
	  name: "John",
	  sizes: {
	    height: 182,
	    width: 50
	  }
	};
	
	alert( user.sizes.height ); // 182



let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

let clone = Object.assign({}, user);

alert( user.sizes === clone.sizes ); // true,同一个对象

// user 和 clone 分享同一个 sizes
user.sizes.width++;       // 通过其中一个改变属性值
alert(clone.sizes.width); // 51,能从另外一个获取到变更后的结果

让 user 和 clone 成为两个真正独立的对象,我们应该使用一个拷贝循环来检查 user[key] 的每个值,如果它是一个对象,那么也复制它的结构。这就是所谓的“深拷贝”。
可以使用递归来实现它。或者为了不重复造轮子,采用现有的实现,例如 lodash 库的 _.cloneDeep(obj)。
使用 const 声明的对象也是可以被修改的
只有当我们尝试将 user=... 作为一个整体进行赋值时,const user 才会报错。

垃圾回收

我们创建的原始值、对象、函数……这一切都会占用内存。

  1. 可达性(Reachability)
    1. “可达”值是那些以某种方式可访问或可用的值。它们被存储在内存中
    2. 固有的可达值的基本集合,这些值明显不能被释放。这些值被称作 根(roots)
        • 当前执行的函数,它的局部变量和参数。
        • 当前嵌套调用链上的其他函数、它们的局部变量和参数。
      1. 全局变量
        • (还有一些其他的,内部实现)
    3. 如果一个值可以从根通过引用或者引用链进行访问,则认为该值是可达的。,如果全局变量中有一个对象,并且该对象有一个属性引用了另一个对象,则  对象被认为是可达的。而且它引用的内容也是可达的。
  2. 例子
    1. 如果 全局变量user 的值(初始值:a)被重写了,这个引用就没了。那之前全局变量user初始化的值a就变得不可达了,引用没了,就不能访问到a了,
    2. 垃圾回收器会认为它是垃圾数据并进行回收,然后释放内存。
  3. 两个引用
    1. 如果全局变量user引用复制给了admin,重写了user里面属性的值,但是之前的属性值可以通过admin访问到。如果我们又重写了 admin,对象就会被删除。
  4. 相互关联的对象
JavaScript
	function marry(man, woman) {
	  woman.husband = man;
	  man.wife = woman;
	
	  return {
	    father: man,
	    mother: woman
	  }
	}
	
	let family = marry({
	  name: "John"
	}, {
	  name: "Ann"
	});
	现在所有对象都可达

//移除两个引用
	delete family.father;
	delete family.mother.husband;
	John现在不可达
  1. 无法到达的岛屿
    1. 几个对象相互引用,但外部没有对其任意对象的引用,这些对象也可能是不可达的,并被从内存中删除。
  2. 内部算法
    1. 垃圾回收的基本算法被称为 “mark-and-sweep”。
    2. 垃圾回收步骤:
        • 垃圾收集器找到所有的根,并“标记”(记住)它们。
        • 然后它遍历并“标记”来自它们的所有引用。
        • 然后它遍历标记的对象并标记 它们的 引用。所有被遍历到的对象都会被记住,以免将来再次遍历到同一个对象。
        • ……如此操作,直到所有可达的(从根部)引用都被访问到。
        • 没有被标记的对象都会被删除。
    3. 分代收集(Generational collection)
      1. 新对象(工作完就是去意义的对象)内存就会把它们及时清理,长期存在的对象检查的频率就会变小
    4. 增量收集(Incremental collection)
      1. 引擎将现有的整个对象集拆分为多个部分,这样会带来许多微小的延迟而不是一个大的延迟。
    5. 闲时收集(Idle-time collection)
      1. 垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。
  3. 总结
      • 垃圾回收是自动完成的,我们不能强制执行或是阻止执行。
      • 当对象是可达状态时,它一定是存在于内存中的。
      • 被引用与可访问(从一个根)不同:一组相互连接的对象可能整体都不可达。
    1. V8 之旅:垃圾回收

对象方法,’this‘

  1. 方法示例
    1. 作为对象属性的函数被称为 方法
JavaScript

	let user = {
	  name: "John",
	  age: 30
	};
	
	user.sayHi = function() {
	  alert("Hello!");
	};
	
	user.sayHi(); // Hello!  调用方法
  1. 面向对象编程
    1. 当我们在代码中用对象表示实体时,就是所谓的 面向对象编程,简称为 “OOP”。
  2. 方法简写
JavaScript
	// 这些对象作用一样
	
	user = {
	  sayHi: function() {
	    alert("Hello");
	  }
	};
	
	// 方法简写看起来更好,对吧?
	let user = {
	  sayHi() { // 与 "sayHi: function(){...}" 一样
	    alert("Hello");
	  }
	}; 

两种方式继承方面有差别
  1. 方法中的 “this”
    1. 为了访问该对象,方法中可以使用 this 关键字。
JavaScript
	let user = {
	  name: "John",
	  age: 30,
	
	  sayHi() {
	    // "this" 指的是“当前的对象”
	    alert(this.name);
	  }
	
	};
	
	user.sayHi(); // John
  1. “this” 不受限制
JavaScript
	let user = { name: "John" };
	let admin = { name: "Admin" };
	
	function sayHi() {
	  alert( this.name );
	}
	
	// 在两个对象中使用相同的函数
	user.f = sayHi;
	admin.f = sayHi;
	
	// 这两个调用有不同的 this 值
	// 函数内部的 "this" 是“点符号前面”的那个对象
	user.f(); // John(this == user)
	admin.f(); // Admin(this == admin)
	
	admin['f'](); // Admin(使用点符号或方括号语法来访问这个方法,都没有关系。)
  1. 在没有对象的情况下调用:this == undefined
    1. 解除 this 绑定的后果,在 JavaScript 中,this 是“自由”的,它的值是在调用时计算出来的,它的值并不取决于方法声明的位置,而是取决于在“点符号前”的是什么对象。
  2. 箭头函数没有自己的 “this”

构造器和操作符 ’new‘

常规的 {...} 语法允许创建一个对象。但是我们经常需要创建很多类似的对象,例如多个用户或菜单项等。这可以使用构造函数和 "new" 操作符来实现。

  1. 构造函数 2. 它们的命名以大写字母开头。 3. 它们只能由 "new" 操作符来执行。
JavaScript
	function User(name) {
	  this.name = name;
	  this.isAdmin = false;
	}
	
	let user = new User("Jack");
	
	alert(user.name); // Jack
	alert(user.isAdmin); // false
1. new对象的步骤:
2.  一个新的空对象被创建并分配给 `this`。
3.  函数体执行。通常它会修改 `this`,为其添加新的属性。
4. 返回 `this` 的值。
  1. 构造器的主要目的 —— 实现可重用的对象创建代码。
  2. 有许多行用于创建单个复杂对象的代码,我们可以将它们封装在一个立即调用的构造函数中:
JavaScript
// 创建一个函数并立即使用 new 调用它
let user = new function() {
  this.name = "John";
  this.isAdmin = false;

  // ……用于用户创建的其他代码
  // 也许是复杂的逻辑和语句
  // 局部变量等
};
这个构造函数不能被再次调用,因为它不保存在任何地方,只是被创建和调用。
  1. 构造器的 return
    1. ,构造器没有 return 语句。它们的任务是将所有必要的东西写入 this,并自动转换为结果。
    2. 如果有return语句
        • 如果 return 返回的是一个对象,则返回这个对象,而不是 this
        • 如果 return 返回的是一个原始类型,则忽略。
      1. 带有对象的 return 返回该对象,在所有其他情况下返回 this
  2. 省略括号
    1. 如果没有参数,我们可以省略 new 后的括号
    2. 例如:let user = new User``; // <-- 没有参数 等同于 let user = new User(); 但是不建议这样写
  3. 构造器中的方法
    1. 使用构造函数来创建对象会带来很大的灵活性。构造函数可能有一些参数,这些参数定义了如何构造对象以及要放入什么。当然,我们不仅可以将属性添加到 this 中,还可以添加方法。

可选链’?‘

这是一个最近添加到 JavaScript 的特性。 旧式浏览器可能需要 polyfills. 可选链 ?. 是一种访问嵌套对象属性的安全的方式。即使中间的属性不存在,也不会出现错误。

  1. “不存在的属性”的问题
    1. 如果一个实例对象的某个属性值没有,但是又不想程序报错,可以:user.address && user.address.street && user.address.street.name
  2. 可选链
    1. 为了简明起见,在本文接下来的内容中,我们会说如果一个属性既不是 null 也不是 undefined,那么它就“存在”。
    2. user?.address?.street
  3. 不要过度使用可选链
    1. 我们应该只将 ?. 使用在一些东西可以不存在的地方。
  4. ?. 前的变量必须已声明
  5. 短路效应
    1. 正如前面所说的,如果 ?. 左边部分不存在,就会立即停止运算(“短路效应”)。
  6. 其它变体:?.(),?.[]
  7. 可选链 ?. 不是一个运算符,而是一个特殊的语法结构。它还可以与函数和方括号一起使用。
    1. 将 ?.() 用于调用一个可能不存在的函数。
  8. 我们可以使用 ?. 来安全地读取或删除,但不能写入
  9. 可选链 ?. 不能用在赋值语句的左侧。

symbol 类型

只有两种原始类型可以用作对象属性键:字符串类型、symbol 类型 如果使用另一种类型,例如数字,它会被自动转换为字符串, obj[1] 与 obj["1"] 相同,而 obj[true] 与 obj["true"] 相同。

  1. symbol
    1. “symbol” 值表示唯一的标识符。
    2. 可以使用 Symbol() 来创建这种类型的值:
    3. 我们可以给 symbol 一个描述(也称为 symbol 名),这在代码调试时非常有用
JavaScript
	let id1 = Symbol("id");
	let id2 = Symbol("id");
	alert(id1 == id2); // false
4. symbol 是带有可选描述的“原始唯一值”
5. symbol 不会被自动转换为字符串
6. 获取 `symbol.description` 属性,只显示描述(description)
  1. “隐藏”属性
    1. 如果我们使用的是属于第三方代码的 user 对象,我们想要给它们添加一些标识符
JavaScript
	let user = { // 属于另一个代码
	  name: "John"
	};
	
	let id = Symbol("id");
	
	user[id] = 1;
	
	alert( user[id] ); // 我们可以使用 symbol 作为键来访问数据
  1. 对象字面量中的 symbol
    1. 如果我们要在对象字面量 {...} 中使用 symbol,则需要使用方括号把它括起来。
JavaScript
	let id = Symbol("id");
	let user = {
	  name: "John",
	  [id]: 123 // 而不是 "id":123
	};
  1. symbol 在 for…in 中会被跳过
    1. symbol 属性不参与 for..in 循环。
  2. 全局 symbol
    1. 全局 symbol 注册表
    2. 从注册表中读取(不存在则创建)symbol,请使用 Symbol.for(key)
    3. 注册表内的 symbol 被称为 全局 symbol
    4. 可以在代码中随处访问 —— 这就是它们的用途。
  3. Symbol.keyFor
    1. Symbol.for(key) 按名字返回一个 symbol。相反,通过全局 symbol 返回一个名字,我们可以使用 Symbol.keyFor(sym)
    2. 如果 symbol 不是全局的,它将无法找到它并返回 undefined
  4. 系统 symbol
    1. 众所周知的 symbol 表的规范中
      • Symbol.hasInstance
      • Symbol.isConcatSpreadable
      • Symbol.iterator
      • Symbol.toPrimitive

对象 —— 原始值转换

  1. 转换规则
    1. 对象原始值转换
      1. 没有转换为布尔值。所有的对象在布尔上下文(context)中均为 true,就这么简单。只有字符串和数字转换。
      2. 1数字转换发生在对象相减或应用数学函数时。例如,Date 对象(将在 日期和时间 一章中介绍)可以相减,date1 - date2 的结果是两个日期之间的差值。
      3. 至于字符串转换 —— 通常发生在我们像 alert(obj) 这样输出一个对象和类似的上下文中。
  2. hint
    1. 类型转换在各种情况下有三种变体。它们被称为 “hint”
    2. "string"、"number" (数学运算(除了二元加法))、"default"
    3. 为了进行转换,JavaScript 尝试查找并调用三个对象方法:
        1. 调用 obj[Symbol.toPrimitive](hint) —— 带有 symbol 键 Symbol.toPrimitive(系统 symbol)的方法,如果这个方法存在的话,
      1. 否则,如果 hint 是 "string" —— 尝试调用 obj.toString() 或 obj.valueOf(),无论哪个存在
      2. 否则,如果 hint 是 "number" 或 "default" —— 尝试调用 obj.valueOf() 或 obj.toString(),无论哪个存在
  3. Symbol.toPrimitive
    1. 有一个名为 Symbol.toPrimitive 的内建 symbol,它被用来给转换方法命名
JavaScript
	obj[Symbol.toPrimitive] = function(hint) {
	  // 这里是将此对象转换为原始值的代码
	  // 它必须返回一个原始值
	  // hint = "string"、"number" 或 "default" 中的一个
	}

let user = {
  name: "John",
  money: 1000,

  [Symbol.toPrimitive](hint) {
    alert(`hint: ${hint}`);
    return hint == "string" ? `{name: "${this.name}"}` : this.money;
  }
};

// 转换演示:
alert(user); // hint: string -> {name: "John"}
alert(+user); // hint: number -> 1000
alert(user + 500); // hint: default -> 1500
  1. toString/valueOf
      • 对于 "string" hint:调用 toString 方法,如果它不存在,则调用 valueOf 方法(因此,对于字符串转换,优先调用 toString)。
      • 对于其他 hint:调用 valueOf 方法,如果它不存在,则调用 toString 方法(因此,对于数学运算,优先调用 valueOf 方法)。
      • toString 方法返回一个字符串 "[object Object]"
      • valueOf 方法返回对象自身。
  2. 转换可以返回任何原始类型
    1. 所有原始转换方法,有一个重要的点需要知道,就是它们不一定会返回 “hint” 的原始值。
    2. 这些方法必须返回一个原始值,而不是对象。
  3. 进一步的转换
    1. 如果我们将对象作为参数传递
      1. 对象被转换为原始值(通过前面我们描述的规则)。
      2. 如果还需要进一步计算,则生成的原始值会被进一步转换。
JavaScript
	let obj = {
	  // toString 在没有其他方法的情况下处理所有转换
	  toString() {
	    return "2";
	  }
	};
	
	alert(obj * 2); // 4,对象被转换为原始值字符串 "2",之后它被乘法转换为数字 2。