Javascript 中的一些缺陷:熟悉并掌握,写更优雅的代码

本文主要参考Douglas Crockford的专著《Javascript语言精粹》的附录A和B。熟练掌握它们,将会使我们写出的代码更加优雅。

1. 全局变量

Javascript的全局变量,在所有作用域中都可见。它在很小的程序中可能不会有问题,甚至能够给我们带来方便。但是,在较大的程序中,它会变得难以处理且降低了程序的可靠性。之所以这么说,是因为,任何一个函数内部都可以访问、修改和生成全局变量,一旦我们生成或修改的变量和已存在的全局变量同名,那么它们将会互相冲突,甚至导致程序无法运行。而这种情况,往往都是难以调试的。

定义全局变量的方式有三种,我们要谨慎注意无意间定义的全局变量:

  1. 在所有函数之外,用 var 定义一个变量
  2. 直接在全局对象上添加一个属性。在Web浏览器环境下,全局对象名为window。
  3. 直接使用未经声明的变量(也称为隐式的全局变量)。
<script>
   
    var a = '使用 var 将 foo 变量要定义在任何函数之外';
    window.a2 = '在全局对象上直接添加属性';
    a3 = '未经声明的变量';
    
    console.log('window.a-->', window.a);
    console.log('window.a2-->', window.a2);
    console.log('window.a3-->', window.a3);
    console.log('window', window);
    
</script>
复制代码

全局变量.jpg

2. ==

我们都知道Javascript有两组相等运算符:== 和 !=,以及 === 和 !==。两者的不同之处在于,前者用来判断两个值是否相等时,若是两个值类型不同,会发生自动转换,这个转换的规则不仅复杂且难以记忆,其得到的结果有时也是出乎意料。而后者会严格按照两个值类型来判断,不会发生自动转换。因此,推荐任何时候都使用 === 和 !== 运算符。

console.log("" == "0"); // false
console.log(0 == ""); // true
console.log(0 == "0"); // true

console.log(false == "false"); // false
console.log(false == "0"); // true
console.log(false == undefined); // false

console.log(false == null); // false
console.log(null == undefined); // true

console.log("\t\r\n" == 0); // true
复制代码

3. 加号运算符

加号运算符( ‘+’ ),即可用于加法运算,也可用于字符串的连接。虽然,这种设计使得Javascript变得更加灵活,但往往也是bug的常见来源。所以,在使用时,一定要谨慎小心。

console.log(1 + 1); // 2
console.log("1" + "0"); // 10

// 若是两个操作项,一个是字符串,另一个是数字,则数字自动转化为字符串。
alert(1 + "1"); // 11
复制代码

4. 自动插入行尾分号

Javascript的所有语句,都必须以分号结尾。但是,它有一个机制:通过自动插入分号来修正有缺陷的程序。换句话说,就是当你忘记加分号时,解释器会为你自动加上分号,而不是报错。然而,这种机制有时会导致一些难以发现的错误。建议,在每行代码后手动加上分号,而不是依赖这个机制,为你自动添加。

下面这个例子就会导致出现无法预期的结果,它会返回undefined,而不是一个对象。其原因,就是这个机制,在return语句后自动插入了分号。

function err() {
    return
      {
        a: '会返回undefined'
      };
}
复制代码

要想避免出现这个问题,就得把{放在一行的尾部。

function err() {
    return {
        a: '问题解决'
    };
}
复制代码

5. NaN

NaN(详细介绍)表示不是一个数字。然而,通过 typeof 判断它时,得到值是’number’。

console.log(typeof NaN);
console.log(typeof NaN === 'number'); // true
复制代码

它还有一些其它的奇怪特性:

NaN === NaN; // false
NaN !== NaN; // true

console.log( 1 + NaN ); // NaN
复制代码

上面这段代码总结起来就是:

  • NaN 与所有值都不相等,包括它自己。
  • 如果 NaN 是数学运算中的一个运算数,那么结果就是 NaN

6. 浮点数

由于Javascript遵循二进制浮点数算数标准(IEEE 754),而二进制的浮点数并不能正确地处理十进制的小数,因此出现一种怪异的数学运算问题:0.1 + 0.2 不等于 0.3 。但值得庆幸的是,浮点数中的整数运算是精确的,我们可以通过它来规避小数运算带来的bug。

console.log( 0.1 + 0.2 === 0.3 ); // false
console.log( ( 0.1 * 10  + 0.2 * 10 ) / 10 === 0.3 ); // true
复制代码

7. typeof

typeof运算符返回一个用于识别其运算数类型的字符串。但不幸的是,typeof并不能区分null和对象,以及数组和对象。这是因为,在Javascript中,null和数组都属于对象。

console.log( typeof null ); // object
console.log( typeof [] ); // object
复制代码
  • 区分null和对象
const val = null;

// 利用所有对象(包括 '{}')都是真值,而null是假值来判断
if (val && typeof val === 'object') {
    console.log('我是一个对象或数组');
} else {
    console.log('我是null');
}
复制代码
  • 区分数组和对象

Javascript中用于区分数组和对象的方式还有很多,这里仅列举了三种。

// 方法一:Object,prototype.toString.call
const toStr = Object.prototype.toString;
const val = {};
const valType = toStr.call(val);

if (val && typeof val === 'object' && valType === '[object Object]') {
    console.log('我是对象', valType);
} else {
    console.log('我是数组', valType);
}

// 方法二:ES6 中的 Array.isArray 用于确定传递的值是否是一个 Array
const bool = Array.isArray(val);

if (val && typeof val === 'object' && bool) {
    console.log('我是数组');
} else {
    console.log('我是对象');
}

// 方法三:通过调用constructor来识别
if (val && typeof val === 'object' && val.constructor === Array) {
    console.log('我是数组——Array');
} else {
    console.log('我是对象——Object');
}
复制代码

8. 假值

Javascript拥有很多假值,若是不熟悉它们,很可能在代码时造成意料之外的错误。

类型
0 Number
”(空字符串) String
false Boolean
null Object
undefined Undefined
NaN(非数字) Number

上面的值全都等同于假,而且它们不可互换。另外要注意,undefined 和 NaN 是全局变量,而不是常量,这意味着你可以改变它们的值,但千万不要这么做

9. 基本类型的包装对象

在Javascript中,数字、字符串和布尔值,这三个基本数据类型都有相应的包装对象,用于生成数字对象、字符串对象和布尔值对象。这些对象都有一个valueof方法,用来返回被包装的值。

const num = new Number(100);
const bool = new Boolean(true);
const str = new String('Hello World');

console.log('数字:' + num.valueOf());
console.log('布尔值:' + bool.valueOf());
console.log('字符串:' + str.valueOf());
复制代码

然而在实际的编程中,它们的作用并不大且易造成混淆。建议,不要使用它们(感觉没必要,也没啥用)。同时建议,尽量避免使用 new Array 和 new Object,用 [] 和 {} 代替。

10. 缺少块的语句

在Javascript中,if、for、while 或 do 语句,不仅能接受一个括在花括号中的代码块,也能接受单行语句。就像下面这样:

if (val) return true;
复制代码

这种单行语句的好处是可以节约两个字节,提升代码简洁度。但是,这个好处是带刺的,它容易模糊程序结构,让人犯错。举个例子:

if (val) 
    const bool = true;
    func();
复制代码

上面代码看起来是变成下面的样子:

if (val) {
    const bool = true;
    func();
}
复制代码

然而实际情况是:

if (val) {
    const bool = true;
}
func();
复制代码

这种让人以为是在做自己期待的事,其实是在做另一件事的程序是非常不靠谱且有时难以理解。因此,为严谨起见,最好不要使用单行语句,而是使用代码块。

11. 保留字

Javascript中的保留字可以被用作对象字面量的键值,但必须被引号括起来。同时,它们只能用括号表示法,而不能用点表示法。

let obj;

obj = { case: 1 }; // 错误
obj = { 'case': 1 }; // 正确

obj.case = 2; // 错误
obj['case'] = 2; // 正确
复制代码

建议,最好不要使用任何保留字。

12. parseInt

parseInt(string, radix) 函数可解析一个字符串,并返回一个整数。它在遇到非数字时停止解析,但不报错,所以在使用它时,务必小心。

console.log(parseInt('10', 10) === parseInt('10w', 10)); // 10
console.log(parseInt('w10', 10)); // NaN
复制代码

parseInt(string, radix) 函数中的第二参数(可选),表示要解析的数字的基数。它指定,parseInt在解析字符串时,应该使用那种进制。由于在字符串以’0’为开始时,旧的浏览器默认使用八进制基数,而 ECMAScript 5 默认使用十进制的基数。而在八进制中,8 和 9 都不是数字,当我们用parseInt(’08’) 或 parseInt(’09’)解析时间和日期时,得的结果就会是 0。因此,强烈建议,始终提供基数

关于parseInt的具体用法,大家可以点击详细介绍

13. eval 和 with 语句

对于 eval 和 with 语句,前者在使用后容易被人篡改运行恶意代码从而被攻击,后者可能会成为混淆错误和兼容性问题的根源。因此,千万不要在代码去应用它们。但是,必要的了解还是需要的。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享