避免 JavaScript 语言中毒瘤和糟粕

昨天看了《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。

undefined and void

最后

本文内容参考自《JavaScript 语言精粹》,可能是书有点老了,很多知识有了新的解决方案。所以我也在抄书的同时添加了不少我对于 JavaScript 的认知。如果有任何问题或者疑问,欢迎评论交流。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,113评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,644评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,340评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,449评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,445评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,166评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,442评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,105评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,601评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,066评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,161评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,792评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,351评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,352评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,584评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,618评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,916评论 2 344

推荐阅读更多精彩内容