[js行为研究] 对象作为对象的键会发生什么?

3,956 阅读4分钟

[写在前面]

本文主要是针对对象作为对象的键会发生什么这一问题, 记录分享自己思考和实践的轨迹。

[问题来源]

众所周知, 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]的背后是指向对象, 那么whiteblack是两个不同的对象, 就应该有两个[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]'] = '小黑鼠'

很容易可以看出对象whiteblack在作为对象的键时, 被转换成了字符串'[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"}

Mapfunction没有问题, 剩余的其他类型就不一一验证了

那么没有.toString()undefinednull呢? undefinednull

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

undefinednull分别变成了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]

我暂时就找到这三个, 存在其他的时机记得告诉我

[最后]

第一次写文章, 写的不好请见谅。其实很早就想写文章了,但是一直认为自己水平太差,害怕写不好。

但是最近遇到了某些事情,让我转变了想法,如果因为认为自己写不好就连开始都不敢,那大概这辈子都不会有机会了。

所以大家如果遇到喜欢的人一定要有勇气写下故事的第一集,这样后面才会有更多的剧情,无论是喜剧还是悲剧,至少不留遗憾。