【学习笔记】JS 的 {} + {} 与 {} + [] 的结果是什么?

前言

在 JS 中,+ 符号是很常见的一种,它有如下的使用状况算法

  • 数字的加法运算,二元运算
  • 字符串的链接运算,二元运算,优先级最高
  • 正号,一元运算,可延伸为强制转换其余类型的运算元为数字类型

另外一个常见的是花括号 {},它有两个用途也很常见编程

  • 对象的字面定义
  • 区块语句

加号运算符 +

除了上面说明的常见状况外,在标准中转换的规则还有如下几个,要注意它的顺序:operand + operand = result数组

  • 使用 ToPrimitive 运算转换左与右运算元为原始数据类型值(primitive)
  • 在第 1 步转换后,如有运算元出现原始数据类型是"字符串"类型值时,则另外一运算元做强制转换为字符串,而后做字符串的链接运算(concatenation)
  • 其余状况时,全部运算元都会转换为原始数据类型的"数字"类型,而后做数学的相加运算(addition)

ToPrimitive 内部运算

所以,加号运算符只能使用于原始数据类型,那么对于对象类型的值要如何转换为原始数据类型?浏览器

JavaScript 对象转换到基本类型值时,会使用 ToPrimitive 算法,这是一个内部算法,是编程语言在内部执行时遵循的一套规则markdown

在 ECMAScript 6th Edition #7.1.1,有一个抽象的 ToPrimitive 运算,它会用于对象转换为原始数据类型,这个运算不仅会用在加号运算符也会用在关系比较或值相等比较的运算中,下面是有关 ToPrimitive 的说明语法编程语言

ToPrimitive(input, PreferredType?)
input 表明代入的值,而 PreferredType 能够是数字(Number)或字符串(String)其中一种,这会表明"优先的"、"首选的"的要进行转换到哪种原始类型,转换的步骤会依这里的值而有所不一样函数

但若没有提供这个值也就是预设状况,则会设置转换的 hint 值为 default,这个首选的转换原始类型的指示(hint 值),是在做内部转换时由 JS 视状况自动加上的,通常状况就是预设值oop

转换算法

当对象发生到基本类型值的转换时,会按照下面的逻辑调用对象上的方法:ui

  • 若是存在 obj[Symbol.toPrimitive],则先调用 objSymbol.toPrimitivethis

  • 不然按下面规则来:
    PreferredType 为数字 Number

    • 当 PreferredType 为数字 Number 时,input 为要被转换的值,如下是转换这个 input 值的步骤
    • 若 input 是原始数据类型,则直接返回 input
    • 不然,若 input 是个对象时则调用对象的 valueOf() 方法,若能获得原始数据类型的值,则返回这个值
    • 不然,若 input 是个对象时则调用对象的 toString() 方法,若能获得原始数据类型的值,则返回这个值
    • 不然,抛出 TypeError 错误

    PreferredType 为字符串 String

    • 当 PreferredType 为字符串 String 时,input 为要被转换的值,如下是转换这个 input 值的步骤:
    • input 是原始数据类型,则直接返回 input
    • 不然,若 input 是个对象时则调用对象的 toString() 方法,若能获得原始数据类型的值,则返回这个值
    • 不然,若 input 是个对象时则调用对象的 valueOf() 方法,若能获得原始数据类型的值,则返回这个值
    • 不然,抛出 TypeError 错误

    PreferredType 没提供时即 hint 为 default 时

    • 此时与 PreferredType 为数字 Number 时的步骤相同

Symbol.toPrimitive

  • Symbol.toPrimitive 是一个内置的 Symbol 值,它是做为对象的函数值属性存在的,当一个对象转换为对应的原始值时,会调用此函数
  • 该函数被调用时会被传递一个字符串参数 hint ,表示要转换到的原始值的预期类型。hint 参数的取值是 "number""string""default" 中的任意一个
  • Symbol.toPrimitive 在类型转换方面优,先级是最高的
let ab = {
    valueOf() {
        return 0;
    },
    toString() {
        return '1';
    },
    [Symbol.toPrimitive]() {
        return 2;
    }
}
console.log(1+ab); // 3
console.log('1'+ab); // 12
复制代码

例子

// 拥有 Symbol.toPrimitive 属性的对象
var obj2 = {
  [Symbol.toPrimitive](hint) {
    if(hint == "number"){
        return 10;
    }
    if(hint == "string"){
        return "hello";
    }
    return true;
  }
}
 
console.log(+obj2);     //10 --hint in "number"
console.log(`${obj2}`); //hello --hint is "string"
console.log(obj2 + ""); //"true"

let obj = {
  [Symbol.toPrimitive](hint) {
    if(hint === 'number'){
      console.log('Number场景');
      return 123;
    }
    if(hint === 'string'){
      console.log('String场景');
      return 'str';
    }
    if(hint === 'default'){
      console.log('Default 场景');
      return 'default';
    }
  }
}
console.log(2*obj); // Number场景 246
console.log(3 + obj); // Default 场景 3default
console.log(obj + "");  // Default场景 default
console.log(String(obj)); //String场景 str
复制代码

valueOf 与 toString 方法

而在 JS 的 Object 原型的设计中,都必定会有两个 valueOf 与 toString 方法,因此这两个方法在全部对象里面都会有,不过它们在转换过程当中有可能会交换被调用的顺序

对于原始类型数据,toString 及 valueOf 方法的使用

const str = "hello", n = 123, bool = true;
console.log(typeof(str.toString()) + "_" + str.toString()) // string_hello
console.log(typeof(n.toString()) + "_" + n.toString())  // string_123
console.log(typeof(bool.toString()) + "_" + bool.toString()) //string_true


console.log(typeof(str.valueOf()) + "_" + str.valueOf()) //string_hello
console.log(typeof(n.valueOf()) + "_" + n.valueOf()) //number_123
console.log(typeof(bool.valueOf()) + "_" + bool.valueOf()) //boolean_true

// console.log(str.valueOf) => ƒ valueOf() { [native code] }
console.log(str.valueOf === str) // false
// console.log(n.valueOf) => ƒ valueOf() { [native code] }
console.log(n.valueOf === n) // false
// bool.valueOf() => true
console.log(bool.valueOf() === bool) // true
复制代码

toString 方法对于原始类型数据而言,其效果至关于类型转换,将原类型转为字符串;valueOf 方法对于原始类型数据而言,其效果将至关于返回原数据

复合对象类型数据使用 toString 及 valueOf 方法

var obj = {};
console.log(obj.toString());    // [object Object] 返回对象类型
console.log(obj.valueOf());     // {} 返回对象自己
复制代码

综合案例

const test = { 
 i: 10, 
 toString: function() { 
    console.log('toString'); 
    return this.i; 
 }, 
 valueOf: function() { 
    console.log('valueOf'); 
    return this.i; 
 } 
} 
alert(test); // 10 toString 
alert(+test); // 10 valueOf 
alert(''+test); // 10 valueOf 
alert(String(test)); // 10 toString 
alert(Number(test)); // 10 valueOf 
alert(test == '10'); // true valueOf 
alert(test === '10'); // false
复制代码

补充 toString() 和 String() 的区别

  • toString() 和 String() 方法均可以转换为字符串类型
  • toString()
    • toString() 能够将全部的数据都转换为字符串,但要排除 null 和 undefined,null 和 undefined 调用 toString() 方法会报错
    • 若当前数据为数字类型,则 toString() 括号中能够写一个数字表明进制,能够将数字转化为对应进制字符串
    var num = 123;
    console.log(num.toString() + '_' + typeof(num.toString()));   // 123_string 
    console.log(num.toString(2) + '_' + typeof(num.toString()));    // 1111011_string
    console.log(num.toString(8) + '_' + typeof(num.toString()));    // 173_string
    console.log(num.toString(16) + '_' + typeof(num.toString()));   //7b_string
    复制代码
  • String()
    String() 能够将 null 和 undefined 转换为字符串,可是无法转进制字符串

注意下面两点: Symbol.toPrimitive 和 toString 方法的返回值必须是基本类型值
valueOf 方法除了能够返回基本类型值,也能够返回其余类型值

数字实际上是预设的首选类型,即在通常状况下加号运算中的对象要做转型时,都是先调用 valueOf 再调用 toString

注意:Date 对象的预设首选类型是字符串 String

JS 对于 Object 与 Array 的设计

在 JS 中所设计的 Object 纯对象类型的 valueOf 与 toString 方法,它们的返回以下:

  • valueOf 方法返回值: 对象自己
  • toString 方法返回值: "[object Object]" 字符串值,不一样的内建对象的返回值是 "[object type]" 字符串
    • type 指的是对象自己的类型识别,例如 Math 对象是返回 "[object Math]" 字符串
    • 但有些内置对象由于覆盖了这个方法,因此直接调用时不是这种值(注意: 这个返回字符串的前面的 "object" 开头英文是小写,后面开头英文是大写)

所以能够利用 Object 中的 toString 来进行各类不一样对象进行判断,这在之前 JS 能用的函数库或方法很少的年代常常看到,不过它须要配合使用函数中的 call 方法,才能输出正确的对象类型值,例如:

Object.prototype.toString.call([])  // "[object Array]"
Object.prototype.toString.call(new Date) // "[object Date]"
复制代码

对象的这两个方法均可被覆盖,可用下面的代码来观察这两个方法的运行顺序,下面这个都是先调用 valueOf 的状况:

let obj = {
  valueOf: function () {
      console.log('valueOf');
      return {}; // object
  },
  toString: function () {
      console.log('toString');
      return 'obj'; // string
  }
}
console.log(1 + obj);  //valueOf -> toString -> '1obj'
console.log(+obj); // // valueOf -> toString -> NaN
console.log('' + obj); // valueOf -> toString -> 'obj'
复制代码

先调用 toString 的状况比较少见,大概只有 Date 对象或强制要转换为字符串时才会看到

let obj = {
  valueOf: function () {
      console.log('valueOf');
      return 1; // number
  },
  toString: function () {
      console.log('toString');
      return {}; // object
  }
}
alert(obj); // toString -> valueOf -> alert("1");
String(obj); // toString -> valueOf -> "1";
复制代码

而下面这个例子会形成错误,由于不论顺序是如何都得不到原始数据类型的值,错误消息是"TypeError: Cannot convert object to primitive value",从这个消息中能够得知它这里面会须要转换对象到原始数据类型:

let obj = {
  valueOf: function () {
      console.log('valueOf');
      return {}; // object
  },
  toString: function () {
      console.log('toString');
      return {}; // object
  }
}
console.log(obj + obj); // valueOf -> toString -> error!
复制代码

数组 Array 很经常使用,虽然它是个对象类型,但它与 Object 的设计不一样,它的 toString 有覆盖,说明一下数组的 valueOf 与 toString 的两个方法的返回值:

  • valueOf 方法返回值:对象自己(与Object同样)
  • toString 方法返回值:至关于用数组值调用 join(',') 所返回的字符串,即 [1,2,3].toString() 会是 "1,2,3",这点要特别注意

Function 对象不多会用到,它的 toString 也有被覆盖,因此并非 Object 中的那个 toString,Function 对象的 valueOf 与 toString 的两个方法的返回值:

  • valueOf 方法返回值:对象自己(与Object同样)
  • toString 方法返回值:函数中包含的代码转为字符串值

Number、String、Boolean 三个包装对象

包装对象是 JS 为原始数据类型数字、字符串、布尔专门设计的对象,全部的这三种原始数据类型所使用到的属性与方法,都是在这上面提供的

包装对象的 valueOf 与 toString 的两个方法在原型上有通过覆盖,因此它们的返回值与通常的 Object 的设计不一样:

  • valueOf 方法返回值:对应的原始数据类型值
  • toString 方法返回值:对应的原始数据类型值,转换为字符串类型时的字符串值

toString 方法会比较特别,这三个包装对象里的 toString 的细部说明以下:

  • Number 包装对象的 toString 方法:能够有一个传参,能够决定转换为字符串时的进位(二、八、16)
  • String 包装对象的 toString 方法:与 String 包装对象中的 valueOf 相同返回结果
  • Boolean 包装对象的 toString 方法:返回 "true""false" 字符串

注意,常被搞混的是直接使用 Number()、String() 与 Boolean() 三个强制转换函数的用法,这与包装对象的用法不一样,包装对象是必须使用 new 关键字进行对象实例化的,如 new Number(123),而 Number('123') 则是强制转换其余类型为数字类型的函数

Number()、String() 与 Boolean() 三个强制转换函数所对应的就是在 ECMAScript 标准中的 ToNumber、ToString、ToBoolean 三个内部运算转换的对照表,而当它们要转换对象类型前,会先用上面说的 ToPrimitive 先将对象为原始数据类型再进行转换到所要的类型值

实例

字符串 + 其余原始类型

字符串在加号运算中有最高的优先运算,与字符串相加一定是字符串链接运算(concatenation)。全部的其余原始数据类型转为字符串,能够参考 ECMAScript 标准中的 ToString 对照表,如下为一些简单的例子

'1' + 123 // "1123"
'1' + false // "1false"
'1' + null // "1null"
1' + undefined // "1undefined" 复制代码

数字 + 其余的非字符串的原始数据类型

数字与其余类型做相加时,除了字符串会优先使用字符串链接运算(concatenation)之外,其余都要依照数字为优先,因此除了字符串以外的其余原始数据类型都要转换为数字来进行数学的相加运算

1 + true // true 转为1, false 转为 0 -> 2
1 + null // null 转化为 0 -> 1
1 + undefined // undefined 转为NaN -> NaN
复制代码

数字/字符串之外的原始数据类型做加法运算

当数字与字符串之外的其余原始数据类型直接使用加号运算时,就是转为数字再运算,这与字符串彻底无关

true + true // 2
true + null // 1
undefined + null // NaN
复制代码

[] + []

[] + [] // ""
复制代码
  • 两个数组相加,依然按照 valueOf -> toString 的顺序
  • valueOf 返回数组自己,因此会以 toString 的返回值才是原始数据类型,而 [] 转化为字符串为 ""
  • 因此最后这个运算至关于两个空字符串在相加,依照加法运算规则第2步骤是字符串链接运算(concatenation),两个空字符串链接最后得出一个空字符串

{} + {}

{} + {} // "[object Object][object Object]"
复制代码
  • 两个空对象相加,依然按照 valueOf -> toString 的顺序
  • valueOf 返回对象自己,因此会以 toString 的返回值才是原始数据类型,即 {} 转化为字符串为 "[object Object]"
  • 因此最后这个运算至关于两个 "[object Object]" 字符串相加,依照加法运算规则第2步骤,是字符串链接运算(concatenation)

特别注意: {} + {} 在不一样的浏览器有不一样结果

  • 有些浏览器如 Firefox、Edge 浏览器会把 {} + {} 直译为至关于 +{} 语句,由于它们会认为以花括号开头 ({) 的是一个区块语句的开头而不是一个对象字面量,因此会略过第一个{},把整个语句认为是个 +{} 的语句
  • 至关于强制求出数字值的 Number({}) 函数调用运算,即 Number("[object Object]"),最后得出的是 NaN

若在第一个空对象加上圆括号 (()),这样 JS 就会认为前面是个对象,就能够得出一样的结果:

({}) + {} // "[object Object][object Object]"
复制代码

或是分开来先声明对象的变量值也能够得出一样的结果,像下面这样:

let foo = {}, bar = {};
foo + bar;
复制代码

注: 上面说的行为与加号运算的对象字面值是否是个空对象无关,就算是里面有值的对象,如 {a:1, b:2} 也是一样的结果

{} + []

上面所述的把 {} 看成区块语句的状况在此也会发生,不过此次全部的浏览器都会有一致结果,若 {} 在前面,而 [] 在后面,则前面(左边)那个运算元会被认为是区块语句而不是对象字面量

因此 {} + [] 至关于 +[] 语句,即至关于强制求出数字值的 Number([]) 运算,即 Number("") 运算,最后得出的是 0数字

{} + [] // 0
{a: 1} + [1,2] // +"1,2" -> NaN
复制代码

特别注意:若第一个是 {} 时,后面加上其余的像 数组、数字或字符串这时加号运算会直接变为一元正号运算,也就是强制转为数字的运算,这是个陷阱要当心

[] + {}

[] + {} // "" + "[object Object]" -> "[object Object]"
[1, 2] + {a:1}  // "1,2[object Object]"
复制代码

Date 对象

Date 对象的 valueOf 与 toString 的两个方法的返回值:

  • valueOf 方法返回值:给定的时间转为 UNIX 时间(自 1 January 1970 00:00:00 UTC 起算),可是以微秒计算的数字值
  • toString 方法返回值:本地化时间的字符串

Date 对象上面有说起是首选类型为"字符串"的一种异常的对象,这与其余的对象的行为不一样(通常对象会先调用 valueOf 再调用 toString ),在进行加号运算时它会优先使用 toString 来进行转换,最后一定是字符串链接运算(concatenation),如如下的结果:

1 + (new Date()) // "1Mon May 17 2021 02:06:39 GMT+0800 (中国标准时间)"
复制代码

要得出 Date 对象中的 valueOf 返回值,须要使用一元加号 + 来强制转换它为数字类型,如如下的代码:

+new Date() // 1621188445596
复制代码

Symbol 类型

ES6 中新加入的 Symbol 数据类型,它不算是通常的值也不是对象,它并无内部自动转型的设计,因此彻底不能直接用于加法运算,使用时会报错