1 概述
对象(object)是 JavaScript语言的核心概念,属于七种数据类型之一,唯一一种复杂类型,也是最重要的数据类型。
什么是对象?简单说,对象就是一组“键值对”(key-value)的集合,是一种无序的复合数据集合。如:
let obj = { 'name': 'frank', 'age': 18 }
let obj = new Object({'name': 'frank'})
console.log({ 'name': 'frank, 'age': 18 })
上面代码中,大括号就定义了一个对象,它被赋值给变量obj,所以变量obj就指向一个对象。该对象内部包含两个键值对(又称为两个“成员”),第一个键值对是'name':'Hello',其中name是“键名”(成员的名称),字符串frank是“键值”(成员的值)。键名与键值之间用冒号分隔。第二个键值对是age: 'World',age是键名,18是键值。两个键值对之间用逗号分隔。
细节:
- 键名是字符串,不是标识符,可以包含任意字符
- 引号可省略,省略之后就只能写标识符。就算引号省略了,键名也还是字符串,这点很重要。
2 奇怪的属性名
如果键名是数值,会被自动转为字符串。
var obj = {
1: 'a',
3.2: 'b',
1e2: true,
1e-2: true,
.234: true,
0xFF: true
};
obj
// Object {
// 1: "a",
// 3.2: "b",
// 100: true,
// 0.01: true,
// 0.234: true,
// 255: true
// }
或 Object.keys(obj)
=> ["1", "100", "255", "3.2", "0.01", "0.234"]
obj['100'] // true
Object.keys(obj) 可以得到 obj 的所有 key
上面代码中,对象obj的所有键名虽然看上去像数值,实际上都被自动转成了字符串。
如果键名不符合标识名的条件(比如第一个字符为数字,或者含有空格或运算符),且也不是数字,则必须加上引号,否则会报错。
var obj = {
1p: 'Hello World'
};
// 报错
var obj = {
'1p': 'Hello World',
'h w': 'Hello World',
'p+q': 'Hello World'
// 不报错
};
上面对象的三个键名,都不符合标识名的条件,所以必须加上引号。
对象的每一个key又称为“属性”(property),value都是对象的属性值,它的“键值”可以是任何数据类型。如果一个属性的值为函数,通常把这个属性称为“方法”,它可以像函数那样调用。
var obj = {
p: function (x) {
return 2 * x;
}
};
obj.p(1) // 2
上面代码中,对象obj的属性p,就指向一个函数。
如果属性的值还是一个对象,就形成了链式引用。
var o1 = {};
var o2 = { bar: 'hello' };
o1.foo = o2;
o1.foo.bar // "hello"
上面代码中,对象o1的属性foo指向对象o2,就可以链式引用o2的属性。
对象的属性之间用逗号分隔,最后一个属性后面可以加逗号(trailing comma),也可以不加。
var obj = {
p: 123,
m: function () { ... },
}
上面的代码中,m属性后面的那个逗号,有没有都可以。
属性可以动态创建,不必在对象声明时就指定。
var obj = {};
obj.foo = 123;
obj.foo // 123
上面代码中,直接对obj对象的foo属性赋值,结果就在运行时创建了foo属性。
变量作属性名
如何用变量做属性名,之前都是用常量做属性名
let p1 = 'name'
let obj = { p1 : 'frank'} 这样写,属性名为 'p1'
let obj = { [p1] : 'frank' } 这样写,属性名为 'name'
注意:
- 不加 [ ] 的属性名会自动变成字符串;加了[]则会当做变量求值。
- 值如果不是字符串,则会自动变成字符串。
对象的隐藏属性:
- JS 中每一个对象都有一个隐藏属性
- 这个隐藏属性储存着其共有属性组成的对象的地址
- 这个共有属性组成的对象叫做原型
也就是说,隐藏属性储存着原型的地址
var obj = {}
obj.toString() // 居然不报错
因为 obj 的隐藏属性对应的对象上有 toString()
除了字符串,symbol 也能做属性名
let a = Symbol()
let obj = { [a]: 'Hello' }
在「迭代」时会用到
3 对象的引用
如果不同的变量名指向同一个对象,那么它们都是这个对象的引用,也就是说指向同一个内存地址。修改其中一个变量,会影响到其他所有变量。
var o1 = {};
var o2 = o1;
o1.a = 1;
o2.a // 1
o2.b = 2;
o1.b // 2
上面代码中,o1和o2指向同一个对象,因此为其中任何一个变量添加属性,另一个变量都可以读写该属性。
此时,如果取消某一个变量对于原对象的引用,不会影响到另一个变量。
var o1 = {};
var o2 = o1;
o1 = 1;
o2 // {}
上面代码中,o1和o2指向同一个对象,然后o1的值变为1,这时不会对o2产生影响,o2还是指向原来的那个对象。
但是,这种引用只局限于对象,如果两个变量指向同一个原始类型的值。那么,变量这时都是值的拷贝。
var x = 1;
var y = x;
x = 2;
y // 1
上面的代码中,当x的值发生变化后,y的值并不变,这就表示y和x并不是指向同一个内存地址。
4 表达式还是语句?
对象采用大括号表示,这导致了一个问题:如果行首是一个大括号,它到底是表达式还是语句?
{ foo: 123 }JavaScript引擎读到上面这行代码,会发现可能有两种含义。第一种可能是,这是一个表达式,表示一个包含foo属性的对象;第二种可能是,这是一个语句,表示一个代码区块,里面有一个标签foo,指向表达式123。
为了避免这种歧义,JavaScript引擎的做法是,如果遇到这种情况,无法确定是对象还是代码块,一律解释为代码块。
{ console.log(123) } // 123
上面的语句是一个代码块,而且只有解释为代码块,才能执行。
如果要解释为对象,最好在大括号前加上圆括号。因为圆括号的里面,只能是表达式,所以确保大括号只能解释为对象。
({ foo: 123 }) // 正确
({ console.log(123) }) // 报错
这种差异在eval语句(作用是对字符串求值)中反映得最明显。
eval('{foo: 123}') // 123
eval('({foo: 123})') // {foo: 123}
上面代码中,如果没有圆括号,eval将其理解为一个代码块;加上圆括号以后,就理解成一个对象。
5 属性的操作
5.1 属性的删除
delete命令用于删除对象的属性,删除成功后返回true。
delete obj.xxx 或 delete obj['xxx']
即可删除 obj 的 xxx 属性
请区分「属性值为 undefined」和「不含属性名」
- 不含属性名
'xxx' in obj === false
- 含有属性名,但是值为 undefined
'xxx' in obj && obj.xxx === undefined
- 注意 obj.xxx === undefined
不能断定 'xxx' 是否为 obj 的属性
类比: 你有没有卫生纸?
A: 没有 // 不含属性名
B: 有,但是没带 // 含有属性名,但是值为 undefined
程序员就是要严谨,没有就是没有,undefined 就是 undefined 绝不含糊。
5.2 查看所有属性
查看自身所有属性
Object.keys(obj)
查看自身+共有属性
console.dir(obj)
或者自己依次用 Object.keys 打印出 obj.proto
判断一个属性是自身的还是共有的
obj.hasOwnProperty('toString')
原型
每个对象都有原型
- 原型里存着对象的共有属性
比如 obj 的原型就是一个对象
obj.__proto__ 存着这个对象的地址
这个对象里有 toString / constructor / valueOf 等属性
- 对象的原型也是对象
所以对象的原型也有原型
obj = {}的原型即为所有对象的原型
这个原型包含所有对象的共有属性,是对象的根
这个原型也有原型,是 null
两种方法查看属性:
点语法:obj.key
中括号语法:obj['key']
坑新人语法:obj[key] // 变量 key 值一般不为 'key'
- 请优先使用中括号语法
点语法会误导你,让你以为 key 不是字符串 等你确定不会弄混两种语法,再改用点语法
- obj.name 等价于 obj['name']
- obj.name 不等价于 obj[name] 简单来说,这里的 name 是字符串,而不是变量。
let name = 'frank'
obj[name] 等价于
obj['frank']
而不是obj['name'] 和 obj.name
代码
let list = ['name', 'age', 'gender']
let person = {
name:'frank', age:18, gender:'man'}
for(let i = 0; i < list.length; i++){
let name = list[i]
console.log(person__???__)
}
使得 person 的所有属性被打印出来
选项:
console.log(person.name)
console.log(person[name])
5.3 修改或增加属性
直接赋值
let obj = {name: 'frank'} // name 是字符串
obj.name = 'frank' // name 是字符串
obj['name'] = 'frank'
obj[name] = 'frank' // 错,因 name 值不确定
obj['na'+'me'] = 'frank'
let key = 'name'; obj[key] = 'frank'
let key = 'name'; obj.key = 'frank' // 错 因为 obj.key 等价于 obj['key']
批量赋值
Object.assign(obj, {age: 18, gender: 'man'})
修改或增加共有属性
无法通过自身修改或增加共有属性
let obj = {}, obj2 = {} // 共有 toString
obj.toString = 'xxx' 只会在改 obj 自身属性
obj2.toString 还是在原型上
若偏要修改或增加原型上的属性:
obj.__proto__.toString = 'xxx' // 不推荐用 __proto__
Object.prototype.toString = 'xxx'
一般来说,不要修改原型,会引起很多问题
修改隐藏属性
不推荐使用 proto
let obj = {name:'frank'}
let obj2 = {name: 'jack'}
let common = {kind: 'human'}
obj.__proto__ = common
obj2.__proto__ = common
推荐使用 Object.create
let obj = Object.create(common)
obj.name = 'frank'
let obj2 = Object.create(common)
obj2.name = 'jack'
规范大概的意思是,要改就一开始就改,别后来再改