你不知道的JS系列——类型、值、强制类型转换

338 阅读15分钟

一生辗转千万里,莫问成败重几许,得之坦然,失之淡然。

共勉

当你的才华还撑不起你的野心的时候,你就应该静下心来学习;
当你的能力还驾驭不了你的目标时,就应该沉下心来,历练;
梦想,不是浮躁,而是沉淀和积累,只有拼出来的美丽,
没有等出来的辉煌,机会永远是留给最渴望的那个人,
学会与内心深处的你对话,问问自己,
想要怎样的人生,静心学习,
耐心沉淀,送给自己。

写在前面

如果你能答对下列表达式的结果或看了底部的答案知晓原理,此文请略过(解析在文章内)

  • parseInt( 1/0, 19 );
  • [] + {};
  • {} + [];
  • [] == ![];
  • "" == [null];

注:此篇文章较长,大家适时可以当做工具文章来查阅。

类型

什么是类型?

类型是值的内部特征,它定义了值的行为,以使其区别于其他值.

八种内置类型

  • 空值(null)
  • 未定义(undefined)
  • 布尔值( boolean)
  • 数字(number)
  • 字符串(string)
  • 对象(object)
  • 符号(symbol,ES6 新增)
  • 大整数(BigInt,ES6 新增)

typeof检测类型的怪异表象

null以外的六种类型均有同名的字符串值与之对应。一起来看下面:

typeof null;    // "object"
typeof function(){};    // "function"
typeof [];     // "object"
typeof NaN;     // "number"
typeof 123n;    // "bigint"

值和类型

JavaScript 中的变量是没有类型的,只有值才有。变量可以随时持有任何类型的值。
在对变量执行 typeof 操作时,得到的结果并不是该变量的类型,而是该变量持有的值的类 型,因为 JavaScript 中的变量没有类型。

理解 undefinedundeclared

  • undefined:已在作用域中声明但还没有赋值的变量
  • undeclared:还没有在作用域中声明过的变量

注意:typeof对于undefinedundeclared的处理方式相同。

var a;
a; // undefined
b; // ReferenceError: b is not defined
typeof a; // "undefined"
typeof b; // "undefined"

合理使用typeof Undeclared

typeofundeclared的处理是一种特殊的安全防范机制,我们可以利用这种机制来为某个缺失的功能写 polyfill。所以这里会有两种方式:

if (!window.obj) {/*...*/}  // 一般写法
if (typeof obj === 'undefined') {/*...*/}  // typeof 写法

注意: 一般写法,利用了对象的[[Get]]机制(查找的属性不存在时返回undefined),缺点:必须提供一个对象(如果直接写if(!obj)会报错) 。

数组

JS中的数组可以容纳任何类型的值,声明后即可向其中加入值,不需要预先设定大小。
注意: 使用 delete 运算符将数组中的单元删除后,数组的 length 属性并不会发生变化。

var arr = [1, 2, 3];
delete arr[2];
arr.length;     // 3

对于数组的空位,数组是特殊的对象,在《你不知道的JS系列——你所忽略的细节》一文已讲过。

类数组

常见的类数组:

  • DOM 查询操作返回的 DOM 元素列表
  • arguments 对象 (函数参数的类数组对象)

注意: arguments.callee(指向当前执行的函数)、arguments.caller(指向调用当前函数的函数) 在严格模式下都会抛出错误。严格模式下修改传入的显示参数值,arguments不会受到影响。

对于类数组向真正数组的转换,常用以下3种方式:

  • Array.prototype.concat.call()
  • Array.prototype.slice.call()
  • Array.from() (ES6)

字符串

字符串和数组很相似,都有length属性、indexOf()concat()方法('123'[0]输出"1")。
在ES6之前我们将字符串转换为字符串数组,我们会'str'.split('')这样写,ES6之后,我们[...'str']这样写。这里需要注意,虽然字符串和数组很像,但字符串毕竟是基本类型之一,它具有不可变性。

字符串不可变是指字符串的成员函数不会改变其原始值,而是创建并返回一个新的字符串。

字符串可以借用数组的非变更方法,但由于不可变性,不可以借用变更方法。
体会一下:

var a = 'str';
var b = Array.prototype.join.call( a, "-" );
var c = Array.prototype.map.call( a, function(v){
 return v.toUpperCase() + ".";
} ).join( "" );
b;  // "s-t-r"
c;  // "S.T.R."
Array.prototype.reverse.call(a);    // 报错

数字

JavaScript 只有一种数值类型:number(数字),包括“整数”和带小数的十进制数。
鉴于以上,所以对于 . 运算符需要给予特别注意,因为它是一个有效的数字字符,会被优先识别为数字常量的一部分,然后才是对象属性访问运算符。

45.toFixed(3);  // SyntaxError
45..toFixed(3); // "45.000"
... 还有很多写法,不一一列举

注意: isNaN()Number.isNaN()的一个不同点:

isNaN('str');   // true     (这是个bug)
Number.isNaN('str');    // false  (修复了bug)

再来看:

isNaN === Number.isNaN;     // false
Number.parseInt === parseInt;   // true
Number.parseFloat === parseFloat;   // true

原生函数

 内部属性 [[Class]]

所有 typeof 返回值为 "object" 的对象(如数组)都包含一个内部属性 [[Class]](我们可以把它看作一个内部的分类,而非传统的面向对象意义上的类)。

这个属性通过 Object.prototype.toString() 来查看:

Object.prototype.toString.call( [1,2,3] );  // "[object Array]"
Object.prototype.toString.call(function(){});   // "[object Function]"
Object.prototype.toString.call( null );     // "[object Null]"
Object.prototype.toString.call( undefined );    // "[object Undefined]"

这里通过Object.prototype.toString()来查看的原因是,Array、Function等对象重写了toString()方法。

封装对象包装

基本类型值 没 有 .length.toString()这样的属性和方法,需要通过封装对象才能访问,此时 JavaScript 会自动为 基本类型值包装一个封装对象

一个封装对象该注意的地方:

var a = new Boolean( false );
if (!a) {
    console.log( "hello" ); // 执行不到这里
}

这里的a是一个对象,并不等于false,即Boolean(new Boolean( false )) == true
封装对象可以使用 valueOf() 函数得到其中的基本类型值,这就叫拆封,有些地方会发生隐式拆封。

var a = new String( "abc" );
var b = a + ""; // b的值为"abc"
typeof a; // "object"
typeof b; // "string"

原生原型

注意: 有些原生原型是一个空的自己本身

typeof Function.prototype; // "function"
Function.prototype(); // 空函数!
RegExp.prototype.toString(); // "/(?:)/"——空正则表达式
"abc".match( RegExp.prototype ); // [""]
Array.isArray( Array.prototype );   // true
Array.prototype(); // 空数组

抽象值操作

ToString

ToString负责处理非字符串到字符串的强制类型转换。
我列举一下:

  • null 转换为 "null"undefined 转换为 "undefined"true 转换为 "true"false 转换为 "false"
  • 数字遵循通用规则,极小和极大数字使用指数形式(一般数值小数位数超过6个零或整数位多于21位)
  • 普通对象返回"[object Object]"
  • 若对象有自己的 toString()方法,字符串化时调用该方法并使用其返回值
  • 数组的默认 toString() 方法经过了重新定义,将所有单元字符串化以后再用 "," 连接起 来

ToNumber

ToNumber负责处理非数字值到数字值的强制类型转换。
转换规则:

  • 如果是 Boolean 值,true 和 false 将分别被转换为 1 和 0。
  • 如果是数字值,只是简单的传入和返回。
  • 如果是 null 值,返回 0。
  • 如果是 undefined,返回 NaN。
  • 如果是字符串,遵循下列规则:
  • 如果字符串中只包含数字(包括前面带正号或负号的情况),则将其转换为十进制数值,即"1" 会变成 1,"123"会变成 123,而"011"会变成 11(注意:前导的零被忽略了);
  • 如果字符串中包含有效的浮点格式,如"1.1",则将其转换为对应的浮点数值(同样,也会忽 略前导零);
  • 如果字符串中包含有效的十六进制格式,例如"0xf",则将其转换为相同大小的十进制整 数值;
  • 如果字符串是空的(不包含任何字符),则将其转换为 0;
  • 如果字符串中包含除上述格式之外的字符,则将其转换为 NaN。
  • 如果是对象,则调用对象的 valueOf()方法,然后依照前面的规则转换返回的值。如果转换 的结果是 NaN,则调用对象的 toString()方法,然后再次依照前面的规则转换返回的字符串值。

ToBoolean

数据类型转换为true的值转换为false的值
Booleantruefalse
String任何非空字符串""(空字符串)
Number任何非零数字值(包括无穷大)0和NaN
Object任何对象null
Undefined不适用undefined

注意: 假值对象document.all,它是一个类数组对象,但ToBoolean时返回fasle

显示强制类型转换

字符串和数字之间的显式转换

字符串和数字之间的转换通过 String() Number() 这两个内建函数来实现。String() 遵循前面讲过的 ToString 规则,Number() 遵循前面讲过的 ToNumber 规则。
一元操作符+操作可以将操作数显式强制类型转换为数字,如下:

 + "3.14";  // 3.14
 + new Date();  // 1591886247476

奇特的 ~ 运算符

~运算符(即字位操作“非”),~x 大致等同于 -(x+1)
因为~-1等同于-0,所以在if判断的时候会有 ~a.indexOf( "i" )这种写法来代替a.indexOf( "i" ) != -1这种写法。
了解一下:存储单元一般应具有存储数据和读写数据的功能,一般以8位二进制作为一个存储单元,也就是一个字节。一个存储单元即为一个字节,一个字节为八位,1k为1024个字节,1M为1024k。
ECMAScript 中的所有数值都以 IEEE-754 64 位格式存储,但位操作符并不直接操作 64位的值。而是先将 64 位的值转换成 32 位的整数,然后执行操作,最后再将结果转换回 64 位。
执行按位非(~)的结果就是返回数值的反码:

var num1 = 25;    // 二进制 00000000000000000000000000011001 
var num2 = ~num1; // 二进制 11111111111111111111111111100110 
alert(num2); // -26

显式解析数字字符串

这里讲parseInt的解析规则:

解析按从左到右的顺序,如果遇到非数字字符就停止。 若解析失败返回NaN

注意点:parseInt() 针对的是字符串值。向 parseInt() 传递数字和其他类型的参数是 没有用的,非字符串参数会首先被强制类型转换为字符串。比如 truefunction(){...}[],都会返回NaN
解析题目:parseInt( 1/0, 19 )
首先1/0返回Infinity,然后Infinity转换为字符串为"Infinity",然后按十九进制(0-9,a-i)解析,首先解析第一个字符I,以 19 为基数时I值为 18,再解析第二个字母n,解析失败,终止解析,所以结果为18。

显式转换为布尔值

即运用封装类型Boolean()来转换,转换规则为上面ToBoolean的表格。
我们一般很少写Boolean(),常用!!来代替,效果相同,意为取反再取反。

隐式强制类型转换

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

1 + ''这种常见操作,就是将数字隐式转换为字符串。关于+操作,下面列举转换规则:

  • 如果有一个操作数是 NaN,则结果是 NaN
  • 如果是 Infinity 加 Infinity,则结果是 Infinity
  • 如果是-Infinity 加-Infinity,则结果是-Infinity
  • 如果是 Infinity 加-Infinity,则结果是 NaN
  • 如果是+0 加+0,则结果是+0
  • 如果是 -0 加 -0,则结果是 -0
  • 如果是 +0 加 -0,则结果是 +0
  • 不过,如果有一个操作数是字符串,那么就要应用如下规则:
  • 如果两个操作数都是字符串,则将第二个操作数与第一个操作数拼接起来;
  • 如果只有一个操作数是字符串,则将另一个操作数转换为字符串,然后再将两个字符串拼接 起来。
  • 如果有一个操作数是对象、数值或布尔值,则调用它们的 toString()方法取得相应的字符串值, 然后再应用前面关于字符串的规则。
  • 对于 undefined 和 null,则分别调用 String()函数并取得字符串"undefined"和"null"。

解析题目:[] + {}{} + []
[] + {}应用+操作,先调用valueOf()得到自己本身[],可惜它不是一个基本类型的值,再调用toString()得到"",得到基本类型值空字符串,字符串与{}相加,再应用+操作,先调用valueOf()得到本身,再调用toString()得到"[object Object]",最后字符串拼接,结果是"[object Object]"
{} + []应用+操作,这里有个坑,{}被识别为代码块,所以只剩+ [][]经过操作得到基本类性值"",再执行+操作转换为数字,结果为0

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

列举一下会发生布尔值隐式强制类型转换的情况(非布尔值被隐式强制类型转换为布尔值):

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

|| 和 &&

没什么好讲的,直接上规则,你看了一目了然:

|| 的规则(有真则真,同假则假)
  • 如果第一个操作数是对象,则返回第一个操作数
  • 如果第一个操作数的求值结果为 false,则返回第二个操作数
  • 如果两个操作数都是对象,则返回第一个操作数
  • 如果两个操作数都是 null,则返回 null
  • 如果两个操作数都是 NaN,则返回 NaN
  • 如果两个操作数都是 undefined,则返回 undefined
0 || undefined; // undefined
0 || NaN;   // NaN
null || 0;  // 0

逻辑或操作符是短路操作符,第一个操作数的求值结果为true,就不会对第二个操作数求值。

&& 的规则(有假则假,同真则真)
  • 如果第一个操作数是对象,则返回第二个操作数
  • 如果第二个操作数是对象,则只有在第一个操作数的求值结果为 true 的情况下才会返回该 对象
  • 如果两个操作数都是对象,则返回第二个操作数
  • 如果有一个操作数是 null,则返回 null
  • 如果有一个操作数是 NaN,则返回 NaN
  • 如果有一个操作数是 undefined,则返回 undefined
 ({}) && 1; //1
 1 && {};  // {}
 0 && {};  // 0

逻辑与操作属于短路操作,即如果第一个操作数能够决定结果,那么就不会再对第二个操作数求值。

符号(Symbol)的强制类型转换

  • ES6 允许从符号到字符串的显式强制类型转换,然而隐式强制类型转换会产生错误
  • 符号不能够被强制类型转换为数字(显式和隐式都会产生错误)
  • 符号可以被强制类型转换为布尔值(显式和隐式结果都是 true)
var str = Symbol( "cool" );
String( str ); // "Symbol(cool)"
str + "";    // TypeError
Number(str); // TypeError
+ str;      // TypeError
Boolean(str);   // true
!! str;     // true

宽松相等和严格相等

在转换不同的数据类型时,宽松相等(==)操作符遵循下列基本规则:

  • 如果有一个操作数是布尔值,则在比较相等性之前先将其转换为数值——false 转换为 0,而 true 转换为 1
  • 如果一个操作数是字符串,另一个操作数是数值,在比较相等性之前先将字符串转换为数值;
  • 如果一个操作数是对象,另一个操作数不是,则调用对象的 valueOf()方法,用得到的基本类 型值按照前面的规则进行比较;
  • null 和 undefined 是相等的
  • 要比较相等性之前,不能将 null 和 undefined 转换成其他任何值
  • 如果有一个操作数是 NaN,则相等操作符返回 false,而不相等操作符返回 true
  • 如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象, 则相等操作符返回 true;否则,返回 false

重要提示: 即使两个操作数都是 NaN,相等操作符也返回 false;因为按照规则,NaN 不等于 NaN。

null == undefined;  // true
"NaN" == NaN;   // false
null == 0;  // false  说明:虽然Number(null)为0,但比较相等性,不允许null转换
undefined == NaN; // false  说明:虽然Number(undefined)为NaN,但这里也不会发生转换

严格相等(===)即先比较类型,类型不同直接返回false,类型相同再比较值,不会发生隐式转换。
关于Object.is()《你不知道的JS系列——你所忽略的细节》已经讲过。
一些例子:

2 == [2];   // true
" " == NaN; // false
0 == "\n";  // true  
// 说明:""、"\n"(或者 " " 等其他空格组合)等空字符串被ToNumber 强制类型转换为0

解析题目: [] == ![]
![]应用ToBoolean规则返回false, []经过操作得到基本类型值"",false转为数值0,""转为数值0,所以最终结果是true
解析题目: "" == [null]
很简单,因为[null].toString()""所以相等,结果为true[undefined].toString()也为"",除这两个特殊外,其他像[NaN].toString(),输出结果为"NaN"就很正常了。

抽象关系比较

><操作都是转换成字符和字符比较(按字母顺序比较)或者数字和数字比较。
这里强调>=<=操作符并不是想你想的那样,即a <= b并不等价于 (a < b) && a == b, 根据规范a <= b等价于!(a > b)。简单理解:a <= ba不大于b,那么我让a大于b再取反,得到的结果一致。

关于类型转换还是有很多坑的,大家要小心应对!!!

答案:

18
"[object Object]"
0 
true
true