JS 基础-对象

303 阅读11分钟

对象

“普通对象(plain object)”,或者就叫对象。

字面量

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

文本和属性

一个对象属性由键值

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

可以通过delete 删除属性

delete user.age;

方括号

访问属性

let user = {};
user["likes birds"] = true;
alert(user["likes birds"]); // true

点符号

let user = {};
user["likes"] = true;
alert(user.likes); // true

计算属性

let fruit = prompt("Which fruit to buy?", "apple");
let bag = {
  [fruit]: 5, // 属性名是从 fruit 变量中得到的
};
alert( bag.apple ); // 5 如果 fruit="apple"

//属性是不是固定,而是随机指定的。

let fruit = prompt("Which fruit to buy?", "apple");
let bag = {};
// 从 fruit 变量中获取值
bag[fruit] = 5;

属性值简写

function makeUser(name, age) {
  return {
    name: name,
    age: age,
  };
}

简写成

function makeUser(name, age) {
  return {
    name, // 与 name: name 相同
    age,  // 与 age: age 相同
  };
}

属性名称限制

对象属性不受系统保留关键字影响。

// 这些属性都没问题
let obj = {
  for: 1,
  let: 2,
  return: 3
};
alert( obj.for + obj.let + obj.return );  // 6

甚至变量为 int 0 也可以,系统会自动转化成 "0"

let obj = {
  0: "test" // 等同于 "0": "test"
};
alert( obj["0"] ); // test
alert( obj[0] ); // test (相同的属性)
obj.0 //这个会失败

属性存在判断

  1. 直接判断 undefined
   let user = {};
   alert( user.aaa === undefined ); // true 意思是没有这个属性.
  1. in 操作符
    let user = { name: "John", age: 30 };
    alert( "age" in user ); // true,user.age 存在
    alert( "blabla" in user ); // false,user.blabla 不存在。
    
    //可以是变量判断
    let user = { age: 30 };
    let key = "age";
    alert( key in user ); // true,属性 "age" 存在
    

注意 如果刚好定义的属性是 undefined 那么 直接判断undefined 和 in 都会返回true

for in 遍历所有键

let user = {
  name: "John",
  age: 30,
  isAdmin: true
};
for (let key in user) {
  // keys
  alert( key );  // name, age, isAdmin
  // 属性键的值
  alert( user[key] ); // John, 30, true
}

属性遍历排序

  • 整数属性会被进行排序
  • 其他属性则按照创建的顺序显示 根据 整数类型排序
let codes = {
  "49": "Germany",
  "41": "Switzerland",
  "44": "Great Britain",
  // ..,
  "1": "USA"
};
for(let code in codes) {
  alert(code); // 1, 41, 44, 49
}

根据创建顺序

let user = {
  name: "John",
  surname: "Smith"
};
user.age = 25; // 增加一个
// 非整数属性是按照创建的顺序来排列的
for (let prop in user) {
  alert( prop ); // name, surname, age
}

总结

对象是具有一些特殊特性的关联数组。 它们存储属性(键值对),其中:

  • 属性的键必须是字符串或者 symbol(通常是字符串)。
  • 值可以是任何类型。 我们可以用下面的方法访问属性:
  • 点符号: obj.property
  • 方括号 obj["property"],方括号允许从变量中获取键,例如 obj[varWithKey]。 其他操作:
  • 删除属性:delete obj.prop
  • 检查是否存在给定键的属性:"key" in obj
  • 遍历对象:for(let key in obj) 循环。

对象引用和复制

对象是通过引用地址,进行存储和复制,而原始类型,则是直接保存实际的字符串,数字或者ture false。

对象复制

只是简单进行引用复制,所有关联的东西还是同一个对象

let user = { name: "John" }; 
let admin = user; // 复制引用
//这时候操作 user 和 admin是等价的

引用比较

let a = {};
let b = a; // 复制引用 
alert( a == b ); // true,都引用同一对象 
alert( a === b ); // true
//通过字面量定义的对象是两个独立的引用地址
let a = {}; 
let b = {}; // 两个独立的对象 
alert( a == b ); // false

对象复制/合并

通过遍历键值 循环赋值

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.assgin

Object.assign(dest, [src1, src2, src3...]) 合并多个属性

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 }

Object.assign 克隆一个对象

let user = {
  name: "John",
  age: 30
};
let clone = Object.assign({}, user); 

注意 有可能复制的对象嵌套对象,这时候就需要深度拷贝。

垃圾回收

可达性

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

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

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

场景1

// user  是 根往下引用的对象
let user = {
  name: "John"
};

user = null;// 当设置null 时候, 原来的 { name: "John"} 对象已经没人指向,则会被回收

场景2 互相关联

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

  return {
    father: man,
    mother: woman
  }
}

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

image.png

//当执行下面代码时候

delete family.father;
delete family.mother.husband;

下面结果

image.png

由于 John 已经没有被引用,虽然他指向 Ann,但是垃圾回收原则是以 是否可达标准。 所以 John 会被回收哦。

  • 同理 则 family = null;
  • 则所有对象都会被回收。

回收的算法

mark-and-sweep 垃圾收集器是 通过定期 标记和清除 来实现 优化方案:

  • 分代收集
  • 增量收集
  • 闲时收集

总结

主要需要掌握的内容:

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

对象方法

通过函数表达式赋值给对象属性的函数被称为 方法

let user = {
  name: "John",
  age: 30
};
//方式1
user.sayHi = function() {
  alert("Hello!");
};
user.sayHi(); // Hello!
//方式2 也可以先声明后赋值  
function sayHi() {
  alert("Hello!");
};
user.sayHi = sayHi;

方法简写

//方式1
user = {
  sayHi: function() {
    alert("Hello");
  }
};
//方式2 
let user = {
  sayHi() { 
    alert("Hello");
  }
};

方法中"this"

this 是 调用该方法的对象。即“点符号前”的是什么对象

let user = {
  name: "John",
  age: 30,
  sayHi() { 
    alert(this.name);//当前对象 
     //等价于  
    alert(user.name); 
  } 
};

this 可以用于任何函数,即使它不是对象的方法。

function sayHi() {  alert( this); }//这里不报错 ,输出 window
  • 严格模式下的 this 值为 undefined
  • 非严格模式下的 this 值为 window this 通过代码上下文决定值的内容。是运行时计算出来的,
let user = { name: "John" };
let admin = { name: "Admin" };
function sayHi() {
  alert( this.name );
} 
user.f = sayHi;
admin.f = sayHi; 
user.f(); // John(this == user)
admin.f(); // Admin(this == admin)

箭头函数没有this

let user = {
  a: "a",
  sayHi() {
    let fn = () => alert(this.a);
    fn();
  }
};
user.sayHi(); // a
  • this 的值是在程序运行时得到的。
  • 一个函数在声明时,可能就使用了 this,但是这个 this 只有在函数被调用时才会有值。
  • 可以在对象之间复制函数。

构造函数

通过构造函数创建对象,等价于字面量{...}

  1. 建议命名:以大写字母开头。
  2. 它们只能由 "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 来运行 构造器主要实现:可重用的对象创建代码

new function() { … }

  • 匿名立即执行函数
  • 用于创建单个复杂对象的代码,
let user = new function() {
  this.name = "John";
  this.isAdmin = false;
};

构造器模式测试:new.target

function User() {
  alert(new.target);
}
// 不带 "new":
User(); // undefined 这里只是调用了一下方法
// 带 "new":
new User(); // function User { ... } 这里是实例了一个对象

构造器的 return

  • 如果 return 返回的是一个对象,则返回这个对象,而不是 this
function BigUser() {
  this.name = "aaa";
  return { name: "bbb" };  // <-- 返回这个对象
}
alert( new BigUser().name );  // aaa,得到了那个对象
//不返回,正常返回
function SmallUser() {
  this.name = "John";
  return; // <-- 返回 this
}
alert( new SmallUser().name );  // John

省略括号

let user = new User; // <-- 没有参数
// 等同于
let user = new User();

构造器中的方法

function User(name) {
  this.name = name;
  this.sayHi = function() {
    alert( "My name is: " + this.name );
  };
}
let john = new User("John");
john.sayHi(); // My name is: John

可选链 "?."

如果 ?. 左边部分不存在,就会立即停止运算(“短路效应”)

let user = {}; // user 没有 address 属性
alert( user?.address?.street ); // undefined(不报错)

let user = null;
alert( user?.address ); // undefined
alert( user?.address.street ); // undefined

?. 前的变量必须已声明

// ReferenceError: user is not defined
user?.address;

?.() 方法调用存在判断

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

?.[] 属性访问存在判断

let user1 = {
  firstName: "John"
};
let user2 = null; // 假设,我们不能授权此用户
let key = "firstName";
alert( user1?.[key] ); // John
alert( user2?.[key] ); // undefined
alert( user1?.[key]?.something?.not?.existing); // undefined
//优化删除属性
delete user?.name;

?. 不能用于赋值

let user = null;
user?.name = "John"; // Error,不起作用
// 因为它在计算的是 undefined = "John"

总结

可选链 ?. 语法有三种形式:

  1. obj?.prop —— 如果 obj 存在则返回 obj.prop,否则返回 undefined
  2. obj?.[prop] —— 如果 obj 存在则返回 obj[prop],否则返回 undefined
  3. obj.method?.() —— 如果 obj.method 存在则调用 obj.method(),否则返回 undefined

Symbol 类型

  • 对象的属性键只能是字符串类型或者 Symbol 类型
  • “Symbol” 值表示唯一的标识符。用于隐式保护变量,有点像java里面的private修饰符概念。
  • 优势在于,当使用第三方库,自己想定义新的属性, 如果原来的第三方库有for..in,则不会遍历出来,不影响原来的代码。
// id 是 symbol 的一个实例化对象
let id = Symbol();
// id 是描述为 "id" 的 Symbol
let id = Symbol("id");

Symbol 不会被自动转换为字符串

let id = Symbol("id");
alert(id); // 类型错误:无法将 Symbol 值转换为字符串。
//应该使用.description 或者 .toString()
let id = Symbol("id");
alert(id.description); // id


let id2 = Symbol("id");
let id3 = Symbol("id");
id2 === id3 // false 每次都是独立的唯一值,id字符串只是用于标识数据

可以动态设置属性,同时不允许直接 user.id 方式访问,保护变量,只能通过user[id]访问

let user = { // 属于另一个代码
  name: "John"
};
let id = Symbol("id");
user[id] = 1;
alert( user[id] ); // 我们可以使用 Symbol 作为键来访问数据

对象字面量中的 Symbol

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

在 for…in 中会被跳过

Symbol 属性不参与 for..in 循环。也不支持 Object.keys(user)

let id = Symbol("id");
let user = {
  name: "John",
  age: 30,
  [id]: 123
};
for (let key in user) alert(key); // name, age (no symbols)
// 使用 Symbol 任务直接访问
alert( "Direct: " + user[id] );

Object.assign可以复制相关信息

let id = Symbol("id");
let user = {
  [id]: 123
};
let clone = Object.assign({}, user);
alert( clone[id] ); // 123

全局 symbol

全局访问,统一保存在注册表里。统一管理

// 从全局注册表中读取
let id = Symbol.for("id"); // 如果该 Symbol 不存在,则创建它
// 再次读取(可能是在代码中的另一个位置)
let idAgain = Symbol.for("id");
// 相同的 Symbol
alert( id === idAgain ); // true

注意 使用 Symbol("id") === Symbol.for("id") 永远不等,因为Symbol("id")每次都是唯一值。

Symbol.keyFor

只要是全局的symbol,可以根据symbol获取name

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

非全局symbol 可以使用description访问name

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

对象 — 原始值转换

由许多期望以原始值作为值的内建函数和运算符自动调用的。

这里有三种类型(hint):

  • "string"(对于 alert 和其他需要字符串的操作)
  • "number"(对于数学运算)
  • "default"(少数运算符)

调用逻辑按

  1. objSymbol.toPrimitive
  2. hint 是 string 调用 obj.toString()obj.valueOf()
  3. hint 是 number/default 调用 obj.valueOf()obj.toString()

ToPrimitive

Symbol.toPrimitive的内建 symbol,它被用来给转换方法命名

obj[Symbol.toPrimitive] = function(hint) {
  // 返回一个原始值
  // hint = "string"、"number" 和 "default" 中的一个
} 
//例子 
let user = {
  name: "John",
  money: 1000, 
  [Symbol.toPrimitive](hint) {
    alert(hint);  
  }
};
// 转换演示:
alert(user); // string
alert(+user); // number
alert(user + 500); // default

toString/valueOf

如果没有 `Symbol.toPrimitive`,那么 JavaScript 将尝试找到它们,并且按照下面的顺序进行尝试:
  • 对于 “string” hint,toString -> valueOf
  • 其他情况,valueOf -> toString
let user = {
  name: "John",
  money: 1000,
  // 对于 hint="string"
  toString() {
    return "string"
  },
  // 对于 hint="number" 或 "default"
  valueOf() {
    return "number/default"
  }
};
alert(user); // string
alert(+user); // number/default
alert(user + 500); // number/default

只保留一种处理方式,那就用toString

let user = {
  name: "John",
  toString() {
    return this.name;
  }
};
alert(user); // toString -> John
alert(user + 500); // toString -> John500

注意 返回值 :必须返回一个原始值,而不是对象。

高阶使用:

let obj = {
  // toString 在没有其他方法的情况下处理所有转换
  toString() {
    return "2";
  }
};
alert(obj * 2); // 4,对象被转换为原始值字符串 "2",之后它被乘法转换为数字 2。