Object(对象):基础知识1 对象 对象引用和复制

      对象
JavaScript 中有八种数据类型。有七种原始类型,因为它们的值只包含一种东西(字符串,数字或者其他)。

相反,对象则用来存储键值对和更复杂的实体。

我们可以通过使用带有可选 属性列表 的花括号 {…} 来创建对象。一个属性就是一个键值对(“key: value”),其中键(key)是一个字符串(也叫做属性名),(value)可以是任何值

我们可以用下面两种语法中的任一种来创建一个空的对象(“空柜子”):

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

文本和属性
我们可以在创建对象的时候,立即将一些属性以键值对的形式放到 {...} 中。

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

我们可以随时添加、删除和读取文件。

// 读取文件的属性:(点符号
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 变量中获取值
bag[fruit] = 5;
……但是看起来更好。

我们可以在方括号中使用更复杂的表达式:

let fruit = 'apple';
let bag = {
[fruit + 'Computers']: 5 // bag.appleComputers = 5
};
方括号比点符号更强大。它允许任何属性名和变量,但写起来也更加麻烦。

所以,大部分时间里,当属性名是已知且简单的时候,就使用点符号。如果我们需要一些更复杂的内容,那么就用方括号。

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

例如:

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
};

属性存在性测试,“in” 操作符
相比于其他语言,JavaScript 的对象有一个需要注意的特性:能够被访问任何属性。即使属性不存在也不会报错!

读取不存在的属性只会得到 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,属性 "age" 存在
为何会有 in 运算符呢?与 undefined 进行比较来判断还不够吗?

确实,大部分情况下与 undefined 进行比较来判断就可以了。但有一个例外情况,这种比对方式会有问题,但 in 运算符的判断结果仍是对的。

那就是属性存在,但存储的值是 undefined 的时候:

let obj = {
test: undefined
};

alert( obj.test ); // 显示 undefined,所以属性不存在?

alert( "test" in obj ); // true,属性存在!
在上面的代码中,属性 obj.test 事实上是存在的,所以 in 操作符检查通过。

这种情况很少发生,因为通常情况下不应该给对象赋值 undefined。我们通常会用 null 来表示未知的或者空的值。因此,in 运算符是代码中的特殊来宾。

“for…in” 循环
为了遍历一个对象的所有键(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 排在第一。

但如果我们执行代码,会看到完全不同的现象:

USA (1) 排在了最前面
然后是 Switzerland (41) 及其它。
因为这些电话号码是整数,所以它们以升序排列。所以我们看到的是 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
}
现在跟预想的一样了。

总结
对象是具有一些特殊特性的关联数组。

它们存储属性(键值对),其中:

属性的键必须是字符串或者 symbol(通常是字符串)。
值可以是任何类型。
我们可以用下面的方法访问属性:

点符号: obj.property。
方括号 obj["property"],方括号允许从变量中获取键,例如 obj[varWithKey]。
其他操作:

删除属性:delete obj.prop。
检查是否存在给定键的属性:"key" in obj。
遍历对象:for(let key in obj) 循环。
我们在这一章学习的叫做“普通对象(plain object)”,或者就叫对象。

JavaScript 中还有很多其他类型的对象:

Array 用于存储有序数据集合,
Date 用于存储时间日期,
Error 用于存储错误信息。
……等等。

        对象引用和复制
原始类型复制:字符串,数字,布尔值等 —— 始终是以“整体值”的形式被复制的。
我们将 message 复制到 phrase:

let message = "Hello!";
let phrase = message;
结果我们就有了两个独立的变量,每个都存储着字符串 "Hello!"。

但是,对象不是这样的。

赋值了对象的变量存储的不是对象本身,而是该对象“在内存中的地址”,换句话说就是对该对象的“引用”。

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

例如:

let user = { name: "John" };

let admin = user; // 复制引用
现在我们有了两个变量,它们保存的都是对同一个对象的引用:

一个对象,现在有两个引用它的变量。

我们可以通过其中任意一个变量来访问该对象并修改它的内容:

let user = { name: 'John' };

let admin = user;

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

alert(user.name); // 'Pete',修改能通过 "user" 引用看到

克隆与合并,Object.assign

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 方法来达成同样的效果。

语法是:

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);

// 现在 user = { name: "John", canView: true, canEdit: true }

如果被拷贝的属性的属性名已经存在,那么它会被覆盖:

let user = { name: "John" };

Object.assign(user, { name: "Pete" });

alert(user.name); // 现在 user = { name: "Pete" }
我们也可以用 Object.assign 代替 for..in 循环来进行简单克隆:

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

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

 

posted on 2021-10-19 17:06  PerfectData  阅读(317)  评论(0编辑  收藏  举报

导航