JS 对象基本用法

976 阅读8分钟

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'

规范大概的意思是,要改就一开始就改,别后来再改