发生了什么

4次阅读

共计 5582 个字符,预计需要花费 14 分钟才能阅读完成。

记不清在某处看见了这一比较,当时对强制转换这块理解的还没有特别清晰,故有此一文。以为我会以标题的表达式来展开?那你就错了,下面直接上 [] == ![] 是如何转换的:

  1. 因为! 运算符的优先级比较高,所以表达式右侧先运行![],得出 false,表达式变为[] == false
  2. 强制将 false 转换为 0,表达式变为[] == 0
  3. 将 [] 强制转换为原始类型后为 ””,表达式变为 ”” == 0
  4. 将 ”” 转换为 Number 类型,表达式变为 0 == 0
  5. 两侧类型相同,直接返回 0 === 0 的结果 true

前言

本文旨在总结 js 中强制转换的规则及触发强制转换的几种场景。ES6 标准中定义了六种原始类型,分别是 Undefined,Null,String,Number,Boolean,Symbol。本文中的强制转换指的是在代码运行时,触发了数值的隐式转换,而不是代码显示的指定转换操作。

原始类型间强制转换

发生在原始类型之间的转换,以个人的理解是其他类型转换为 String,Number 或者 Boolean 类型。

转换为 String 类型

其他原始类型转换为 String 类型通常发生在 + 两边存在字符串时,会将 + 另一边的值转换为 String 类型。
考虑如下代码:

var strAddNum = "test" + 1;
var numAddStr = 1 + "test";
var boolAddStr = true + "test";
var undAddStr = undefined + "";
var nullAddStr = null + "";
console.log(strAddNum);
console.log(numAddStr);
console.log(boolAddStr);
console.log(undAddStr);
console.log(nullAddStr);

代码传送门,以上代码的运行结果均为字符串。其他原始类型转换为 String 类型基本是其值的字符串形式,具体如下:

  • Undefined,”undefined”
  • Null,”null”
  • Boolean,”true” 或 ”false”
  • Number,值为 NaN,”NaN”
  • Number,值为 + 0 或 -0,”0″
  • Number,值为 +Infinity,”Infinity”
  • Number,值为 -Infinity,”-Infinity”

Number 转为字符串具体可参考 ES2018 7.1.12.1 章节

注意:Symbol 类型无法转换为 String 类型。

转换为 Number 类型

转换为 Number 类型的情况,+-*/% 等运算中,除了 + 之外其他运算均会转换成 Number 类型,+ 运算时需要满足两侧未出现 String 类型,该值才会被转换为 Number 类型。+ 运算时情况较为复杂,后面会专门描述其相关转换规则。考虑如下代码:

var trueAddTrue = true + true;
var trueAddFalse = true + false;
var trueAdda0 = true + 0;
var nullAddTrue = null + true;
var undefinedAdd0 = undefined + 0;
var strAdd0 = "" + 0;
console.log(trueAddTrue);
console.log(trueAddFalse);
console.log(trueAdda0);
console.log(nullAddTrue);
console.log(undefinedAdd0);
console.log(strAdd0);

代码传送门,在运行代码之前可以先考虑下以上代码答打印的结果分别是什么?然后再运行,看是否符合你的预期。其他原始类型转换为 Number 类型的具体如下:

  • Undefined,NaN
  • Null,+0
  • Boolaen,值为 true,1
  • Boolean,值为 false,+0
  • String,不可转为 Number 的,NaN
  • String,可转为 Number 的就是其对应的 Number 值(具体可参考 ES2018 7.1.3.1)

注意:Symbol 类型同样无法转换为 Number 类型。

转换为 Boolean 类型

转换为 Boolean 类型的情况较为简单,除了以下情况转换为 Boolean 类型会是 false,其他情况均是 true

  • Undefined
  • Null
  • Number,+0,-0,NaN
  • String,长度为 0 的字符串

这几种 false 的情况在 ES 标准中有明确规定 7.1.2

对象强制转换为原始类型

ES 中将对象转换为原始类型的算法,大致可描述为三种情形:

  1. 如果该对象设置了[Symbol.toPrimitive],调用该函数,如果其返回值为非 Object 类型则返回结果,否则抛出 TypeError 异常
  2. 若未指定转换提示则转换提示为 ”default”
  3. 若转换提示为 ”default”,则将其置为 ”number”
  4. 当指定转换提示为 ”number” 时先调用该对象的 valueOf 函数并判断其结果,如果是原始类型则返回结果,否则调用该对象的 toString 函数并判断其返回结果,如果结果为原始类型则返回,否则抛出异常 TypeError
  5. 当指定转换提示为 ”string” 时先调用 toString 函数并判断其返回结果,如果是原始类型则返回结果,否则调用该对象的 valueOf 函数并判断其返回结果,如果结果为原始类型则返回,否则抛出异常 TypeError

上述三种情形中第一种情形优先级最高,第二三种情形优先级并列,具体需要根据使用场景判断是哪一种。其中的指定转换提示是 ES 标准内部调用该算法时指定的。

第一种情形只有 Symbol 对象和 Date 对象内置了 [Symbol.toPrimitive],且该属性的 writeable 为 false,enumerable 为 false,configurable 为 true
对象转换为原始类型时发生的强制转换非特殊情况均为第二种,第三种情况较为少见。在正常编码工作中应该使用第二种情形就够用了,第三种情形几乎不会出现,要了解更多细节可查阅 ES 标准。

var test = {[Symbol.toPrimitive]: function(hint) {console.log(hint)
  },
  valueOf: function() {console.log("valueOf")
  },
  toString: function() {console.log("toString")
  }
}
test + "";  //"default"test * 0;   //"number"String(test);   //"string"

代码传送门上述代码指定了分别指定了 test 对象的 [Symbol.toPrimitive],valueOf 和 toString 函数,可以观察到并 valueoOf 和 toString 函数均未被调用,指定的[Symbol.toPrimitive] 函数可以接受一个提示参数,这个参数就是强制转换时的强制转换提示。这样我们在函数中就可以根据转换场景的不同分别返回不同的值。

原始类型强制转换为对象(装箱)

在开始描述这个问题之前,可以先思考一下,都有哪些场景会是强制的将原始类型转换为对象,其实这种场景几乎在 js 代码中随处可见,考虑如下代码:

var str = "testString";
str.replace("test", "");

如上代码中定义的 str 的值并不是一个对象而是一个原始类型 String,原始类型显然是没有方法可以调用的。

实际上这里的 str 在执行 str.replace 时 str 其值会被强制转换为对象,得到一个 String 类型的实例对象,而该实例的原型上定义了一系列方法,且该实例是无法被获取的,在执行完这行代码后,该实例就会被回收,所以这里的 str 依然是一个字符串。

考虑如下代码:

var a = 3;
a.fn = function(){};
a.fn();

强制转换的几种场景

在 js 代码中会出现强制转换的场景通常有三种:

  • + 运算
  • -,*,/,% 运算
  • == 比较
  • 作为判断条件

+ 运算

一元 + 运算

做一元 + 运算时,均会被强制转为 Number 类型,例如

var a = {[Symbol.toPrimitive]: function(hint) {console.log(hint);  // number
        if(hint === "number") {return 2;} else {return 9;}
    }
};
console.log(+a);   // 2

var b = "3";
console.log(+b);    // 3

代码传送门

二元 + 运算

二元 + 运算为几种强制转换中复杂度仅次于 == 比较的一种情形,个人总结其转换步骤如下:

  1. 先将两侧数值强制转换为原始类型(未指定转换提示,即转换提示为 hint default);
  2. 若两侧存在 String 类型,均转换为 String 类型,则返回两侧拼接的字符串;
  3. 若第 2 未返回,则两侧数值强制转换为 Number 类型,返回计算结果;
var a = "";
var b = {[Symbol.toPrimitive]: function(hint) {console.log(hint);  // "default"
        if(hint === "default") {return 2;} else {return 9;}
    }
};
var c = a + b;  // 这里 b 转换为原始类型返回的是 Number 类型 2,由于 a 是 "",所以 b 被转换为"2",后与"" 拼接返回 "2"
console.log(c); // "2"

var d = 3;
var e = {[Symbol.toPrimitive]: function(hint) {console.log(hint);  // "default"
        if(hint === "default") {return 2;} else {return 9;}
    }
};
var f = d + e;  // 这里 e 转换为原始类型返回的是 Number 类型 2,由于两侧均没有 String 类型,则至第 3 步,强制转换为 Number 后返回两侧相加的结果 5
console.log(f); // 5

代码传送门

-,*,/,% 运算

这几个运算符这涉及的强制转换都是转换为 Number 类型的,所以这里只要搞清楚转换为 Number 是怎样的过程就可以了。上文中已经对原始类型转换为 Number 类型做了描述,这里补充一下 Object 转换为 Number 的过程:

  1. 将对象转换为原始类型,且转换时会指定转换提示为 ”number”;
  2. 转换为原始类型后再根据原始类型转换为 Number 类型进行转换;
var a = 8;
var b = {[Symbol.toPrimitive]: function(hint) {console.log(hint);  // "number"
        if(hint === "number") {return 2;} else {return 9;}
    }
};
console.log(a-b);   //  6
console.log(a/b);   // 4
console.log(a*b);   // 16
console.log(a%b);   // 0

console.log(undefined * 0);   //NaN
console.log(null * -1); // 0
console.log(false * -1);    //0 
console.log(true * -1); // -1
console.log("1" * -1);  // -1

代码传送门

== 比较

== 比较的基础 === 比较

x === y,其具体比较步骤如下:

  1. 若 x 和 y 的类型不一致,返回 false;
  2. 若 x 和 y 为 Number 类型,则若 x 和 y 中有一个为 NaN 返回 false;若 x 和 y 的值相等则返回 true;若 x 是 +0,y 是 - 0 或者 x 是 -0,y 是 + 0 则返回 true;其他情况返回 false;
  3. 若 x 和 y 为 Undefined 类型,返回 true
  4. 若 x 和 y 为 Null 类型,返回 true
  5. 若 x 和 y 为 String 类型,其值相同则返回 true,否则返回 false
  6. 若 x 和 y 为 Boolean 类型,其值均为 true 或均为 false 返回 true,否则返回 false
  7. 若 x 和 y 为 Symbol 类型,其值为同一个 Symbol 值则返回 true,否则返回 false
  8. 若 x 和 y 为 Object 类型,其值为同一个对象(其引用地址相同)则返回 true,否则返回 false

x == y 规则

== 比较的转换规则虽然稍微多一点,实际上也就几条规则,两侧的数值类型符合哪种就按哪种去转换,只不过有的可能需要转两次,具体如下:

  1. 如果两侧类型相等则直接返回 === 的结果;
  2. 若 x 为 undefined 和 y 为 null 或 x 为 null 和 y 为 undefined,返回 true
  3. 若两侧为 String 类型和 Number 类型,将 String 类型转换为 Number 类型,继续用 == 比较
  4. 若有一侧存在 Boolean 类型,将 Boolean 类型转换为 Number 类型,继续用 == 比较
  5. 若两侧为 String,Number 或 Symbol 类型和 Object 类型,将 Object 类型转换原始类型,继续用 == 比较
  6. 其他返回 false

下面列举一些可能有点违反直觉的比较

"0" == false; // true
false == 0; // true
false == ""; // true
false == []; // true
""== 0; // true"" == []; // true
0 == []; // true
[] == ![];  //true

作为条件判断

这种情形到没有太多可说的,基本上就是,除了 undefined,null,+0,-0,NaN,”” 这六个值会被转为 false,其他情况均为 true;

出现将不是 Boolean 类型的值强制转换的情况为

  1. if(…)
  2. for(…;…;…)第二个条件表达式
  3. while(…)和 do…while(…)中的条件表达式
  4. …?…:… 三元表达式中的第一个条件表达式
  5. || 和 &&

结论

其实上面描述了这么多,日常开发环境中用到比较多的应该是作为判断条件,+,== 这三种情况了,这三种中最常见的应该是判断条件的情况了,这种情况反而是最简单的一种了。

正文完
 0