本文为学习笔记,参考Javascript.info,菜鸟等文章编写。
跳转连接
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”
构造函数
构造函数在技术上是常规函数。不过有两个约定:
- 它们的命名以大写字母开头。
- 它们只能由
"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
操作符执行时,它按照以下步骤:
- 一个新的空对象被创建并分配给
this
。 - 函数体执行。通常它会修改
this
,为其添加新的属性。 - 返回
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
- ……等等。
原始值转换
转换规则
- 没有转换为布尔值。所有的对象在布尔上下文(context)中均为
true
。只有字符串和数字转换。 - 数字转换发生在对象相减或应用数学函数时。例如,
Date
对象(将在 日期和时间 一章中介绍)可以相减,date1 - date2
的结果是两个日期之间的差值。 - 至于字符串转换 —— 通常发生在我们像
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 尝试查找并调用三个对象方法:
- 调用
obj[Symbol.toPrimitive](hint)
—— 带有 symbol 键Symbol.toPrimitive
(系统 symbol)的方法,如果这个方法存在的话, - 否则,如果 hint 是
"string"
—— 尝试调用obj.toString()
或obj.valueOf()
,无论哪个存在。 - 否则,如果 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 将尝试寻找 toString
和 valueOf
方法
- 对于
"string"
hint:调用toString
方法,如果它不存在,则调用valueOf
方法(因此,对于字符串转换,优先调用toString
)。 - 对于其他 hint:调用
valueOf
方法,如果它不存在,则调用toString
方法(因此,对于数学运算,优先调用valueOf
方法)。
toString
和 valueOf
方法很早己有了。它们不是 symbol(那时候还没有 symbol 这个概念),而是“常规的”字符串命名的方法。它们提供了一种可选的“老派”的实现转换的方法。
默认情况下,普通对象具有 toString
和 valueOf
方法:
toString
方法返回一个字符串"[object Object]"
。valueOf
方法返回对象自身。