JavaScript 语法陷阱

没有一门编程语言是完美的,JavaScript 也不例外,它语法陷阱重重,防不胜防:

  • 加号
  • "with"
  • 分号自动插入
  • 声明提升
  • "eval"
  • 多行字符串
  • 变量泄漏
  • "arguments.callee"
  • ...

了解和熟悉这些陷阱,并在开发时注意规避它们,可以给我们省去很多麻烦事。

加号

作为二元运算符时,+ 既是数学运算的加法,也是字符串的拼接。另外,它还可以作为一元符号,表示正数。

看看下面的代码:

JavaScript 语法陷阱
// 1
console.log(   1 + 2   ); // 3
console.log( "3" + "4" ); // "34"
// 2
console.log(   1 + "3" ); // "13"
console.log( "3" + 1   ); // "31"
// 3
console.log( 1 + null      );
console.log( 1 + undefined );
console.log( 1 + NaN       );
// 4
console.log( "3" + null      );
console.log( "3" + undefined );
console.log( "3" + NaN       );
// 5
console.log( 1 + {} );
console.log( 1 + [] );
// 6
console.log( "3" + {} );
console.log( "3" + [] );
JavaScript 语法陷阱

也许你可以准确的说出第1组代码的结果,甚至第2组也能答上,但剩下的几组你能毫不犹豫地给出答案吗?

在 JavaScript 中,是如何决定一段代码中的 + 是数学运算还是字符串拼接呢?答案请看下面这段逻辑:

JavaScript 语法陷阱
a + b:
    pa = ToPrimitive(a)
    pb = ToPrimitive(b)
    if (pa is string || pb is string)
        return concat(ToString(pa), ToString(pb))
    else
        return add(ToNumber(pa), ToNumber(pb))
JavaScript 语法陷阱

 

  1. 收集 + 两端的操作数的原始值。
  2. 如果其中之一是字符串,则进行字符串拼接。
  3. 否则,执行数学加法。

需要注意的是,JavaScript 的原始值类型包括 number, string, boolean, undefined, 而 null 也是一种特殊的原始值。另一方面,对于非原始值类型(即复合类型,也即 object )的变量,其原始值被认为是字符串。

按这个逻辑,之前的测试结果就容易理解了。当然,像上面那样使用加号是不被推荐的,为了避免混淆,利用上面的加号逻辑,我们通常可以这样使用加号:

JavaScript 语法陷阱
// 确保数字相加
a = +b + (+c);
// 确保变量 d 为字符串
d = "" + d;
JavaScript 语法陷阱

 

"with"

使用 with 语句,可以将一个语句块的上下文绑定为一个指定对象。

JavaScript 语法陷阱
with (document) {
    write("foo");
    getElemntById("bar").innerHTML = "foobar";
    alert("Hello world!");
}

// 等同于以下代码
document.write("foo");
document.getElemntById("bar").innerHTML = "foobar";
window.alert("Hello world!");
JavaScript 语法陷阱

但是咱们不推荐使用 with ,事实上,ECMAScript 5 中引入的严格模式也禁止使用 with :

  1. JavaScript 解释器引擎将难以对代码执行优化。解释器引擎的执行优化是建立在“明确的知道这个变量在运行时所指向的引用”的基础上的。而在 with 语句块中的变量或函数,在解释阶段无法判断其是属于 with 的上下文,还是其所在作用域,只有等到代码运行时才能确定。
  2. 代码可阅读性差。

分号自动插入

在语句结束时,你不必手动输入分号,换行即可。

JavaScript 语法陷阱
function foo() {
    var bar = "value"
    return bar
}

// `{}` 包围的语句块的最后一个语句的分号也可省略
function bar() { return "foo" }
JavaScript 语法陷阱

开发者们每写一行代码,就可以少敲打一次键盘,这看起来很人性化。但过于依赖分号自动插入,会带来一些潜在问题。

JavaScript 语法陷阱
function foo() {
    return
    {
        bar: 1
    }
}

function bar() {
    var a, b, c, d, e
    a = b + c
    (d + e).toString()
}
JavaScript 语法陷阱

看看上面的代码,foo() 将返回什么? bar() 又将怎么运行?

事实上,前者将返回 undefined ,而后者的后两行代码将被理解为 a = b + c(d + e).toString()

JavaScript 的分号自动插入的规则并不那么清晰可辨,老实地多敲几次键盘,可以避免那些让你摸不着头绪的bug在某一天突然出现。

声明提升

看看下面这段代码,我们将得到什么结果?

JavaScript 语法陷阱
var foo = 1;
function bar() {
    // 这个条件成立吗?
    if (! foo) {
        var foo = 10;
    }
    alert(foo);
}
bar();
JavaScript 语法陷阱

那么这段代码呢?

JavaScript 语法陷阱
var a = 1;
function b() {
    a = 10;
    return;
    function a() {}
}
b();
alert(a);
JavaScript 语法陷阱

第1个例子,也许你会觉得是 "1" ,因为 ! 1 为假,if 里的代码不会执行。而第2个例子,可能你认为应该是 "10" 。

事实上,结果相反,我们将分别得到 "10" 和 "1" 。

在 JavaScript 中,变量、函数的声明会被提升到当前函数主体的顶部,而不管这个声明语句是否出现在了不可到达的地方。

上面的两段其实等同于:

JavaScript 语法陷阱
var foo = 1;
function bar() {
    var foo;
    if (! foo) {
        foo = 10;
    }
    alert(foo);
}
bar();

var a = 1;
function b() {
    function a() {}
    a = 10;
    return;
}
b();
alert(a);
JavaScript 语法陷阱

需要注意的是,只有变量或函数的声明被提升了,而赋值语句并没有。

"eval"

eval 是 JavaScript 的动态特性之一,在运行时, eval 可以将给定的字符串当作代码语句执行:

JavaScript 语法陷阱
<script>
var func = <?php echo json_encode($user_send[func]); ?>;
eval(func + "()");
function sayHello() {}
function sayGoodbye() {}
</script>
JavaScript 语法陷阱

在代码中用一组字符串与变量拼出另一串代码来运行,这看起来吊爆了。

但请在使用 eval 之前考虑下它将带来的潜在问题:

  1. 使用了 eval 的代码可阅读性很差,你读到这样的代码时很难判断它究竟要做啥,即使那是你自己几天前写的。
  2. JavaScript 解释器引擎难以对代码执行优化。
  3. 如果 eval 中的字符串包含用户输入的数据,这会给攻击者有机可乘。
  4. 如果你是有经验的开发者,大多数情况下你可以使用更高效的函数嵌套(闭包)等来解决问题;如果你没有足够的经验,那更不要使用 eval ,如果你不想你或你的用户遭受攻击。

多行字符串

JavaScript 中不能直接书写多行的字符串,需要在行尾输入一个反斜杠 \

假设我们的项目中有一段这样的代码:

JavaScript 语法陷阱
var multiStr = "this is a multi-line string, and this is the second line. yes, the string ends here";
JavaScript 语法陷阱

然后做了一些维护和更新:

JavaScript 语法陷阱
var multiStr = "this is a multi-line string, and this is the second line. \ 
now i want to insert a line right here, yes, the string ends here";
JavaScript 语法陷阱

凭肉眼似乎没看出毛病,但运行时却得到了一个语法错误,这之前你可能已经注意到语法高亮已经失效了。几经周折,你终于注意到了第2行行尾的那个不起眼的空格。。

变量泄漏

JavaScript 的全局作用域给了我们很多便利,有时我们无需使用 var 来声明变量。

很多 JavaScript 的入门开发者,喜欢利用这个“便利”。但事实上它是一个陷阱。它很可能让我们的一些敲打错误被隐藏和掩盖。

JavaScript 语法陷阱
function foo() {
    var type = "first";
    if (something) {
        typo = "second";
    }
    return type;
}
JavaScript 语法陷阱

这段代码可以让项目长久地稳定运行,但随后的某天,我们吃惊地发现,所有的 type 都是 "first" !在找到并修复这个手误之前,我们以此得到的数据或结论可能都要被废弃。

直接使用没有声明的变量,将自动创建一个全局变量,滥用会导致全局变量污染,或者让类似上面这样的手误逍遥法外。合理的声明变量,并利用作用域链与闭包,是 JavaScript 解决很多问题的思路。

"arguments.callee"

写一个递归函数,我们通常这样:

JavaScript 语法陷阱
function factorial(n) {
    return n <= 1 ? 1 : factorial(n - 1) * n;
}
[1, 2, 3, 4, 5].map(factorial);
JavaScript 语法陷阱

有时我们不想污染命名空间,需要递归调用一个匿名函数,怎么办?

JavaScript 语法陷阱
[1, 2, 3, 4, 5].map(function(n) {
    return n <= 1 ? 1 : /* what goes here? */ (n - 1) * n;
});
JavaScript 语法陷阱

还好我们有 arguments.callee

JavaScript 语法陷阱
[1, 2, 3, 4, 5].map(function(n) {
    return n <= 1 ? 1 : arguments.callee(n - 1) * n;
});
JavaScript 语法陷阱

但是同样不推荐使用 arguments.callee :

  1. 访问 arguments.callee 的开销是昂贵的。
  2. 使用它将导致 JavaScript 解释器难以执行优化。
  3. 从 ECMAScript 3 开始,已经支持命名函数表达式

命名函数表达式:

JavaScript 语法陷阱
[1, 2, 3, 4, 5].map(function factorial(n) {
    return n <= 1 ? 1 : factorial(n - 1) * n;
});
JavaScript 语法陷阱

注意,这里的函数 factorial() 并不是函数声明,而是命名函数表达式,factorial 所处的作用域是其函数本身的作用域(与参数 n 属同一个作用域),而不是当前的全局作用域。但是,在 IE8 及以下浏览器中,情况则不同,它将属于全局作用域。

避开陷阱

JavaScript 有这么多的语法陷阱,如何规避,并保证我们的代码质量呢?后面再谈。

JavaScript 语法陷阱,布布扣,bubuko.com

JavaScript 语法陷阱

上一篇:多线程同步中的门道(一)


下一篇:微信小程序地图开发总结