前言
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 ] )
转为原始值。
场景
- 关系比较, 比如 >=, <等
- 宽松相等 ==
- 二元 +
- BigInt, Date实例化, Date.prototype.toJSON
- 其他转换,例如 ToNumber ( argument ), ToNumeric ( value ),ToBigInt ( argument ),ToString ( argument ),ToPropertyKey ( argument ) 等
- 其他场景
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 也会影响转换逻辑。
基本流程如下:
- 如果 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方法,如果某个返回的是原始值,就直接返回。 - 抛出异常
- 如果preferredType 是 string, 依次检查并调用对象的
- 对象如果定义了 Symbol.toPrimitive 方法
- 直接返回input
协议内部有两个方法组成
- ToPrimitive ( input [ , preferredType ] )
检查对象是否有Symbol.toPrimitive方法,有择调用,否则 调用OrdinaryToPrimitive ( O, hint ) - OrdinaryToPrimitive ( O, hint )
按照规则依次调用toString,valueOf


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的场景
- 转为属性键的时候,包括排序属性
- 字符串模板
- parseFloat, parseInt , decodeURI , encodeURI, new Function, eval, Symbol 等等各种方法,期望传入的是字符串,而传入的是对象时
- 其他情况
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
注意事项
- 转为原始值的过程是可能抛出TypeError的
- 比如对象和原型上没有 Symbol.toPrimitive, toString,valueOf三个方法
- 比如 Symbol.toPrimitive, toString,valueOf 三个返回的都是对象
- 转为原始值的操作在协议内部出现频次非常高,尤其是间接的出现,比如后面的ToNumber ( argument ), ToNumeric ( value ),ToBigInt ( argument ),ToString ( argument ),ToPropertyKey ( argument ) 等,务必掌握。
ToString(argument)
很多时候,需要把值转为字符串,该方法定义了转换逻辑。
如果是argument是对象,会调用上面的 ToPrimitive ( input [ , preferredType ] ) 转为原始值,然后再调用
ToString(argument)。
场景
- 二元加法,当一边是字符串时
- 属性键
- 模板字符串
- parseFloat, decodeURI/encodeURI, decodeURIComponent/encodeURIComponent, Symbol调用, Symbol.for, new Error, Number.prototype.toFixed, Number.prototype.toPrecision, String调用, String.prototype.endsWith等等期望传入的参数是字符串的方法。
- 其他场景
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])
......
流程

注意事项
- 如果转换的是Symbol, 会抛出异常
`${new Symbol("222")}` // Uncaught TypeError: Symbol is not a constructor
- 转为原始值的过程也可能抛出异常
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));
流程
- 转为原始值, preferredType为string
- 如果是Symbol直接返回
- 转为字符串
具体的协议描述如下:
注意事项
- 因为转为原始值这个步骤可能发生异常,所以转为属性键也可能发生异常。
ToBigInt ( argument )
场景
- 实例化 BigInt, BigInt.asIntN, BigInt.asUintN 等方法
- BigInt64Array , BigUint64Array等TypedArray
- Atomics.store, Atomics.compareExchange , Atomics.wait等相关操作
- 其他操作
var obj = {
value: 99,
valueOf(){
return this.value;
}
};
BigInt('10') // 10n
BigInt(obj); // 99n
流程
- 将 argument 转为原始值 prim
- 如果 prim 类型是 BigInt ,直接返回
- 如果 prim 类型 是 Undefined, Null, Number, Symbol, 抛出 TypeError
- 如果 prim 类型是 Boolean
- 如果 prim 是 true, 返回 1n
- 如果 prim 是 false, 返回 0n
- 如果 prim 类型是 String, 尝试转为 BigInt,
- 转换成功返回其值,
- 否则抛出 TypeError
协议描述在第七章,协议本身只有两步,但是嘛,实际有不少操作。
字符串转换的时候,有单独的协议描述 StringToBigInt ( str )。

StringToBigInt ( str )
字符串转为BigInt。

parseText的过程和解析节点的结构就不细说了,了解即可。留意一下注意事项
- 字符串可以是10进制也可是别的进制
BigInt('0xA'); // 10n
BigInt('0b1000') // 8n
- 前后可以有空格
BigInt(' 0xA '); // 10n
- 不会像 parseInt友好处理后面的非数字
paseInt('10n') // 10
BigtInt('10n') // Uncaught SyntaxError: Cannot convert 10n to a BigInt
注意事项
ToNumber ( argument )
转为数字,也就是Number类型。和后面的 ToNumeric ( value ) 有关联。
场景
- 宽松比较
- 一元+, -
- isFinite,isNaN
- Math的静态方法,Date的初始化和实例方法, String的实例方法
- Array 实例化
- String.fromCodePoint, String.prototype.lastIndexOf,Function.prototype.bind
- Atomics.wait, JSON.stringify
- 其他类型转换的方法
var obj = {};
// 宽松比较
"1" == 1;
// 一元+
+ "1", + obj
流程
- 如果是 Number,直接返回
- 如果是 Symbol或者 BigInt,抛出 TypeError
- 如果是 undefined ,返回NaN
- 如果是 null 或者 false, 返回 +0
- 如果是 true 返回 1,
- 如果是字符串,调用 StringToNumber(argument) 转为数字
- 如果是 Object, 先转为原始值,再调用 ToNumber ( argument )
协议的描述如下:
注意事项
ToNumeric ( value )
转为数值,Number或者BigInt类型, 底层可能调用 ToNumber ( argument )。
场景
- 关系比较,例如>=, <等
- a++,a--, --a, ++a
- 一元运算符 -,~
- **, *, /, %, +, -, <<, >>, >>>, &, ^, or |
- 实例化Number对象 Number ( value )
- 其他场景
逻辑
- 转为原始值
- 如果BigInt,直接返回
- 返回ToNumber ( argument )的值

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

ToObject ( argument )
转为对象。
场景
- Object(), new Object(obj)
- 非严格模式
Function.prototype.call,Function.prototype.apply,Function.prototype.bind的调用 - with语句
- Object的静态方法和原型方法,比如Object.assign, Object.entries等
- Date.prototype.toJSON,String.raw
- Array.from和Array的原型方法
- 其他场景
逻辑
- null 和 undfined 抛出TypeError
- Object直接返回
- 其他值返回对应的Object类型
- true, false 返回对应的 Boolean 对象
- 数字 返回对应的 Number 对象
- 字符串 返回对应的 String 对象
- Symbol 返回对应的 Symbol 对象
- BigInt 返回对应的 BigInt 对象
协议详情 Table 13: ToObject Conversions

注意事项
- null 和 undefined 是不能转为对象
ToBoolean ( argument )
场景
- 一元!
- &&,&&=, || ,||=,三目?
- if(), do while(), while
- Boolean, new Boolean
- Array.prototype.every, Array.prototype.filter, Array.prototype.findLastIndex,Array.prototype.some
- 其他场景
逻辑
- 如果是布尔值,直接返回
- 如果是 undefined , null , +0, -0, NaN 或者空字符串,返回false
- 返回true

注意事项
- 空字符串
""返回false ,但是" "返回的是true
CanonicalNumericIndexString ( argument )
转为规范数值索引字符串。 属性键可以是数字,字符串也可以是Symbol。
其实数字属性和字符串属性的存取还是不一样的,数字属性是排序属性,字符串属性为普通属性。
这个方法就是尝试把某个值转为数字属性。
场景
- 字符串按索引取值
- 对象取排序属性的值
- 其他场景
// 字符串按索引取值
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"
流程
- 如果是
"-0"返回 -0 - argument转为数字n, n再转为字符串,如果还等于argument,返回n
这一步主要是去除数字的隐式转换,比如null, fasle,true, undefined这类。 - 返回 undefined
协议内容

注意事项
- "-0" 和 "0" 作为键,取值的时候,结果可能是不一样的