强制转换

March 24, 2021

前言

类型与文法,在两个月以前其实已经看完了,但看完‘this与对象原型’和‘类型与文法’章节后,却自以为早已经掌握,没有什么可以谈的,于是便束之高阁。直到前一阵子,部门分享python基础的时候,提到python没有变量声明,拿来就用。着不是和JS很像?JS为什么没有变量声明呢?记得C语言都用变量声明,为何JS没有呢?变量声明有什么作用?好不好?

知乎问题为什么像 Java、C、C++ 这样的静态语言会比 Python、Ruby 这样的动态语言流行得多?

再结合另外一个知乎问题弱类型、强类型、动态类型、静态类型语言的区别是什么?里面介绍到:

Program Errors • trapped errors。导致程序终止执行,如除0,Java中数组越界访问 • untrapped errors。 出错后继续执行,但可能出现任意行为。如C里的缓冲区溢出、Jump到错误地址

Forbidden Behaviours 语言设计时,可以定义一组forbidden behaviors. 它必须包括所有untrapped errors, 但可能包含trapped errors. Well behaved、ill behaved • well behaved: 如果程序执行不可能出现forbidden behaviors, 则为well behaved。 • ill behaved: 否则为ill behaved…

强、弱类型 • 强类型strongly typed: 如果一种语言的所有程序都是well behaved——即不可能出现forbidden behaviors,则该语言为strongly typed。 • 弱类型weakly typed: 否则为weakly typed。比如C语言的缓冲区溢出,属于trapped errors,即属于forbidden behaviors..故C是弱类型

动态、静态类型 • 静态类型 statically: 如果在编译时拒绝ill behaved程序,则是statically typed; • 动态类型dynamiclly: 如果在运行时拒绝ill behaviors, 则是dynamiclly typed。

可以发现像JS这样的动态语言弱类型,没有如JAVA这样明显的编译过程,只是在浏览器或者Node运行时存在解析过程,并且在解析的时候检查有无forbidden behaviors。这样的动态语言有什么好处了,如部分答主所说的动如脱兔,如同草书般洒脱,不像其他语言一笔一划,讲求中正平稳。看看几年前JavaScript文件,程序如果比较大,几百行JavaScript都可以看得人头晕脑胀,也有没有IDE这样跳转工具,虽然现在有webStorm,对于大规模开发自然是不友好的,这种乱的感觉自然让很多人避开它。 到现在ECMAscript已经发展到ES7甚至ES8了,在Vue-router的开发里面,甚至都用上了Facebook的flow,来验证变量类型,提高代码质量,现在的JS已经是越来越旺盛了。 于是对变量声明的更进一步理解,于是又开始看‘类型与文法’这一章,没想到收获很多

开始

在JavaScript中,变量没有类型 -- 值才有类型。变量可以在任何时候,持有任何值

这句话深入我心,JS里面有var的变量声明,后面又有let和const。声明的变量可以是任何值,从基本变量到Object都是木有问题的,甚至在非严格模式下,不声明直接使用变量也不会报错,而且变量还可以随意更改,从number变到Object,完全没有问题,那为何要称变量为类型呢?明明说的就是变量背后的值是什么类型;

强制转换

falsy列表:

  1. undefined
  2. null
  3. false
  4. +0, 0, NaN
  5. '' 这些在Boolean强制转换的时候都会变成false,值得注意的是String类型里面仅有唯一一个空字符串('')会被转换为false。 同时,类似的如[], {}以及-1等等的都是true值,这些简单有用的true/false还是值得注意的。

明确的强制转换

一元操作符'+'能将String明确的转换为number,同样能将Date类型变为number如:

var d = new Date( "Mon, 18 Aug 2014 08:53:06 CDT" );
+d; // 1408369986000

但是通常都不会这么做,没有语义,容易误解,不如常用的'getTime()'好使

需要小心的是parseInt传入非string类型的时候产生的bug。

隐含的强制转换

下面的都是是这一章的重点了,明确的强制转换大多好懂,而且不容易犯错。相比之下,隐含的强制转换就深邃的多了。

var a = [1,2];
var b = [3,4];

a + b; // "1,23,4"

上面例子中当操作数不是number的时候,它们都被强制转换为String类型,我们知道'42' + '1' = '420'通过String来实现字符串拼接,但是Array为什么也会这么做?有深度的内容来了:

根据ES5语言规范的11.6.1部分,+的算法是(当一个操作数是object值时),如果两个操作数之一已经是一个string,或者下列步骤产生一个string表达形式,+将会进行连接。所以,当+的两个操作数之一收到一个object(包括array)时,它首先在这个值上调用ToPrimitive抽象操作(9.1部分),而它会带着number的上下文环境提示来调用[[DefaultValue]]算法(8.12.8部分)。

如果你仔细观察,你会发现这个操作现在和ToNumber抽象操作处理object的过程是一样的(参见早先的“ToNumber”一节)。在array上的valueOf()操作将会在产生一个简单基本类型时失败,于是它退回到一个toString()表现形式。两个array因此分别变成了"1,2"和"3,4"。现在,+就如你通常期望的那样连接这两个string:"1,23,4"。

这上面两段话以为着什么?说的是当你用object做加法操作的时候,object会调用其valueOf方法并返回基本类型,如果valueOf返回的基本类型失败(如没有valueOf方法或则返回的是object),那么就退回去用toString()方法,如果object没有toString()方法,那就返回错误;当然对象自然都是有toString方法的。 其他的String到number的常见的转换就是如'42' + 2 = 44通过字符串和数字直接相加。

上面提到的都是'+'加法计算,那减法呢? 字符串相加可以理解为拼接的过程,但是相减就完全不一样了,如下:

[3] + [1] // '31'
[3] - [1] // 2

恐怖吧?减法直接将其强制转为String,再由String转换为Number类型。

||和&&操作符

||和&&操作符其实没有特别的,只是当你去研究它的时候,又会发现它很特别 在判断的时候如if语言或则是三目判断,常会用到||和&&操作符。但是:

引用ES5语言规范的11.11部分: 一个&&或||操作符产生的值不见得是Boolean类型。这个产生的值将总是两个操作数表达式其中之一的值。

||和&&操作符只是意味着选择,而不是其他语言那样的逻辑判断! 想想确实也用过||和&&来做选择,比如以前很常用的:

e = event || window.event

用来做兼容处理。这里用的就是选择,而常见的cb&&cb()也是做选择。那我们所谓的逻辑判断呢?这个时候用选择的思维考虑一下,马上豁然开朗,逻辑的意思也就是返回最后被选择的数,如果这个数是true那就是真咯。

宽松等价与严格等价

提到== 和 ===操作,大多数开发者对前者经常是避而不及,省心的选择就是用后者,做严格的判断,毕竟前者太过花哨了。。。。。。还是先用心了解一下吧: 这里不得不提几条规范:

4.如果Type(x)是Number而Type(y)是String, 返回比较x == ToNumber(y)的结果。 5.如果Type(x)是String而Type(y)是Number, 返回比较ToNumber(x) == y的结果。 6.如果Type(x)是Boolean, 返回比较 ToNumber(x) == y 的结果。 7.如果Type(y)是Boolean, 返回比较 x == ToNumber(y) 的结果。

当String类型和Number比较的时候,String类型会被ToNumber为数字进行对比。同样的Boolean类型会被ToNumber,转换为0或者1。于是就有了如下:

'42' == false // false
'42' == true // false

可以看出'42'既不是0也不1,但是上面表达式却显得42不true也不false。

2.如果x是null而y是undefined,返回true。 3.如果x是undefined而y是null,返回true。 8.如果Type(x)是一个String或者Number而Type(y)是一个Object, 返回比较 x == ToPrimitive(y) 的结果。 9.如果Type(x)是一个Object而Type(y)是String或者Number, 返回比较 ToPrimitive(x) == y 的结果。

对于null和undefined自然是自身不true也不false,就等于null或则undefined,这个还是很容易理解的;那Object呢? 前面在'+'加法操作的时候提到过Object的转换问题,能通过valueOf返回基本类型就返回,不能就通过toString()方法,这里也是适用的。 值得一提的是形如{}这样的对象,其toString()之后是[Object Object]而不是'0',和平时用的Object.prototype.toString.call还是很接近的。 来看看下面例子

Number.prototype.valueOf = function() {
    return 3;
};

new Number( 2 ) == 3;   // true

这种就是业界毒瘤了,希望不要有傻逼这么写。。。。。哈哈哈哈

抽象关系比较

前面提到了加法和双等于判断,接下来怎么可以不提到大于小于判断呢?有了前面的基础,对于强制转换还是很容易理解的。看一下例子

var a = { b: 42 };
var b = { b: 43 };

a < b;  // false
a > b;  // false
a == b;  // false

a <= b; // true
a >= b; // true

这里Object对象自然都是被强制转换为[object Object],那肯定是不是大小于关系的,那==呢?[object Object]难道不等于[object Object]?这个时候就不要陷入思维误区了,就像'' == '0'为false一样,明明转换后都是数字0为何不成立?因为他俩都是String,不用强制转换直接对比。同样的Object和Object用==判断自然也不用强制转换。值得一提的是Object只有和自己做==判断的时候才为true。 那后面的小于等于和大于等于呢?妈呀,看着都要乱了,这个要根据语言规范了,谁叫他是老大呢?

因为语言规范说,对于a <= b,它实际上首先对b < a求值,然后反转那个结果。因为b < a也是false,所以a <= b的结果为true。

原来是反转。。。。。。JS的逻辑还真会玩

文法

语句是有完成值的,比如你打开浏览器的控制台,输入var a = 1回车,这个时候,显示的下一行是undefined,而不是1,但你敲入a会车的时候,才显示1,这里的undefined和1就是语句的完成值。 关于强制转换,一个经常被引用的坑是[] + {}{} + [],这两个表达式的结果分别是[object Object]0,因为后面的{}表示的是缺省';'的语句于是变成{}; + []自然是0了。 另外文中还提到结构解析,操作优先级,自动分号(ASI)和错误的问题,这些都是基础部分,这里就不介绍了。