【外评】JavaScript 变得很好

大约从 2012 年起,我就开始全职使用 JavaScript,这让我感到既幸运又不幸。说它 “不幸”,是因为在 2015 年左右,ECMAScript 规范开始有了重大改进,在此之前,这种语言在日常使用中非常麻烦。

不过,我也很幸运,因为虽然 JavaScript 自那以后有了很多改进,但其基本工作方式仍然相同,因此在调试、处理遗留项目或深入挖掘底层代码时,深入了解新语法(不含糖分)非常有用。此外,我还能更好地体会到现在的 JS 开发人员有多么优秀。

背景简介

早在 2012 年,我在一家名为 Lanica 的小型初创公司工作时,就开始接触 JavaScript。这是一家由 Appcelerator 资助的公司,为 Titanium SDK 开发了一个移动游戏引擎(我们在加州山景城的 Appcelerator 办公室工作)。

Lua 是一种简单但非常 “理智 “的脚本语言(我在之前的公司大量使用过这种语言),而 JavaScript 则是学习和使用 JavaScript 的噩梦。后来,我转而使用 JavaScript 开发传统的网络前端(有人记得 Backbone.js 吗?),并使用 Node.js 进行后端开发。

我最不喜欢的是函数作用域变量、调用者上下文、回调地狱,以及写 “function() function() function() function()……”(到处都是!)。所有这些问题加在一起,不仅在编写 JavaScript 代码时令人沮丧,而且在阅读时也令人沮丧。

函数变量与块作用域变量

let 和 const 关键字几乎完全取代了 var 来定义变量。除了知道变量是否可变这一显而易见的好处外,还有一个鲜为人知的好处,那就是用 let 和 const 定义的变量是块作用域变量,而不是函数作用域变量(var),这有助于防止许多微妙的错误潜入代码。

以前,我们不得不使用自执行匿名函数来伪造变量:

function example() {
  // without block-scoping:
  for (var i = 0; i < list.length; i += 1) {
    var x = list[i];

    // x looks like it is block-scoped but it's NOT!
  }

  // although defined in the loop's block above,
  // x is accessible anywhere in the function
  console.log(x);
  console.log(i); // the iterator is also available here

  // block scoping could be "faked" with a
  // self-executing anonymous function:
  for (i = 0; i < list.length; i += 1) {
    (function() {
      var y = list[i];
      
      // y is only accessible within this function
    })();
  }
}

第二个循环中的块范围是 “伪造 “的,但与真正的块范围相比,它的可读性要差得多,也更容易出错(关于这一点,稍后会有详细介绍)。JavaScript 代码库中到处都是这样的代码,以解决这种语言的怪异之处。

函数上下文(this)

如今,如果你使用 this 关键字,很可能是在为引用当前实例的类方法编写代码(与其他面向对象的语言类似)。而在 JavaScript 的上一个时代,你必须对一种叫做 “函数上下文”(也称为 “this “上下文)的东西有敏锐的感知,它所指的内容可能会根据函数的调用方式而有所不同。

如果在 ES6 之后学习 JavaScript 语言的开发人员不知道函数上下文是什么,我也不会感到惊讶,因为它已经不像以前那样被直接使用了。

简而言之,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 example() {
  console.log("1:", this.someVar);   // 1: "hello"

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

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

我已经不记得上一次在现代 JavaScript 代码库中使用 call()apply() 是什么时候的事了,也不记得上一次修复 bug 是因为什么了。但我记得曾经有一段时间,我不得不随时注意函数调用者的上下文,这让我很痛苦。如果我再也不用 call() apply(),那我也就心满意足了。

回调地狱

在 promises 和 async/await built-in之前,还有回调地狱。如果你还没有在那个时代使用过 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;
        }

        // return result via top-level callback argument
        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 中处理异步代码的方式(或其变体),由于嵌套回调的复杂性,它被称为 “回调地狱”(试想一下,如果每个嵌套函数都更大更复杂)。

相比之下,我们今天使用 promisesasync/await(以及对象速记语法和错误处理)就轻松多了:

async function getResults() {
  const result1 = await makeFirstRequest()
  const result1 = 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 的异步特性是回调地狱真正盛行的地方,但在前端(即进行网络请求)也经常遇到这种情况。

面向未来

JavaScript 在 ECMAScript 2015(ES6)中得到了极大的 “提升”,之后,JavaScript 语言不断得到改进,而且越来越好。由于实现方式多种多样,转译器的使用已成为社区的标准配置,因此我们可以根据每个项目的具体情况利用 JavaScript 的最新功能,而不必(过于)担心每个 JS 引擎支持哪些功能。

JavaScript 的后续版本并没有去掉任何与该语言相关的 “缺陷”。你仍然可以用与以前完全相同的方式编写代码(如果你是个受虐狂的话),但新功能增加了编写代码的更好方法,从而有效地淘汰了旧的方法(尽管仍有一些奇怪的地方需要注意)。再加上 ESLint 和(最近的)Biome.js 等良好的内核工具,如今的 JavaScript 几乎与过去不可同日而语。

事后看来,这种语言能够在保持与以前的语言规范几乎完全向后兼容的同时取得如此大的发展,确实令人惊叹。在更广泛的编程社区中,仍然有很多人对 JavaScript 不屑一顾,但我认为这是因为人们对昨天的 JavaScript 已经伤痕累累(我并不责怪他们!)。但我认为,如果根据现在的语言来评判 JavaScript,很多人会发现它现在其实很不错。

我后来 “全情投入 “到 TypeScript 中,但作为一种超集,它最终仍然 “只是 JavaScript”(带有类型)。如今,如果没有 TypeScript,我个人是不会启动一个新的 JavaScript 项目的,但这是后话了。

更新:还记得我说过 “在更广泛的编程社区中,仍然有很多人对 JavaScript 不屑一顾 “吗?请参阅Hacker News的主题,以了解这一点。这可能是一个不受欢迎的观点(也可能是斯德哥尔摩综合症在作怪),但我个人现在很喜欢 JavaScript。

本文文字及图片出自 At some point, JavaScript got good

你也许感兴趣的:

共有 1 条讨论

  1. dd 对这篇文章的反应是飘过~

发表回复

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