乐趣区

关于javascript:JavaScript类型转换详解

在 JavaScript 中,数据类型总共有以下几种:

Number
String
Boolean
null
undefined
Object
BigInt
Symbol

JS 自身是一门弱类型的动静语言。

所谓的弱类型,也就是说,不同的类型之间能够进行间接运算。比方

let a = 1 + "2"; // "12"

而在这其中,起到关键性作用的就是类型转换。

类型转换

类型转换,分为两种类型:显式类型转换 隐式类型转换

什么是显式和隐式呢?简略来讲,一眼能看明确的,就是显式的。一眼看不出来的,就是隐式的。举个例子:

let a = 1;
let b = String(a); // "1"

这里咱们用了一个函数叫做 String,字面上的意义就是,将数字类型的变量 a,转换成字符串类型,赋值给 b。

与此绝对的,隐式的例子如下:

let a = 1;
let b = a + ""; //"1"

在这里,咱们没有用申明式的语言来形容这样一个过程。而只有你理解了加号运算符到底做了什么之后,你能力明确这实际上是一个类型转换的操作。

当然,对于理解外部原理的开发者来讲,这两种形式对他们来讲是雷同的。显式与隐式,实际上也只是一个绝对的概念。你了解的,就是显式;你不了解的,就是隐式。

为什么要学习类型转换

类型转换,在大多数开发者看来,仿佛是一个不应该去触碰的深坑,类型转换仿佛是 JavaScript 这门语言设计时候的缺点。

从设计的角度来看,语言一开始设计为弱类型是为了升高开发者上手的难度,能够不必在各种类型之间做简单的转换,能够不必去过多地理解简单的类型零碎。对于初学者来讲,这确实是敌对的。

然而,在深刻学习语言之后,咱们却发现,为了真正的了解这门语言,咱们不仅要对各种类型进行粗浅的了解,甚至还要对语言外部转换的原理进行学习,这实际上是减少了隐形的学习老本。

然而回归到初衷上来,如果能真正的了解语言外部是如何对这种类型转换进行计算的,对咱们写出优雅的代码是有益处的。

显式类型转换

要真正理解类型转换,首先咱们要来理解以下三种显式的类型转换:ToNumber、ToString、ToBoolean。

这三种办法,是在标准实现的,咱们并不能间接调用。然而,语言也有暴露出一些接口,能够让咱们调用到这些语言外部实现的接口。

之所以要先钻研这三种类型的转换操作,是因为这是三种最根本的类型转换操作。所有的类型转换操作,实质上都是这几种类型的转换。

1. ToNumber

顾名思义,这就是将类型值转换为数字类型的办法。

它的一些转换例子转换如下:

Value Number
null 0
undefined NaN
true 1
false 0
“” 0
“1” 1
“abc” NaN
10n 10
{} NaN
“Infinity” Infinity
[1] 1
[“1”] 1
[1, 2] NaN
/123/ NaN

ToNumber 操作,返回的值肯定是数字类型的,数字类型的值,有这样这几种取值的可能:

  1. 整数
  2. 浮点数
  3. NaN
  4. Infinity/-Infinity

ToNumber 操作的法则是这样子的:

  1. 如果值为数字类型,那么就不变
  2. 如果值为 null,则为 0;如果值为 undefined,则为 NaN
  3. 如果值为布尔值,则如果为 true,则为 1,为 false,则为 0
  4. 如果值为 BigInt,那么就转换为相应的 Number 类型
  5. 如果值为字符串,那么就有多种可能的后果:

    1. 如果值为空字符串,或者蕴含有多个空格的字符串,则为 0,如“”=> 0,“”=> 0
    2. 如果值为一个整数或浮点数的字符串模式,则转换为相应的数值,如:“1.2”=> 1.2
    3. 如果值为“Infinity”或“-Infinity”,则转换为相应的 Infinity 或 -Infinity
    4. 如果值为其余,则为 NaN
  6. 如果值为对象,则执行 ToPrimitive 操作,再将返回值转换为 Number 类型

对于外部的 ToNumber 操作,咱们实际上能够通过 Number 内建函数或者一元加操作符来进行调用。比方:

let a = "1";
let b = Number(a); // 1
let c = +a; // 1

一般来讲,一元加操作符在业内也常被认为是显式类型转换的操作。

2. ToString

ToString 操作,是返回一个值的字符串模式。

它的转换规则如下:

Value String
1 “1”
Infinity “Infinity”
NaN “NaN”
null “null”
undefined “undefined”
true “true”
false “false”
1n “1”
[1, 2, null, undefined] “1,2,,”
{} “[object Object]”
/123/ “/123/”

ToString 操作的返回值肯定是字符串,它的法则比拟简单明了,法则如下:

  1. 对于根本类型(null、undefined、Number、Boolean、BigInt、String)以及正则表达式,都是间接返回其字符串模式
  2. 对于对象来说,则调用其 toString 办法,取得其返回值,将其转换为 String 类型,然而这里是须要分状况探讨的:

    1. 首先对于一般对象来说,其原型链上的对象原型(也就是 Object.prototype)上,有一个 toString 办法,其返回值为 ”[object Object]”
    2. 对于不继承对象原型的对象,比方应用 Object.create(null)办法创立的对象,没有 toString 办法,则会报语法错误
    3. 对于数组类型 Array,其 toString 办法是通过重写的,返回的是其各项值的 ToString 值,之后用逗号“,”进行拼接。这里须要留神的是,null 值和 undefined 值会转换为空字符串 ””
    4. 对于日期类型 Date,则间接返回一个相似 ”Tue Nov 17 2020 21:58:53 GMT+0800 (中国规范工夫)” 格局的字符串
    5. 对于函数类型,则间接返回其函数定义的字符串模式

咱们能够通过 String 内建函数,或者应用二元加操作符与空字符串相加,来调用外部的 ToString 操作,比方:

let a = 1;
let b = String(a); // "1"
let c = a + ""; //"1"

3. ToBoolean

ToBoolean 操作,返回一个值的布尔值模式。

返回 true 的值,称之为 truthy;返回 false 的值,称之为 falsy。

实际上,truthy 列表是有限长的,而 falsy 值则是可枚举的。咱们只须要记住 falsy 值即可。

Value Boolean
0 false
-0 false
0n false
“”/”/“ false
NaN false
null false
undefined false
false false

其法则如下:

  1. 对象(包含其所有子类型)都是 truthy
  2. 数字类型中,0、- 0 和 NaN 都是 falsy
  3. 空字符串是 falsy,然而蕴含空格的空字符串则不是
  4. BigInt 的 0n 值是 falsy
  5. 布尔值 false 自身也是 falsy

咱们能够通过 Boolean 内建函数,以及双重逻辑非来调用外部的 ToBoolean 操作:

let a = 1;
let b = Boolean(a); // true
let c = !!a; // true

4. ToPrimitive

理解完了这三种根本类型转换操作,咱们还要来理解一种对象的转换操作。这种操作能够将对象转换为根本类型值。

在此之前,咱们首先要理解几个对象上的办法,别离是

obj[Symbol.toPrimitive]();
obj.toString();
obj.valueOf();

1. [Symbol.toPrimitive]

这是一个新增的对象办法。默认的对象是不存在这个办法的。须要咱们手动设置。它的写法个别如下:

let a = {[Symbol.toPrimitive](hint){if(hint === "string"){return "string"}
        if(hint === "number"){return 1;}
        if(hint === 'default'){return "any"}
    }
};

console.log(Number(a)); // 1
console.log(String(a)); // "string"
console.log(a + 1); // "any1"

首先,[Symbol.toPrimitive]接管一个字符串参数,其取值的可能为:

  1. “string”,示意要转换为 string 类型的值
  2. “number”,示意要转换为 number 类型的值
  3. “default”,示意语言不晓得要转换为何种类型的值

当咱们明确调用 String 内建函数的时候,ToPrimitive 函数会传入一个 ”string” 参数,返回一个值。

这里须要留神的是,这里必须返回一个根本类型的值,如果返回一个援用类型的值,会返回一个报错信息。

之后,当返回一个根本类型的值后,再对他执行 String 办法,转换成字符串。举个例子:

let a = {[Symbol.toPrimitive](hint){if(hint === "string"){return {}; // 这里返回一个对象
            }
    }
}

String(a); // VM247:1 Uncaught TypeError: Cannot convert object to primitive value

let a = {[Symbol.toPrimitive](hint){if(hint === "string"){return true; // 这里返回一个布尔值}
    }
}

String(a); // "true" // 后果还是失去了一个字符串

能够看到,[Symbol.toPrimitive]办法十分明了地定义了一个对象类型转换的所有行为,如果咱们须要自定义对象的类型转换规定,应该优先应用该办法。

2. valueOf

valueOf 办法,执行的是一个拆封的操作,他的行为会依据对象的不同而发生变化。

这里咱们把对象分为一般对象与包装对象。

1. 包装对象

所谓包装对象,就是指根本类型值的对象模式。

那么什么是包装对象呢?为了阐明这个问题,咱们首先举一个例子:

let a = "1";
a.toString === String.prototype.toString; // true

在这里咱们能够看到,咱们首先申明了一个字符串。而后咱们发现这个字符串上的 toString 办法,实际上就是 String 内建函数原型上的 toString 办法。

问题在于,办法是对象的办法,而不是字符串的。为什么根本类型的值能够调用到对象上的办法呢?这里实际上是引擎外部帮咱们新建了一个包装对象,再执行解封:

let a = "1";

function toString(val){var _temp = new String(val); // 这里应用 new 操作符新建了一个字符串类型的包装类型对象,也就是所谓包装
    var result = _temp.valueOf(); // 这里应用了 valueOf 操作把字符串包装对象外面的原始值字符串提取进去,也就是所谓拆封
    _temp = null;
    return result;
}

a.toString(); // "1"
toString(a); // "1"

因而,对于包装类型而言,其 valueOf 办法就是一个解封的操作,提取出其根本类型值。

new String("1").valueOf(); // "1"
new Number(1).valueOf(); // 1
new Boolean(true).valueOf(); // true
2. 一般对象

然而对于一般对象而言,valueOf 操作返回的是对象自身,这其中就包含一般对象、函数、数组、正则表达式。

这里有一个例外,就是 Date 对象,其 valueOf 返回的是其数字类型的毫秒值。

还有一点要留神的是,valueOf 办法,是存在于 Object.prototype 下面的。因而,精确来讲,只有继承了对象原型的对象,能力领有 valueOf 办法。相似于 Object.create(null)办法创立的对象,是不存在 valueOf 办法的,调用的时候会报错。

3. toString

toString 办法,是将对象转换成字符串的办法。在不同的内建函数原型上有不同的体现。

1. Object.prototype.toString

首先咱们最罕用的,就是对象原型上的 toString:

let a = {};
a.toString === Object.prototype.toString; // true

这个办法,返回的是一个对象的类型示意字符串[object type],如 ”[object Object]”,”[object Array]”,”[object RegExp]”。

2. Array.prototype.toString

数组原型上的 toString 办法笼罩了对象原型上的 toString 办法,它的行为是这样的:

对数组中的每一项执行 String 办法,而后将后果值用逗号 ”,” 进行拼接。

须要留神的是,null 值和 undefined 会转成空字符串 ”” 而不是相应的 ”null” 和 ”undefined”。比方:

let a = [null, undefined, {}, function(){}, 2, [3, 5], /123/, new Date(), true, "string"];

Array.prototype.toString.call(a); // ",,[object Object],function(){},2,3,5,/123/,Wed Nov 18 2020 14:19:17 GMT+0800 (中国规范工夫),true,string"

数组的 toString 办法,能够用于字符串数组的拍平,因为其会递归地对外部所有数组调用 toString。

3. Function.prototype.toString

函数原型的 toString 办法,会返回该函数的源代码字符串,比方:

function fn(a, b){return a + b;}

Function.prototype.toString.call(fn);
/*
"function fn(a, b){return a + b;}"
*/
4. Date.prototype.toString

日期对象原型的 toString 办法返回一个字符串,示意该 Date 对象,比方:

let a = new Date();
Date.prototype.toString.call(a); // "Wed Nov 18 2020 14:25:41 GMT+0800 (中国规范工夫)"
5. RegExp.prototype.toString

正则表达式对象原型的 toString 办法返回一个字符串,示意该正则表达式对象,比方:

let a = /123/;
RegExp.prototype.toString.call(a); // "/123/"
6. 自定义 toString

除了以上这些在原型上事后定义好的 toString 办法外,咱们也能给本人的对象本人定义一个 toString 办法:

let a = {toString(){return "toString";}
}
String(a); // "toString"

须要留神的是,返回的值必须为根本类型值,如果为援用类型值,会抛出一个语法错误:

let a = {toString(){return [];
    }
}
String(a); // Uncaught TypeError: Cannot convert object to primitive value

4. 调用法则

对于 ToPrimitive 操作,它必须返回一个根本类型值,否则会抛出类型谬误。

法则如下:

  1. 如果存在 [Symbol.toPrimitive] 办法,则调用该办法,如果该办法返回援用类型值,则抛出类型谬误
  2. 如果不存在 [Symbol.toPrimitive] 办法,则再做判断

    1. 如果指明调用字符串类型转换,则优先调用 toString

      1. 如果 toString 返回一个援用类型值,则转而调用 valueOf
      2. 如果 valueOf 也返回一个援用类型值,则抛出类型谬误
    2. 如果不指明字符串类型转换,则默认优先调用 valueOf

      1. 如果 valueOf 返回一个援用类型值,则转而调用 toString
      2. 如果 toString 也返回一个援用类型值,则抛出类型谬误

隐式类型转换

介绍了所有显式类型转换,咱们来聊聊隐式的类型转换。

隐式类型转换,在很多场景下都会产生,这里咱们只讲最常见的几种:

相等操作符 ==

在 JavaScript 中,判断相等性有两种操作符:相等操作符 == 与全等操作符 ===。

有一部分的开发者认为,全等操作符 ===,不仅判断值,还要判断类型。而相等操作符 ==,则仅仅判断值。

实际上这样子的了解是谬误的。

正确的解释是:相等操作符会对两边的表达式进行隐式类型转换,而全等操作符则直接判断相等与否。

相等操作符的相等判断操作如下:

  1. 首先判断类型是否雷同,类型雷同则间接执行全等判断

    1. 如果是根本类型值,则判断两个值是否雷同
    2. 如果是援用类型值,则判断两个对象的内存地址是否雷同,简略来讲就是判断是否为同一个对象
  2. 如果类型不同,则进行类型转换,转换规则如下:

    1. 如果存在对象,首先对对象执行 ToPrimitive 操作,而后再进行相等判断
    2. 这个时候,如果存在字符串与布尔值,则对值执行 ToNumber 操作,转成数字类型,而后再进行相等判断

有了以上的法则,咱们就能够看看以下例子:

"42" == true; // false
"42" == true; // false

咋一看,咱们会感觉很奇怪,字符串 ”42″ 既不是真,也不是假,给人一种十分奇怪的感觉。

但实际上咱们应该对其做这样子的转换:

Number("42") == Number(true);
// =>
42 == 1 // false

将两边的值都转换为数字类型,再进行比拟,就高深莫测了。

另外还有一道很容易踩坑的题目:

[] == ![]; // true

咋一看,一个值与它的逻辑非值应该是相同的,然而在这里的确相等的,为什么呢?实际上应该做如下转换:

[] == false;
// =>
"" == false;
// =>
Number("") == Number(false);
// =>
0 == 0 // true

另外还有一道与之类似的题目:

{} == !{}; // false
// =>
{} == false;
// =>
"[object Object]" == false;
// =>
Number("[object Object]") == Number(false);
// =>
NaN == 0; // false

二元加操作符 +

另一个要介绍的操作符就是二元加。

二元加操作符,能够执行的操作有两种:

  1. 字符串拼接
  2. 数值相加

二元加操作符的操作法则如下:

  1. 如果两端有对象,首先对对象执行 ToPrimitive 操作,转换成根本类型值
  2. 如果一端有字符串类型,则对另一端执行 ToString 操作,而后执行字符串拼接操作
  3. 如果没有字符串类型,则对两端执行 ToNumber 操作,而后进行相加

接下来咱们看几个例子:

let a = {} + 3; // "[object Object]3"
// =>
let a = "[object Object]" + 3;
// =>
let a = "[object Object]" + "3"; // "[object Object]3"

这里留神,后面的赋值表达式 = 不能够去掉。咱们常常还会见到这样的陷阱:

{} + "3"; // 3

这样的问题,让咱们匪夷所思。实际上,花括号在 Javascript 中的语义是多样的。在这样的一行中,没有赋值操作,引擎将其了解为一个代码块。因而,实际上这里的代码应该被了解为如下:

{};
+"3"; // 3

换句话说,这里是一个陷阱,这里的加号并不是一个二元加,而是一个一元加。对于一元加表达式,执行的是 ToNumber 操作。

编程格调

对于相等操作符 == 来讲,有几个值是绝对不太平安的:

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

个别状况下,倡议不要将布尔值(true/false)、数值 0,空数组与空字符串放在相等操作符中,而改用全等操作符,就不容易呈现大的问题。

退出移动版