7 个 JavaScript里令人惊讶的 “特性”

在过去的几个月里,我对 JSHint 做了一些改进,主要是,学习 ES6(我最自豪的是重新实现了变量作用域)的过程中我碰到了几个特性,它们让我惊讶,其中大部分是关于 ES6 的特性但也有一部分是 ES3 特性,这些特性我以前从未用过,而现在我将开始使用它们。

从任何一个代码块中 break

你应该已经知道你可以从任意循环中 break 和 continue —— 这是一个相当标准的程序设计语言结构。但你可能没有意识到,你可以给循环添加一个 label ,然后跳出任意层循环:

outer: for(var i = 0; i < 4; i++) {
    while(true) {
        continue outer;
    }
}

label 特性同样适用于 breakcontinue。你在 switch 语句中肯定见过 break:

switch(i) {
   case 1:
       break;
}

顺便说一句,这是为什么 Crockford 建议你的 case 不应该缩进 —— 因为 break 跳出的是 switch 而不是 case,但是我认为缩进 case 的可读性更好。你也可以给 switch 语句添加 label:

myswitch: switch(i) {
   case 1:
       break myswitch;
}

你可以做的另一件事是创建任意块(我知道你可以在 C# 里面这么写,我期望其他语言也可以)。

{
  {
      console.log("I'm in an abritrary block");
  }
}

因此,我们可以把 label 和 break 放在一起,用来从任意代码块中跳出。

outer: {
  inner: {
      if (true) {
        break outer;
      }
  }
  console.log("I will never be executed");
}

注意到,这只适用于 break —— 因为你只能在一个循环中 continue。我从未见过 label 被使用在 JavaScript 中,我想知道为什么 —— 我想可能因为如果我需要 break 两层,说明把这个代码块放在一个函数里可能更好,这样我可以使用一个单层的 break 或者一个提前的 return 来达到同样的目的。

尽管如此,如果我想要保证每个函数只有一个 return 语句(这不是我的菜),那么我可以使用带 label 的 brock。例如,看下面这个多个 return 语句的函数:

function(a, b, c) {
  if (a) {
     if (b) {
       return true;
     }
     doSomething();
     if (c) {
       return c;
     }
  }
  return b;
}

而如果使用 label:

function(a, b, c) {
  var returnValue = b;
  myBlock: if (a) {
     if (b) {
       returnValue = true;
       break myBlock;
     }
     doSomething();
     if (c) {
       returnValue = c;
     }
  }
  return returnValue;
}

还有另一种选择,用更多代码块……

function(a, b, c) {
  var returnValue = b;
  if (a) {
     if (b) {
       returnValue = true;
     } else {
       doSomething();
       if (c) {
         returnValue = c;
       }
    }
  }
  return returnValue;
}

我最喜欢原版,然后是使用 else 的版本,最后才是使用 label 的版本 —— 但是,这可能是因为我的写码习惯?

解构一个已存在的变量

首先,有个怪异的写法我无法解释。貌似 ES3 中你可以添加一个小括号到一个简单的赋值语句左边的变量上,而这样写不会有问题:

var a;
(a) = 1;
assertTrue(a === 1);

如果你能想到为什么这样写可以,请在底下评论!

解构的过程是一个将变量从一个数组或者一个对象中拉取出来的过程。最常见的是以下例子:

function pullOutInParams({a}, [b]) {
  console.log(a, b);
}
function pullOutInLet(obj, arr) {
  let {a} = obj;
  let [b] = arr;
  console.log(a, b);
}
pullOutInParams({a: "Hello" }, ["World"]);
pullOutInLet({a: "Hello" }, ["World"]);

而你可以不使用 varletconst。对数组你可以让下面的代码如你的期望运行:

var a;
[a] = array;

但是,对于对象,你必须将整个赋值语句用小括号括起来:

var a;
({a} = obj);

必 须这样写的理由是,不加括号无法区分代码是解构赋值还是块级作用域,因为你可以使用匿名代码块而 ASI(automatic semi-colon insertion,自动插入括号)会将变量转成可以执行的表达式(如下面的例子所示,能够产生副作用……),这样就产生了歧义。

var a = {
   get b() {
     console.log("Hello!");
   }
};
with(a) {
  {
    b
  }
}

回到原始的例子,我们给我们的赋值语句里的变量加了圆括号 —— 你可能认为它也适用于解构,但它不是。

var a, b, c;
(a) = 1; //这句不是变量解构
[b] = [2];
({c} = { c : 3 });

对数值进行解构

解构的另一个方面你可能也没有意识到,属性名不是必须要是不带引号的字符串,它们也可以是数值:

`var {1 : a} = { 1: true };`

或者带引号的字符串:

`var {"1" : a} = { "1": true };`

或者你可能想要用一个计算的表达式作为名字:

var myProp = "1";
var {[myProp] : a} = { [myProp]: true };

这会很容易写出造成困惑的代码:

var a = "a";
var {[a] : [a]} = { a: [a] };

类声明是块级作用域的

函数声明会被提升,意味着你可以将函数声明写在函数调用之后:

func();
function func() {
  console.log("Fine");
}

函数表达式与此相反,因为赋值一个变量的时候,变量声明被提升,但是具体赋值没有被提升。

func(); // func 被声明, 但是值为 undefined, 所以这里抛出异常: "func is not a function"
var func = function func() {
  console.log("Fine");
};

类(Classes)成为 ES6 流行的部分,并且已被广泛吹捧为函数的语法糖。所以你可能会认为以下代码是可以工作的:

new func();

class func {
  constructor() {
    console.log("Fine");
  }
}

然而,尽管它基本上是语法糖,但前面的代码是不能工作的。这实际上等价于:

new func();

let func = function func() {
  console.log("Fine");
}

这意味着我们的 func 调用在暂时性死区(TDZ),这会导致引用错误。

同名参数

我认为不可能指定同名的参数,然而,却可以!

function func(a, a) {
  console.log(a);
}

func("Hello", "World");
// 输出 "World"

在严格模式下不行:

function func(a, a) {
  "use strict";
  console.log(a);
}

func("Hello", "World");
// 在 chrome 下报错 - SyntaxError: Strict mode function may not have duplicate parameter names

typeof 不安全

好吧,我偷了这篇文章的结论,但是这值得强调。

在 ES6 之前,众所周知使用 typeof 总是能安全地找出某个变量的定义,不管它是否被声明:

if (typeof Symbol !== "undefined") {
  // Symbol 可用
}
// 下面的代码抛异常,如果 Symbol 没有被声明 
if (Symbol !== "undefined") {
}

但是,现在这个在不使用 let 或者 const 声明变量的时候才好使。因为有了 TDZ,会导致变量未声明时产生引用错误。从本质上讲,变量被提升到块级作用域的开始,但是在声明前的任何访问都会产生引用错误。在 JSHint 的作用域管理中,我必须记录一个变量的用法,如果它使用 let 或者 const 声明于当前块级作用域或者它的父级作用域,提前访问就会有引用错误。而如果是使用 var 语句声明的,那么它就是可用的,但是 JSHint 会给出一个警告,而如果它没有被声明,那么它使用全局作用域,JSHint 可能会有另外一种警告。

if (typeof Symbol !== "undefined") {
  // Symbol 不可用,产生 reference error
}
let Symbol = true;

新数组

我总是避免使用 new Array 构造函数,一部分原因是因为它的参数既可以是一个长度又可以是一个元素列表:

new Array(1); // [undefined]
new Array(1, 2); // [1, 2]

但是,一个同事最近使用它遇到了一些我以前没有见过的东西:

var arr = new Array(10);
for(var i = 0; i < arr.length; i++) {
  arr[i] = i;
}
console.dir(arr);

上面的代码产生一个 0 到 9 的数组。然而,如果将它重构为使用 map:

var arr = new Array(10);
arr = arr.map(function(item, index) { return index; });
console.dir(arr);

上面的代码运行后 arr 没有变化。似乎 new Array(length) 用指定长度创建了一个数组,但是没有设置任何值,所以引用它的长度可以工作,但是枚举元素不可以。如果我设置一个数值会怎么样?

var arr = new Array(10);
arr[8] = undefined;
arr = arr.map(function(item, index) { return index; });
console.dir(arr);

现在我得到了一个数组,第 8 个元素等于 8,但是其他所有的值依然是 undefined。看一下 map 的 polyfill 实现,它循环每一个元素(这是为什么 index 是正确的),但是它使用的是 in 来检查一个属性是否被设置。你如果使用数组直接量,也会得到同样的结果。

var arr = [];
arr[9] = undefined;
// or
var arr = [];
arr.length = 10;

其他

Mozilla 的开发者博客有一篇很棒的文章关于箭头函数,其中包含使用 <!-- 作为一种官方的 ES6 注释的细节。一整个系列的博客文章也都值得仔细阅读。

余下全文(1/3)
分享这篇文章:

请关注我们:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注