一生辗转千万里,莫问成败重几许,得之坦然,失之淡然。
共勉
当你的才华还撑不起你的野心的时候,你就应该静下心来学习;
当你的能力还驾驭不了你的目标时,就应该沉下心来,历练;
梦想,不是浮躁,而是沉淀和积累,只有拼出来的美丽,
没有等出来的辉煌,机会永远是留给最渴望的那个人,
学会与内心深处的你对话,问问自己,
想要怎样的人生,静心学习,
耐心沉淀,送给自己。
写在前面
如果你能答对下列表达式的结果或看了底部的答案知晓原理,此文请略过(解析在文章内)
- 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 中的变量没有类型。
理解 undefined 和 undeclared
undefined:已在作用域中声明但还没有赋值的变量undeclared:还没有在作用域中声明过的变量
注意:typeof对于undefined 和 undeclared的处理方式相同。
var a;
a; // undefined
b; // ReferenceError: b is not defined
typeof a; // "undefined"
typeof b; // "undefined"
合理使用typeof Undeclared
typeof对undeclared的处理是一种特殊的安全防范机制,我们可以利用这种机制来为某个缺失的功能写 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的值 |
|---|---|---|
| Boolean | true | false |
| 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() 传递数字和其他类型的参数是
没有用的,非字符串参数会首先被强制类型转换为字符串。比如 true、function(){...} 和 [],都会返回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 <= b即a不大于b,那么我让a大于b再取反,得到的结果一致。
关于类型转换还是有很多坑的,大家要小心应对!!!
答案:
18
"[object Object]"
0
true
true