在某个时刻,JavaScript变得出色了

原文信息: 查看原文查看原文

At some point, JavaScript got good

- Jonathan Beebe

我自2012年左右开始全职从事JavaScript工作,这既是幸运也是不幸。不幸的是,在2015年左右,当ECMAScript规范开始出现重大改进之前,这门语言每天都在让我头疼。

然而,我也很幸运,因为自那以后JavaScript进行了很多改进,但语言的基本工作原理仍然相同,所以对新语法(去掉糖衣)有更深入的理解,在调试、处理遗留项目或深入研究底层代码时非常有用。我也能够更好地欣赏到我们这些JavaScript开发者现在有多好。

简短的背景

我最早在2012年为一家名为Lanica的小公司专业地开始使用JavaScript。这是一家由Appcelerator资助的公司,它为Titanium SDK制作了一个移动游戏引擎(我们在加利福尼亚州山景城的Appcelerator办公室工作)。

我来自Lua,这是一种简单但非常“理智”的脚本语言(我在前一家公司大量使用),JavaScript学习(并使用)起来简直是一场噩梦。后来我开始使用JavaScript在传统web前端工作(还有人记得Backbone.js吗?)以及使用Node.js进行后端开发。

我最不喜欢的是函数作用域变量、调用者上下文、回调地狱,以及到处写“function() function() function()…”。所有这些因素结合起来,不仅使编写JavaScript代码变得令人沮丧,而且阅读也是如此。

函数作用域与块作用域变量

letconst关键字几乎完全取代了var用于定义变量。除了明显的好处,即知道一个变量是否可变之外,一个较少为人所知的好处是,使用letconst定义的变量是块作用域的,而不是函数作用域(var),这有助于防止许多微妙的错误潜入你的代码。

我们过去不得不用自执行匿名函数来伪造:

function example() {
  // 没有块作用域:
  for (var i = 0; i < list.length; i += 1) {
    var x = list[i];

    // x看起来像是块作用域,但它不是!
  }

  // 尽管在上述循环块中定义,
  // x在函数的任何地方都可以访问
  console.log(x);
  console.log(i); // 迭代器在这里也是可用的

  // 块作用域可以被“伪造”为
  // 自执行匿名函数:
  for (i = 0; i < list.length; i += 1) {
    (function() {
      var y = list[i];

      // y只在该函数内可访问
    })();
  }
}

第二个循环中伪造的块作用域是有效的,但它的可读性要低得多,也更容易出错,而不是真正的块作用域(稍后会更多讨论)。JavaScript代码库到处都是这样的东西,以解决语言的怪癖。

函数上下文(this)

现在,如果你使用this关键字,你很可能正在为一个类方法编写代码,引用当前实例(类似于其他面向对象的语言)。在JavaScript的早期时代,你必须非常清楚一个叫做“函数上下文”(也称为“this”上下文)的东西,this所引用的内容可能会因函数的调用方式而有所不同。

如果JavaScript开发者是在ES6之后学习这门语言的,他们可能不知道什么是函数上下文,因为它不像以前那样直接使用。

简而言之,JavaScript为每个函数调用都有一个映射到this关键字的“上下文”。记得前面的例子中我们定义了一个自调用的匿名函数吗?如果我们试图在那个函数中访问外部函数的this关键字,会导致问题:

function example() {
  console.log("1:", this.someVar);   // 1: "hello"

  (function() {
    console.log("2:", this.someVar); // 2: undefined
  })();
}

example.call({ someVar: "hello" });

因为每个函数都有自己的调用者上下文,内部函数不会继承外部函数的上下文,所以this不会引用相同的东西。你需要在一个单独的变量中存储外部函数的this值,或者使用call()apply()来显式设置内部函数的上下文(thisArg)。

ECMAScript 2015(ES6)引入了“箭头函数”,表面上看,它是一种让我们不必每次都输入“function”的方式(它确实是),但它不仅仅是这样。这些箭头函数还继承了它们定义时父作用域的上下文。所以这个例子按预期工作:

function example() {
  console.log("1:", this.someVar);   // 1: "hello"

  (() => {
    console.log("2:", this.someVar); // 2: "hello"
  })();
}

example.call({ someVar: "hello" });

我不记得上次在现代JavaScript代码库中使用call()apply()是什么时候了,或者因为this不是它看起来的样子而修复bug。但我还记得,我必须时刻痛苦地意识到函数调用者上下文。如果我再也不用call()apply(),我会很高兴。

回调地狱

在Promise和async/await内置之前,有回调地狱。如果你没有在那些日子里使用JavaScript的荣幸,那你很幸运。例如:

function getResults(callback) {
  makeFirstRequest(function(result1, err1) {
    if (err1) {
      callback(null, err1);
      return;
    }

    makeSecondRequest(function(result2, err2) {
      if (err2) {
        callback(null, err2);
        return;
      }

      makeThirdRequest(function(result3, err3) {
        if (err3) {
          callback(null, err3);
          return;
        }

        // 通过顶级回调参数返回结果
        callback({
          result1: result1,
          result2: result2,
          result3: result3,
        }, null);
      })
    });
  });
}

getResults(function(result, err) {
  if (err) {
    console.log(err.message);
    return;
  }
  console.log("Got results:", results);
});

上面的代码只是为了说明概念,当然有更干净的方式来编写它,但无论你给这只猪涂多少口红,它仍然看起来很痛苦(更不用说编写了)。

示例调用了一个函数(getResults()),该函数执行了三个请求,合并了结果,并通过回调函数“返回”了一个对象(或一个错误)。这就是JavaScript中处理异步代码的方式,被称为“回调地狱”,因为嵌套回调的迷宫(想象一下,如果每个嵌套函数都更大更复杂)。

与现在使用Promise和async/await(以及对象简写语法和错误处理)的便利性形成对比:

async function getResults() {
  const result1 = await makeFirstRequest()
  const result2 = await makeSecondRequest();
  const result3 = await makeThirdRequest();

  return {
    result1,
    result2,
    result3,
  };
}

try {
  const results = await getResults();
  console.log("Got results:", results);
} catch (err) {
  console.log(err.message);
}

Node.js的异步特性是回调地狱真正流行的地方,但在前端(例如进行网络请求)也不少见。

展望未来

在ECMAScript 2015(ES6)给JavaScript带来了巨大的“推动”之后,语言持续改进,它只会变得更好。由于实现范围广泛,使用转译器已经成为社区的标准,所以我们都可以在每个项目的基础上利用最新的JavaScript特性,而不必担心每个单独的JS引擎支持什么。

后来的JavaScript版本并没有移除通常与该语言相关的“瑕疵”。你仍然可以像以前一样以完全相同的方式编写代码(如果你是个受虐狂),但新添加的特性提供了更好的编写代码的方式,有效地使旧的做事方式变得过时(尽管仍然有一些需要注意的奇怪之处)。结合像ESLint这样的优秀linting工具(以及最近出现的Biome.js),今天的JavaScript几乎无法从它过去的样子中认出来。

回顾过去,这门语言能够在保持与之前语言规范几乎完全向后兼容的同时,发展如此之多,实际上是相当令人惊叹的。在更广泛的编程社区中,JavaScript仍然有很多蔑视,但我认为这是因为人们被昨天的JavaScript吓坏了(我并不怪他们!)。但我认为,如果它基于今天的语言来评判,许多人会看到它实际上现在相当不错。

我现在已经“全力以赴”地使用TypeScript,但作为超集,归根结底它仍然是“只是JavaScript”(带类型)。我个人现在不会在没有它的情况下开始新的JavaScript项目,但这又是另一天的话题。

更新: 记得当我说过“在更广泛的编程社区中,JavaScript仍然有很多蔑视”吗?看看Hacker News的讨论串 来证明这一点。这可能是一个不受欢迎的观点(可能还有一些斯德哥尔摩综合症在起作用),但我个人现在喜欢它

分享于 2024-06-05

访问量 72

预览图片