你不知道的JavaScript-----强制类型转换

2023-11-07

目录

值类型转换

抽象值的操作

JSON 字符串化

ToNumber: 非数字值到数字值 Number(value)

ToBoolean: 转换为布尔类型 Boolean(value)

强制类型转换

字符串和数字之间的显式强制类型转换

奇特的~运算符

字位截除

显式解析数字字符串

显式转换为布尔值

隐式强制类型转换

字符串与数字之间的隐式强制类型转换

布尔值到数字的隐式强制类型转换

||和&&

 字符串和数字之间的相等比较

其他类型和布尔类型之间的相等比较

null和undefined之间的相等比较

对象和非对象之间的相等比较

比较少见的情况

1、返回其他数字:

2、假值的相等比较

3、极端情况

4、完整性检查

 抽象关系比较

valueOf()和toString()详解


值类型转换

  • 将值从一种类型转换成另一种类型通常称为类型转换,这是显式的情况;隐式的情况称为强制类型转换。
  • 强制类型转换主要将值转换为相应的字符串、数字、布尔值,不会返回对象和函数。
  • 下面两种方式都是将数字转换成字符串
var a = 42;
var b = a + ""; // 隐式强制类型转换
var c = String(a); // 显式强制类型转换

抽象值的操作

JSON 字符串化

对大多数简单的值都可以使用JSON.stingify(...)字符串化。

JSON.stringify(..) 在对象中遇到 undefined、function 和 symbol 时会自动将其忽略,在数组中则会返回 null

var a = {
  name: 'Tina',
  age: 20,
  sayHi: function() {
    alert('Hi')
  },
  height: null,
  weight: undefined,
  hobbies: [
    'swimming',
    'reading',
    'singing',
    null,
    undefined,
    function () { console.log('hobbies') }
  ]
}
JSON.stringify(a)
// "{"name":"Tina","age":20,"height":null,"hobbies":["swimming","reading","singing",null,null,null]}"

如果要对含有非法 JSON 值的对象做字符串化,或者对象中的某些值无法被序列化时,就需要定义 toJSON() 方法来返回一个安全的 JSON 值。
如果对象中定义了 toJSON() 方法,JSON 字符串化时会首先调用该方法,然后用它的返回值来进行序列化。

图中循环引用会产生错误

 小技巧

JSON.stringify(value[, replacer [, space]])

replacer表示要字符化的属性
space用于美化输出

ToNumber: 非数字值到数字值 Number(value)

非数字值到数字值转化规则:

值类型 转换规则
null 0
undefined undefined
true 1
false, null 0
转换出错 NaN

ToBoolean: 转换为布尔类型 Boolean(value)

JavaScript 中的值可以分为以下两类:

  • (1)  假值:可以被强制类型转换为 false 的值  undefined   +0, -0, NaN   false  null    ""
  • (2)  真值 :其他(被强制类型转换为 true 的值)  假值列表之外的值

假值对象

虽然 JavaScript 代码中会出现假值对象,但它实际上并不属于 JavaScript 语言的范畴。

var a = new Boolean(false)
var b = new Number(0)
var c = new String("")

强制类型转换


字符串和数字之间的显式强制类型转换

  • 字符串与数字是通过 String(..) 和 Number(..) 这两个内建函数(比较常用)
  • .toString()
  • +(比较少用)
var a = 42;
var b = a.toString();

var c = '66.6'
var d = +c

日期显式转换为数字

var d = +new Date; // 可直接转换为时间戳, 不建议这么用,知道可以这么用就可以了
var timestamp = Date.now();  // 获取时间戳

奇特的~运算符

0 | -0; // 0
0 | NaN; // 0
0 | Infinity; // 0
0 | -Infinity; // 0

indexOf(..)不仅能够得到子字符串的位置,还可以用来检查字符串中是否包含指定的子字符串,相当于一个条件判断。例如:

var a = "Hello World";
if (a.indexOf("lo") >= 0) { // true
    // 找到匹配!
}
if (a.indexOf("lo") != -1) { // true
    // 找到匹配!
}
if (a.indexOf("ol") < 0) { // true
    // 没有找到匹配!
}
if (a.indexOf("ol") == -1) { // true
    // 没有找到匹配!
}

= 0 和== -1这样的写法不是很好,称为“抽象渗漏”,意思是在代码中暴露了底层的实现细节,这里是指用-1作为失败时的返回值,这些细节应该被屏蔽掉。
现在我们终于明白有什么用处了!和indexOf()一起可以将结果强制类型转换(实际上仅仅是转换)为真/假值:

var a = "Hello World";
~a.indexOf("lo"); // -4 <-- 真值!
if (~a.indexOf("lo")) { // true
    // 找到匹配!
}
~a.indexOf("ol"); // 0 <-- 假值!
!~a.indexOf("ol"); // true
if (!~a.indexOf("ol")) { // true
    // 没有找到匹配!
}

如果 indexOf(..) 返回 -1, ~ 将其转换为假值 0,其他情况一律转换为真值。
从技术角度来说, if (~a.indexOf(..)) 仍然是对 indexOf(..) 的返回结果进行隐式强制类型转换, 0 转换为 false,其他情况转换为 true。但我觉得 ~ 更像显式强制类型转换,前提是我对它有充分的理解。

字位截除


Math.floor(x) => 找到小于 x,而且离 x 最近的整数

~~ 后面接正数的时候与 Math.floor() 相同, 负数时是找到离 x 最近的大于 x 的整数

Math.floor(4.5)  // 4
Math.floor(-4.5)  // -5

~~4.5  // 4
~~-4.5  // -4

显式解析数字字符串

解析字符串中的数字和将字符串强制类型转换为数字的返回结果都是数字。但解析和转换两者之间还是有明显的差别。

解析允许字符串中含有非数字字符,解析按从左到右的顺序,如果遇到非数字字符就停止。而转换不允许出现非数字字符,否则会失败并返回 NaN。

解析和转换之间不是相互替代的关系。它们虽然类似,但各有各的用途。如果字符串右边的非数字字符不影响结果,就可以使用解析。而转换要求字符串中所有的字符都是数字,像 "42px" 这样的字符串就不行。

var a = '42'
var b = '42px'

parseInt(a)  // 解析字符串 42
Number(a)  // 显式强制类型转换 42

parseInt(b)  // 42
Number(b)  // 转换失败,返回 NaN

parseInt(s, ?radix) -- 第一个参数是要解析的字符串,第二个参数是转变之后的数字基底(2进制、10进制),默认十进制
从 ES5 开始 parseInt(..) 默认转换为十进制数,除非另外指定。如果你的代码需要在 ES5 之前的环境运行,请记得将第二个参数设置为 10。

注:parseInt(..) 先将参数强制类型转换为字符串再进行解析,这样做没有任何问题。因为传递错误的参数而得到错误的结果,并不能归咎于函数本身。

parseFloat() 解析浮点数

显式转换为布尔值

使用 Boolean(x)

Boolean(undefined)  // false
Boolean(null)  // false

Boolean({})  // true
Boolean([])  // true

一元运算符 ! 显式的将值转换为其自身布尔类型的相反的值,根据这个特性,使用 !! 显式的将值转换为对应的布尔值。

if(xxx) {} 背后的原理:

在 if(..).. 这样的布尔值上下文中,如果没有使用 Boolean(..) 和 !!,就会自动隐式地进行 ToBoolean 转换。建议使用 Boolean(..) 和 !! 来进行显式转换以便让代码更清晰易读。

JSON.stringify()

var arr = [1, function () {}, 3]
JSON.stringify(arr)  // "[1,null,3]"

显式 ToBoolean 的另外一个用处,是在 JSON 序列化过程中将值强制类型转换为 true 或false,而不是只显示 null

显式转换布尔值在三元运算符中的应用:

var a = 42
var b = a ? true : false  // true
// 更简便的写法
var b = !!a
var b = Boolean(a)

这里涉及隐式强制类型转换,因为 a 要首先被强制类型转换为布尔值才能进行条件判断。这种情况称为“显式的隐式”,有百害而无一益,我们应彻底杜绝。
所以在使用三元运算符的时候尽量使用显式强制类型转换对待判断值做显式处理,更容易让人理解(!!a Boolean(a))。

隐式强制类型转换

隐式强制类型转换指的是那些隐蔽的强制类型转换,副作用也不是很明显。换句话说,你自己觉得不够明显的强制类型转换都可以算作隐式强制类型转换。
显式强制类型转换旨在让代码更加清晰易读,而隐式强制类型转换看起来就像是它的对立面,会让代码变得晦涩难懂。

字符串与数字之间的隐式强制类型转换

  • 操作符规则
    如果两个操作数都是数字,将执行加法操作;
    如果有一个操作数是字符串(或者说能被转换成字符串),将执行字符串拼接操作;

根据 ES5 规范 11.6.1 节,如果某个操作数是字符串或者能够通过以下步骤转换为字符串的话, + 将进行拼接操作。如果其中一个操作数是对象(包括数组),则首先对其调用 ToPrimitive 抽象操作(规范 9.1 节),该抽象操作再调用 [[DefaultValue]](规范 8.12.8节),以数字作为上下文。

var a = 42
var b = '42'

var c = [4, 2]
var d = [2]

a + 0  // 42
b + 0  // '420'

c + d  // "4,22"

对于两个数组或者一个数组和一个对象相加,会执行以下操作:

  • c.toString() // "4,2"
  • d.toString() // "2"
  • "4,2" + "2" // "4,22"
  • 数字减法运算符
    为了执行减法运算,左右两边的数都要转换成数字,它们首先被转换为字符串(通过强制类型转换toString()),然后再转换为数字。

布尔值到数字的隐式强制类型转换

隐式强制类型转换为布尔值

发生布尔值隐式强制类型转换的情况:

  • if (..) 语句中的条件判断表达式。
  • for ( .. ; .. ; .. ) 语句中的条件判断表达式(第二个)。
  • while (..) 和 do..while(..) 循环中的条件判断表达式。
  • ? : 中的条件判断表达式。
  • 逻辑运算符 ||(逻辑或)和 &&(逻辑与)左边的操作数(作为条件判断表达式)。

||和&&


&&和||运算符的返回值并不一定是布尔类型,而是两个操作数其中一个的值。

var a = 42;
var b = "abc";
var c = null;
a || b; // 42
a && b; // "abc"
c || b; // "abc"
c && b; // null

下面是一个十分常见的||的用法:

function foo(a, b) {
    a = a || "hello";
    b = b || "world";
    console.log(a + " " + b);
}
foo(); // "hello world"
foo("yeah", "yeah!"); // "yeah yeah!"

有一种用法对开发人员不常见,然而JavaScript代码压缩工具常用。就是如果第一个操作数为真值,则&&运算符“选择”第二个操作数作为返回值,这也叫做“守护运算符”,即前面的表达式为后面的表达式“把关”:

function foo() {
    console.log(a);
}
var a = 42;
a && foo(); // 42

foo()只有在条件判断a通过时才会被调用。如果条件判断未通过,a&&foo()就会悄然终止(也叫做“短路”),foo()不会被调用。这样的用法对开发人员不太常见,开发人员通常使用if(a){foo();}

var a = 42;
var b = null;
var c = "foo";
if (a && (b || c)) {
    console.log("yep");
}

这里a&&(b||c)的结果实际上是“foo”而非true,然后再由if将foo强制类型转换为布尔值,所以最后结果为true。
现在明白了吧,这里发生了隐式强制类型转换。如果要避免隐式强制类型转换就得这样:

if (!!a && (!!b || !!c)) {
    console.log("yep");
}

 字符串和数字之间的相等比较

var a = 42;
var b = "42";
a === b; // false
a == b; // true

具体是怎么转换?是a从42转换为字符串,还是b从“42”转换为数字?
ES5规范这样定义:

  • (1) 如果Type(x) 是数字,Type(y) 是字符串,则返回x == ToNumber(y) 的结果。
  • (2) 如果Type(x) 是字符串,Type(y) 是数字,则返回ToNumber(x) == y 的结果。

其他类型和布尔类型之间的相等比较


==最容易出错的一个地方是true和false与其他类型之间的相等比较。

var a = "42";
var b = true;
a == b; // false

我们都知道“42”是一个真值,为什么==的结果不是true呢?
规范是这样说的:

  • (1) 如果Type(x) 是布尔类型,则返回ToNumber(x) == y 的结果;
  • (2) 如果Type(y) 是布尔类型,则返回x == ToNumber(y) 的结果。

所以建议,无论什么情况下都不要使用==true和==false。请注意,这里说的只是==,===true和===false不允许强制类型转换,所以并不涉及ToNumber。

var a = "42";
// 不要这样用,条件判断不成立:
if (a == true) {
    // ..
}
// 也不要这样用,条件判断不成立:
if (a === true) {
    // ..
}
// 这样的显式用法没问题:
if (a) {
    // ..
}
// 这样的显式用法更好:
if (!!a) {
    // ..
}
// 这样的显式用法也很好:
if (Boolean(a)) {
    // ..
}

null和undefined之间的相等比较


null和undefined之间的==也涉及隐式强制类型转换:

  • (1) 如果x 为null,y 为undefined,则结果为true。
  • (2) 如果x 为undefined,y 为null,则结果为true。

在==中null和undefined相等(它们也与其自身相等),除此之外其他值都不存在这种情况。
也就是说在==中null和undefined是一回事,可以相互进行隐式强制类型转换:

var a = null;
var b;
a == b; // true
a == null; // true
b == null; // true
a == false; // false
b == false; // false
a == ""; // false
b == ""; // false
a == 0; // false
b == 0; // false

下面是显式的做法,其中不涉及强制类型转换,个人觉得更繁琐一些(大概执行效率也会更低):

var a = doSomething();
if (a === undefined || a === null) {
    // ..
}

对象和非对象之间的相等比较


ES5规定:

  • (1) 如果Type(x) 是字符串或数字,Type(y) 是对象,则返回x == ToPrimitive(y) 的结果;
  • (2) 如果Type(x) 是对象,Type(y) 是字符串或数字,则返回ToPrimitive(x) == y 的结果。
var a = 42;
var b = [ 42 ];
a == b; // true

[42]首先调用ToPrimitive抽象操作,返回“42”,变成“42”==42,然后又变成42==42,最后二者相等。

之前介绍过的ToPrimitive抽象操作的所有特性(如toString()、valueOf())在这里都适用。

之前我们介绍过“拆封”,即“打开”封装对象,返回其中的基本数据类型值。==中的ToPromitive强制类型转换也会发生这样的情况:

var a = "abc";
var b = Object( a ); // 和new String( a )一样
a === b; // false
a == b; // true

但有一些值不这样,原因是==算法中其他优先级更高的规则:

var a = null;
var b = Object( a ); // 和Object()一样
a == b; // false
var c = undefined;
var d = Object( c ); // 和Object()一样
c == d; // false
var e = NaN;
var f = Object( e ); // 和new Number( e )一样
e == f; // false

因为没有对应的封装对象,所以null和undefined不能够被封装,Object(null)和Object()均返回一个常规对象。
NaN能够被封装为数字封装对象,但拆封之后NaN==NaN返回false,因为NaN不等于NaN。

比较少见的情况


首先来看看更改内置原生原型会导致哪些奇怪的结果:


1、返回其他数字:

Number.prototype.valueOf = function () {
    return 3;
};
new Number(2) == 3; // true

2==3不会有这个问题,因为2和3都是数字基本类型值,不会调用Number.prototype.valueOf()方法。而Number(2)涉及ToPrimitive强制类型转换,因此会调用valueOf()。

还有更奇怪的情况:

if (a == 2 && a == 3) {
    // ..
}

你也许觉得这不可能,因为a不会同时等于2和3,但“同时”一词并不准确,因为a==2在a==3之前执行。
如果让a.valueOf()每次调用都产生副作用,比如第一次返回2,第二次返回3,就会出现这样的情况。这实现起来很简单:

var i = 2;
Number.prototype.valueOf = function () {
    return i++;
};
var a = new Number(42);
if (a == 2 && a == 3) {
    console.log("Yep, this happened.");
}

2、假值的相等比较

"0" == null; // false
"0" == undefined; // false
"0" == false; // true -- 晕!
"0" == NaN; // false
"0" == 0; // true
"0" == ""; // false
false == null; // false
false == undefined; // false
false == NaN; // false
false == 0; // true -- 晕!
false == ""; // true -- 晕!
false == []; // true -- 晕!
false == {}; // false
"" == null; // false
"" == undefined; // false
"" == NaN; // false
"" == 0; // true -- 晕!
"" == []; // true -- 晕!
"" == {}; // false
0 == null; // false
0 == undefined; // false
0 == NaN; // false
0 == []; // true -- 晕!
0 == {}; // false

3、极端情况

[] == ![] // true

让我们看看!运算符都做了些什么?根据ToBoolean规则,它会进行布尔值的显式强制类型转换(同时反转奇偶校验位)。所以[]==![]变成了[]==false。前面我们讲过false==[],最后的结果就顺理成章了。

2 == [2]; // true
"" == [null]; // true
0 == "\n"; // true
42 == "43"; // false
"foo" == 42; // false
"true" == true; // false
42 == "42"; // true
"foo" == [ "foo" ]; // true

4、完整性检查

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

其中有4中情况涉及==false,之前我们说过应该避免,应该不难掌握。现在剩下后面3种。
正常情况下我们应该不会这样来写代码,我们应该不太可能会用==[]来做条件判断,而是用==""或者==0,如:

function doSomething(a) {
    if (a == "") {
        // ..
    }
}

如果不小心碰到doSomething(0)和doSomething([])这样的情况,结果会让你大吃一惊。
又如:

function doSomething(a,b) {
    if (a == b) {
        // ..
    }
}

doSomething("",0) 和doSomething([],"") 也会如此。

5、安全运用隐式强制类型转换
我们要对==两边的值认真推敲,以下两个原则可以让我们有效地避免出错:

  • 如果两边的值中有true或者false,千万不要使用==。
  • 如果两边的值中有[]、“”或者0,尽量不要使用==。

这时最好用===来避免不经意的强制类型转换。这两个原则可以让我们避开几乎所有强制类型转换的坑。

有一种情况下强制类型转换是绝对安全的,那就是typeof操作。typeof总是返回七个字符串之一,其中没有空字符串。所以在类型检查过程中不会发生隐式强制类型转换。typeof x=="function"是100%安全的,和typeof x==="function"一样。

 抽象关系比较

a<b中涉及的隐式强制类型转换不太引人注意,不过还是很有必要深入了解一下。
比较双方首先调用ToPrimitive,如果结果出现非字符串,就根据ToNumber规则将双方强制类型转换为数字来进行比较。

var a = [ 42 ];
var b = [ "43" ];
a < b; // true
b < a; // false

如果比较双方都是字符串,则按字母顺序来进行比较:

var a = [ "42" ];
var b = [ "043" ];
a < b; // false

a和b并没有被转换为数字,因为ToPrimitive返回的是字符串,所以ToPrimitive返回的是字符串,所以这里比较的是“42”和“043”两个字符串,它们分别以“4”和“0”开头。因为“0”在字母顺序上小于“4”,所以最后结果为false。
同理:

var a = [ 4, 2 ];
var b = [ 0, 4, 3 ];
a < b; // false

a转换为“4,2”,b转换为“0,4,3”,同样是按字母顺序进行比较。
再比如:

var a = { b: 42 };
var b = { b: 43 };
a < b; // ??

结果还是false,因为a是[object Object],b也是[object Object],所以按照字母顺序a<b并不成立。
下面的例子就有些奇怪了:

var a = { b: 42 };
var b = { b: 43 };
a < b; // false
a == b; // false
a > b; // false
a <= b; // true
a >= b; // true

根据规范a<=b被处理为b<a,然后将结果反转。因为b<a的结果是false,所以a<=b的结果是true。
这可能与我们设想的大相径庭,即<=应该是“小于或者等于”。实际上JavaScript中<=是“不大于”的意思(即!(a>b),处理为!(b<a))。同理a>=b处理为b<=a。
相等比较有严格相等,关系比较却没有“严格关系比较”。也就是说如果要避免a>b中发生隐式强制类型转换,我们只能确保a和b为相同的类型,除此之外别无他法。

valueOf()和toString()详解

valueOf()函数用于返回指定对象的原始值

该方法属于Object对象,由于所有的对象都"继承"了Object的对象实例,因此几乎所有的实例对象都可以使用该方法。

所有主流浏览器均支持该函数。

语法

valueOf()函数用于返回指定对象的原始值

该方法属于Object对象,由于所有的对象都"继承"了Object的对象实例,因此几乎所有的实例对象都可以使用该方法。

所有主流浏览器均支持该函数。

语法  object.valueOf( )

JavaScript的许多内置对象都重写了该函数,以实现更适合自身的功能需要。因此,不同类型对象的valueOf()方法的返回值和返回值类型均可能不同。

 

    // 对象在参与运算及比较的时候,js引擎会自动的帮我们去调用这两个方法.
    
    // 调用规则:
    //  1. 默认先调用valueOf方法(valueOf 来源于 Object的原型),尝试把对象转成简单数据类型,
        2. 如果没有得到简单数据类型,再继续去调用toString方法
    //  作用: 将对象转成原始值(简单数据类型),但是Object原型上的valueOf达不到目的, 把对象自身给返回了

    /*  var arr = [1,2,3];
        console.log(arr + 1); // 1,2,31
    //  arr.valueOf()  ==> arr
    //  arr.toString() ==> "1,2,3"
    //  "1,2,3" + 1    ==> "1,2,31"
    
    console.log(arr - 0); // NaN  
    // "1,2,3" - 0
    console.log(arr + "6班学习好努力"); // 1,2,36班学习好努力*/

    // var obj = {
    //     name: "lw"
    // };
    // console.log(obj * 1);  // NaN 
    // obj.valueOf() ==> obj
    // obj.toString() ==> "[object Object]"
    // "[object Object]" * 1
    // console.log(obj + "1");  // [object Object]1

    // console.log([] == ![]);  // true
    //  [] == false  ==> 都转数值进行比较
    //  [].valueOf() ==> []
    //  [].toString()  ==> ""
    //  +""  ==> 0

    //  console.log({} == !{});
    // {} == false   ==> 都转数值进行比较
    // {}.valueOf()  ==> {}
    // {}.toString() ==> "[object Object]"
    // +"[object Object]" ==> NaN (字符串转成数值 ===>  NaN, 不是一个数字)
    
    例1:
        // var obj = {};
        // console.log(obj.toString()); // "[objectObject]"
    例2:
        // var a = {},
        // b = { key: 'b' };
        // c = { key: 'c' };
        // a[b] = 123;
        // a[c] = 456;
        // console.log(a[b]);   //456
    
    // 给如果对象的属性类型不是一个字符串时,就会去转换.
        // js会自动让对象去调用这两个方法,默认先valueOf(),如果没有得到简单数据类型,再去调用toString()
        // 其中对象a中的属性 b ==> "[object,Object]" ,等同于 给对象a,使用[]语法,去添加属性并赋值
        // 再看对象a中的属性 c ==> "[object,Object]" ,等同于 修改同一个属性的属性值,所以最终的到的结果是456

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

你不知道的JavaScript-----强制类型转换 的相关文章