昨天看了《JavaScript 语言精粹》一书,其中收获最大的就是附录中对于 JavaScript 语言的毒瘤和糟粕的讲解。所以写了篇博客自己学习下~
我们应该尽量避免下面一些写法的出现来保证更优质、更优雅的代码。
毒瘤
全局变量
全局变量是在所有作用域中都可见的变量。因为全局变量可以被程序的任何部分在任意时间修改,所以全局变量变得很难维护、很不靠谱。
有三种方式可以定义全局变量:
// 全局作用域中定义变量
var foo = value;
// 为 window 定义变量
window.foo = value;
// 隐式全局变量
foo = value;
其中第三种是最不合理也最难被发现的。
所以,在写代码的时候一定要处理好全局变量的逻辑,尽量避免使用全局变量。
作用域
在 ES6 之前,JavaScript 没有真正意义上的块作用域,所以导致在块中创建的变量可以在外部访问到。
if (true) {
var a = 3;
}
a // 3
幸好,在 ES6 中添加了 const 和 let 命令可以创建块作用域解决。所以,尽量使用 const 和 let 替代 var 来定义变量。
自动插入分号
JavaScript 有一个自动修复机制,如果语句末尾没有分号,将自动插入补全。但是这有时候会带来问题:
function test() {
var a = 12;
return
{
123;
}
}
test() // undefined
所以,注意书写规范不要随意换行,否则可能会由于自动插入分号而导致奇怪的报错。
保留字
不要使用 JavaScript 的保留字来命名变量和参数。
Unicode
在 JavaScript 中,Unicode 编码从 U+0000 到 U+FFFF 内的字符长度为 1,而超出范围的字符长度为 2。
typeof
typeof 运算符返回一个用于识别其运算数类型的字符串。但是 typeof 有一些 bug:
typeof null // object
这是 JavaScript 的一个 bug,理论上应该是返回 null 的。
另外,各种 JavaScript 中的 typeof 对于正则的返回结果不太一致。
typeof /a/
有的返回 function 有的返回 object。这点在使用时需要注意~
parseInt
parseInt 是把字符串转为数字的函数,但是它有一些行为比较诡异。
parseInt('5 fans'); // 5
parseInt('0x123'); // 291
当遇到字符串中有非数字时,函数会停止解析并返回当前结果。所以第一个表达式返回 5,而其实这串字符串表达的并非完全是数字。
当遇到二进制、八进制、十六进制的字符串时,返回的数字也可能会有所不同,所以建议所有的 parseInt 函数都加上第二个参数表明当前数字的进制位。
parseInt('77', 10); // 77
parseInt('08', 8); // 0
parseInt('0x123', 16); // 291
+
- 运算符可以用于加法运算或者字符串连接。由于有两种用途所以要注意加号两边的运算值的类型。
var num = 1
var str = '3'
num + num // 2
str + str // '33'
str + num // '31'
注:个人认为字符串的拼接避免使用 + 号而是用 ES6 的模板字符串来定义,让 + 好回归它数值相加的功能会更好。
浮点数
一个挺有名的 JavaScript 浮点数问题是 0.1 + 0.2 不等于 0.3,这是因为 JavaScript 使用了二进制浮点数运算标准。所以浮点数会有很小的数值偏差。
解决方法是:虽然浮点数的计算有偏差,但是整数的运算是不会偏差的。所以可以通过将值乘以某个数值 x 转换为整数进行计算,得到结果后再除以数值 x 就可以得到准确结果了。
0.1 + 0.2 // 0.30000000000000004
(0.1 * 100 + 0.2 * 100) / 100 // 0.3
NaN
NaN 表示不是一个数字(not a number),它的类型却是 number。
typeof NaN // 'number'
首先要注意 NaN 是一个非自反值:
NaN === NaN // false
NaN !== NaN // true
判断 NaN 的方法是使用 isNaN 函数。
isNaN(NaN) // true
由于 NaN 的类型是 number,所以判断值是否是数字就带来了一些麻烦,书中提供的判断数值的方式是使用 isFinite 函数配合 typeof 运算符。
var isNumber = function(value) {
return typeof value === "number" && isFinite(value);
};
伪数组
JavaScript 中的数组其实是一个对象,而并没有传统意义上的数组那样需要设置维度。
问题来了,如何辨别对象是否为数组对象呢?
var arr = [ 1, 2, 3 ]
typeof arr // "Object"
Object.prototype.toString.apply(arr) // "[object Array]"
typeof 一如既往的不靠谱,它显示数组 arr 是一个 Object。正确的判断方式是使用对象原型中的 toString 方法。
另外要注意,JavaScript 函数中的参数 arguments 并非数组,它只是一个带有 length 成员属性的对象。
function test() {
console.log(Object.prototype.toString.apply(arguments));
}
test(1, 2, 3, 4); // [object Arguments]
假值
在 JavaScript 中有很多假值:
值 | 类型(typeof) |
---|---|
0 | Number |
NaN | Number |
’‘ | String |
false | Boolean |
null | Object |
undefined | Undefined |
当使用上表中的值作为布尔值时,将返回 false。如何转为布尔值可以参考布尔值的强制类型转换。下面举几个例子
!! 0 // false
Boolean(null) // false
if (NaN) {
// 不会到这里
}
另外注意一点,在 JavaScript 中 undefined 和 NaN 并非常量,而是全局变量,所以这两个值是可以修改的。这也是公认的 JavaScript 的设计错误问题。
hasOwnProperty
对象的 hasOwnProperty 方法可以很好的判断对象是否包含某个属性,但是如果在对象上修改 hasOwnProperty 属性,原本的功能就失效了。
var obj = { a: 123 }
obj.hasOwnProperty('a') // true
obj.hasOwnProperty = null
obj.hasOwnProperty('a') // TypeError
为什么呢?因为对象 obj 的原型继承了 Object,所以可以使用 Object 中的 hasOwnProperty 函数,但是如果在 obj 上定义了 hasOwnProperty 那么就会屏蔽上层原型链内的属性了。
所以,不止是 hasOwnProperty,其他的属性也可能被原型屏蔽覆盖。所以尽量避免在对象上定义和 API 同名的属性。
糟粕
==
复习一个知识点 === 和 == 的区别在哪里?=== 判断两个对变量的值和类型是否都相等,而 == 在判断相等时如果发现类型不同会进行强制类型转换后再对比。
相对应的 !== 和 != 也是这样的逻辑。
这种强制类型转换的行为会造成很多奇怪行为:
'' == 0 // true
false == 'false' // false
false == '0' // true
false == undefined // false
false == null // false
null == undefined // true
'\t\r\n' == 0 // true
== 的这种强制类型转换简直是魔鬼,建议只使用 === 和 !== 来进行数值比较。
with
with 这个冷门的关键词通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。
var obj = {
a: 1,
b: 2,
c: 3
};
// 单调乏味的重复 "obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 简单的快捷方式
with (obj) {
a = 3;
b = 4;
c = 5;
}
看似好意的写法却隐藏着两个问题,一个是它的写法看似方便却难以追踪变量的变化(相比于 obj.a 而言 with 里面定义的 a 很难理解,而且 with 中的作用域是类块作用域的)。二则是如果定义了 obj 对象中没有的值,这个值会定义到全局变量上而非 obj 上(如下面的代码),这也是一个很奇怪的行为。
var obj = {
a: 1,
b: 2,
c: 3
};
with (obj) {
d = 11;
}
console.log(obj) // { a: 1, b:2, c:3 }
console.log(d) // 11
不建议使用 with 语句。
eval
eval 函数传递一个字符串给 JavaScript 编译器,并且执行器结果。但 eval 的写法让代码难以阅读、还会显著降低性能。
由于 eval 中的字符串可以被编译,很有可能会执行到一些外部传递的恶意代码,非常不安全。
eval('console.log(123)')
// 123
和 eval 函数同样能够编译字符串的有: Function 构造器、setTimeout 函数和 setInterval 函数。
不要使用 eval 和 Function!使用 setTimeout 和 setInterval 函数时不要传入字符串而是传入函数。
// good
setTimeout(() => {
console.log("hello");
}, 1000);
// bad
setTimeout('console.log("hello")', 2000);
continue
continue 本身并无问题,但是 continue 语句会影响性能。
switch 穿越
缺少块的语句
如果条件语句后面只有一行代码,可以省略 {}
但是这样的行为很容易出现错误,建议全部加上大括号。
// bad
if (ok)
num = 4;
// good
if (ok) {
num = 5;
}
++ --
递增和递减运算符虽然用着方便,但是这会使得代码看着拥挤、复杂和隐晦。所以不建议使用递增和递减运算符。
// bad
a++;
a--;
// good
a += 1;
a -= 1;
在一些规范中推荐使用 += 和 -= 来替代递增和递减运算符。
位运算符
位运算符是好东西,可以减少代码冗余。但是要注意不要写错。比如 & 写成 && 运算符。
function 语句对比 function 表达式
考虑两种情况:
var func = function() {};
function func2() {}
其实两者的效果是一样的,唯一不同的点就在于变量提升。在变量提升完成后的代码是酱紫的:
function func2() {}
var func;
func = function() {};
function 作为一等公民拥有函数优先特性,优先提升。然后是变量提升,最后才是赋值操作。
那么这两种方式孰优孰劣呢,书中认为使用定义变量的方式会更好。因为很多时候并不需要将 function 函数提升,而且在 if 语句中只能使用定义变量的写法定义函数。
类型的包装对象
不要使用类型的包装函数来创建类型,而是推荐使用直接创建的方式来创建不同类型的值。
// bad
var bool = new Boolean(true) // Object
var str = new String('123') // Object
bool.valueOf() // true
str.valueOf() // '123'
// good
var bool = true
var str = '123'
var arr = []
new
在使用 new 运算符创建一个继承构造器原型的新对象时,别忘了 new 运算符。
如果可以,最好能够不用 new 去创建对象。
function Func() {}
// 注意两者的不同
var obj = new Func()
var obj2 = Func();
// 最好
var obj3 = {}
var obj4 = Object.create(obj3)
void
书中认为 void 没有什么用,不过也有一些规范认为用 void 来替代 undefined 可以避免 undefined 是一个全局变量的问题,所以要求使用 void 替换 undefined。
最后
本文内容参考自《JavaScript 语言精粹》,可能是书有点老了,很多知识有了新的解决方案。所以我也在抄书的同时添加了不少我对于 JavaScript 的认知。如果有任何问题或者疑问,欢迎评论交流。