正如咱们在 数据类型 一章学到的,JavaScript 中有七种数据类型。有六种原始类型,由于他们的值只包含一种东西(字符串,数字或者其余)。javascript
相反,对象则用来存储键值对和更复杂的实体。在 JavaScript 中,对象几乎渗透到了这门编程语言的方方面面。因此,在咱们深刻理解这门语言以前,必须先理解对象。html
咱们能够经过使用带有可选 属性列表 的花括号 {…}
来建立对象。一个属性就是一个键值对("key: value"),其中键(key
)是一个字符串(也叫作属性名),值(value
)能够是任何值。java
咱们能够把对象想象成一个带有签名文件的文件柜。每一条数据都基于键(key
)存储在文件中。这样咱们就能够很容易根据文件名(也就是“键”)查找文件或添加/删除文件了。react
咱们能够用下面两种语法中的任一种来建立一个空的对象(“空柜子”):算法
let user = new Object(); // “构造函数” 的语法
let user = {}; // “字面量” 的语法
复制代码
一般,咱们用花括号。这种方式咱们叫作字面量。编程
咱们能够在建立对象的时候,当即将一些属性以键值对的形式放到 {...}
中。数组
let user = { // 一个对象
name: "John", // 键 "name",值 "John"
age: 30 // 键 "age",值 30
};
复制代码
属性有键(或者也能够叫作“名字”或“标识符”),位于冒号 ":"
的前面,值在冒号的右边。微信
在 user
对象中,有两个属性:数据结构
"name"
,值是 "John"
。"age"
,值是 30
。生成的 user
对象能够被想象为一个放置着两个标记有 "name" 和 "age" 的文件的柜子。app
咱们能够随时添加、删除和读取文件。
可使用点符号访问属性值:
// 读取文件的属性:
alert( user.name ); // John
alert( user.age ); // 30
复制代码
属性的值能够是任意类型,让咱们加个布尔类型:
user.isAdmin = true;
复制代码
咱们能够用 delete
操做符移除属性:
delete user.age;
复制代码
咱们也能够用多字词语来做为属性名,但必须给它们加上引号:
let user = {
name: "John",
age: 30,
"likes birds": true // 多词属性名必须加引号
};
复制代码
列表中的最后一个属性应以逗号结尾:
let user = {
name: "John",
age: 30,
}
复制代码
这叫作尾随(trailing)或悬挂(hanging)逗号。这样便于咱们添加、删除和移动属性,由于全部的行都是类似的。
对于多词属性,点操做就不能用了:
// 这将提示有语法错误
user.likes birds = true
复制代码
这是由于点操做须要的键是一个有效的标识符,不能有空格和其余的一些限制。
有另外一种方法,就是使用方括号,可用于任何字符串:
let user = {};
// 设置
user["likes birds"] = true;
// 读取
alert(user["likes birds"]); // true
// 删除
delete user["likes birds"];
复制代码
如今一切均可行了。请注意方括号中的字符串要放在引号中,单引号或双引号均可以。
方括号一样提供了一种能够经过任意表达式来获取属性名的方法 —— 跟语义上的字符串不一样 —— 好比像相似于下面的变量:
let key = "likes birds";
// 跟 user["likes birds"] = true; 同样
user[key] = true;
复制代码
在这里,变量 key
能够是程序运行时计算获得的,也能够是根据用户的输入获得的。而后咱们能够用它来访问属性。这给了咱们很大的灵活性。
例如:
let user = {
name: "John",
age: 30
};
let key = prompt("What do you want to know about the user?", "name");
// 访问变量
alert( user[key] ); // John(若是输入 "name")
复制代码
点符号不能以相似的方式使用:
let user = {
name: "John",
age: 30
};
let key = "name";
alert( user.key ) // undefined
复制代码
咱们能够在对象字面量中使用方括号。这叫作 计算属性。
例如:
let fruit = prompt("Which fruit to buy?", "apple");
let bag = {
[fruit]: 5, // 属性名是从 fruit 变量中获得的
};
alert( bag.apple ); // 5 若是 fruit="apple"
复制代码
计算属性的含义很简单:[fruit]
含义是属性名应该从 fruit
变量中获取。
因此,若是一个用户输入 "apple"
,bag
将变为 {apple: 5}
。
本质上,这跟下面的语法效果相同:
let fruit = prompt("Which fruit to buy?", "apple");
let bag = {};
// 从 fruit 变量中获取值
bag[fruit] = 5;
复制代码
……可是看起来更好。
咱们能够在方括号中使用更复杂的表达式:
let fruit = 'apple';
let bag = {
[fruit + 'Computers']: 5 // bag.appleComputers = 5
};
复制代码
方括号比点符号更强大。它容许任何属性名和变量,但写起来也更加麻烦。
因此大部分时间里,当属性名是已知且简单的时候,就是用点符号。若是咱们须要一些更复杂的内容,那么就用方括号。
像 "for"、"let" 和 "return" 等保留字段不能用做变量名。
对于对象的属性,没有这些限制。任何名字均可以:
let obj = {
for: 1,
let: 2,
return: 3
}
alert( obj.for + obj.let + obj.return ); // 6
复制代码
通常来讲,任何名字均可以,只有一个特殊的:"__proto__"
由于历史缘由要特别对待。好比,咱们不能把它设置为非对象的值:
let obj = {};
obj.__proto__ = 5;
alert(obj.__proto__); // [object Object],这样没法得到预期效果
复制代码
咱们从代码中能够看出来,把它赋值为 5
的操做被忽略了。
若是咱们打算在一个对象中存储任意的键值对,并容许访问者指定键,那么这可能会成为 bug 甚至漏洞的来源。
好比,访问者可能选择 __proto__
做为键,这个赋值的逻辑就失败了(像上面那样)。
有一种让对象把 __proto__
做为常规属性进行对待的方法,在后面章节会讲到,但如今咱们须要先来学习更多对象的相关知识。
还有另一种数据结构 Map,咱们会在后面的 Map and Set(映射和集合) 这章节学习它,它支持任意的键。
在实际开发中,咱们一般用已存在的变量当作属性名。
例如:
function makeUser(name, age) {
return {
name: name,
age: age
// ……其余的属性
};
}
let user = makeUser("John", 30);
alert(user.name); // John
复制代码
在上面的例子中,属性名跟变量名同样。这种经过变量生成属性的应用场景很常见,在这有一种特殊的 属性值缩写 方法,使属性名变得更短。
能够用 name
来代替 name:name
像下面那样:
function makeUser(name, age) {
return {
name, // 与 name: name 相同
age // 与 age: age 相同
// ...
};
}
复制代码
咱们能够把属性名简写方式和正常方式混用:
let user = {
name, // 与 name:name 相同
age: 30
};
复制代码
对象的一个显著的特色就是其全部的属性都是可访问的。若是某个属性不存在也不会报错!访问一个不存在的属性只是会返回 undefined
。这提供了一种广泛的用于检查属性是否存在的方法 —— 获取值来与 undefined 比较:
let user = {};
alert( user.noSuchProperty === undefined ); // true 意思是没有这个属性
复制代码
这里一样也有一个特别的操做符 "in"
来检查属性是否存在。
语法是:
"key" in object
复制代码
例如:
let user = { name: "John", age: 30 };
alert( "age" in user ); // true,user.age 存在
alert( "blabla" in user ); // false,user.blabla 不存在。
复制代码
请注意,in
的左边必须是 属性名。一般是一个带引号的字符串。
若是咱们省略引号,则意味着将测试包含实际名称的变量。例如:
let user = { age: 30 };
let key = "age";
alert( key in user ); // true,从 key 获取属性名并检查这个属性
复制代码
undefined
的属性使用 "in"一般,检查属性是否存在时,使用严格比较 "=== undefined"
就够了。但在一种特殊状况下,这种方式会失败,而 "in"
却能够正常工做。
那就是属性存在,可是存储值为 undefined
:
let obj = {
test: undefined
};
alert( obj.test ); // 显示 undefined,因此属性不存在?
alert( "test" in obj ); // true,属性存在!
复制代码
在上面的代码中,属性 obj.test
事实上是存在的,因此 in
操做符检查经过。
这种状况不多发生,由于一般状况下是不会给对象赋值 undefined 的,咱们常常会用 null
来表示未知的或者空的值。
为了遍历一个对象的全部键(key),可使用一个特殊形式的循环:for..in
。这跟咱们在前面学到的 for(;;)
循环是彻底不同的东西。
语法:
for (key in object) {
// 对此对象属性中的每一个键执行的代码
}
复制代码
例如,让咱们列出 user
全部的属性:
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
}
复制代码
注意,全部的 "for" 结构体都容许咱们在循环中定义变量,像这里的 let key
。
一样,咱们能够用其余属性名来替代 key
。例如 "for(let prop in obj)"
也很经常使用。
对象有顺序吗?换句话说,若是咱们遍历一个对象,咱们获取属性的顺序是和属性添加时的顺序相同吗?这靠谱吗?
简短的回答是:“有特别的顺序”:整数属性会被进行排序,其余属性则按照建立的顺序显示。详情以下:
例如,让咱们考虑一个带有电话号码的对象:
let codes = {
"49": "Germany",
"41": "Switzerland",
"44": "Great Britain",
// ..,
"1": "USA"
};
for(let code in codes) {
alert(code); // 1, 41, 44, 49
}
复制代码
对象可用于面向用户的建议选项列表。若是咱们的网站主要面向德国观众,那么咱们可能但愿 49
排在第一。
但若是咱们执行代码,会看到彻底不一样的景象:
由于这些电话号码是整数,因此它们以升序排列。因此咱们看到的是 1, 41, 44, 49
。
这里的“整数属性”指的是一个能够在不做任何更改的状况下转换为整数的字符串(包括整数到整数)。
因此,"49" 是一个整数属性名,由于咱们把它转换成整数,再转换回来,它仍是同样。可是 "+49" 和 "1.2" 就不行了:
// Math.trunc 是内置的去除小数部分的方法。
alert( String(Math.trunc(Number("49"))) ); // "49",相同,整数属性
alert( String(Math.trunc(Number("+49"))) ); // "49",不一样于 "+49" ⇒ 不是整数属性
alert( String(Math.trunc(Number("1.2"))) ); // "1",不一样于 "1.2" ⇒ 不是整数属性
复制代码
……此外,若是属性名不是整数,那它们就按照建立时候的顺序来排序,例如:
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 message = "Hello!";
let phrase = message;
复制代码
结果是咱们获得了两个独立变量,每一个变量存的都是 "Hello!"
。
对象跟这个不同。
变量存储的不是对象自己,而是“内存中的地址”,换句话说就是对象的“引用”。
下面是这个对象的存储结构图:
let user = {
name: "John"
};
复制代码
在这里,对象被存储在内存中的某个位置。变量 user
有一个对它的引用。
当对象被复制的时候 — 引用被复制了一份, 对象并无被复制。
若是咱们将对象想象成是一个抽屉,那么变量就是一把钥匙。拷贝对象是复制了钥匙,可是并无复制抽屉自己。
例如:
let user = { name: "John" };
let admin = user; // 复制引用
复制代码
如今咱们有了两个变量,可是都指向同一个对象:
咱们能够经过其中任意一个变量访问抽屉并改变其中的内容:
let user = { name: 'John' };
let admin = user;
admin.name = 'Pete'; // 被经过名为 "admin" 的引用修改了
alert(user.name); // 'Pete',经过名为 "user" 的引用查看修改
复制代码
上面的例子证明了只存在一个对象。就像咱们的一个抽屉带有两把钥匙,若是使用其中一把钥匙(admin
)打开抽屉并改变抽屉里放的东西,稍后使用另一把钥匙(user
)打开抽屉的时候,就会看到变化。
等号 ==
和严格相等 ===
操做符对于对象来讲没差异。
两个对象只有在它们实际上是一个对象时才会相等。
例如,若是两个变量引用指向同一个对象,那么它们相等:
let a = {};
let b = a; // 复制引用
alert( a == b ); // true,两个变量指向同一个对象
alert( a === b ); // true
复制代码
若是是两个独立的对象,则它们不相等,即便它们都是空的:
let a = {};
let b = {}; // 两个独立的对象
alert( a == b ); // false
复制代码
对于像 obj1 > obj2
这样两个对象的比较,或对象与原始值的比较 obj == 5
,对象会被转换成原始值。咱们很快就会学习到对象的转化是如何实现的,可是事实上,这种比较真的极少用到,这种比较的出现常常是代码的 BUG 致使的。
一个被 const
修饰的对象是 能够 被修改。
例如:
const user = {
name: "John"
};
user.age = 25; // (*)
alert(user.age); // 25
复制代码
看起来好像 (*)
这行代码会致使错误,但并无,这里彻底没问题。这是由于 const
修饰的只是 user
自己存储的值。在这里 user
始终存储的都是对同一个对象的引用。(*)
这行代码修改的是对象内部的内容,并无改变 user
存储的对象的引用。
若是你想把其余内容赋值给 user
,那就会报错了,例如:
const user = {
name: "John"
};
// 错误(不能再给 user 赋值)
user = {
name: "Pete"
};
复制代码
……那么若是咱们想要建立不可变的对象属性,应该怎么作呢?想让 user.age = 25
这样的赋值报错,这也是能够的。咱们会在 属性的标志和描述符 这章学习这部份内容。
复制一个对象变量会建立指向此对象的另外一个引用。
那若是咱们须要复制一个对象呢?建立一份独立的拷贝,一份克隆?
这也是可行的,可是有一点麻烦,由于 JavaScript 中没有支持这种操做的内置函数。实际上,咱们不多这么作。在大多数时候,复制引用都很好用。
但若是咱们真想这么作,就须要建立一个新的对象,而后遍历现有对象的属性,在原始级别的状态下复制给新的对象。
像这样:
let user = {
name: "John",
age: 30
};
let clone = {}; // 新的空对象
// 复制全部的属性值
for (let key in user) {
clone[key] = user[key];
}
// 如今的复制是独立的了
clone.name = "Pete"; // 改变它的值
alert( user.name ); // 原对象属性值不变
复制代码
咱们也能够用 Object.assign 来实现。
语法是:
Object.assign(dest,[ src1, src2, src3...])
复制代码
dest
和 src1, ..., srcN
(你须要多少就能够设置多少,没有限制)是对象。src1, ..., srcN
这些全部的对象复制到 dest
。换句话说,从第二个参数开始,全部对象的属性都复制给了第一个参数对象,而后返回 dest
。例如,咱们能够用这个方法来把几个对象合并成一个:
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 }
复制代码
若是用于接收的对象(user
)已经有了一样属性名的属性,已有的则会被覆盖:
let user = { name: "John" };
// 覆盖 name,增长 isAdmin
Object.assign(user, { name: "Pete", isAdmin: true });
// 如今 user = { name: "Pete", isAdmin: true }
复制代码
咱们能够用 Object.assign
来替代循环赋值进行简单的克隆操做:
let user = {
name: "John",
age: 30
};
let clone = Object.assign({}, user);
复制代码
它将对象 user
的全部的属性复制给了一个空对象并返回。实际上和循环赋值没什么区别,只是更短了。
直到如今,咱们都是假设 user
的全部属性都是原始值。可是属性也能够是其余对象的引用。这种咱们应该怎么操做呢?
例如:
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
alert( user.sizes.height ); // 182
复制代码
如今,仅仅进行 clone.sizes = user.sizes
复制是不够的,由于 user.sizes
是一个对象,这个操做只能复制这个对象的引用。因此 clone
和 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++; // 在这里改变一个属性的值
alert(clone.sizes.width); // 51,在这里查看属性的值
复制代码
为了解决这个问题,咱们在复制的时候应该检查 user[key]
的每个值,若是它是一个对象,那么把它也复制一遍,这叫作深拷贝(deep cloning)。
有一个标准的深拷贝算法,用于解决上面这种和一些更复杂的状况,叫作 结构化克隆算法(Structured cloning algorithm)。为了避免重复造轮子,咱们可使用它的一个 JavaScript 实现的库 lodash,方法名叫作 _.cloneDeep(obj)。
对象是具备一些特殊特性的关联数组。
它们存储属性(键值对),其中:
咱们能够用下面的方法访问属性:
obj.property
。obj["property"]
,方括号容许从变量中获取键,例如 obj[varWithKey]
。其余操做:
delete obj.prop
。"key" in obj
。for(let key in obj)
循环。对象是经过引用被赋值或复制的。换句话说,变量存储的不是“对象的值”,而是值的“引用”(内存地址)。因此复制这样的变量或者将其做为函数参数进行传递时,复制的是引用,而不是对象。基于复制的引用(例如添加/删除属性)执行的全部的操做,都是在同一个对象上执行的。
咱们可使用 Object.assign
或者 _.cloneDeep(obj) 进行“真正的复制”(一个克隆)。
咱们在这一章学习的叫作“基本对象”,或者就叫对象。
JavaScript 中还有不少其余类型的对象:
Array
用于存储有序数据集合,Date
用于存储时间日期,Error
用于存储错误信息。它们有着各自特别的特性,咱们将在后面学习到。有时候你们会说“数组类型”或“日期类型”,但其实它们并非自身所属的类型,而是属于一个对象类型即 "object"。它们以不一样的方式对 "object" 作了一些扩展。
JavaScript 中的对象很是强大。这里咱们只接触了冰山一角。在后面的章节中,咱们将频繁使用对象进行编程,并学习更多关于对象的知识。
先本身作题目再看答案。
重要程度:⭐️⭐️⭐️⭐️⭐️
按下面的要求写代码,一条对应一行代码:
user
。name
,值是 John
。surname
,值是 Smith
。name
的属性的值改为 Pete
。name
的属性。重要程度:⭐️⭐️⭐️⭐️⭐️
写一个 isEmpty(obj)
函数,当对象没有属性的时候返回 true
,不然返回 false
。
应该像这样:
let schedule = {};
alert( isEmpty(schedule) ); // true
schedule["8:30"] = "get up";
alert( isEmpty(schedule) ); // false
复制代码
重要程度:⭐️⭐️⭐️⭐️⭐️
有可能改变用 const
声明的对象吗?你怎么看?
const user = {
name: "John"
};
// 这样有效吗?
user.name = "Pete";
复制代码
重要程度:⭐️⭐️⭐️⭐️⭐️
咱们有一个保存着团队成员工资的对象:
let salaries = {
John: 100,
Ann: 160,
Pete: 130
}
复制代码
写一段代码求出咱们的工资总和,将计算结果保存到变量 sum
。从所给的信息来看,结果应该是 390
。
若是 salaries
是一个空对象,那结果就为 0
。
重要程度:⭐️⭐️⭐
建立一个 multiplyNumeric(obj)
函数,把 obj
全部的数值属性都乘以 2
。
例如:
// 在调用以前
let menu = {
width: 200,
height: 300,
title: "My menu"
};
multiplyNumeric(menu);
// 调用函数以后
menu = {
width: 400,
height: 600,
title: "My menu"
};
复制代码
注意 multiplyNumeric
函数不须要返回任何值,它应该就地修改对象。
P.S. 用 typeof
检查值类型。
在微信公众号「技术漫谈」后台回复 1-4-1
获取做业答案。
现代 JavaScript 教程:开源的现代 JavaScript 从入门到进阶的优质教程。React 官方文档推荐,与 MDN 并列的 JavaScript 学习教程。
在线免费阅读:zh.javascript.info
扫描下方二维码,关注微信公众号「技术漫谈」,订阅更多精彩内容。