JavaScript类型转换(下):掌握 Object 类型 与 隐性 的转换逻辑😎

引言

写完 JS 类型转换(上)后发现这个知识点是庞大的,还是需要第二篇来进行补充的,所以让我们接着学习JS 的类型转换吧。

原始值转对象(Primitive to Object)

当我们想要将 Primitive 类型转为 Object 时,我们可以通过 new 运算符来显式地进行强转,例如:

javascript 复制代码
var a = 1.23;
console.log(typeof a); // Number 类型
var b = new Number(a); // Number 对象
console.log(typeof b); // Object

在上述代码中我们将通过new运算符将a强转为Number对象,后续通过typeof可以查看到bObject类型。

而据我们所知,在Number对象的prototype上有toFixed()这个方法,而Primitive类型本身上没有任何的方法和属性,所以当出现以下代码时,我们会认为console.log(a.toFixed(1));会报错。

javascript 复制代码
var a = 1.23;
var b = new Number(a);

console.log(b.toFixed(1));
console.log(a.toFixed(1));

但是当我们运行后会发现神奇的事情出现了:

并没有发生报错,而是两行代码都能执行,这是为什么呢?这里我们就需要了解 JS 中包装类这一机制。

包装类的概念:

JavaScript中"一切皆对象"

JavaScript的语言特性毫无疑问就是 "一切皆对象" ,而当我们越了解 JS,就会发现这一特性在无限放大,而包装类也是为了实现这一特性而为原始数据类型量身定做的机制。即:所有的数据类型都可以被视为对象。

尽管基本数据类型不是真正的对象,但为了方便操作,JavaScript允许我们像对待对象一样来使用它们。这意味着我们可以直接在原始值上调用方法或访问属性。

包装类的机制

当我们对原始值调用方法或访问属性时,JavaScript会临时创建一个对应的对象实例来执行该操作,执行完后立即销毁这个临时对象。

这样就能解释我们上述代码中为什么没有报错了

javascript 复制代码
var a = 1.23;
console.log(a.toFixed(1));
// 在对a执行toFixed()操作时,JS 临时创造了一个Number对象,这样a也就具有了Number对象的方法和属性
// 就像执行了:(new Number(a)).toFixed(),只是不用我们显示地转换

而且在执行后a仍然是number原始数据类型。

对象转原始值(Object to Primitive)

Object --> Boolean

对象转Boolean只需要注意一个点,即:"所有对象(包括数组和函数)转换为 Boolean 都为 true"。

例如:

javascript 复制代码
console.log(Boolean(new Boolean(false)))
// 我们可能惯性认为这会输出一个 false
// 但是我们包装了Boolean对象
// 而只要是对象,Boolean 强转就会是 true

console.log(Boolean({}))
console.log(Boolean([]))

执行后就可以得到以下结果:

Object --> String/Number

想要理解对象如何转换为StringNumber,我们必须跨过拦在转换路程上的"左右护法"---toStringvalueOf,让我们先单独理解一下两个函数。

toString:

想必我们对toString()的使用并不陌生,对于所有对象除了nullundefined 都包含这个方法,而这个方法在多种情况下自动调用,或者显式地使用 String() 函数或模板字符串来转换对象。

当我们调用对象的toString方法时,调用的是Object.prototype上的toString方法,而其会返还格式为:[object class]的结果,并且String() 函数也是相同的结果,例如:

javascript 复制代码
console.log(({a: 2}).toString()); // [object Object]
console.log(Object.prototype.toString.call({a: 2})); // [object Object]
console.log(({a: 2}).toString === Object.prototype.toString); // true

console.log(String({a: 2})); // [object Object]

当然也可以通过call()来精确返还内容的类型

javascript 复制代码
console.log(Object.prototype.toString.call({a: 1})) // [object Object]
console.log(Object.prototype.toString.call([1, 2])) // [object Array]

但是许多内置对象重写了 toString() 方法,以提供更有意义的字符串表示形式,但是这些方法仍然来自于Object.prototype

  • 数组:返还以逗号间隔的数组元素组成的字符串
  • 日期:返还日期和时间的字符串表示
  • 函数:返回函数的源代码字符串

例如执行以下代码:

javascript 复制代码
console.log([1,2,3].toString());
console.log((new Date(2024,12,18)).toString());
console.log((function() {var a = 1;}).toString());

console.log(Array.prototype.__proto__ === Object.prototype) // true

valueOf:

在默认情况下,valueOf() 方法返回对象本身,但对于大多数内置对象,valueOf() 提供了一个更有意义的原始值表示形式。

例如:

javascript 复制代码
console.log(({a: 1}).valueOf()); // {a: 1}
console.log([1,2,3].valueOf()); // [1,2,3]
console.log(("a").valueOf()); // a
console.log((new Date(2024,12,18)).valueOf()); // 1737129600000

唯一值得注意的是对于Date返还的是该日期对象所代表的时间戳(timestamp),其表示自1970年1月1日00:00:00 UTC(协调世界时)以来的毫秒数。(可以理解为 JS 认为转换为毫秒更利于精确地表示和计算时间)

正片开始:

当你理解了上述的两个函数后,我们才能真正开始学习对象转String和Number,因为在转换操作时,我们主要就是关注这两个方法。

Object --> Number:

当我们在对象字面量里面添加 valueOf 和 toString 方法时,会优先使用本身的方法,而不去寻找原型链上的方法。

例如:

javascript 复制代码
let specialObj = {
    valueOf: function() {
        console.log("Calling valueOf...")
        return 123;
    },
    toString: function() {
        console.log("Calling toString...")
        return "hello";
    }
}
console.log(specialObj.valueOf()); // 123
console.log(specialObj.toString()); // NaN

在我们自行创建这两个方法后,那我要是进行转换为Number对象会发生什么事吗?

javascript 复制代码
console.log(Number(specialObj));

执行后我们得到以下结果:

我们发现并没有执行toString的操作,这是因为在执行转化Number时,valueOftoString的优先级高,会先调用valueOf方法,而执行valueOf已经可以得到一个合理的返还了,所以并不需要再执行toString了,但是当我们将valueOf的返还值改为this呢?

javascript 复制代码
valueOf: function() {
    console.log("Calling valueOf...")
    return this;
},

当我们再执行转换Number对象时,我们得到了以下的结果:

我们发现在执行valueOf后并没有得到一个有效的值,所以会再执行toString操作,返还了NaN。

哎🧐👆,在我脑海中产生了一个大胆的想法,如果我们将toString的返还值也改为this后会发生什么事情呢?

javascript 复制代码
toString: function() {
    console.log("Calling toString...")
    return this;
}
//为了保险起见,我们使用try catch
try {
    Number(specialObj)
} catch (e) {
    console.log(e)
}

结果也不出所料,我们得到了一个大大的报错,所以我们可以得出结论,当toString和valueOf方法都返回对象本身时,会抛出异常。

通过上述的操作后,我们就可以得出Object转Number的一个完整流程:

    1. 如果这个对象具有 valueOf 方法,并且可以返回一个原始值,JS 就会将这个原始值转换为数字并返回这个数字
    1. 否则,如果对象具有 toString 方法,并且可以返回一个原始值,则 JS 就会将其转换并返回。
    1. 当1和2都行不通的时候,就会报错:cannot convert object to primitive value

Object --> String

同理,其实转String和转Number很相似,只是优先级不同,显然toString对于转String更重要一点,所以转String为以下步骤:

    1. 如果这个对象具有 toString 方法,并且可以返回一个原始值,JS 就会将这个原始值转换为数字并返回这个数字
    1. 否则,如果对象具有 valueOf 方法,并且可以返回一个原始值,则 JS 就会将其转换并返回。
    1. 当1和2都行不通的时候,就会报错:TypeError: Cannot convert object to primitive value

传统 VS es6新增的Symbol.toPrimitive

在上文中我们讲述了在es6之前我们传统的转换,但是从ES6,引入了一个新的方法------Symbol.toPrimitive。其允许我们自定义对象到原始值的转换行为。并且在执行转换时,其优先级是高于传统的toStringvalueOf的。

在引入 Symbol.toPrimitive 之前,有时会由于 toString()valueOf() 的行为不够明确,导致意外的结果,特别是在不同的上下文中。因此,Symbol.toPrimitive 提供了一种更为直观和可控的方法来定义对象到原始值的转换规则。

自定义 toPrimitive 转换

toPrimitive 内部方法接收一个提示(hint),这个提示可以是 "string""number""default"。而根据提供的提示,它会决定如何选择合适的对象方法来获取原始值。当然我们可以自定义一个toPrimitive来改变转换的逻辑。这样就可以更精确地控制对象在不同上下文中的行为。

例如:

javascript 复制代码
let obj = {
  [Symbol.toPrimitive](hint) {
    if (hint === 'string') {
      return '[object Custom]';
    } else {
      return 42;
    }
  },
  toString() {
    return '[object Object]';
  },
  valueOf() {
    return 43;
  }
};

console.log(String(obj)); // "[object Custom]" 使用了 Symbol.toPrimitive 并传递了 'string' 提示
console.log(Number(obj)); // 42                使用了 Symbol.toPrimitive 并传递了 'number' 提示

补充:

在我学习这段知识点时,一直有一个疑惑,我们了解有toStringvalueOfSymbol.toPrimitive,但是如果在对象字面量里面包含其他方法,那么其他方法不会对返还值产生影响吗?

而最后得到的结果就是:

在JavaScript中,当需要将一个对象转换为原始类型时,会按照以下步骤进行判断:

  1. 检查Symbol.toPrimitive:如果对象实现了Symbol.toPrimitive,则直接调用它,并依据提示返回相应的原始值。
  2. 如果没有Symbol.toPrimitive,则按照传统的toPrimitive内部机制工作:
    • 对于string提示,先尝试toString,若未成功再试valueOf
    • 对于number或"default"提示,先尝试valueOf,若未成功再试toString
  3. 如果 toStringvalueOf 都不行,就报错:cannot convert object to primitive value

即使我们在对象字面量添加其他方法,但不是Symbol.toPrimitive,那么这个方法不会直接影响到对象到原始值的转换过程。

隐式类型转换:

在JavaScript中,我们日常编程有几种常见的隐式类型转换情况,想必大家并不陌生,例如:

  • if语句或循环的条件中,一些值会被转换为Boolean类型。
  • 在使用比较运算符时,有可能会将部分内容转换为数值比较。
  • 在使用+运算符时,不单单进行加法运算,还有字符串拼接也会发生隐式转换。

而这里我们重点介绍使用+运算符和==运算符的情况。

一元运算符:

"+"运算符在进行一元运算时有个特性,那就是会默认进行一次toNumber转换

我们先来看最简单操作:

javascript 复制代码
console.log(+ '12'); // 12
console.log(+ [1,2,3]); // NaN

很明显,默认对'12'[1,2,3]进行了toPrimitive操作,'12' => 12,而[1,2,3] => 1,2,3,由于1,2,3不是一个有效的数字符串,那么返还NaN,到这里还是很容易理解的。

那么再来看看以下操作:

javascript 复制代码
console.log(+[]);         //a
console.log(+['1']);      //b
console.log(+['1,2,3']);  //c
console.log(+['1,']);     //d
console.log(+{});         //e

是不是开始头皮发麻了,这啥玩意呀?不急,让我们一个个来分析。

  • +[]操作:
    • a、b、c、d我们可以归为一类,都是与数组操作,而当数组进行toPrimitive操作时,会先调用valueOf,观察a、b、c、d,我们发现只有b是最有希望变为数字的,所以b返还的是1
    • 再来看a、c、d,都需要进行toString操作,分别变成a: ""c: "1,2,3"d: "1,",在这其中我们发现,a是空字符串,那么就会进行隐式的Number("")转换,得到0,而其他两个进行隐式的Number转换实在没办法变成有效的数字,那么就返还NaN
  • +{}操作:
    • 一个空对象?管他什么对象我们没有对其进行诱导那么就先看能不能执行valueOf,然后就无功而返了,老老实实执行toString操作,就会得到返还[object Object](可不要忘了前面讲的对象的toString操作是返还格式为:[object class]的结果哦),然后发现Number("[object class]")只能返还NaN了。
    • 给个流程图:{} ==(valueOf)==> {} ==(toString)==> [Object object] => NaN

所以我们最后得到的是:

javascript 复制代码
console.log(+[]);         //0
console.log(+['1']);      //1
console.log(+['1,2,3']);  //NaN
console.log(+['1,']);     //NaN
console.log(+{});         //NaN

和你一开始思考的答案一样吗🫡?

二元运算符:

哎~又是"+"运算符,而当"+"运算符两端只要有一端不是数字,"+"就是做字符串拼接

但是我们有了一元运算符的前车之鉴,直接给它秒了:

javascript 复制代码
console.log([] + []);
console.log([] + {});
console.log({} + {});

首先,管他三七二十一,你只要不是Primitive类型,先给我去进行toPrimitive操作再说(新概念神了),[]进行toString变为""{}也进行toString变为"[object Object]",这步完成了,接下来就是最简单的字符串拼接了。所以答案为:

javascript 复制代码
console.log([] + []); // (空)
console.log([] + {}); // [object Object]
console.log({} + {}); // [object Object][object Object]

当然如果都数字那就是正常的加法了。

==运算符:

在进行==运算符操作的规范实在太多了,为方便各位记忆,就分为以下情况。

  1. 相同类型比较

    • 如果xy是同一类型,则直接根据类型的特定规则进行比较:
      • Undefined 和 Null :如果两者都是undefinednull,返回true
      • Number
        • 如果xyNaN,返回false。(不理解请看面试基础之JS类型转换(上)😎
        • 如果xy都是+0-0,无论符号如何,都视为相等,返回true
        • 否则,如果x等于y,返回true;否则返回false
      • String :完全相等则返回true,否则返回false
      • Boolean :只有当两者都为true或都为false时,返回true
      • Object :只有当xy引用同一个对象实例时,返回true
  2. 不同类型的比较

    • Null 和 Undefined :如果一个为null另一个为undefined,返回true
    • Number/String
      • 如果有一端是Number,那么就将另一端进行toNumber操作。
      • 如果有一端是NaN,那么就为false
    • Boolean与其他类型
      • 如果是Boolean,那么就默认进行toNumber操作。
    • Object与其他类型
      • 如果x不是字符串或数字而y是对象,判断x == ToPrimitive(y)
      • 如果x是对象而y不是字符串或数字,判断ToPrimitive(x) == y
  3. 默认情况

    • 如果上述所有条件都不满足,则返回false

看完了吗?看完了就开始紧张刺激的实战了哦~

两端相同:

先来几个两端类型相同的开开胃

javascript 复制代码
console.log([] == []);
console.log(NaN == NaN);
console.log(+0 == -0);

显而易见,由于两个数组的地址不同,所以不等;NaN 只要存在在一端就不等;+0和-0在==运算符看来是一样的(但是使用Object.is(+0, -0)就是不等哦~它更严谨) 所以结果为:

javascript 复制代码
console.log([] == []); // false
console.log(NaN == NaN); // false
console.log(+0 == -0); // true
两端不同:

接下来就是两端不等的了,看点稍微刁钻一点的:

javascript 复制代码
console.log(true == '6');       // a
console.log(null == undefined); // b
console.log(2 == ['2']);        // c
console.log(false == []);       // d
console.log("" == [null])       // e

我们一个个来分析

  • a:首先当我们看到有Boolean类型时,就直接将其转换为Number就行了,那么就变成了1 == '6' 也就是1 == 6当然是不同的 ===> false
  • b:只要是nullundefined进行比较,那么就是相等 ===> true
  • c:首先将对象进行toPrimitive操作,['2']变为了'2',式子也就变成了2 == '2',也就是2 == 2,显然相同 ===> true
  • d:先将false变为0,即0 == [],而[]进行toPrimitive操作后变为了"",再由于是与Number进行比较,所以对""进行隐式的Number转换,得到0,即0 == 0,显然相同 ===> true
  • e:先将[null]进行toPrimitive操作变为了"",即"" == "",显然相同 ===> true

所以结果为:

javascript 复制代码
console.log(true == '6');       // false
console.log(null == undefined); // true
console.log(2 == ['2']);        // true
console.log(false == []);       // true
console.log("" == [null])       // true

最后再来看一个不太一样的例子:

javascript 复制代码
let x = 4;
let y = {
    valueOf: function() {
        return 4;
    }
}
console.log(x == y);

聪明的小伙伴应该已经把这题秒了,答案是true,对象字面量要进行valueOf操作变为4,即:4 == 4显然相同。

---欢迎各位点赞、收藏、关注,如果觉得有收获或者需要改进的地方,希望评论在下方,不定期更新

相关推荐
子兮曰12 分钟前
DeepSeek TUI:原生 Rust 打造的终端 AI 编码 Agent
前端·javascript·后端
暗不需求21 分钟前
# 深入 React Todos:从零实现一个状态提升与本地持久化的待办应用
javascript·react.js·全栈
子兮曰28 分钟前
深入 Superpowers:180k Stars 的开源 AI 编程方法论是如何工作的
前端·javascript·后端
隔壁的大叔1 小时前
Markdown 渲染如何穿插自定义组件
前端·javascript·vue.js
薯老板1 小时前
JavaScript原型,原型链
javascript
愚者Pro2 小时前
Flutter基础学习
前端·javascript·vue.js
时光足迹2 小时前
Tiptap 简单编辑器模版
前端·javascript·react.js
吴声子夜歌2 小时前
Vue3——使用Mock.js
javascript·vue·mock.js
时光足迹2 小时前
ThreeJS之GUI控制器
前端·javascript·three.js
时光足迹2 小时前
Tiptap编辑器
前端·javascript·react.js