[写在前面]
本文主要是针对对象作为对象的键会发生什么这一问题, 记录分享自己思考和实践的轨迹。
[问题来源]
众所周知, ES6
有一种新的数据结构叫做 Map
,他跟对象一样, 是存的也是键值对(key => value)
,但是Map
技高一筹,他的 key
既可以是基本数据类型,也可以是引用数据类型。
那么问题来了, 对象的key
难道就不能是引用数据类型吗?接下来我们动手来验证。
[动手实验]
说明: 本次测试代码是在
chrome
浏览器控制台下运行的, 所以在打印变量时直接输入变量, 省略了console.log()
开门见山
首先我们直接把一个对象作为另一个对象的键:
let obj = {} // 对象容器
const white = { name: '阿白' } // 这次实验的小白鼠, 是一个对象
obj[white] = '小白鼠' // 把小白鼠的身份证(引用)放进去
obj[white] // "小白鼠"
表面上看起来没什么问题,但实际上如果打印obj
:
obj // {[object Object]: "小白鼠"}
作为键的对象white
已经不知所踪, 取而代之的是完全陌生的[object Object]
,那么这个[object Object]
究竟是何物?
分析[object Object]
发现给对象扩展(新增)属性的外壳[]
还在, 但一个首字母小写的object
+ 一个首字母大写的Object
, 似乎是在暗示着这里面是一个对象, 那么会不会存在这样一种可能性:
控制台打印对象时, 由于键名显示不了对象, 用来这个符号来指向white
对象呢?
验证猜想
于是我叫来了小白鼠的兄弟小黑鼠
obj // {[object Object]: "小白鼠"}
const black = { name: '阿黑' }
obj[black] = "小黑鼠"
obj // {[object Object]: "小黑鼠"}
obj
对象并没有扩展出 一个新的属性, 依然只有一个属性, 键名是[object Object]
, 但是对应的值从小白鼠
替换成了小黑鼠
目测是对象的属性被重新赋值
如果[object Object]
的背后是指向对象, 那么white
和black
是两个不同的对象, 就应该有两个[object Object]
, 而事实上只有一个, 这就说明之前的猜想是错误的, [object Object]
可能只是一个单纯的字符串string
对obj
仅有的这一个键进行类型判断:
Object.keys(obj) // ["[object Object]"]
typeof Object.keys(obj).pop() // "string"
事实证明果真如此
寻找看不到的手
首先来梳理一下刚刚的流程
原代码如下:
obj[wihte] = '小白鼠'
obj[black] = "小黑鼠"
实际代码相当于:
obj['[object Object]'] = '小白鼠'
obj['[object Object]'] = '小黑鼠'
很容易可以看出对象white
和black
在作为对象的键时, 被转换成了字符串'[object Object]'
对象转换成字符串? 那岂不是...
JSON.stringify()
: 你们看我干嘛??
Object.prototype.toString()
: 没错,正是在下!
.toString()
方法
首先.toString()
不止是对象prototype
上的方法
// 各种类型的 toString()
({}).toString() // "[object Object]"
new Map().toString() // "[object Map]"
new Set().toString() // "[object Set]"
new WeakMap().toString() // "[object WeakMap]"
new WeakSet().toString() // "[object WeakSet]"
(v => v).toString() // "v => v"
(996).toString() // "996"
'2sheng'.toString() // "2sheng"
true.toString() // "true"
Symbol('2s').toString() // Symbol(2s)"
NaN.toString() // "NaN"
// 数组比较特别, 会对每个元素进行toString, 用逗号得开
[].toString() // ""
[1,'s',{},[]].toString() // "1,s,[object Object],"
// undefined 和 null 没有 toString() 方法
验证是否调用了 .toString()
一个正常Object
对象调用 .toString()
后返回 '[object Object]'
, 现在我们来
改写.toString()
方法, 让其返回其他值, 然后观察作为对象键时键名是否被改变
obj = {}
function myToString() {
return this.name
}
white.toString = myToString // white = { name: '阿白' }
black.toString = myToString // black = { name: '阿黑' }
obj[white] = '小白鼠'
obj[black] = '小黑鼠'
obj // {阿白: "小白鼠", 阿黑: "小黑鼠"}
果然改写了.toString()
后键名也改变了, 证明确实是调用了.toString()
验证对象外的其他类型也会默认调用.toString()
obj = {}
obj[new Map()] = 'I am Map'
obj[(v => v)] = 'I am function'
obj // {[object Map]: "I am Map", v => v: "I am function"}
Map
和function
没有问题, 剩余的其他类型就不一一验证了
那么没有.toString()
的 undefined
和 null
呢?
undefined
和 null
obj = {}
obj[undefined] = 'I am undefined'
obj[null] = 'I am null'
obj // {undefined: "I am undefined", null: "I am null"}
obj['undefined'] === obj[undefined] // true
obj['null'] === obj[null] // true
undefined
和null
分别变成了string
类型的'undefined'
和'null'
[总结]
obj[key] = value
实际上是
function processingKey(key) {
if (key === undefined) {
return 'undefined'
} else if (key === null) {
return 'null'
} else {
return key.toString()
}
}
obj[processingKey(key)] = value
[番外]
1. Symbol.toStringTag
Symbol.toStringTag
顾名思义, 修改对象在.toString()
时候的tag
原始的.toString()
方法返回的是'[object `${tag}`]'
, 这个 tag
是活的, 默认是Object
, 通过 [Symbol.toStringTag]
修改, Map
,Set
等就是设置了该属性:
new Map()[Symbol.toStringTag] // "Map"
new Set()[Symbol.toStringTag] // "Set"
验证.toString()
内部使用了[Symbol.toStringTag]
属性
obj = {}
const purple = { name: '阿紫' }
purple[Symbol.toStringTag] = 'Symbol.toStringTag'
obj[purple] = '第一次'
purple.toString = function() {
return 'toString'
}
obj[purple] = '第二次'
obj // {[object Symbol.toStringTag]: "第一次", toString: "第二次"}
2. Symbol.toPrimitive
实际上有让对象作为属性不执行.toString()
的方法存在, 利用
Symbol.toPrimitive
这里只利用了
Symbol.toPrimitive
转成string
的情况, 它还可以描述转成number
的情况,具体参考 文档
obj = {}
obj[black] = '是我'
black[Symbol.toPrimitive] = function(hint) {
return '嘿嘿嘿'
}
obj[black] = '又是我'
obj // {阿黑: "是我", 嘿嘿嘿: "又是我"}
当一个对象o
被转成string
类型时, 如果该对象存在Symbol.toPrimitive
属性的方法, 会调用o[Symbol.toPrimitive]()
, 否则调用.toString()
Symbol.toPrimitive]() > .toString()
3.补: 对象转成字符串行为
以对象o
为例
- 用
String()
方法转换时,String(o)
- 在字符串模板中被解析时
${o}
- 作为对象的键
obj[o]
或者 数组的下标arr[o]
我暂时就找到这三个, 存在其他的时机记得告诉我
[最后]
第一次写文章, 写的不好请见谅。其实很早就想写文章了,但是一直认为自己水平太差,害怕写不好。
但是最近遇到了某些事情,让我转变了想法,如果因为认为自己写不好就连开始都不敢,那大概这辈子都不会有机会了。
所以大家如果遇到喜欢的人一定要有勇气写下故事的第一集,这样后面才会有更多的剧情,无论是喜剧还是悲剧,至少不留遗憾。