JavaScript 进阶(五)易混淆概念null vs undefined, == vs ===, string vs String

先看一段代码

[javascript] view plaincopy
 
  1. var foo = {}  
  2. foo.a = null  
  3. alert(foo.a == null)     //true  
  4. alert(foo.a === undefined)  //false  
  5. alert(foo.b == null)     //true  
  6. alert(foo.b === null)   //false   

直观上来看,foo.a 是 null, foo.b 是undefined,所以null == undefined 为true,null === undefined  为false。null与undefined似乎都代表空,== 与 === 都代表相等判断,那么JavaScript为什么要引入这两对相似的概念,它们又有什么区别呢?我们往下看。

null 与 undefined

null,代表空值,这个概念在绝大多数编程语言中都有,一般代表空的指针,比如在c和java中。一个生硬的解释就是“这个指针是存在的,但是指针是空值”,如果你尝试访问一个空指针指向的对象或地址,则会抛出“NPE(NullPointerException)”。而undefined似乎就是JavaScript的特产了,至少C和Java都没有这个概念。undefined顾名思义,就是未定义,那么为C/Java没有呢?事实上它们俩也是有的,但是undefined在编译的时候就已经被处理掉了。“变量未定义”这个编译错误我相信写过C/Java的人都遇到过吧。有了上面的解释,那我们来重新理解一下null和undefined,null指的是“变量存在,其值为空”,undefined则代表”该变量不存在,或未初始化“。此外,它们的类型也是不同的,typeof(null) == "object", typeof(undefined) == "undefined"
在参与数字运算时,null则被当成0处理,undefined参与任何数字运算返回值均为NaN(Not a Number. 但typeof(NaN) == "number", 擦,这不是自抽耳光么,WTF)。null 与 undefined与字符串相加时,分别被转换成“null”, "undefined"

== 与 ===

这两种等于号对于不同的数据类型要分开理解
1. 对于string,数字,布尔。
== 时候,如果类型相同,那么用它们的值进行比较,判断true/false,如果类型不同,则先转换成相同类型,在比较值。转换类型的规则是:布尔转成数字(true->1, false->0),数字转成String,或是布尔转成数字,再转成string(true->"1", false->"0"),转成类型相同之后,再比较值。对于===,如果类型不同,直接返回false,类型相同则同==。举例如下:
[javascript] view plaincopy
 
  1. false == 0  
  2. true != 2  
  3. 1 == "1"  
  4. true != "true"  
  5. true == "1"  
2. 对于对象(数组也是对象,String不是)
== 与 === 没有区别,都是比较的它们的地址,不考虑它们的值,举例如下:
[javascript] view plaincopy
 
  1. var a = {}  
  2. var b = a  
  3. a == b, a === b;  
  4. {} != {}  
3. 如果是一方是对象,一方是基本数据类型呢?
===的时候,由于类型不同,则一定为false。==的情况下,先对对象调用toString函数,用toString的返回值与另一方的基本数据类型,用规则一进行比较。举例如下:
[javascript] view plaincopy
 
  1. "abc" !== new String("abc")   
  2. "abc" == new String("abc")     
  3. 1 == new String("1") // 坑爹的string和String,下文会详细的讲“字符串”  
  4. var a = {}  
  5. a == "[object Object]"  
  6. a.toString = function(){return "1";}  
  7. a == 1  

坑爹的String和string

为什么我说String很坑爹呢?我发觉直到我写这篇文章之前,我对JavaScript的String的理解都是错误的。我之前都把string理解为对象,一个不可变的对象。比如我的几周之前的上一篇博文还是这么理解的http://blog.csdn.net/qq_21930351/article/details/40627649
事实上是这样的
[javascript] view plaincopy
 
  1. var a = "abc";  
  2. typeof(a) == "string"     
  3. var b = new String("abc")  
  4. typeof(b) == "object"  
  5. a == b //参考上一段的规则3,b.toString() == "abc"  
  6. //之前一直认为a, b 都是对象, 并且类型相同,都是那个“string”,直到我今天为了写博文,测试了如下代码:  
  7. var s1 = new String("abc");  
  8. var s2 = new String("abc");  
  9. s1 != s2 //what a fuck  
  10. //其中==是比较两个string的内容,事实上 s1 != s2,所以我就纳闷了。事实上他们俩都是object,适用于上一篇的规则2.  

所以代码中直接用双引号产生的string和new String出来的东西还是有点区别的。
尽管如此,它们在绝大多数情况是可以混用的,除了typeof。String类型的toString函数就是返回一个字符串的常量,所以可以直接用==来比较内容。此外,它们俩还共享prototype,你扩展了String的prototype之后,string的prototype也会跟着改。
所以到底是String还是string,具体问题具体看吧。

有什么用?

这个有什么用?一个知识说出来,总有人会问这个问题。你可能会说,你这个纯学院派的,冷门知识扣那么细,有啥用啊,还不是孔乙己“‘茴’有四种写法”。我给的答案是,大部分情况,的确没什么用。比如那个坑爹的string,我写了那么久的JS,一直都是错误理解,还不是照写不误?然而看到“有什么用?”这个问题的时候,我总是会想起乔布斯的演讲的片段:
我没预期过学这些东西能在我生活中起些什么实际作用,不过十年后,当我在设计第一台麦金塔(Macintosh)时,我想起了当时所学的东西,所以把这些东西都设计进了Mac里,那是第一台使用了漂亮印刷字体的电脑。如果我当时没有退学,就不会有机会去参加这个我感兴趣的美术字课程,Mac就不会有这么多丰富的字体,以及赏心悦目的字体间距。因为Windows照抄了Mac,所以现在个人电脑才能有这么美妙的字型。
上面是鸡汤啊,看起来没啥料,下面加点私货,说说我对编程中的一些所谓“冷门知识”的观点:
1. 大部分情况它们就是“茴”字的四种写法,确实没用,因为这些生僻的语言元素很少用到,并且即使用到了,也往往有不冷门的替代方案。
2. 然而语言的设计者都是计算机界的佼佼者,他们不会无缘无故的放一些没用的元素在里面的,你敢说你比这些语言的设计者更牛?既然放进去,就一定有其道理。
3. 如果觉得它们没用,那么一般就是功力或经验还没达到一定的高度的缘故。
4. 下面两种场景下,这些“冷门”的知识就很好用了,一是某个匪夷所思的bug就是因为对语言理解不深导致。二是当开发一个大型项目的底层库和框架的时候,用好这些知识能够事半功倍。
 
上面的内容仍然是大道理啊,还是没能解释一下这篇文章看了对JavaScript的实战有何帮助。下面有点干货,不多,充分说明这些元素真的很少被使用,或者是我的火候还不够啊。
1. 函数重载。 比如一个库函数foo(a, b). 当调用方调用foo(a)的时候,foo函数内部的b就是undefined。在foo函数内部,检查参数是否===undefined来确定调用方到底是foo(a) 还是foo(a, null).
2. 断言。开发阶段使用断言能够让代码在出错点立刻崩溃,防止错误蔓延,从而提高调试效率。在使用断言时,要确保值和类型都正确无误,所以要使用===。
3.  到底是返回null还是undefined? 对于数值计算,如果计算参数不合适,要么抛出异常,要么返回null或0作为空值。避免返回undefined,这样做会导致它的上层算式变成NaN,可能会导致错误蔓延。对于需要返回string的地方,如果出错,则要么抛出异常,要么返回空串,避免返回null或是undefined,否则就会出现“null”, "undefined"的字符串。如果应该返回对象,则应该返回null,而避免返回undefined。null和对象类型相同,防止在需要判断变量类型的地方由于一个“undefined”导致控制逻辑跑到不该去的地方。然而对于一个没有返回值的过程,则应该明确的返回undefined或是直接写return或者干脆不写return语句。
4. 如果你需要判断一个对象是否拥有某一个属性,则应该使用hasOwnProperty函数。这个函数无论该属性是null还是undefined,只要属性存在,则都返回true。此外,如果你需要明确的删除一个属性,则使用delete,而不要仅仅将其置null,undefined。null,undefined不仅仅会被hasOwnProperty返回true,同时这个属性仍然是可枚举的,就是用 for - in 来遍历对象的时候仍然能够被遍历出来。
5. 修改String的prototype。比如我们的代码中经常需要反转一个字符串,那么每次都这样调用reverse(s1)。这样则需要在全局名空间定义一个reverse函数。然后可以通过修改prototype来给所有的String对象提供该函数。String.prototype.reverse() = function(){}. 当这么做以后,无论是new String还是 “”字符串常量,均可以使用"abc".reverse()来返回一个反转后的字符串。
6. 编写代码的时候尽量依赖内容判断而非类型。因为 == 和 === 都判断相等,。null 和 undefined都代表”无“,在阅读代码的时候很容易就直接在逻辑层面就认为它们等价了。在一段逻辑中,如果是处理数字,那么推荐在逻辑入口处将所有的入参转为数字,那么逻辑内部就不会因为== 和 ===问题出错,字符串也类似。在判断值是否为空时,尽量让分支逻辑不依赖null还是undefined。如果你的代码中typeof比较多而散布的时候,就要重新考虑一下设计的问题了。让业务逻辑层面关注内容,至于typeof,==/===, null/undefined的处理,尽量集中地放在底层库,并且由资深程序员处理。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。