弱类型的代价 – WAT!


在程序语言的分类上,有所谓强类型(Strong type)、弱类型(Weak type)语言,强弱之别是相对的,没有绝对之分野,端看语言对类型检查的严格程度、类型转换规则是否多元。

静态定型语言不一定就是强类型语言,例如 Scala 可以使用implicit来定义隐式转换(Implicit conversion)规则,愿意的话,可让整个语言看来偏向弱类型;动态定型语言也不一定就是弱类型,举例来说,Ruby、Python 就偏向强类型语言。

Java 偏向强类型语言,在 Java 中运算或操作,较少有自动之类型转换,举例来说,字符串在 Java 中不能直接作以下之操作:

String number1 = "3";
String number2 = "2";
int result = number1 - number2;

在偏向强类型的语言中,多数情况下类型转换或解析必须明确指定。例如:

String number1 = "3";
String number2 = "2";
int result = Integer.parseInt(number1) - Integer.parseInt(number2);

并非偏向强类型的语言,就不会发生自动类型。例如,Java 中的+运算符,只要有运算数有一个是字符串,就会尝试将另一个运算数转为字符串,而后进行字符串连接:

String number = 1 + "23";  // "123",基本类型按字面转为字符串
Person person = new Person();
String description = "Person: " + person; // 调用 person 的 toString()

JavaScript 偏向弱类型语言,字符串的减法操作是可行的:

var result = '3' - '2';
console.log(result);        // 1
console.log(typeof result); // number

偏向强类型的语言,多数情况下必须明确进行类型转换或解析,避免了许多非预期的自动类型转换造成的错误,然而带来了语法上的冗长,弱类型语言则相反,获取了语法简洁的优点,但必须多注意非预期类型转换带来的问题。

JavaScript 的基本类型numberstringboolean,会在必要的时候,自动类型转换为对应的包裹对象NumberStringBoolean。例如:

> var number = 10;
undefined
> number.toString(2);
'1010'
> (10).toString(2);
'1010'
>

number变量指定的 10,类型是number,但在操作的toString对象才会有的方法,此时会自动使用Number实例来包裹 10 这个number,因此才可以操作toString。如果是字面量表示,则可以加上()后直接操作toString, 同样会为你自动进行类型转换。

类似地,在对string作操作时,若必要,也会自动包裹为String的实例。例如:

> 'caterpillar'.toUpperCase();
'CATERPILLAR'
>

toUpperCaseString上定义的方法,执行环境必要时,会将string使用String包裹,才让你操作toUpperCase方法。

你可以直接创建包裹对象。例如:

> typeof 10;
'number'
> typeof 'caterpillar';
'string'
> typeof new Number(10);
'object'
> typeof new String('caterpillar');
'object'
>

关于NumberStringBoolean上可操作的方法,可参考:

  • Number
  • String
  • Boolean

你也可以使用parseIntparseFloat将字符串转换为数值,其好处是会自动忽略字符串尾端非数字部份。例如:

> parseInt('10 years old...XD');
10
> parseFloat('3.14159......');
3.14159
> parseInt('0x10');
16
> parseInt('010');
10
> parseInt('010', 10);
10
> parseInt('010', 8);
8
>

后两个parseInt分别指定了基数为 10 进制或 8 进制,如果字符串以 0x 开头,基数默认为 16 进制,以 0 开头,建议直接指定基数为 10 或 8 进制,其他字符串则默认为 10 进制。

对于字符串,如果代表数字的话,可使用+-*/来作运算,不过要注意,+优先作字符串的连接,而非转换为数字作数值加法,-*/则会转换为数字。例如:

> '6' + '2';
'62'
> '6' - '2';
4
> '6' * '2';
12
> '6' / '2';
3
>

在 JavaScript 中若结合布尔值作+-*/等运算,true会被当作 1,而false会被当作 0。例如:

> 1 + true;
2
> 1 + false;
1
>

布尔值很有趣,在真假判断式中,所有东西都可以转换为布尔值。一个口诀是 …

除了 0、NaN''nullundefinied是假的(false)之外,其他都是真的(true)。

0、NaN''nullundefinied就是所谓 False Family 成员。

例如,若对象上不存在某个特性,直接取用该特性会得到undefined的值,所以若想知道某对象上是否存在该特性,则可以如下:

> var o = {};
undefined
> o.x ? 'has x' : 'has no x';
'has no x'
> o.x = 10;
10
> o.x ? 'has x' : 'has no x';
'has x'
>

在布尔值判断式中,如果获取undefined,则会当作false。如果你想避免x被设为 0、NaN''null而造成误判,则可以作更严格的检查。例如:

function hasX(obj) {
    return typeof(obj.x) !== 'undefined';
}

console.log('has x? ' + hasX({}));        // has x? false
console.log('has x? ' + hasX({x : 10}));  // has x? true

类型转换也会发生在相等性比较时,在 JavaScript 中有两个相等性运算符=====,都可以判断值或对象参考是否相同,简单来说,前者会尝试将==两边转换为同一类型,再比较是否相等,但后者只要===两边类型不一,就会判断为false。例如:

> '' == 0;
true
> '' === 0;
false
> null == undefined;
true
> null === undefined;
false
> 1 == true;
true
> 1 === true;
false
>

简单来说,==执行较宽松的比较,可允许类型转换后的比较,===执行较严格的比较,类型必须相同才有可能为true(当然,!=!==则是不相等的比较)。建议执行严格的比较,也就是使用===!==来比较相等或不相等。

在弱类型语言中,类型转换自动发生的规则多,若是不确定,最好还是实际测试了解结果,避免不必要的类型转换而发生误判或错误的运算结果。

以下来看几个初学者容易 WAT 脱口而出的例子:

> [] + [];
''
> [] + {};
'[object Object]'
> {} + [];
0
> {} + {};
NaN
>

这个例子其实是来自 Gary Bernhardt 在 CodeMash 2012 时的一场WAT 闪电秀,有时间的话,上去看看笑一笑吧!


展开阅读全文