Skip to content
快速预览

类型转换

✍️ chunyi.wang 🕒 2023-07-17 09:58:22(10 months ago) 🔗 A.前端知识整理

按照正常理解来说原始类型并非对象类型,所以从理论上来说,它们是没有办法获取属性或者调用方法

下面案例name 是一个字符串类型,但是可以像对象一样使用调用length 方法

js
var  name = "Hello World"
console.log(name.length)

这就涉及到了类型转换

装箱转换

上面是因为JavaScript为了可以使其可以获取属性和调用方法,对其封装了对应的包装类型,常见的包装类型有:String、Number、Boolean、Symbol、BigInt类型

每一种基本类型 Number、String、Boolean、Symbol 在对象中都有对应的类,所谓装箱转换,正是把基本类型转换为对应的对象

抛开symbol 看其他类型的装箱转换

js
// Number
1
new Number(1)

// String
'aaa'
new String('aaa')

// Boolean
true
new Boolean(true)

特殊的Symbol 由于Symbol 类型是不支持new 来调用的,因此转换我们可以使用一个call来帮助 var symbolObject = (function(){ return this; }).call(Symbol("a"));

js
console.log(typeof symbolObject); //object
console.log(symbolObject instanceof Symbol); //true
console.log(symbolObject.constructor == Symbol); //true

当调用一个原始类型的属性或者方法时,会进行如下操作:

  1. 根据原始值,创建一个原始类型对应的包装类型对象
  2. 调用对应的属性或者方法,返回一个新的值
  3. 创建的包装类对象被销毁
js
// var name = "Hello World"
// console.log(name.length)
var name = new String("Hello World")
console.log(name.length)

注:JavaScript引擎会进行很多的优化,它可以跳过创建包装类的过程在内部直接完成属性的获取或者方法的调用,null、undefined没有任何的方法,也没有对应的对象包装类

拆箱转换

拆箱转换指的是将包装对象转换成对应的基本数据类型的过程

  • 在 JavaScript 标准中,规定了 ToPrimitive 函数,它是对象类型到基本类型的转换(即,拆箱转换)。
  • 拆箱转换会尝试调用 valueOf 和 toString 来获得拆箱后的基本类型。如果 valueOf 和 toString 都不存在,或者没有返回基本类型,则会产生类型错误 TypeError。

以数字的拆箱转换,将定义的对象 进行拆箱变为基本类型

  1. 当执行o*2 时候由于2 是数字因此先调用valueOf,没有值 就调用toString,也没值就报错
  2. 如果是 o+'' 就会先调用toString没值,调用valueOf 没值就报错
js
var o = {
    valueOf : () => {console.log("valueOf"); return {}},
    toString : () => {console.log("toString"); return {}}
}

o * 2
// valueOf
// toString
// TypeError

在 es6 开始允许对象通过显式指定 @@toPrimitive Symbol 来覆盖原有的行为,因此整个过程变成了先 @@toPrimitive Symbol =》valueOf 或者 toString toPrimitive

js
var o = {
    valueOf : () => {console.log("valueOf"); return {}},
    toString : () => {console.log("toString"); return {}}
}

o[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"}


console.log(o + "")
// toPrimitive
// hello

类型转换可以总结为下表

图 0

里面有些小细节除了上面整体按照 拆箱和 装箱的 逻辑外,在一些特殊情况略有不同

  • 需要注意:当 Number 绝对值较大或者较小时,字符串表示则是使用科学计数法表示的
js
console.log(String(1000000000000000000000000000000000000000))
打印结果
1e+39

触发类型转换的一些操作

  1. 运算相关的操作符包括 +、-、+=、++、* 、/、%、<<、& 等。
  2. 数据比较相关的操作符包括 >、<、== 、<=、>=、===
  3. 逻辑判断相关的操作符包括 &&、!、||、三目运算符

+ 号类型转换

  1. '+' 可表示数字计算,字符串拼接
    • 当加号两边一边是字符串时候为字符串拼接
    • 当加号两边为数字时候是数字计算
    • 加号两边有一边是对象的时候先调用 obj[Symbol.toPrimitive]('default')" => 没有这个属性,则再次valueOf=> valueOf获取的不是原始值,则继续toString,此时获取的结果是字符串,+就变为字符串拼接了' ,说明原始值像const a = {}; a.valueOf() 得到的是{ } 就是非基本类型原始值,则会继续调用toString
  2. '+' 只有一边 ,一般都是转换为数字,举个例子 +true 则为 1

更多的 Symbol.toPrimitive 案例说明

js
console.log(10 + [10]);
// 没有Symbol.toPrimitive -> valueOf获取的也不是原始值 -> 调用toString "10"  => "1010"
console.log(10 + {});
// 没有Symbol.toPrimitive -> valueOf获取的也不是原始值 -> 调用toString  "[object Object]"  => "10[object Object]"
console.log(10 + new Date());
// 调用日期的Symbol.toPrimitive('default') => "10Sun Jul 25 2021 11:28:37 GMT+0800 (中国标准时间)"
console.log(10 + new Number(10));
// 没有Symbol.toPrimitive -> valueOf 10 => 20
console.log(10 + new String('10'));
// 没有Symbol.toPrimitive -> valueOf "10" => "1010" 

// --------------------

注意案例 {}+1 直接在浏览器输入显示结果 为 1 原因是 此时{} 不代表对象而是'块' ,也就是只有 +1有自己含义,即为1

'==' 和 '==='

  1. '=='相等,两边数据类型不同,需要先转为相同类型,然后再进行比较

    • 对象==字符串 对象转字符串 「Symbol.toPrimitive -> valueOf -> toString」
    • null==undefined -> true null/undefined和其他任何值都不相等
    • 对象==对象 比较的是堆内存地址,地址相同则相等
    • NaN!==NaN
    • 除了以上情况,只要两边类型不一致,剩下的都是转换为数字,然后再进行比较的
  2. '==='绝对相等,如果两边类型不同,则直接是false,不会转换数据类型「推荐」

求a 满足a == 1 && a == 2 && a == 3

  1. 首先明确问题 即问题中使用的是双等,当双等时在和数字比较时候需要转换为数字,转换数字的过程会依次调用,即对这三种方法任意一种重写即可

    • Symbol.toPrimitive
    • valueOf 获取的非原始值则继续向下找
    • toString
  2. 利用对象基于数据劫持完成对应的操作,基于 var/function 声明的变量,其实可以看做 var a=? ==> window.a=?,即只要劫持window 对象中的a 即可,使用劫持'proxy' 或者'Object.defineProperty'

  3. 总结一个误区,比较时候调用是内部机制默认触发其对应方法行为,在这期间我错误思路记录我想让a 通过闭包连续调用则出现下面代码

js
var i = 0

// a 已经执行完了 固定是1
window.a = (function () {
    return ++this.i
})()

if (a == 1 && a == 2 && a == 3) {
    console.log('OK')
}
js
// 解决方案一:利用 == 比较的时候,会把对象转换为数字 Number(a)
//   + Symbol.toPrimitive
//   + valueOf 获取的非原始值则继续向下找
//   + toString
//   + 把字符串变为数字
var a = {
    i: 0,
}
a[Symbol.toPrimitive] = function toPrimitive() {
    // this -> a
    return ++this.i
}
if (a == 1 && a == 2 && a == 3) {
    console.log('OK')
}

var a = [1, 2, 3]
a.toString = a.shift
if (a == 1 && a == 2 && a == 3) {
    console.log('OK')
}

// 解决方案二:在全局上下文中,基于 var/function 声明的变量,并不是给VO(G)设置的全局变量「基于let/const声明的变量才是」,而是给GO(window)全局对象设置的属性      var a=?  ==> window.a=?
// 我们基于数据劫持完成对应的操作
var i = 0
Object.defineProperty(window, 'a', {
    get() {
        return ++i
    },
})
if (a == 1 && a == 2 && a == 3) {
    console.log('OK')
}

Released under the MIT License.