# 面向对象-对象基础

# 对象

# 对象的定义

ECMA-262 将对象定义为一组属性的无序集合。严格来说,这意味着对象就是一组没有特定顺序的值。对象的每个属性或方法都由一个名称来标识,这个名称映射到一个值。正因为如此(以及其他还未讨论的原因),可以把 ECMAScript 的对象想象成一张散列表,其中的内容就是一组名/值对,值可以是数据或者函数。

# 对象的属性

属性分两种:数据属性和访问器属性

数据属性包含一个保存数据值的位置

  • Configurable 是否可以通过 delete 删除并重新定义
  • Enumerable 是否可以通过 for-in 循环
  • Writable 其值是否可以被修改
  • Value 属性实际的值

访问器属性不包含数值,它包含一个 getter 函数和一个 setter 函数

  • Configureable
  • Enumerable
  • Get
  • Set

# 增强的对象语法

    1. 属性值简写
    let name = "gauss";
    let person = {
      name
    };
    
    1. 方法名简写
    let person = {
      sayName(name) {
        console.log(`My name is ${name}`);
      }
    };
    
    1. 可计算属性
    const nameKey = "name";
    const ageKey = "age";
    const jobKey = "job";
    let person = {
      [nameKey]: "gauss",
      [ageKey]: 24,
      [jobKey]: "Software Engineer"
    };
    
    console.log(person); // { name: 'gauss', age: 24, job: 'Software Engineer' }
    

# 对象解构

ES6 新增了对象解构语法,可以在一条语句中使用过嵌套数据实现一个或多个赋值操作,简单地说,对象解构就是使用与对象匹配的解构来实现对象属性赋值。

// 不使用对象解构
let person = {
  name: "matt",
  age: 27
};
let personName = person.name;
let personAge = person.age;
console.log(personName, personAge); /// matt 27
// 使用对象解构
let person = {
  name: "matt",
  age: 27
};
let { name: personName, age: personAge } = person;
console.log(personName, personAge); /// matt 27
// 简写
let person = {
  name: "matt",
  age: 27
};
let { name, age } = person;
console.log(name, age); /// matt 27
// 不存在
let person = {
  name: "matt",
  age: 27
};
let { name, age, job } = person;
console.log(name, age, job); /// matt 27 undefined
// 默认值
let person = {
  name: "matt",
  age: 27
};
let { name, age, job = "Software Engineer" } = person;
console.log(name, age, job); /// matt 27 Software Engineer

TIP

解构在内部使用函数ToObject()将源数据转换为对象。

let { length } = "foobar";
console.log("foobar".length); // 6
console.log(length); // 6

# 对象引用和复制

与原始类型相比,对象的根本区别之一是对象是“通过引用”被存储和复制的,与原始类型值相反:字符串,数字,布尔值等 —— 始终是以“整体值”的形式被复制的。

let user = { name: "John" };

let admin = user;

admin.name = "Pete"; // 通过 "admin" 引用来修改

alert(user.name); // 'Pete',修改能通过 "user" 引用看到

# 通过引用来比较

仅当两个对象为同一对象时,两者才相等。

let a = {};
let b = a;
let c = {};
console.log(a == b); // true
console.log(a == c); // false

# 对象专用相等判定

ES6 规范新增了Object.is(),这个方法与===很像,但考虑到了上述边界情况

console.log(Object.is(+0, -0)); // fasle
console.log(Object.is(+0, 0)); // true
console.log(Object.is(-0, 0)); // false
console.log(Object.is(NaN, NaN)); // true

# 合并对象

ECMAScript6 专门为合并对象提供了Object.assign()方法,这个方法接收一个目标对象和一个或多个源对象作为参数。 Object.assign()实际上对每个源对象执行的是浅复制,如果多个源对象有相同的属性,则使用最后一个复制的值

let dest = { id: "dest" };
let src1 = { id: "src1", a: "foo", b: "bob" };
let src2 = { id: "src2", a: "foo", b: "bar", c: "cat" };
let result = Object.assign(dest, src1, src2);
console.log(JSON.stringify(result));
// {"id":"src2","a":"foo","b":"bar","c":"cat"}

# 深度克隆

到现在为止,我们都假设 user 的所有属性均为原始类型。但属性可以是对其他对象的引用。那应该怎样处理它们呢?

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++; // 通过其中一个改变属性值

为了解决此问题,我们应该使用会检查每个 user[key] 的值的克隆循环,如果值是一个对象,那么也要复制它的结构。这就叫“深拷贝”。

# 垃圾回收

对于开发者来说,JavaScript 的内存管理是自动的、无形的。我们创建的原始值、对象、函数……这一切都会占用内存。

当我们不再需要某个东西时会发生什么?JavaScript 引擎如何发现它并清理它?

# 可达性(Reachability)

JavaScript 中主要的内存管理概念是 可达性。

简而言之,“可达”值是那些以某种方式可访问或可用的值。它们一定是存储在内存中的。

  1. 固有的可达值的基本集合,这些值被称作 根(roots)。
    1. 当前函数的局部变量和参数。
    2. 嵌套调用时,当前调用链上所有函数的变量与参数。
    3. 全局变量。
  2. 如果一个值可以通过引用或引用链从根访问任何其他值,则认为该值是可达的

在 JavaScript 引擎中有一个被称作 垃圾回收器 的东西在后台执行。它监控着所有对象的状态,并删除掉那些已经不可达的。

# 一个简单的例子

现在让我们通过一个简单的例子来验证第二点

// user 具有对这个对象的引用
let user = {
  name: "John"
};

如果 user 的值被重写了,这个引用就没了

user = null;

如果我们先把 user 的引用复制给 admin,再执行刚刚的那个操作

let admin = user;
user = null;

然后对象仍然可以被通过 admin 这个全局变量访问到,所以对象还在内存中。如果我们又重写了 admin,对象就会被删除。

# 一个复杂的例子

现在来看一个更复杂的例子。这是个家庭:

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;

经过垃圾回收:

let body = $$("body")[0];
let style = { background: "red", color: "blue" };
for (key in style) {
  body.style[key] = style[key];
}

# 无法到达的岛屿

几个对象相互引用,但外部没有对其任意对象的引用,这些对象也可能是不可达的,并被从内存中删除。

源对象与上面相同。然后:

family = null;

内存内部状态将变成:

# 内部算法

垃圾回收的基本算法被称为 “mark-and-sweep”。

定期执行以下“垃圾回收”步骤:

  • 垃圾收集器找到所有的根,并“标记”(记住)它们。
  • 然后它遍历并“标记”来自它们的所有引用。
  • 然后它遍历标记的对象并标记 它们的 引用。所有被遍历到的对象都会被记住,以免将来再次遍历到同一个对象。
  • ……如此操作,直到所有可达的(从根部)引用都被访问到。
  • 没有被标记的对象都会被删除。

这是垃圾收集工作的概念。JavaScript 引擎做了许多优化,使垃圾回收运行速度更快,并且不影响正常代码运行。

# 总结

主要需要掌握的内容:

  • 垃圾回收是自动完成的,我们不能强制执行或是阻止执行。
  • 当对象是可达状态时,它一定是存在于内存中的。
  • 被引用与可访问(从一个根)不同:一组相互连接的对象可能整体都不可达。