ECMASCript 类型转换 上

163 阅读6分钟

前言

ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。

通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。

欢迎关注和订阅专栏 重学前端-ECMAScript协议上篇

Why?

JavaScript 是弱类型语言,一个变量的值可以是任何数据类型。

当不确定的A需要进行某些运算或者操作:

var A = {
  age: 10,
  valueOf(){
    return this.age
  }
};

+A;
`${A}` ;
A[A] = A;

或者A遇到了B,他们之间擦出一些火花

var A = {
  age: 10,
  valueOf(){
    return this.age
  }
};
var B = {
  name: 'age',
  toString(){
    return this.name
  }
};

A <= B;
A + B;
A[B];

为了达成目的或者一致的目的,A,B需要做一些转变,这就是类型转换。

其实转换还分为两种,显式转换和隐式转换。

显式转换

就是明示,要转换成啥,使用某些方法明确告诉解释器将值从一种类型转换为另一种类型。

parseInt("10");
({name:"name"}).toString();

隐式转换

唉,转成什么看场景。 开头的示例都算是。

隐式转换里面最重要的两个转换:

转为字符串 其底层又调用了 转为原始值, 所以ToPrimitive ( input [ , preferredType ] ) 可以说是最为重要的转换。

实际上,协议内部定义了20+种类型转换。

汇总

协议7.1章节列出了各种类型转换,先看一个总表。会详细讲其中的几个。

方法作用场景
ToPrimitive ( input [ , preferredType ] )转为原始值。关系操作,数据类型转换等等
ToBoolean ( argument )转为布尔值
ToNumeric ( value )转为数值(BigInt和Number)
ToNumber ( argument )转为数字(Number)
ToIntegerOrInfinity ( argument )转为整数或者无穷
ToInt32 ( argument )转为32位整数parseInt, 位移操作,位运算等等时有使用到。
ToUint32 ( argument )转为32无符号整数位移操作,数组长度设置,属性描述度定义等有使用到。
ToString ( argument )转为字符串
ToObject ( argument )转为对象
ToPropertyKey ( argument )转为属性键,字符串或者Symbol
ToLength ( argument )转为整数。用作类数组对象长度的整数
CanonicalNumericIndexString ( argument )转为规范数值索引字符串字符串,整数索引的特异对象。
ToIndex ( value )转为整数索引BigInt, ArrayBuffer,SharedArrayBuffer , DataView等
ToBigInt ( argument )转为BigInt关系操作(大于小于等等),原子操作等
StringToBigInt ( str )字符串转BigInt关系操作(大于小于等等)
ToInt16 ( argument )转为16位整数Int16Array等
ToUint16 ( argument )转为16位无符号整数String.fromCharCode,Uint16Array等
ToInt8 ( argument )转为8位整数Int8Array等
ToUint8 ( argument )转为8位无符号整数Uint8Array等
ToUint8Clamp ( argument )转为0~255的无符号整数Uint8ClampedArray等
ToBigInt64 ( argument )转为64位的BigIntBigInt64Array, 原子操作等
ToBigUint64 ( argument )转为64位的无符号BigIntBigUint64Array

ToPrimitive ( input [ , preferredType ] )

转为原始值。

场景

  1. 关系比较, 比如 >=, <等
  2. 宽松相等 ==
  3. 二元 +
  4. BigInt, Date实例化, Date.prototype.toJSON
  5. 其他转换,例如 ToNumber ( argument ), ToNumeric ( value )ToBigInt ( argument )ToString ( argument )ToPropertyKey ( argument )
  6. 其他场景
var obj = {
  name: "name",
  value: 1684137112653,
  toString(){
    return this.name
  },
  valueOf(){
    return this.value
  }
}

// 宽松比较
obj == "name";              // false
// 关系比较
obj >= 1684137112653        // true
// 二元加
obj + obj                   // 3368274225306
// 实例化Date
new Date(obj)               // Mon May 15 2023 15:51:52 GMT+0800 (China Standard Time)

// 属性键
obj[obj] = "obj name"
console.log(obj["name"])    // "obj name"

逻辑

先看参数

参数名说明备注
input需要被转换的值。可以是原始值也可以是Object
preferredType期望被转换成的类型string 或者 number内部还有一个default的值选项

如果是对象,对象上是否定义Symbol.toPrimitive 也会影响转换逻辑。

基本流程如下:

  1. 如果 input 是 Object
    • 对象如果定义了 Symbol.toPrimitive 方法
      • 如果 referredType 未定义,设置 hint 为 default
      • 否则, 如果 preferredType 为 string,设置 hint 为 string
      • 否则
        • 如果是number , hint 为 number
        • 否则抛出异常
      • result = input[Symbol.toPrimitive](hint)执行结果
      • 如果 result 不是对象,返回result
      • 抛出异常
    • 如果 preferredType 未定义,设置 preferredType 为 number
    • 调用自身方法进行转换
      • 如果preferredType 是 string, 依次检查并调用对象的 toString, valueOf 方法,如果某个返回的是原始值,就直接返回。**
      • 否则 依次检查并调用 对象的valueOf, toString方法,如果某个返回的是原始值,就直接返回。
      • 抛出异常
  2. 直接返回input

协议内部有两个方法组成

Symbol.toPrimitive

不是对象的时候,很简单直接返回。 复杂的地方就是参数是对象时,是对象又分为两种情况

  • 定义了 Symbol.toPrimitive 方法,会直接调用返回该方法,并返回执行结果
  • 未定义 Symbol.toPrimitive 方法,关键在于 preferredType,
    • 如果 string, 先尝试调用 toString, 然后是 valueOf,
    • 否则,先尝试调用 valueOf, 然后是toString

定义了 Symbol.toPrimitive

var obj = {
  name: "name",
  value: 100,
  [Symbol.toPrimitive](preferredType = 'default'){
    if(preferredType === "string"){
      return this.name;
    }
  	return this.value
  }
};

obj[Symbol.toPrimitive]();    // 100
// preferredType=string
`${obj}`                      // "name"
1 + obj;                      // 101
// preferredType=string
"1" + obj;                    // "1001"

obj.value = Date.now();
new Date(obj);               //  Mon May 15 2023 16:39:04 GMT+0800 (China Standard Time)

未定义 Symbol.toPrimitive

var obj = {
  name: "name",
  value: 100,
  valueOf(){
    return this.value;
  },
  toString(){
    return this.name
  }
};

// preferredType=string 调用顺序toString, valueOf
`${obj}`                      // "name"
// 调用顺序valueOf,toString
1 + obj;                      //  101
// preferredType=string 调用顺序toString, valueOf
"1" + obj;                    // "1001"

obj.value = Date.now();
// 调用顺序valueOf,toString
new Date(obj);               //  Mon May 15 2023 16:39:04 GMT+0800 (China Standard Time)

注意了,这里是定义valueOf 以及 toString方法,如果未定义,会按照顺序调用下一个。

未定义toString 或者 valueOf

var obj = {
  name: "name",
  value: 100,
  valueOf(){
    return this.value;
  }
};

delete Object.prototype.toString

//调用valueOf
  
`${obj}`                      // "100"
1 + obj;                      //  101
"1" + obj;                    // "1100"

obj.value = Date.now();
new Date(obj);    

preferredType

preferredType: number, string或者是default。

如果未自定义Symbol.toPrimitive方法,preferredType的值default和number逻辑是一样的,方法调用顺序是 valueOf ,toString。

那么如果能清晰的知道,哪些地方的 preferredType的值是 string,是不是就简化了需要记忆的东西。

preferredType为string的场景

  1. 转为属性键的时候,包括排序属性
  2. 字符串模板
  3. parseFloat, parseInt , decodeURI , encodeURI, new Function, eval, Symbol 等等各种方法,期望传入的是字符串,而传入的是对象时
  4. 其他情况
var obj = {
  name:'2',
  value: 0,
  toString(){return this.name},
  valueOf(){return this.value}
};


// 作为属性键
obj[obj]                  // "2"
// 模板字符串
`我的名字是${obj}`         // "我的名字是2"

// 期望是字符串
parseFloat(obj);          //  2
Symbol(obj)               // Symbol(2)
new Error(obj)            // Error: 2
"0-".concat(obj)          // 0-2

注意事项

  1. 转为原始值的过程是可能抛出TypeError的
    1. 比如对象和原型上没有 Symbol.toPrimitive, toString,valueOf三个方法
    2. 比如 Symbol.toPrimitive, toString,valueOf 三个返回的都是对象
  2. 转为原始值的操作在协议内部出现频次非常高,尤其是间接的出现,比如后面的ToNumber ( argument ), ToNumeric ( value )ToBigInt ( argument )ToString ( argument )ToPropertyKey ( argument ) 等,务必掌握。

ToString(argument)

很多时候,需要把值转为字符串,该方法定义了转换逻辑。

如果是argument是对象,会调用上面的 ToPrimitive ( input [ , preferredType ] ) 转为原始值,然后再调用 ToString(argument)

场景

  1. 二元加法,当一边是字符串时
  2. 属性键
  3. 模板字符串
  4. parseFloat, decodeURI/encodeURI, decodeURIComponent/encodeURIComponent, Symbol调用, Symbol.for, new Error, Number.prototype.toFixed, Number.prototype.toPrecision, String调用, String.prototype.endsWith等等期望传入的参数是字符串的方法。
  5. 其他场景
var obj = { 
   age: 10,
   toString(){
     return "10"
   }
};


10 + "10"                 // "1010"

// 二元加 , 一边是字符串
"str" + obj               // 'str10'
"str" + Symbol.for("cc")  //  Cannot convert a Symbol value to a string
  

// 属性键
var obj2 = {
  "10": "obj"
}
obj2[obj]     // 'obj'

// 模板字符串
`${obj}`     // '10'
`${null}`    // 'null'


// 各种期望参数是字符串的方法
parseFloat(obj)        //  10
encodeURI(obj)         // '10'
encodeURI(true)        // 'true'
encodeURI(BigInt(10))  // '10'
Symbol({})             //  Symbol([object Object])
......

流程

注意事项

  1. 如果转换的是Symbol, 会抛出异常
`${new Symbol("222")}`    // Uncaught TypeError: Symbol is not a constructor
  1. 转为原始值的过程也可能抛出异常
delete Object.prototype.toString
delete Object.prototype.valueOf
var obj = {};
`${obj}`  // caught TypeError: Cannot convert object to primitive value

ToPropertyKey ( argument )

场景

普通的对象,本质是键值对的集合,值本身没有要求,但是键是有限制的,键本身只能是 字符串和 Symbol。但是也可以传入其他类型,这个时候会进行隐式的转换,执行的逻辑就是ToPropertyKey ( argument )

比如,如下的代码执行完毕,obj会有几组键值对呢?

var obj = {};
var obj2 = {};
var propUndefined;
const symbolX = Symbol.for('symbolX');
const bigInt10 = 10n;

obj[obj] = 'obj';
obj[obj2] = 'obj2';

obj[1] = 1;
obj['1'] = 'str1';

obj[symbolX] = symbolX;

obj[undefined] = undefined
obj[propUndefined]  = propUndefined
obj[bigInt10] = bigInt10

console.log(Object.keys(obj));

流程

  1. 转为原始值, preferredType为string
  2. 如果是Symbol直接返回
  3. 转为字符串

具体的协议描述如下:

注意事项

  1. 因为转为原始值这个步骤可能发生异常,所以转为属性键也可能发生异常。

ToBigInt ( argument )

场景

  1. 实例化 BigInt, BigInt.asIntN, BigInt.asUintN 等方法
  2. BigInt64Array , BigUint64Array等TypedArray
  3. Atomics.store, Atomics.compareExchange , Atomics.wait等相关操作
  4. 其他操作
var obj = {
   value: 99,
   valueOf(){
     return this.value;
   }
};


BigInt('10')    // 10n
BigInt(obj);    // 99n

流程

  1. 将 argument 转为原始值 prim
  2. 如果 prim 类型是 BigInt ,直接返回
  3. 如果 prim 类型 是 Undefined, Null, Number, Symbol, 抛出 TypeError
  4. 如果 prim 类型是 Boolean
    1. 如果 prim 是 true, 返回 1n
    2. 如果 prim 是 false, 返回 0n
  5. 如果 prim 类型是 String, 尝试转为 BigInt,
    1. 转换成功返回其值,
    2. 否则抛出 TypeError

协议描述在第七章,协议本身只有两步,但是嘛,实际有不少操作。

字符串转换的时候,有单独的协议描述 StringToBigInt ( str )

StringToBigInt ( str )

字符串转为BigInt。

parseText的过程和解析节点的结构就不细说了,了解即可。留意一下注意事项

  1. 字符串可以是10进制也可是别的进制
BigInt('0xA');       // 10n
BigInt('0b1000')     // 8n
  1. 前后可以有空格
BigInt('  0xA   ');   // 10n
  1. 不会像 parseInt友好处理后面的非数字
paseInt('10n')    // 10
BigtInt('10n')    // Uncaught SyntaxError: Cannot convert 10n to a BigInt

注意事项

ToNumber ( argument )

转为数字,也就是Number类型。和后面的 ToNumeric ( value ) 有关联。

场景

  1. 宽松比较
  2. 一元+, -
  3. isFinite,isNaN
  4. Math的静态方法,Date的初始化和实例方法, String的实例方法
  5. Array 实例化
  6. String.fromCodePoint, String.prototype.lastIndexOf,Function.prototype.bind
  7. Atomics.wait, JSON.stringify
  8. 其他类型转换的方法
var obj = {};
// 宽松比较
"1" == 1;
// 一元+
+ "1", + obj

流程

  1. 如果是 Number,直接返回
  2. 如果是 Symbol或者 BigInt,抛出 TypeError
  3. 如果是 undefined ,返回NaN
  4. 如果是 null 或者 false, 返回 +0
  5. 如果是 true 返回 1,
  6. 如果是字符串,调用 StringToNumber(argument) 转为数字
  7. 如果是 Object, 先转为原始值,再调用 ToNumber ( argument )

协议的描述如下:

注意事项

ToNumeric ( value )

转为数值,Number或者BigInt类型, 底层可能调用 ToNumber ( argument )

场景

  1. 关系比较,例如>=, <等
  2. a++,a--, --a, ++a
  3. 一元运算符 -,~
  4. **, *, /, %, +, -, <<, >>, >>>, &, ^, or |
  5. 实例化Number对象 Number ( value )
  6. 其他场景

逻辑

  1. 转为原始值
  2. 如果BigInt,直接返回
  3. 返回ToNumber ( argument )的值

注意事项

  1. 一元+调用的是ToNumber ( argument ) , 而一元-调用的是 ToNumeric ( value )
+10n // caught TypeError: Cannot convert a BigInt value to a number
-10n // -10n

image.png

ToObject ( argument )

转为对象。

场景

  1. Object(), new Object(obj)
  2. 非严格模式 Function.prototype.call, Function.prototype.apply, Function.prototype.bind的调用
  3. with语句
  4. Object的静态方法和原型方法,比如Object.assign, Object.entries等
  5. Date.prototype.toJSON,String.raw
  6. Array.from和Array的原型方法
  7. 其他场景

逻辑

  1. null 和 undfined 抛出TypeError
  2. Object直接返回
  3. 其他值返回对应的Object类型
    1. true, false 返回对应的 Boolean 对象
    2. 数字 返回对应的 Number 对象
    3. 字符串 返回对应的 String 对象
    4. Symbol 返回对应的 Symbol 对象
    5. BigInt 返回对应的 BigInt 对象

协议详情 Table 13: ToObject Conversions

注意事项

  1. null 和 undefined 是不能转为对象

ToBoolean ( argument )

场景

  1. 一元!
  2. &&,&&=, || ,||=,三目?
  3. if(), do while(), while
  4. Boolean, new Boolean
  5. Array.prototype.every, Array.prototype.filter, Array.prototype.findLastIndex,Array.prototype.some
  6. 其他场景

逻辑

  1. 如果是布尔值,直接返回
  2. 如果是 undefined , null , +0, -0, NaN 或者空字符串,返回false
  3. 返回true

注意事项

  1. 空字符串""返回false ,但是 " "返回的是true

CanonicalNumericIndexString ( argument )

转为规范数值索引字符串。 属性键可以是数字,字符串也可以是Symbol。

其实数字属性和字符串属性的存取还是不一样的,数字属性是排序属性,字符串属性为普通属性。

这个方法就是尝试把某个值转为数字属性。

场景

  1. 字符串按索引取值
  2. 对象取排序属性的值
  3. 其他场景
// 字符串按索引取值
var str = "12345";
str[1];

// 对象取排序属性的值
var obj = {1:"1", name: "name"};
obj["1"];
var arr = ["1", "2"];
arr["1"]    // "1"

// "-0"哦豁
arr["-0"]   // undefined
arr["0"]   // "1"
arr[0]      // "1"

流程

  1. 如果是 "-0"返回 -0
  2. argument转为数字n, n再转为字符串,如果还等于argument,返回n
    这一步主要是去除数字的隐式转换,比如null, fasle,true, undefined这类。
  3. 返回 undefined

协议内容

注意事项

  1. "-0" 和 "0" 作为键,取值的时候,结果可能是不一样的

引用

Type Conversion