2020.05.13 补充关于null的内容
2020.09.09 补充关于运算符优先级的一些解释
什么是类型?我觉得吧,两个值的具有的行为不同,它们的类型也应该不同。
事实上,变量没有类型,值才有。虽然定义争论很多,但我还是觉得强类型和弱类型语言的区别,大概就在于在不经过显式强制类型转换的情况下,变量是否始终保持初始化时获得的类型。比如,对于Java来说,int a = 1;
之后,如果不经过转换,变量a就一直是int类型了,如果赋值为其他类型会报错。显然,a = "foo";
是会报错的。而对于JavaScript来说,var a = 1;
之后(let和const也是一样的,并不影响变量的类型,只是影响了变量的作用域而已),变量a还可以赋值为其他类型,a = "foo";
是不受影响的。从这个角度来说,强类型是不允许隐式类型转换的,也就是俗称的“类型安全”了;但弱类型是允许的,也就不“安全”了。
其实,所谓的安全与不安全,显式与隐式,都是相对来说的。你要是对其中的机制很清楚,类型不安全的语言不仅可以做到“安全”,而且还更灵活一点(可能缺陷在代码提示和类型检查上?)。隐式转换同理;在熟悉的人眼中,它们相当于是显式的。
JavaScript有类型吗?答案很显然:当然有。对于编程语言来说,类型只是强弱之分,而非有无之分。如果没有类型,怎么才能让行为不同呢。在这门语言里,typeof
返回的是变量持有的值的类型,这也从某个层面印证了这个观点。
涉及到类型,就有了强制类型转换;强制类型转换又有显式和隐式之分。显式的强制类型其实并没有太多可说的,符合显式原则的东西一般不会干扰理解。问题就在这个隐式强制类型转换上,虽然有时候我们是它的受益者:
var a = 'foo';
if (a) {
// ...
}
var b;
if (b == null) {
// ...
}
类似于这种的实现,我们平时用得很多。这种写法能省去显式转换的麻烦。上面那段代码看起来应该比这段要好一点,更简洁,可读性似乎还有所上升:
var a = 'foo';
if (Boolean(a)) {
// ...
}
var b;
if (b === undefined || b === null) {
// ...
}
而且字符串'foo'
被转换成true也是顺理成章的事,并不会带来什么误解。换句话说,这里的隐式类型转换并不会降低可读性,甚至还能提高可读性。另外这里也说明了一件事,==
不见得就要完全摒弃,如果能适当使用,也能发挥不错的效果,我也觉得Douglas有点矫枉过正了。但他的《JavaScript语言精粹》是好书这个事实并没有改变,值得反复去读。
在大部分时候,隐式强制类型转换都有点问题。下面这几个结果简直难以置信:
[] == false // true
{} == false // false
[] == ![] // true
不仅是这个,+
在使用的时候也有各种各样隐藏的坑点。可能大家都知道,+
在左右操作数类型不同的时候会有隐式的类型转换,比如说“字符串+其他类型”会被当成字符串拼接:
5 + "foo" // "5foo"
"foo" + false // "foofalse"
还有更诡异的:
[] + {} // "[object Object]"
{} + [] // 0
虽然说这里不全是+
的锅,但确实值得注意。
举完例子,该说说理论了。按照EMCA语言规范,关于类型,有四个重要的抽象方法:ToPrimitive
、ToString
、ToNumber
、ToBoolean
。我觉得搞清楚了这几个方法就相当于搞清楚了JavaScript的类型转换。
具体的细节可以自行去查阅ECMA语言规范,就不在此赘述了。需要注意的是,比起5.1版本,从ES6起增加了Symbol类型,导致ToPrimitive的算法发生了变化。以前是通过valueOf
、toString
方法来获取对象的原始类型值,现在还可以通过覆写[Symbol.toPrimitive]
方法来做到这一点,而且[Symbol.toPrimitive]
的优先级还更高。当然了,基本类型调用ToPrimitive的行为是不会变的。
说来说去,还是为了解决(隐式)强制类型转换的问题。整个强制类型转换,概括下来,大概就是一句话:发生转换的时候,如果只有一个操作数,先拆箱,然后直接转换成目标类型;类型不同时,先拆箱(如果需要),再尽量转换成数字(NaN按照规范是属于数字类型的;如果转换不了,就报错。不过,这里有一个特例,就是null和undefined,这两个值互相相等,但是和0这类不等,需要注意)。这里的拆箱并不是狭义上的unwrap,从包装对象中取出包装值(比如从Number(1)
包装过的Number对象中取出1来),因为我觉得所有的对象都可以看成是数据的某种包装,这里的拆箱实际上指的是,通过调用ToPrimitive
抽象方法来获取对象对应的原始值。如果类型相同呢?类型相同的时候还需要类型转换吗(笑)。
拿上面几个例子来说说:
在if语句里,因为只有一个操作数,会直接调用ToBoolean进行转换,所以非空字符串被转换为true。
undefined和null都是基本类型,所以不用拆箱,直接转换成数字,相当于0===0。
[]是对象,而且没有覆写[Symbol.toPrimitive]
,先调用valueOf,获得了自身,还不是基本类型,所以再调用toString,获取到了"",然后转换成0;false转换成了0。所以还是相当于0===0。
{}是对象,没有覆写[Symbol.toPrimitive]
,,先调用valueOf,获得了自身,还不是基本类型,所以再调用toString,获取到了"[object Object]",然后转换成NaN;false转换成了0。NaN不等于任何值。
![]直接调用ToBoolean进行转换,对象一定为true(别忘了数组是对象),所以这里变成了false,然后转换成0;前面已经说过,[]会被转换成0。所以,相当于0===0。但是看起来很奇怪了……
(稍微再补充一句,并不是说ToBoolean的优先级更高,而是取反运算符本身的优先级更高。这个表达式会先对[]取反变成false,然后再进行[]==false的比较;这自然是true了。)
+也是先拆箱,但是因为有运算符重载的情况,所以要特殊一点:如果任何一边出现了字符串,都转换成字符串。所以5 + "foo" === "5foo"
,"foo" + false === "foofalse"
并不奇怪。
而[]拆箱后是"",{}拆箱后是"[object Object]",两个字符串拼接,变成了"[object Object]"。
至于{} + [],涉及到JavaScript的语法问题,不仅仅是强制类型转换。在这里,{}被当成了一个空的块,所以这一句相当于是+ []。单操作数直接拆箱进行转换,将""转换成了0。
先说这么多吧……坑太大,一时半会也说不清楚。等哪天想说了再说吧。
补充一点。今天看到了一个有趣的例子:
0 == null // false
这个是为什么呢?看起来null
是可以拆箱成0的,并且调用Number也是可以让它变成0的:
Number(null); // 0
但是有一点需要注意,抽象相等算法里只规定了null == undefined
,虽然看起来null
应该和0相等,但是因为规范里规定null
的类型是Null,不是数字、字符串、布尔值、Symbol、对象中的任何一种,所以必须执行到最后一步,也就是返回false。
附录
-
关于类型的详细定义,以下选自ECMA 2019 语言规范(太长不看系列):
Algorithms within this specification manipulate values each of which has an associated type. The possible value types are exactly those defined in this clause. Types are further subclassified into ECMAScript language types and specification types.
An ECMAScript language type corresponds to values that are directly manipulated by an ECMAScript programmer using the ECMAScript language. The ECMAScript language types are Undefined, Null, Boolean, String, Symbol, Number, and Object. An ECMAScript language value is a value that is characterized by an ECMAScript language type.
A specification type corresponds to meta-values that are used within algorithms to describe the semantics of ECMAScript language constructs and ECMAScript language types. The specification types include Reference, List, Completion, Property Descriptor, Lexical Environment, Environment Record, and Data Block. Specification type values are specification artefacts that do not necessarily correspond to any specific entity within an ECMAScript implementation. Specification type values may be used to describe intermediate results of ECMAScript expression evaluation but such values cannot be stored as properties of objects or values of ECMAScript language variables.
-
Symbol类型的toPrimitive方法:
Symbol.prototype [ @@toPrimitive ] ( hint )
This function is called by ECMAScript language operators to convert a Symbol object to a primitive value. The allowed values for hint are
"default"
,"number"
, and"string"
.When the
@@toPrimitive
method is called with argument hint, the following steps are taken:Return ? thisSymbolValue(this value).
The value of the
name
property of this function is"[Symbol.toPrimitive]"
.This property has the attributes { [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: true }.
-
抽象相等算法:
The comparison x == y, where x and y are values, produces true or false. Such a comparison is performed as follows:
- If Type(x) is the same as Type(y), then
2. Return the result of performing Strict Equality Comparison x === y. - If x is null and y is undefined, return true.
- If x is undefined and y is null, return true.
- If Type(x) is Number and Type(y) is String, return the result of the comparison x == ! ToNumber(y).
- If Type(x) is String and Type(y) is Number, return the result of the comparison ! ToNumber(x) == y.
- If Type(x) is Boolean, return the result of the comparison ! ToNumber(x) == y.
- If Type(y) is Boolean, return the result of the comparison x == ! ToNumber(y).
- If Type(x) is either String, Number, or Symbol and Type(y) is Object, return the result of the comparison x == ToPrimitive(y).
- If Type(x) is Object and Type(y) is either String, Number, or Symbol, return the result of the comparison ToPrimitive(x) == y.
- Return false.
- If Type(x) is the same as Type(y), then