从规范的角度解析对象 — 原始值转换

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 从规范的角度解析对象 — 原始值转换

640.png


对象 — 原始值转换


当对象相加 obj1 + obj2,相减 obj1 - obj2,或者使用 alert(obj) 打印时会发生什么?


在这种情况下,对象会被自动转换为原始值,然后执行操作。

类型转换 一章中,我们已经看到了数值,字符串和布尔转换的规则。但是我们没有讲对象的转换规则。现在我们已经掌握了方法(method)和 symbol 的相关知识,可以开始学习对象原始值转换了。


  1. 所有的对象在布尔上下文(context)中均为 true。所以对于对象,不存在 to-boolean 转换,只有字符串和数值转换。
  2. 数值转换发生在对象相减或应用数学函数时。例如,Date 对象(将在 日期和时间[1] 一章中介绍)可以相减,date1 - date2 的结果是两个日期之间的差值。
  3. 至于字符串转换 —— 通常发生在我们像 alert(obj) 这样输出一个对象和类似的上下文中。


ToPrimitive


我们可以使用特殊的对象方法,对字符串和数值转换进行微调。

下面是三个类型转换的变体,被称为 "hint",在 规范[2] 中有详细介绍(译注:当一个对象被用在需要原始值的上下文中时,例如,在 alert 或数学运算中,对象会被转换为原始值):


"string": 对象到字符串的转换,当我们对期望一个字符串的对象执行操作时,如 "alert":


// 输出
alert(obj);
// 将对象作为属性键
anotherObj[obj] = 123;


"number": 对象到数字的转换,例如当我们进行数学运算时:


// 显式转换
let num = Number(obj);
// 数学运算(除了二进制加法)
let n = +obj; // 一元加法
let delta = date1 - date2;
// 小于/大于的比较
let greater = user1 > user2;


"default": 在少数情况下发生,当运算符“不确定”期望值的类型时。


例如,二进制加法 + 可用于字符串(连接),也可以用于数字(相加),所以字符串和数字这两种类型都可以。因此,当二元加法得到对象类型的参数时,它将依据 "default" hint 来对其进行转换。


此外,如果对象被用于与字符串、数字或 symbol 进行 == 比较,这时到底应该进行哪种转换也不是很明确,因此使用 "default" hint。


// 二元加法使用默认 hint
let total = obj1 + obj2;
// obj == number 使用默认 hint
if (user == 1) { ... };


<> 这样的小于/大于比较运算符,也可以同时用于字符串和数字。不过,它们使用 "number" hint,而不是 "default"。这是历史原因。


实际上,我们没有必要记住这些奇特的细节,除了一种情况(Date 对象,我们稍后会学到它)之外,所有内建对象都以和 "number" 相同的方式实现 "default" 转换。我们也可以这样做。


没有 "boolean" hint请注意 —— 只有三种 hint。就这么简单。

没有 "boolean" hint(在布尔上下文中所有对象都是 true)或其他任何东西。如果我们将 "default""number" 视为相同,就像大多数内建函数一样,那么就只有两种转换了。


为了进行转换,JavaScript 尝试查找并调用三个对象方法:


  1. 调用 obj[Symbol.toPrimitive](hint "Symbol.toPrimitive") —— 带有 symbol 键 Symbol.toPrimitive(系统 symbol)的方法,如果这个方法存在的话,
  2. 否则,如果 hint 是 "string" —— 尝试 obj.toString()obj.valueOf(),无论哪个存在。
  3. 否则,如果 hint 是 "number""default" —— 尝试 obj.valueOf()obj.toString(),无论哪个存在。


Symbol.toPrimitive


我们从第一个方法开始。有一个名为 Symbol.toPrimitive 的内建 symbol,它被用来给转换方法命名,像这样:


obj[Symbol.toPrimitive] = function(hint) {
  // 返回一个原始值
  // hint = "string"、"number" 和 "default" 中的一个
}


例如,这里 user 对象实现了它:


let user = {
  name: "John",
  money: 1000,
  [Symbol.toPrimitive](hint "Symbol.toPrimitive") {
    alert(`hint: ${hint}`);
    return hint == "string" ? `{name: "${this.name}"}` : this.money;
  }
};
// 转换演示:
alert(user); // hint: string -> {name: "John"}
alert(+user); // hint: number -> 1000
alert(user + 500); // hint: default -> 1500


从代码中我们可以看到,根据转换的不同,user 变成一个自描述字符串或者一个金额。单个方法 user[Symbol.toPrimitive] 处理了所有的转换情况。


toString/valueOf


方法 toStringvalueOf 来自上古时代。它们不是 symbol(那时候还没有 symbol 这个概念),而是“常规的”字符串命名的方法。它们提供了一种可选的“老派”的实现转换的方法。


如果没有 Symbol.toPrimitive,那么 JavaScript 将尝试找到它们,并且按照下面的顺序进行尝试:

  • 对于 "string" hint,toString -> valueOf
  • 其他情况,valueOf -> toString

这些方法必须返回一个原始值。如果 toStringvalueOf 返回了一个对象,那么返回值会被忽略(和这里没有方法的时候相同)。

默认情况下,普通对象具有 toStringvalueOf 方法:

  • toString 方法返回一个字符串 "[object Object]"
  • valueOf 方法返回对象自身。

下面是一个示例:


let user = {name: "John"};
alert(user); // [object Object]
alert(user.valueOf() === user); // true


所以,如果我们尝试将一个对象当做字符串来使用,例如在 alert 中,那么在默认情况下我们会看到 [object Object]


这里提到默认值 valueOf 只是为了完整起见,以避免混淆。正如你看到的,它返回对象本身,因此被忽略。别问我为什么,那是历史原因。所以我们可以假设它根本就不存在。


让我们实现一下这些方法。


例如,这里的 user 执行和前面提到的那个 user 一样的操作,使用 toStringvalueOf 的组合(而不是 Symbol.toPrimitive):


let user = {
  name: "John",
  money: 1000,
  // 对于 hint="string"
  toString() {
    return `{name: "${this.name}"}`;
  },
  // 对于 hint="number" 或 "default"
  valueOf() {
    return this.money;
  }
};
alert(user); // toString -> {name: "John"}
alert(+user); // valueOf -> 1000
alert(user + 500); // valueOf -> 1500


我们可以看到,执行的动作和前面使用 Symbol.toPrimitive 的那个例子相同。

通常我们希望有一个“全能”的地方来处理所有原始转换。在这种情况下,我们可以只实现 toString,就像这样:


let user = {
  name: "John",
  toString() {
    return this.name;
  }
};
alert(user); // toString -> John
alert(user + 500); // toString -> John500


如果没有 Symbol.toPrimitivevalueOftoString 将处理所有原始转换。


返回类型


关于所有原始转换方法,有一个重要的点需要知道,就是它们不一定会返回 "hint" 的原始值。


没有限制 toString() 是否返回字符串,或 Symbol.toPrimitive 方法是否为 hint "number" 返回数字。


唯一强制性的事情是:这些方法必须返回一个原始值,而不是对象。


历史原因

由于历史原因,如果 toStringvalueOf 返回一个对象,则不会出现 error,但是这种值会被忽略(就像这种方法根本不存在)。这是因为在 JavaScript 语言发展初期,没有很好的 "error" 的概念。

相反,Symbol.toPrimitive必须 返回一个原始值,否则就会出现 error。


进一步的转换


我们已经知道,许多运算符和函数执行类型转换,例如乘法 * 将操作数转换为数字。

如果我们将对象作为参数传递,则会出现两个阶段:


  1. 对象被转换为原始值(通过前面我们描述的规则)。
  2. 如果生成的原始值的类型不正确,则继续进行转换。

例如:


let obj = {
  // toString 在没有其他方法的情况下处理所有转换
  toString() {
    return "2";
  }
};
alert(obj * 2); // 4,对象被转换为原始值字符串 "2",之后它被乘法转换为数字 2。
  1. 乘法 obj * 2 首先将对象转换为原始值(字符串 "2")。
  2. 之后 "2" * 2 变为 2 * 2(字符串被转换为数字)。

二元加法在同样的情况下会将其连接成字符串,因为它更愿意接受字符串:


let obj = {
  toString() {
    return "2";
  }
};
alert(obj + 2); // 22("2" + 2)被转换为原始值字符串 => 级联


总结


对象到原始值的转换,是由许多期望以原始值作为值的内建函数和运算符自动调用的。

这里有三种类型(hint):


  • "string"(对于 alert 和其他需要字符串的操作)
  • "number"(对于数学运算)
  • "default"(少数运算符)


规范明确描述了哪个运算符使用哪个 hint。很少有运算符“不知道期望什么”并使用 "default" hint。通常对于内建对象,"default" hint 的处理方式与 "number" 相同,因此在实践中,最后两个 hint 常常合并在一起。


转换算法是:


  1. 调用 obj[Symbol.toPrimitive](hint "Symbol.toPrimitive") 如果这个方法存在,
  2. 否则,如果 hint 是 "string"
  • 尝试 obj.toString()obj.valueOf(),无论哪个存在。
  1. 否则,如果 hint 是 "number" 或者 "default"
  • 尝试 obj.valueOf()obj.toString(),无论哪个存在。


在实践中,为了便于进行日志记录或调试,对于所有能够返回一种“可读性好”的对象的表达形式的转换,只实现以 obj.toString() 作为全能转换的方法就够了。

目录
相关文章
|
1月前
|
存储 编译器 C语言
C++类与对象深度解析(一):从抽象到实践的全面入门指南
C++类与对象深度解析(一):从抽象到实践的全面入门指南
48 8
|
1月前
|
Python
深入解析 Python 中的对象创建与初始化:__new__ 与 __init__ 方法
深入解析 Python 中的对象创建与初始化:__new__ 与 __init__ 方法
17 1
|
1月前
|
物联网 5G UED
|
2月前
|
设计模式 存储 人工智能
深度解析Unity游戏开发:从零构建可扩展与可维护的游戏架构,让你的游戏项目在模块化设计、脚本对象运用及状态模式处理中焕发新生,实现高效迭代与团队协作的完美平衡之路
【9月更文挑战第1天】游戏开发中的架构设计是项目成功的关键。良好的架构能提升开发效率并确保项目的长期可维护性和可扩展性。在使用Unity引擎时,合理的架构尤为重要。本文探讨了如何在Unity中实现可扩展且易维护的游戏架构,包括模块化设计、使用脚本对象管理数据、应用设计模式(如状态模式)及采用MVC/MVVM架构模式。通过这些方法,可以显著提高开发效率和游戏质量。例如,模块化设计将游戏拆分为独立模块。
180 3
|
2月前
|
JavaScript 前端开发 API
Javaweb之javascript的BOM对象的详细解析
BOM为Web开发提供了强大的API,允许开发者与浏览器进行深入的交互。合理使用BOM中的对象和方法,可以极大地增强Web应用的功能性和用户体验。需要注意的是,BOM的某些特征可能会在不同浏览器中表现不一致,因此在开发过程中需要进行仔细的测试和兼容性处理。通过掌握BOM,开发者能够制作出更丰富、更动态、更交互性的JavaWeb应用。
31 1
|
3月前
|
存储 数据采集 JSON
数据存储的正确规范:csv/xlsx和JSON全方位解析
数据存储的正确规范:csv/xlsx和JSON全方位解析
54 1
|
3月前
|
存储 JavaScript 前端开发
一文带你深度解析:JavaScript中对象与数组的威力究竟有多大?
一文带你深度解析:JavaScript中对象与数组的威力究竟有多大?
|
6月前
|
存储 算法 Java
滚雪球学Java(42):探索对象的奥秘:解析Java中的Object类
【5月更文挑战第17天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
53 2
滚雪球学Java(42):探索对象的奥秘:解析Java中的Object类
|
5月前
|
消息中间件 监控 安全
深入解析Linux命令ipcmk:IPC对象管理新视角
`ipcmk`非标准Linux命令,假设的IPC对象创建工具,用于演示如何管理消息队列、信号量和共享内存。虽无此命令,但理解其概念有助于掌握IPC管理。例如,创建命名消息队列`my_mq`,最大1000消息,可模拟使用`ipcmk -t mq -n my_mq -q 1000`。实际操作中,应根据需求选择合适IPC机制,设置安全权限,监控使用并及时清理。
|
5月前
|
存储 缓存 Java
Java对象内存布局深度解析
Java对象内存布局深度解析

推荐镜像

更多