详解Javascript对象
在Javascript中有7种类型,其中6个为原始类型,因为他们的值仅包含单个内容(如字符串、数值或其他)。
反之对象通常存储包含不同类型的键集合,更加复杂。在Javascript中,对象几乎涉及方方面面,所有我们在深入使用前必须首先理解他们。
定义
对象使用{}创建,可以包含若干个属性。属性是“键:值”方式,其中键必须是字符串类型,即属性名称,值可以是任意类型。
我们想像是带有名称的文档盒子,每个属性值通过键的方式存储在其中,通过名称(键)很方面查找其中的文件(属性值),也可以添加、删除其他文件。
有两种方式定义对象,后者通常被使用。
let user = new Object(); // "object constructor" syntax
let user = {}; // "object literal" syntax
对象属性
我们可以通过键值对的方式增加属性:
let user = { // an object
name: “John”, // by key “name” store value “John”
age: 30// by key “age” store value 30
};
属性在“:”前有键(名称或标识),值在冒号右边。上面的示例user对象有两个属性。
1、name属性,值为“John”。
2、age属性,值为30.
对象user可想像为有两个文档的盒子,分别命名为“name”和“age”。
我们随时可以增加、删除、查找文档。属性值可以通过.号访问。
// get fields of the object:
alert( user.name ); // John
alert( user.age ); // 30
值可以是任意类型,我们增加一个布尔值。
user.isAdmin = true;
使用delete可以删除属性,示例如下:
delete user.age;
我们也能使用多个单词作为属性的名称,但必须使用引号:
let user = {
name: “John”,
age: 30,
“likes birds”: true // multiword property name must be quoted
};
最后属性后面可以有一个多余的逗号。
let user = {
name: "John",
age: 30,
}
这个特性,是我们更容易增加、删除属性,因为所有行看起来一致。
方括号
对于多个单词的属性,不能直接访问:
// this would give a syntax error
user.likes birds = true
因为.号需要键必须为有效的变量进行标识,即不能有空格或其他限制,另一种方式使用方括号。
let user = {};
// set
user["likes birds"] = true;
// get
alert(user["likes birds"]); // true
// delete
delete user["likes birds"];
现在可以访问了,需要注意的是方括号内的字符串需要引号括起来(单引号或双引号)。
方括号也提供了通过变量名称访问属性:
let key = “likes birds”;
// same as 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");
// access by variable
alert( user[key] ); // John (if enter "name")
计算属性
我们能使用方括号在对象体内,一般称为计算属性。示例如下:
let fruit = prompt("Which fruit to buy?", "apple");
let bag = {
[fruit]: 5, // the name of the property is taken from the variable fruit
};
alert( bag.apple ); // 5 if fruit="apple"
计算属性的意义很明了,[fruit]意味着属性名称应该来自fruit。所以用户输入“apple”,则bag对象的内容为{apple:5}.对应代码的表现为:
let fruit = prompt("Which fruit to buy?", "apple");
let bag = {};
// take property name from the fruit variable
bag[fruit] = 5;
方括号比点号更强大,其容许任何一个属性名称和变量,当然也更难写。所以一般属性名称已知且简单,使用点号;如果我们需要更复杂或灵活的方式,使用方括号。
简化属性值
实际开发中我们通常使用变量作为属性值,示例如下:
function makeUser() {
let name = prompt("Name?");
let age = prompt("Age?");
return {
name: name,
age: age
};
}
let user = makeUser("John", 30);
alert(user.name); // John
示例中,属性名称与赋值变量同名,这种情况下,可以简写。
代替name:name,为name,代码如下:
function makeUser(name, age) {
return {
name, // same as name: name
age // same as age: age
};
}
也可以在对象中同时使用正常属性和简写方式:
let user = {
name, // same as name:name
age: 30
};
属性存在性检查
可标识对象特性是其任何属性都可以访问,如果属性不存在也不会报错。访问不存在的属性,仅返回undefined。这时一个常规方法测试属性是否存在,使其和undefined比较。
let user = {};
alert( user.noSuchProperty === undefined ); // true means "no such property"
也可以使用in操作来检查属性。语法是:"key" in object
。示例如下:
let user = { name: "John", age: 30 };
alert( "age" in user ); // true, user.age exists
alert( "blabla" in user ); // false, user.blabla doesn't exist
注意:在in的左边必须是属性名称,通常需要使用引号括起来。如果省略引号,则意味着是变量,其包含实际被测试的值。示例如下:
let user = { age: 30 };
let key = "age";
alert( key in user ); // true, takes the name from key and checks for such property
使用in比较存储undefined的值
通常使用严格比较“===undefined”检查没有问题。但是有个特列,这时使用in没有问题。即当对象属性存在,但是其值为undefined
:
let obj = {
test: undefined
};
alert( obj.test ); // it's undefined, so - no such property?
alert( "test" in obj ); // true, the property does exist!
上面代码,属性obj.test确实存在,所以in测试没有问题。这种场景很少发生,因为undefined
通常不用来赋值,我们大多数使用null或“unknown”、“empty”值。
“for…in” 循环
为了遍历对象中所有属性(键),有个特别的循环语法:for..in
,这个与for(;;)
语法不同。语法如下:
for(key in object) {
// executes the body for each key among object properties
}
示例,我们遍历user对象所有属性:
let user = {
name: “John”,
age: 30,
isAdmin: true
};
for(let key in user) {
// keys
alert( key ); // name, age, 30
// values for the keys
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)等。
属性的输出顺序是按照数字的升序排序顺序,因为他们是Integer,所有是 1 41 44 49
。
Integer属性
integer属性是一个字符串,能够转为整数,其值不变。所以“49”是一个Integer属性名称,因为当他转成整数,仍然相同,但是“+49”和“1.2”则不是。
// Math.trunc is a built-in function that removes the decimal part
alert( String(Math.trunc(Number("49"))) ); // "49", same, integer property
alert( String(Math.trunc(Number("+49"))) ); // "49", not same ⇒ not integer property
alert( String(Math.trunc(Number("1.2"))) ); // "1", not same ⇒ not integer property
另外,如果键是非整数,那么他们按照创建顺序输出,示例:
let user = {
name: "John",
surname: "Smith"
};
user.age = 25; // add one more
// non-integer properties are listed in the creation order
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
}
按引用拷贝
对象和原始值本质不同是他们存储和按引用拷贝。原始值:string、numbers、boolean,是作为一个整体进行赋值或拷贝。示例:
let message = "Hello!";
let phrase = message;
结果是,我们有两个独立的变量,每个都存储着字符串“Hello!”。
对象与之不同。对象变量不存储对象自身,而是内存地址,即其引用。图示如下:
let user = {
name: “John”
};
这里,对象存储在内存某个地址,变量user有一个引用指向它。当一个对象变量被拷贝(赋值),是引用被拷贝,对象值没有被拷贝。
如果我们想像对象是一个盒子,然后对象有一个键指向它,拷贝变量则赋值键,而不是盒子本身。示例:
let user = { name: “John” };
let admin = user; // copy the reference
现在我们有两个变量,每个都有一个引用,指向同一对象。
let user = { name: 'John' };
let admin = user;
admin.name = 'Pete'; // changed by the "admin" reference
alert(user.name); // 'Pete', changes are seen from the "user" reference
上面代码演示了只有一个对象,就如我们有一个盒子,但是带有两个键,我们通过“admin”修改其值,然后通过另一个“user”获取值,确实已经改变。
比较引用
“==”和严格相对“===”操作,对对象作用是一样的。两个对象相等仅如果他们是同一个对象。
let a = {};
let b = a; // copy the reference
alert( a == b ); // true, both variables reference the same object
alert( a === b ); // true
如果是两个独立的对象,则不相对,即使都为空。
let a = {};
let b = {}; // two independent objects
alert( a == b ); // false
常量对象
对象声明为const可以被改变,示例如下:
const user = {
name: “John”
};
user.age = 25; // (*)
alert(user.age); // 25
你可能会认为号行会出错,但是不会,完全没有问题。那是因为const不允许user值自身,即user始终存储同一个对象引用。号行是在对象内部,并没有给user重新赋值。
如果你试图给user赋值,则会出错,示例如下:
const user = {
name: "John"
};
// Error (can't reassign user)
user = {
name: "Pete"
};
至于如何实现让user.age不能修改,以后再谈。
克隆和合并,对象赋值
如上所说,拷贝对象变量,只是创建了对同一对象的一个或多个针引用。但我们如何实现复制对象,是对立的拷贝,即克隆。其实是可行的,但有点困难,因为Javascript没有内置方法,事实上,这很少需要,大多数情况下都是赋值引用。
但是如果你确实需要,那么你需要创建一个新对象,然后通过迭代每个属性,然后复制他们在原始值级别,完整地复制原对象的结构。示例:
let user = {
name: "John",
age: 30
};
let clone = {}; // the new empty object
// let's copy all user properties into it
for (let key in user) {
clone[key] = user[key];
}
// now clone is a fully independant clone
clone.name = "Pete"; // changed the data in it
alert( user.name ); // still John in the original object
我们也可以使用Object.assign方法实现.语法为 Object.assign(dest[, src1, src2, src3...])
。参数都是对象,它拷贝src1、…,srcN所有对象的所有属性至dest对象。即从第二个开始的所有参数的属性都复制到第一个,然后返回dest。
示例,我们可以合并几个对象至一个:
let user = { name: "John" };
let permissions1 = { canView: true };
let permissions2 = { canEdit: true };
// copies all properties from permissions1 and permissions2 into user
Object.assign(user, permissions1, permissions2);
// now user = { name: "John", canView: true, canEdit: true }
如果接收对象(user)已经有同名属性,则会覆盖:
let user = { name: "John" };
// overwrite name, add isAdmin
Object.assign(user, { name: "Pete", isAdmin: true });
// now user = { name: "Pete", isAdmin: true }
我们也可以使用Object.assign方式代替loop实现简单克隆:
let user = {
name: "John",
age: 30
};
let clone = Object.assign({}, user);
这里拷贝了user所有属性值空对象,然后返回,与loop效果一样,但不都一样。因为我们上面user对象所有属性都是原始类型,但是属性可能是引用类型,指向其他对象,这种情况会怎么呢?代码如下:
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
alert( user.sizes.height ); // 182
现在做 clone.sizes = user.sizes
是不够的,因为 users.sizes
是对象,只复制了引用。所以clone和user指向同一个对象。
代码如下:
let user = {
name: “John”,
sizes: {
height: 182,
width: 50
}
};
let clone = Object.assign({}, user);
alert( user.sizes === clone.sizes ); // true, same object
// user and clone share sizes
user.sizes.width++; // change a property from one place
alert(clone.sizes.width); // 51, see the result from the other one
为了实现深复制,我们需要判断user[key]类型,如果为对象类型,需继续复制其下面每个属性,有兴趣的读者可以参考结构克隆算法。
本文参考链接:https://blog.csdn.net/neweastsun/article/details/69053673