Object 基础知识

对象

相比于其余7种原始类型只包含一种东西(字符串/数字等),对象用来存储键值对和更复杂的实体信息。

对象创建

创建空对象:

// “构造函数” 的语法
let user = new Object(); 
// “字面量” 的语法
let user = {};  

创建带有属性的对象:

let user = {     // 一个对象
  name: "John",  // 键 "name",值 "John"
  age: 30,       // 键 "age",值 30
};

// 访问属性
console.info( user.name ); // John
console.info( user.age ); // 30
// 或者
console.info( user["age"] ); // 30

// 动态添加属性
user.isAdmin = true;
console.info( user.isAdmin ); // true

// 移除属性
delete user.age;
// 或者
delete user["age"];

// 若key是赋值得到,那么 "." 运算符将不再适用
let key = "name";
// 只能使用[]
console.info( user[key] ); // John

对象属性 key

计算属性

let fruit = "apple";

let bag = {
  [fruit]: 5, // 属性名是从 fruit 变量中得到的
  // 或者更加复杂
  [fruit + 'Computers']: 5
};

console.info( bag.apple ); // 5
console.info( bag.appleComputers ); // 5

对象属性名称不受保留字限制(但不推荐)

let obj = {
  for: 1,
  let: 2,
  return: 3
};

console.info( obj.for + obj.let + obj.return );  // 6

对象属性键只能为字符串

会自动进行转换,无论哪种非字符串类型。

let obj = {
  0: "test" // 等同于 "0": "test"
};

// 都会输出相同的属性(数字 0 被转为字符串 "0")
// test
console.info( obj["0"] ); 
// test (相同的属性)
console.info( obj[0] ); 

对象遍历操作

in 属性存在操作符

能够很好地区分属性存在,但存储值为undefined的情况。

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

// true,user.age 存在
let key = "age";
console.info( key in user ); 
// false,user.blabla 不存在。
console.info( "blabla" in user ); 

for..in 循环

for(;;) 循环本质上完全不一样。

对象内部属性存在排序:

  • 整数属性会被进行排序。

    “整数属性”指的是一个可以在不做任何更改的情况下与一个整数进行相互转换的字符串。

  • 其他属性则按照创建的顺序显示。
let codes = {
    name: "John",
    "49": "Germany",
    "41": "Switzerland",
    "44": "Great Britain",
    // ..,
    "1": "USA",
    surname: "Smith",
};

for(let code in codes) {
    //1, 41, 44, 49, name, surname
    console.info(code); 
}

对于不想被默认排序的整数属性,可以写成非整数属性名称来解决:

let codes = {
    "+49": "Germany",
    "+41": "Switzerland",
    "+44": "Great Britain",
    // ..,
    "+1": "USA"
};

for (let code in codes) {
    // 49, 41, 44, 1
    console.info( +code ); 
}

引用与复制

原始类型-复制

字符串、数字、布尔值等 —— 总是“作为一个整体”复制。

let message = "Hello!";
let phrase = message;

messagephrase两个变量分别存储了一份"Hello!"

对象-引用

当一个对象变量被复制 —— 引用被复制,而该对象自身并没有被复制。

let user = { name: "John" };
// 对象的引用被复制
let admin = user; 
// 通过 "admin" 引用来修改
admin.name = 'Pete'; 
// 'Pete',修改能通过 "user" 引用看到
console.info(user.name);

// 对象比较
let userCopy = { name: "John" };

console.info(user == admin); // true
console.info(user === admin); // true
console.info(user == userCopy); // false

克隆与合并

浅层拷贝

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

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

// 将 user 中所有的属性拷贝到其中
// 若属性为对象引用,则无法进行深层次拷贝
for (let key in user) {
  clone[key] = user[key];
}

// 现在 clone 是带有相同内容的完全独立的对象
// 改变了其中的数据
clone.name = "Pete"; 
// 原来的对象中的 name 属性依然是 John
console.info( user.name ); 

内建方法实现:

Object.assign(dest, [src1, src2, src3...])
  • 参数 dest 是指目标对象。
  • 参数 src1, ..., srcN(可按需传递多个参数)是源对象。

该方法将所有源对象的属性拷贝到目标对象 dest 中。换句话说,从第二个开始的所有参数的属性都被拷贝到第一个参数的对象中。调用结果返回 dest。

let user = { name: "John" };

let permissions1 = { canView: true };
let permissions2 = { canEdit: true };

// 将 permissions1 和 permissions2 中的所有属性都拷贝到 user 中
Object.assign(user, permissions1, permissions2);

// { name: "John", canView: true, canEdit: true }
console.info( user ); 

深层拷贝
存在对象嵌套时,无法对深层对象进行克隆

可使用递归进行循环拷贝

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

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

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

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

const修饰的对象可修改

user 的值是一个常量,它必须始终引用同一个对象,但该对象的属性可以被自由修改。

const user = {
  name: "John"
};

user.name = "Pete"; // (*)

console.info(user.name); // Pete

垃圾回收

可达性

进行垃圾回收的判断标准。

“可达”值是那些以某种方式可访问或可用的值。
对外引用不重要,只有传入引用才可以使对象可达。

  1. 固有的可达值的基本集合:
    • 当前执行的函数,它的局部变量和参数。
    • 当前嵌套调用链上的其他函数、它们的局部变量和参数。
    • 全局变量。
    • 其余内部实现。

    这些值被称为 根 root

  2. 一个值可以从根通过引用或者引用链进行访问,则认为该值是可达的。

单个引用

// 一个对象,通过user进行引用
// 则此时此对象可达
let user = {
  name: "John"
};

// 此时对象将不可达
user = null;
// 引擎会自动进行回收

多个引用

// 一个对象,通过user进行引用
// 则此时此对象可达
let user = {
  name: "John"
};
let admin = user;
// 此时对象仍然通过admin可达
// 引擎不会进行回收
user = null;

循环引用
应当尽量避免循环引用的发生。会导致垃圾回收无法正常完成。

function marry(man, woman) {
  woman.husband = man;
  man.wife = woman;

  return {
    father: man,
    mother: woman
  }
}

let family = marry({
  name: "John"
}, {
  name: "Ann"
});

同样,如果几个对象之间存在相互引用,但没有外部对其任意对象的引用,那也认为这些对象不可达,称为无法到达的岛屿

回收算法【标记-清除】

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

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

优化方法:

  • 分代收集(Generational collection)

    对象被分成两组:“新的”和“旧的”。在典型的代码中,许多对象的生命周期都很短:它们出现、完成它们的工作并很快死去,因此在这种情况下跟踪新对象并将其从内存中清除是有意义的。那些长期存活的对象会变得“老旧”,并且被检查的频次也会降低。

  • 增量收集(Incremental collection)

    如果有许多对象,试图一次遍历并标记整个对象集,则可能需要一些时间,并在执行过程中带来明显的延迟。因此,引擎将现有的整个对象集拆分为多个部分,然后将这些部分逐一清除。这样就会有很多小型的垃圾收集,而不是一个大型的。这需要它们之间有额外的标记来追踪变化,但是这样会带来许多微小的延迟而不是一个大的延迟。

  • 闲时收集(Idle-time collection)

    垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。

相关文章:V8 引擎-垃圾回收

对象 this

在非严格模式的情况下,this 将会是 全局对象,也就是浏览器中的 window
JavaScript 中,this 是“自由”的,它的值是在调用时计算出来的,它的值并不取决于方法声明的位置,而是取决于在“点符号前”的是什么对象。不存在绑定情况。

对象方法需要访问对象中存储的信息才能完成其工作时,使用this来代替当前对象。

this 的值是在代码运行时计算出来的,它取决于代码上下文。

let user = { name: "John" };
let admin = { name: "Admin" };

function sayHi() {
  console.info( this.name );
}

// 在两个对象中使用相同的函数
user.f = sayHi;
admin.f = sayHi;

// 这两个调用有不同的 this 值
// 函数内部的 "this" 是“点符号前面”的那个对象
user.f(); // John(this == user)
admin.f(); // Admin(this == admin)

// Admin
// 使用方括号语法来访问这个方法
// 本质上就是一个属性
admin['f'](); 

箭头函数无this
在箭头函数中引用 this,this 值取决于最近一层函数的词法环境所对应的对象。

let user = {
  firstName: "Ilya",
  sayHi() {
    let arrow = () => console.info(this.firstName);
    arrow();
  }
};

user.sayHi(); // Ilya

构造器与 new

构造函数本质就是函数,但存在约定:

  • 命名大写字母开头。
  • 只能由new进行创建。

执行new后:

  1. 新的空对象被创建并分配给 this
  2. 执行函数体,修改this,添加新属性
  3. 返回 this
function User(name) {
  // this = {};(隐式创建)

  // 添加属性到 this
  this.name = name;
  this.isAdmin = false;

  // return this;(隐式返回)
}

let user = new User("Jack");
// 等同于
let user = {
  name: "Jack",
  isAdmin: false
};

new.target

检查函数是否被使用 new 进行调用

function User() {
  console.info(new.target);
  // 通过判断自动添加new
  if (!new.target) {
    return new User(name);
  }
}

// 不带 "new":
// undefined
User(); 
// 带 "new":
// function User { ... }
new User(); 

构造器的return

规则:

  • return 的为对象,则返回对象。
  • return 的为原始类型,忽略,返回默认this。
function BigUser() {

  this.name = "John";
  // <-- 返回这个对象
  return { name: "Godzilla" };  
}
// Godzilla,得到了那个对象
console.info( new BigUser().name );  

可选链 ?.

value?.prop

  • value 存在,则结果与 value.prop 相同
  • valueundefined/null 时则返回 undefined

可选链不可滥用,只将 ?. 使用在一些东西可以不存在的地方。可选链之前的对象必须已声明。
user 对象必须存在,但 address 是可选的,应当写为user.address?.street

使用已有语法进行替换:

user.address ? user.address.street : undefined
// 或者
user.address && user.address.street && user.address.street.name

短路效应

?. 左边部分不存在,就会立即停止运算

其他变体

可选链 ?. 不是一个运算符,而是一个特殊的语法结构。

?.():用于调用一个可能不存在的函数

let userAdmin = {
  admin() {
    console.info("I am admin");
  }
};

let userGuest = {};
// I am admin
userAdmin.admin?.(); 
// 啥都没发生(没有这样的方法)
userGuest.admin?.(); 

?.[]:从一个可能不存在的对象上安全地读取属性

let key = "firstName";

let user1 = {
  firstName: "John"
};

let user2 = null;

console.info( user1?.[key] ); // John
console.info( user2?.[key] ); // undefined

安全删除

// 如果 user 存在,则删除 user.name
delete user?.name;

symbol 类型

规范定义:对象属性键只有两种类型

  • 字符串类型
  • symbol类型

symbol 值表示唯一的标识符。

// id1/2 是描述为 "id" 的 symbol
let id1 = Symbol("id");
let id2 = Symbol("id");

console.info(id1 == id2); // false

为了更好的区分,symbol类型无法隐式转换为字符串类型

// 隐式转换:
// TypeError: can't convert symbol to string
console.info(`${Symbol("12")}`);
// 显示转换
// "Symbol(12)"
console.info(String(Symbol("12")));
console.info(Symbol("12").toString());

"隐藏"属性:

代码的任何其他部分都不能意外访问或重写这些属性,因为 symbol 总是不同的,即使它们有相同的描述名称。

同时symbol 属性不参与 for..in 循环。
Object.keys(obj)也会忽略symbol 属性。

Object.assign会同时复制字符串和symbol属性

字面量symbol

let id = Symbol("id");

let user = {
  name: "John",
  [id]: 123 // 而不是 "id":123
};

全局symbol
通常所有的 symbol 都是不同的,即使它们有相同的描述名称。但应用程序的不同部分想要访问的 symbol "id" 指的是完全相同的属性时可以从全局注册表获取。

// 从全局注册表中读取
let id = Symbol.for("id"); // 如果该 symbol 不存在,则创建它

// 再次读取(可能是在代码中的另一个位置)
let idAgain = Symbol.for("id");

// 相同的 symbol
console.info( id === idAgain ); // true

通过全局 symbol 返回一个名字:

不适用于非全局 symbol
如果 symbol 不是全局的,它将无法找到它并返回 undefined。

// 通过 name 获取 symbol
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");

// 通过 symbol 获取 name
console.info( Symbol.keyFor(sym) ); // name
console.info( Symbol.keyFor(sym2) ); // id

// 全局与非全局
let globalSymbol = Symbol.for("name");
let localSymbol = Symbol("name");
// name,全局 symbol
console.info( Symbol.keyFor(globalSymbol) ); 
// undefined,非全局
console.info( Symbol.keyFor(localSymbol) ); 
// name
console.info( localSymbol.description ); 

系统symbol表:symbol表

对象原始值转换

为了进行转换,JavaScript 尝试查找并调用三个对象方法:

  1. 调用 obj[Symbol.toPrimitive](hint) —— 带有 symbolSymbol.toPrimitive(系统 symbol)的方法,如果这个方法存在的话,
  2. 否则,如果 hint 是 "string" —— 尝试调用 obj.toString()obj.valueOf(),无论哪个存在。
  3. 否则,如果 hint 是 "number" 或 "default" —— 尝试调用 obj.valueOf()obj.toString(),无论哪个存在。

toString 方法返回一个字符串 "[object Object]"

valueOf 方法返回对象自身。

只存在toString时,其将处理所有原始转换,大部分场景只实现此方法即可

类型转换中hint存在三种变体:

  • string:对象到字符串转换
  • number:对象到数字转换
  • default:当期望值类型不确定时

    二元加法可用于数字和字符串,因此会根据default进行转换

Symbol.toPrimitive

内建 symbol,被用来给转换方法命名

let user = {
  name: "John",
  money: 1000,
  // 此方法存在,则它会被用于所有 hint
  [Symbol.toPrimitive](hint) {
    console.info(`hint: ${hint}`);
    return hint == "string" ? `{name: "${this.name}"}` : this.money;
  }
};

// 转换演示:
// hint: string -> {name: "John"}
console.info(user); 
// hint: number -> 1000
console.info(+user); 
// hint: default -> 1500
console.info(user + 500); 

toStringvalueOf

仅当内建 symbol方法不存在时

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

  // 对于 hint="string"
  toString() {
    return `{name: "${this.name}"}`;
  },

  // 对于 hint="number" 或 "default"
  valueOf() {
    return this.money;
  }

};

console.info(user); // toString -> {name: "John"}
console.info(+user); // valueOf -> 1000
console.info(user + 500); // valueOf -> 1500