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显然相同。

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

相关推荐
我码玄黄2 小时前
JS设计模式之中介者模式
javascript·设计模式·中介者模式
xue03052 小时前
react自定义hooks函数
javascript·react.js
前端熊猫3 小时前
组件十大传值
前端·javascript·vue.js
GISer_Jing3 小时前
React 工具和库面试题(一)
javascript·react.js·ecmascript
oumae-kumiko3 小时前
【JS/TS鼠标气泡跟随】文本提示 / 操作提示
前端·javascript·typescript
YG·玉方3 小时前
键盘常见键的keyCode和对应的键名
前端·javascript·计算机外设
我码玄黄4 小时前
在THREEJS中加载3dtile模型
前端·javascript·3d·threejs
悠悠华4 小时前
使用layui的table提示Could not parse as expression(踩坑记录)
前端·javascript·layui
JohnYan4 小时前
对非对称加密的再思考
javascript·后端·安全