JavaScript对象学习笔记[Web开发]


JavaScript对象

本文为学习笔记,参考Javascript.info,菜鸟等文章编写。

跳转连接

  1. JavaScript基础知识学习笔记[Web开发]
  2. JavaScript对象学习笔记[Web开发]
  3. JavaScript数据类型学习笔记[Web开发]
  4. JavaScript函数学习笔记[Web开发]

Object(对象)

对象基础

在 JavaScript 中,对象几乎渗透到了这门编程语言的方方面面。

两种语法创建一个空的对象(“空柜子”):通常用花括号。这种方式我们叫做 字面量

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

属性有键(或者也可以叫做“名字”或“标识符”),位于冒号 ":" 的前面,值在冒号的右边。

可以使用符号访问属性值,随时添加、删除和读取属性。

// 可以在创建对象的时候,立即将一些属性以键值对的形式放到 `{...}` 中。
let user = {     // 一个对象
	name: "John",  // 键 "name",值 "John"
	age: 30,        // 键 "age",值 30
	"likes birds": true,  // 多词属性名必须加引号
};

// 读取文件的属性:
alert( user.name ); // John
alert( user.age ); // 30

//增加对象的属性
user.isAdmin = true;

//用 `delete` 操作符移除属性
delete user.age;

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

根据规范,只有两种原始类型可以用作对象属性键:

  • 字符串类型
  • symbol 类型

否则,如果使用另一种类型,例如数字,它会被自动转换为字符串。所以 obj[1]obj["1"] 相同,而 obj[true]obj["true"] 相同。

多词属性

可以用多字词语来作为属性名,但必须给它们加上引号

let user = {     // 一个对象
	"likes birds": true,  // 多词属性名必须加引号
};

对于多词属性,点符号不可访问。点符号要求 key 是有效的变量标识符。这意味着:不包含空格,不以数字开头,也不包含特殊字符(允许使用 $ 和 _)。

需要使用方括号访问多词属性

user.likes birds = true// 错误
// JavaScript 理解不了。它认为我们在处理 user.likes,然后在遇到意外的 birds 时给出了语法错误。

user["likes birds"] = true; //正确

//或者
let key = "likes birds";
user[key] = true;
计算属性

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

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

[fruit] 含义是属性名应该从 fruit 变量中获取。

属性值简写

可以把属性名简写方式和正常方式混用

在实际开发中,我们通常用已存在的变量当做属性名。

function makeUser(name, age) {
return {
name: name,
age: age,
// ……其他的属性
};
}
let user = makeUser("John", 30);
alert(user.name); // John

在上面的例子中,属性名跟变量名一样。这种通过变量生成属性的应用场景很常见,在这有一种特殊的 属性值缩写 方法,使属性名变得更短。

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

注意:一个名为 __proto__ 的属性不能被设置为一个非对象的值。

“in” 操作符:属性存在测试

JavaScript 的对象能够被访问任何属性。即使属性不存在也不会报错!

读取不存在的属性只会得到 undefined

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

let user = {};
alert( user.noSuchProperty === undefined ); // true 意思是没有这个属性

检查属性是否存在的操作符 "in"

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

alert( "age" in user ); // true,user.age 存在
alert( "blabla" in user ); // false,user.blabla 不存在。

let key = "age";
alert( key in user ); // true,user.age 存在

大部分情况下与 undefined 进行比较来判断即可。

但有一个例外情况:该属性存在,但存储的值是 undefined 的时候。

这时候只有 in 运算符的判断结果是对的。

对象属性排序

遍历一个对象,获取属性的顺序:整数属性会被进行排序,其他属性则按照创建的顺序显示。

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
}

为了解决电话号码的问题,我们可以使用非整数属性名来 欺骗 程序。只需要给每个键名加一个加号 "+" 前缀就行了。

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

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

对象引用和复制

对象存储

对象是“通过引用”存储和复制的。对象的变量存储的不是对象本身,而是该对象“在内存中的地址”

通过引用来比较:仅当两个对象为同一对象时,两者才相等。

let user = { name: 'John' };

let admin = user;

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

alert(user.name); // 'Pete',修改能通过 "user" 引用看到
合并对象
// 语法
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);

Object.assign(user, { name: "Pete" }); //被拷贝的属性的属性名已经存在,那么它会被覆盖
alert(user.name); // 现在 user = { name: "Pete" }

克隆对象

for...in克隆对象

// 克隆测试
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克隆对象

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

let clone = Object.assign({}, user);
深层克隆

对象的属性属性可以是对其他对象的引用。普通克隆的时候引用也会直接传递到另一个对象,导致两个对象共用一个引用属性。

为了解决这个问题,并让 克隆对象 与 被克隆对象 成为两个真正独立的对象,我们应该使用一个拷贝循环来检查每个值,如果它是一个对象,那么也复制它的结构。这就是所谓的“深拷贝”。

我们可以使用递归来实现它。或者为了不重复造轮子,采用现有的实现,例如 lodash 库的 _.cloneDeep(obj)

// 此函数只能处理非数组、非继承情况下的对象深克隆
let ObjDeepClone = function(obj) {
    let clone = {};
    for (let key in obj) {
            clone[key] = (typeof obj[key]==="object") ? ObjDeepClone(obj[key]) : obj[key];
    }
    return clone;
}

构造器和操作符 “new”

构造函数

构造函数在技术上是常规函数。不过有两个约定:

  1. 它们的命名以大写字母开头。
  2. 它们只能由 "new" 操作符来执行。
function User(name) {
  this.name = name;
  this.isAdmin = false;
}

let user = new User("Jack");

alert(user.name); // Jack
alert(user.isAdmin); // false
new

常规的 {...} 语法允许创建一个对象。但是我们经常需要创建很多类似的对象,例如多个用户或菜单项等。

这可以使用构造函数和 "new" 操作符来实现。

当一个函数被使用 new 操作符执行时,它按照以下步骤:

  1. 一个新的空对象被创建并分配给 this
  2. 函数体执行。通常它会修改 this,为其添加新的属性。
  3. 返回 this 的值。
换句话说,new User(...) 做的就是类似的事情:

function User(name) {
  // this = {};(隐式创建)

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

  // return this;(隐式返回)
}
所以 new User("Jack") 的结果是相同的对象:

let user = {
  name: "Jack",
  isAdmin: false
};
new function() { … }

如果我们有许多行用于创建单个复杂对象的代码,我们可以将它们封装在一个立即调用的构造函数中,像这样:

// 创建一个函数并立即使用 new 调用它
let user = new function() {
  this.name = "John";
  this.isAdmin = false;

  // ……用于用户创建的其他代码
  // 也许是复杂的逻辑和语句
  // 局部变量等
};

这个构造函数不能被再次调用,因为它不保存在任何地方,只是被创建和调用。因此,这个技巧旨在封装构建单个对象的代码,而无需将来重用。

构造器模式测试:new.target

在一个函数内部,我们可以使用 new.target 属性来检查它是否被使用 new 进行调用了。

对于常规调用,它为 undefined,对于使用 new 的调用,则等于该函数

function User() {
  alert(new.target);
}

// 不带 "new":
User(); // undefined

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

我们也可以让 new 调用和常规调用做相同的工作,像这样:

function User(name) {
if (!new.target) { // 如果你没有通过 new 运行我
return new User(name); // ……我会给你添加 new
}

this.name = name;
}

let john = User("John"); // 将调用重定向到新用户
alert(john.name); // John

这种方法有时被用在库中以使语法更加灵活。这样人们在调用函数时,无论是否使用了 new,程序都能工作。

不过,到处都使用它并不是一件好事,因为省略了 new 使得很难观察到代码中正在发生什么。而通过 new 我们都可以知道这创建了一个新对象。

构造器的 return

通常,构造器没有 return 语句。它们的任务是将所有必要的东西写入 this,并自动转换为结果。

但是,如果这有一个 return 语句,那么规则就简单了:

  • 如果 return 返回的是一个对象,则返回这个对象,而不是 this
  • 如果 return 返回的是一个原始类型,则忽略。

换句话说,带有对象的 return 返回该对象,在所有其他情况下返回 this

例如,这里 return 通过返回一个对象覆盖 this

function BigUser() {

this.name = "John";

return { name: "Godzilla" };  // <-- 返回这个对象
}

alert( new BigUser().name );  // Godzilla,得到了那个对象

这里有一个 return 为空的例子(或者我们可以在它之后放置一个原始类型,没有什么影响):

function SmallUser() {

this.name = "John";

return; // <-- 返回 this
}

alert( new SmallUser().name );  // John
省略括号

如果没有参数,我们可以省略 new 后的括号:

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

这里省略括号不被认为是一种“好风格”,但是规范允许使用该语法。

构造器中的方法

例如,下面的 new User(name) 用给定的 name 和方法 sayHi 创建了一个对象:

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

可选链 “?.”

“不存在的属性”的问题

举例说明,假设我们有很多个 user 对象,其中存储了我们的用户数据。

我们大多数用户的地址都存储在 user.address 中,街道地址存储在 user.address.street 中,但有些用户没有提供这些信息。

在这种情况下,当我们尝试获取 user.address.street,而该用户恰好没提供地址信息,我们则会收到一个错误:

let user = {}; // 一个没有 "address" 属性的 user 对象

alert(user.address.street); // Error!

但是在很多实际场景中,我们更希望得到的是 undefined(表示没有 street 属性)而不是一个错误。

普通解决办法

①访问该值的属性之前,使用 if 或条件运算符 ? 对该值进行检查

let user = {};
alert(user.address ? user.address.street : undefined);

这样可以,这里就不会出现错误了……但是不够优雅。就像看到的,"user.address" 在代码中出现了两次。

对于嵌套层次更深的属性,代码会变得更丑,因为需要更多的重复。

②使用 && 运算符

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

user.address 被重复写了三遍。

③使用可选链

可选链

如果可选链 ?. 前面的值为 undefined 或者 null,它会停止运算并返回 undefined

下面这是一种使用 ?. 安全地访问 user.address.street 的方式:

let user = {}; // user 没有 address 属性
alert( user?.address?.street ); // undefined(不报错)
//如果未声明变量 user,那么 user?.anything 会触发一个错误
user?.address;// ReferenceError: user is not defined

请注意:?. 语法使其前面的值成为可选值,但不会对其后面的起作用。

不要过度使用可选链

我们应该只将 ?. 使用在一些东西可以不存在的地方。

例如,如果根据我们的代码逻辑,user 对象必须存在,但 address 是可选的,那么我们应该这样写 user.address?.street,而不是这样 user?.address?.street

那么,如果 user 恰巧为 undefined,我们会看到一个编程错误并修复它。否则,如果我们滥用 ?.,会导致代码中的错误在不应该被消除的地方消除了,这会导致调试更加困难。

其他变体

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

let userAdmin = {
  admin() {
    alert("I am admin");
  }
};

let userGuest = {};

userAdmin.admin?.(); // I am admin

userGuest.admin?.(); // 啥都没发生(没有这样的方法)

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

let key = "firstName";

let user1 = {
  firstName: "John"
};

let user2 = null;

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

?.delete 一起使用

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

symbol 类型

“symbol” 值表示唯一的标识符,是带有可选描述的“原始唯一值”。

symbol生成的值作为属性或者方法的时候,一定要保存下来,否则后续无法使用

可以使用 Symbol() 来创建这种类型的值:

let i = Symbol();
let id = Symbol("id");// id 是描述为 "id" 的 symbol

如果我们要在对象字面量 {...} 中使用 symbol,则需要使用方括号把它括起来。

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

symbol 保证是唯一的。即使我们创建了许多具有相同描述的 symbol,它们的值也是不同。描述只是一个标签,不影响任何东西。

let id1 = Symbol("id");
let id2 = Symbol("id");

alert(id1 == id2); // false
"隐藏"属性

symbol 允许我们创建对象的“隐藏”属性,代码的任何其他部分都不能意外访问或重写这些属性。

//后面的括号可以给symbol做上标记便于识别
let name=Symbol('name');
let say=Symbol('say');
let obj= {
  //如果想 使用变量作为对象属性的名称,必须加上中括号,.运算符后面跟着的都是字符串
  [name]: 'lnj',
  [say]: function () {
    console.log('say')
  }
}
obj.name='it666';	// 以字符串为键的形式创建属性,没有覆盖 lnj
obj[Symbol('name')]='it666' // 以新的symbol形式生成属性,依然没有覆盖 lnj

//运行完成之后,obj内的属性如下
//{ name :"it666", Symbol(name):"lnj", Symbol(name):"it666", Symbol(say): f} }
symbol 不会被自动转换为字符串

JavaScript 中的大多数值都支持字符串的隐式转换。例如,我们可以 alert 任何值,都可以生效。symbol 比较特殊,它不会被自动转换。

这是一种防止混乱的“语言保护”,因为字符串和 symbol 有本质上的不同,不应该意外地将它们转换成另一个。

如果我们真的想显示一个 symbol,我们需要在它上面调用 .toString(),如下所示:

let id = Symbol("id");
alert(id.toString()); // Symbol(id),现在它有效了

或者获取 symbol.description 属性,只显示描述(description):

let id = Symbol("id");
alert(id.description); // id
使用 Symbol() 作为键的好处

由于 user 对象属于另一个代码库,所以向它们添加字段是不安全的,因为我们可能会影响代码库中的其他预定义行为。但 symbol 属性不会被意外访问到。第三方代码不会知道新定义的 symbol,因此将 symbol 添加到 user 对象是安全的。

标识符之间不会有冲突,因为 symbol 总是不同的,即使它们有相同的名字。

但如果使用字符串 而不是用 symbol,那么 就会 出现冲突:

let user = { name: "John" };
// 我们的脚本使用了 "id" 属性。
user.id = "Our id value";
// ……另一个脚本也想将 "id" 用于它的目的……
user.id = "Their id value"
// 无意中被另一个脚本重写了 id!
symbol 在 特定情况 会被忽略

symbol 属性不参与 for..in 循环。

Object.keys(user) 也会忽略它们。这是一般“隐藏符号属性”原则的一部分。如果另一个脚本或库遍历我们的对象,它不会意外地访问到符号属性。

相反,Object.assign 会同时复制字符串和 symbol 属性:这里并不矛盾,就是这样设计的。这里的想法是当我们克隆或者合并一个 object 时,通常希望 所有 属性被复制(包括像 id 这样的 symbol)。

全局 symbol

有时我们想要名字相同的 symbol 具有相同的实体。例如,应用程序的不同部分想要访问的 symbol "id" 指的是完全相同的属性。

为了实现这一点,这里有一个 全局 symbol 注册表。我们可以在其中创建 symbol 并在稍后访问它们,它可以确保每次访问相同名字的 symbol 时,返回的都是相同的 symbol。

Symbol.for(key) :从注册表中读取(不存在则创建)symbol ,返回该 symbol

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

通过全局 symbol 返回一个名字,我们可以使用 Symbol.keyFor(sym)

注意,不适用于非全局 symbol。

如果 symbol 不是全局的,它将无法找到它并返回 undefined

// 通过 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

JavaScript 内部有很多“系统” symbol,我们可以使用它们来微调对象的各个方面。

它们都被列在了 众所周知的 symbol 表的规范中:

  • Symbol.hasInstance
  • Symbol.isConcatSpreadable
  • Symbol.iterator
  • Symbol.toPrimitive
  • ……等等。

原始值转换

转换规则
  1. 没有转换为布尔值。所有的对象在布尔上下文(context)中均为 true。只有字符串和数字转换。
  2. 数字转换发生在对象相减或应用数学函数时。例如,Date 对象(将在 日期和时间 一章中介绍)可以相减,date1 - date2 的结果是两个日期之间的差值。
  3. 至于字符串转换 —— 通常发生在我们像 alert(obj) 这样输出一个对象和类似的上下文中。
hint

类型转换在各种情况下有三种变体。它们被称为 “hint”

"string"

// 输出
alert(obj);

// 将对象作为属性键
anotherObj[obj] = 123;

"number"

// 显式转换
let num = Number(obj);

// 数学运算(除了二元加法)
let n = +obj; // 一元加法
let delta = date1 - date2;

// 小于/大于的比较
let greater = user1 > user2;

"default"

在少数情况下发生,当运算符“不确定”期望值的类型时。

例如,二元加法 + 可用于字符串(连接),也可以用于数字(相加)。因此,当二元加法得到对象类型的参数时,它将依据 "default" hint 来对其进行转换。

此外,如果对象被用于与字符串、数字或 symbol 进行 == 比较,这时到底应该进行哪种转换也不是很明确,因此使用 "default" hint。

<> 这样的小于/大于比较运算符,也可以同时用于字符串和数字。不过,它们使用 “number” hint,而不是 “default”。这是历史原因。

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

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

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

obj[Symbol.toPrimitive] = function(hint) {
  // 这里是将此对象转换为原始值的代码
  // 它必须返回一个原始值
  // hint = "string"、"number" 或 "default" 中的一个
}

如果 Symbol.toPrimitive 方法存在,则它会被用于所有 hint,无需更多其他方法。

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
toString/valueOf

如果没有 Symbol.toPrimitive,那么 JavaScript 将尝试寻找 toStringvalueOf 方法

  • 对于 "string" hint:调用 toString 方法,如果它不存在,则调用 valueOf 方法(因此,对于字符串转换,优先调用 toString)。
  • 对于其他 hint:调用 valueOf 方法,如果它不存在,则调用 toString 方法(因此,对于数学运算,优先调用 valueOf 方法)。

toStringvalueOf 方法很早己有了。它们不是 symbol(那时候还没有 symbol 这个概念),而是“常规的”字符串命名的方法。它们提供了一种可选的“老派”的实现转换的方法。

默认情况下,普通对象具有 toStringvalueOf 方法:

  • toString 方法返回一个字符串 "[object Object]"
  • valueOf 方法返回对象自身。
  • 8
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值