前文说到,JavaScript里面除了基础的数据类型之外,剩余的就是对象类型了。那JavaScript的对象类型到底是怎么回事,本文来探讨一下。

object还是Object

看下面的例子:

typeof({"a": 1}) // object
typeof(Object()) // object
typeof(new Object()) // object

上面的例子中,第一行的{"a": 1}定义了一个对象。本质上,Javascript的对象是一个键值对集合(key-value pairs)。以{"a": 1}为例,这个对象包含了一个从"a"1的键值对。

在下面这个例子中,我们定义一个拥有更多键值对的对象:

var obj = {
  100 : "I'm number!",
  "hi" : "I'm string!",
  null : "I'm null!",
  false : "I'm boolean!",
  undefined: "I'm undefined!",
  nobody: "I'm nobody!",
}

上面的例子中可以看出,不同的基础数据类型都可以做对象的键(key)。重要的是,这些键值都是通过字符串的形式保存下来,访问的时候,也是以字符串的形式访问:

console.log(obj[100]) // I'm number!
console.log(obj["100"]) // I'm number!
console.log(obj["hi"]) // I'm string!
console.log(obj[null]) // I'm null!
console.log(obj["null"]) // I'm null!
console.log(obj[false]) // I'm boolean!
console.log(obj["false"]) // I'm boolean!
console.log(obj["undefined"]) // I'm undefined!
console.log(obj[undefined]) // I'm undefined!
console.log(obj["nobody"]) // I'm nobody!

在通过obj[...]的形式允许使用一个键访问对象里相应的值。上面的例子中,可以看到不同的类型都可以用来作为键。但是值得注意的是,对象里面存储的键必须是字符串类型的,所以obj[...]里面的访问符...都会变先转化为字符串,然后再去对象里面查找相应的键。对于null, false, undefined这些值作为访问符的话,javascript可以直接将他们转化为对应的字符串;而像100这种数字的值的话,需要将基础数据类型对象化,也就是转化为Number对象之后,再调用Number的toString方法将其转化为字符串。

通过Object.getOwnPropertyNames()方法可以获取一个对象所有的键:

// obj在之前的例子中定义
Object.getOwnPropertyNames(obj) // (6) ["100", "hi", "null", "false", "undefined", "nobody"]

object的prototype

javascript中的对象有prototype的概念。也就是说,如果A成为B的prototype,那么A中所有的方法和属性都可以被B所获取和使用。一个对象的prototype可以用属性__proto__来获取:

var obj = {}
obj.__proto__

从另外一个角度看,对象的__proto__属性决定了对象的行为。

在javascript里面,所有的对象都从Object派生出来的,所以Object.prototype是最基础的prototype。对象1的prototype可以是对象2,对象2的prototype可以是对象3,但归根结底,在这个链条的最后一环,则是Object.prototype

var obj = {}
obj.__proto__ === Object.prototype // true

var arr = [1,2,3] // an array
arr.__proto__ === Array.prototype // true
arr.__proto__.__proto__ === Object.prototype // true

object和数组

再javascript里面,数组其实也可以看成是键值对:

var obj = {0:1, 1:2, 2:3}
var arr = [1, 2, 3]
Object.getOwnPropertyNames(obj) //  ["0", "1", "2"]
Object.getOwnPropertyNames(arr) // ["0", "1", "2", "length"]

从上面的例子可以看出,javascript里面的数组,其实是以数字为键值的对象,并且额外带有一个length键来表示数组长度。使用下面这个办法,可以把上面例子中原始的对象obj改造成一个数组对象:

obj.length = 3
obj.__proto__ = Array.prototype

这样一来就可以在obj上使用Array的方法和属性了,比如:obj.shift()

object和Symbol类型

前面提到javascript对象是一个键值对集合,而且键的类型是字符串,值可以是任何类型。那javascript是如何将字符串的键映射到值的呢。这里需要使用到一个叫做hashmap的数据结构。hashmap先把字符串类型的键转为中间类型Symbol,再通过Symbol去查找最终的值,这个过程如下:

任意类型的键 -> 转化为字符串类型的键 -> 转化为symbol -> 查找最终的值

既然上面的步骤有点冗长,如果一个键值对是以数字类型为键,那么这个数字会被转化为一个Number对象,然后调用Number.toString()方法将这个数字转化为字符串,然后再将字符串转化为symbol,最后再查找值。

为了简化上述操作,ES6直接把symbol的一个新的基础数据类型提供给用户,这样你可以直接使用symbol作为对象的键,就省掉了前面一串过程:

var sym = Symbol(100)
var obj = {}
obj[sym] = "I'm 100!"
Object.getOwnPropertySymbols(obj) // [Symbol(100)]

symbol类型的键必须通过Object.getOwnPropertySymbols而不是Object.getOwnPropertyNames来获取。

参考链接

(系列完)