这是 JavaScript 吗?

OH:这只是 JavaScript 吧?我懂 JavaScript。我的同事今天剩下的时间肯定要用来调试一个 Electron 问题——@jonkuperman.com 在 BlueSky

“这只是 JavaScript!”你可能听过这句话。我本人也用过很多次。这句话经常被用来暗示某个项目易于上手,因为它可以用我们都熟悉和喜爱的通用标准化脚本语言——JavaScript——来实现。将你在浏览器中移动像素的经验,应用到运行服务器和查询数据库上。你可以用同一种语言实现这两者,这就是 JavaScript!但等等,JavaScript 到底是什么?任何写在 .js 文件中的代码都是”只是 JavaScript”吗?让我们玩一个我称之为”这是 JavaScript 吗?”的小游戏。

浏览器 JavaScript

let el = document.querySelector("#root");
window.location = "https://www.webhek.com"

这是 DOM 相关操作,即浏览器 API。这是 JavaScript 吗?”如果它在浏览器中运行,那就是 JavaScript”似乎是一个不错的经验法则。但如果它仅在浏览器中运行,你能说”它就是 JavaScript”吗?反之,如果代码无法在浏览器中运行,但在其他地方可以运行,又该如何界定?

服务器端 JavaScript

const fs = require('fs');
const content = fs.readFileSync('./data.txt''utf8');

这段代码可以在 Node 环境(或兼容 Node 的环境,如 Deno)中运行,但无法在浏览器中运行。它算是”纯 JavaScript”吗?元素周期表

环境变量

你可能在 .js 文件中见过这样的代码:

const apiUrl = process.env.API_URL

但这是遵循 Node 规范,这意味着该 .js 文件在浏览器中可能无法正常运行,但在服务器上可以。如果代码能执行但仅在了解运行时规范的情况下才能正常工作,它算是”纯 JavaScript”吗?

JSX

那么这个文件 MyComponent.js 呢?

function MyComponent() {
  const handleClick = () => {/* do stuff */}
  return (
    <Button onClick={handleClick}>Click me</Button>
  )
}

它无法在浏览器中运行。它需要一个编译步骤将其转换为 React.createElement(...)(或可能是其他内容),这样才能在浏览器中运行。或者等等,它也可以在服务器上运行。因此它可以在服务器或浏览器中运行,但现在需要编译步骤。它算是”纯 JavaScript”吗?

编译指令

这个小片段呢?

/** @jsx h */
import { h } from "preact";
const HelloWorld = () => <div>Hello</div>;

这些是影响 JavaScript 代码解释和编译的魔术注释(Tom MacWright 对此有一篇精彩的文章)。如果代码包含指导其编译和后续执行的魔术注释,它还是”纯 JavaScript”吗?

TypeScript

那么:

const namestring = "Hello world"

你随处可见它,它似乎几乎与 JavaScript 同义,你会认为它是”纯 JavaScript”吗?

导入

你很可能遇到过一个 .js 文件,顶部看起来像这样。

import icon from './icon.svg';
import data from './data.json';
import styles from './styles.css';
import foo from '~/foo.js';
import foo from 'bar:foo';

但其中很多语法是非标准的(我之前曾详细讨论过这个话题),并且需要某种编译——这算是”纯粹的 JavaScript”吗?

原生

这是一个 .js 文件:

var foo = 'bar'

我可以在这里运行它(在浏览器中)。我可以在那里运行它(在服务器上)。我可以在任何地方运行它。它不需要编译器、魔法语法、打包工具、转译器或运行时特定的语法。它在任何地方都会以相同的方式运行。这似乎确实是”纯 JavaScript”。

始终如此,上下文至关重要

你每天看到的很多 JavaScript 都是非标准的。尽管它可能非常普遍——例如看到 process.env.*——但很多 JS 代码需要你”了解内情”才能理解它实际上是如何工作的,因为它没有遵循 ECMAScript 标准 的任何部分。要理解一个 .js 文件,你需要掌握以下几个关键上下文信息:

  • 该代码将在哪个运行时环境中执行?浏览器?服务器端环境如 Node、Deno 或 Bun?还是其他环境如 Cloudflare Workers?
  • 执行该代码前需要哪些工具进行编译?(如 vite、esbuild、webpack、rollup、TypeScript 等)
  • 代码中隐含了哪些框架?例如,是否存在非标准的全局变量如 Deno.*,或特殊关键字导出如 export function getServerSideProps(){...}

当有人说”这只是 JavaScript”时,更明确的说法应该是”这只是适用于…的 JavaScript”,例如:

  • 这是适用于浏览器的 JavaScript
  • 它只是用于 Node 的 JavaScript
  • 它只是用于 Next.js 的 JavaScript

那么,你如何称呼可以在上述任何上下文中运行的 JavaScript?我想,你可能会称它为”只是 JavaScript”。

你也许感兴趣的:

共有 53 条讨论

  1. 你可以开玩笑,但事实是,能够直接对Electron、React、开发工具或Node本身启动调试器,确实具有极高的价值。当然,“直接”在这里承担了大量工作,但我职业生涯中曾在不同阶段使用过上述所有工具,并取得了相当不错的成果。我认为并非所有东西都应该用 JavaScript 编写,但有时它作为底层实现确实非常方便。

    1. 没错!在我早期参与的一家初创公司中,我们的一位早期客户(当时客户数量还不多)报告了一个令人烦躁的 bug,导致其工作流程受阻。于是,我们的工程师登录到客户的 production 环境;复现了该 bug;在 Chrome 调试器中设置了断点;分析了 bug 的根本原因;并在数小时内修复并部署了补丁。客户非常满意。

      我认为没有多少编程语言能做到这一点?有些语言可能甚至更好,比如Smalltalk…

      1. 使用PHP甚至不需要部署,只需在生产服务器上编辑文件,保存即可完成。:D 不过我并不推荐这种做法。

        1. 哈哈,是的,我以前用过PHP,我们偶尔会用Notepad++之类的工具在服务器上实时编辑文件。

          我也不推荐这种做法,但这是最快的热修复方式。

        2. 具体取决于环境配置,JS也可能适用这种方式,但现实中大多数JS现在都是编译后的。

          1. 从经验来看,你绝对可以SSH到生产环境的Web服务器并实时编辑编译/压缩后的JS文件。

      2. >我认为没有多少语言能做到这一点

        这不是所有语言都具备的基本功能吗?

        1. 至少从我个人角度来看,核心区别在于我使用同一工具处理所有这些不同任务。如今任何成熟语言都提供强大的调试器,但不需在不同工具间切换意味着你能更熟练地掌握所用工具。

        2. 任何在生成二进制文件时不会丢弃必要信息的语言。这意味着默认情况下,源代码可分发的语言具有优势。但这样的语言仍然很多。

          1. >任何在生成二进制文件时不会丢弃必要信息的语言

            只有当你要求编译器丢弃这些信息时,它们才会被丢弃。你可以始终发布一个调试版本。

      3. 你可以用C++和gdb做到这一点。只要你有调试符号。

        其他语言就不用说了。

        1. 本地当然可以。你能对部署在客户那里的C++二进制文件这样做吗?

          1. 当然可以。你可以将 gdbserver 附加到远程机器上运行的进程,并通过本地 gdb 连接到它。

            编辑:此外,如果发生崩溃,你通常可以获取核心转储,这样你可以检查程序的精确状态,再次使用 GDB。

      4. 好的,但抱歉,这里 JavaScript 有什么特别之处?我对老旧的 Java 服务做过数百次类似操作。当天或次日即可完成热修复。远程调试支持多种语言,并非仅限于 JS。

        JVM 支持实时可视化性能分析——许多工程师通过此方式识别生产环境中的性能问题。我认为 Node.js 在开箱即用的便利性上无法与之匹敌。(据我所知,其性能分析功能目前仍处于基础阶段)

      5. 我们在 JVM 上已经这样做几十年了。

    2. 是的,这很方便,但 React Native 成为移动开发主流也令人烦恼。这就是为什么原本 200 KB 的应用现在变成了 20 MB 的小费计算器应用。

      TSMC 给予的,React Native 夺走……

  2. 其实你也可以用几乎任何语言写出同样的文章。

    “是C语言吗?”——在Linux上运行的C、在Windows上运行的C、在嵌入式芯片上进行实时处理的C……这些之间有很大区别。

    “是Java吗?”——桌面版?服务器版?Android版?Java ME?信用卡?!

    列表还可以继续……

  3. “这只是JavaScript,所以前端开发人员可以处理后端“这句话对我来说从来都没有意义。

    我认为这是针对人力资源/招聘部门的。

    公司在招聘时倾向于考虑具体技能。例如:我们使用PostgreSQL,因此会在高级后端工程师职位的职位描述中写上”3年PostgreSQL经验”。

    除非工程团队明确参与招聘流程,否则人力资源部门会自动筛掉你的简历,如果你只提到“5年MySQL和SQLite经验”而未提及Postgres。

    1. > 除非工程部门明确参与招聘流程,否则人力资源部门会自动筛选掉你的简历,如果你只提到“5年MySQL和SQLite经验”而未提及Postgres。

      我在这两年中通过亲身经历明白了这一点。

      如果你之前有过带领团队的经验,他们可能会给你一些宽容,但如果没有,你的申请就会被排到列表底部,因为你不符合确切的关键词,而且旁边有一个很大的数字。

      1. 这种情况自2000年代初就已存在,甚至更早之前可能也如此。

        我原本以为行业已经成熟,但事实并非如此,仍在沿用老一套流程!

  4. 我认为尽管可用API会根据上下文变化,但“这只是JavaScript”的说法更常指其语法仍遵循JavaScript规范。这意味着对于习惯于浏览器/前端 JavaScript 的开发者来说,开始尝试 Node.js 脚本要比尝试具有类似功能的 Python 脚本容易得多。从这个意义上说,它确实是“只是”JavaScript。

    指出这些差异仍然是有用的,就像文章中所做的那样。因为不同 JavaScript 环境之间的过渡并非无缝衔接。

    1. 我同意,熟悉基本语法是一种不错的策略,可以帮助人们克服尝试新事物的恐惧(实际上是让他们认为这根本不是新事物)。但这也是最低的共同点,如果理解基本语法是你唯一能提供的东西,哦,你很快就会遇到那堵墙……

      1. 事情不止於此。即使在不同平台上,也有很多共通的 API 和原則。但確實,你仍然需要進一步理解你現在使用該語言的上下文。更不用說你可能還需要進行一些進一步的學習。

        但总体而言,我认为这仍然会让切换语言变得显著容易。尤其是在当前的生态系统中,前端开发人员很可能已经安装了 Node.js,对 npm 也有所了解等。

        即使我们只关注语法,仅仅在切换上下文时不必担心这一方面,其好处也不应被低估,我认为。

    2. 我们至少可以达成共识,那些在提问时提到 JavaScript 却说“TypeScript”的人是错误的?

      1. 我承认这很烦人。但既然 TypeScript 是 JavaScript 的超集,我不能说他们完全错误。

  5. 经典的沟通错误是假设人们在提到 $LANGUAGE 时所指的内容。

    有些人指的是语言本身,有些人指的是语言+标准库,有些人指的是语言+标准库+工具,有些人指的是语言+标准库+工具+生态系统。

    同一个人在不同时间可能有不同含义,这取决于他们当时的想法。

  6. React引入了绝对令人难以置信的复杂性:

    * 你不再知道代码何时执行——这是魔法

    * 状态可以以~5种不同方式管理,伴随着大量繁琐的仪式和特殊性

    它绝对是一种eDSL(`useState`和`useEffect`使用JS语法引入了新语义),而一个真正的DSL会容易得多。

    1. > 你不再知道代码何时执行——这就像魔法一样

      有点像。在我看来,这就是 React 作为框架而非库的原因。不过,它仍然可以学习和管理(在纯 React 中)。

      > 状态可以以大约 5 种不同的方式进行管理,伴随着大量繁琐的仪式和特殊性

      这不是一个有效的论点。只需使用 React 状态即可。如果你选择其他方案,那确实如此。但坦白说,其他框架也存在同样的问题,甚至在后端也是如此。

      > 它绝对是一个 eDSL(`useState` 和 `useEffect` 使用 JavaScript 语法引入了新的语义),而一个合适的 DSL 会容易得多。

      这绝对正确且有道理。这主要是 JavaScript/TypeScript 的局限性,因为在长链元素中传递依赖项/属性会非常麻烦。因此,在不足的编程语言中,你只能在类型安全+冗长(React 没有钩子)和简洁+不安全(钩子不具备类型安全,它们不是普通函数,因此无法像普通函数那样重构等)之间做出选择。基本上,它们破坏了可组合性。

      坦白说,前端世界围绕这个问题打转,每隔一段时间就会有人认为找到了解决办法,然后放弃之前的优势以换取新的优势。这个循环不断重复。

      我好奇未来 effect.website 是否能改变这种局面。它具备这种潜力。

      1. > 这是 JavaScript/TypeScript 的一个主要缺点,因为在元素的漫长链条中传递依赖项/属性会非常烦人。

        你知道有没有哪种语言在语言层面解决了这个问题?

        我能想到的唯一类似的东西是 Lisp 中的动态变量。乍一看,它们让我想起了 React Context。

        1. 我个人在 Scala 中使用 ZIO 时会采用这种风格。因为基本上所有语言都存在同样的问题。我认为越来越多的语言在“功能”一词下捕捉到了类似的东西。从某种意义上说,Rust 的借用检查器是一个非常特殊的案例。

          https://effect.website/ 在其效果类型中提供了非常相似的东西,即“环境”参数。

          基本上,假设你有许多类似 `fun foo1(props: Foo1Props): string {…}` 的 foo 函数,它们相互调用。现在,最底层有一个 foo999,它被最顶层的 foo1 调用。但不是直接调用。foo1 调用 foo5,foo5 调用 foo27,依此类推,最终调用 foo999。

          现在,如果 foo999 需要一个新的依赖项(假设它需要访问之前不需要的用户配置文件信息),你必须修改类型签名并更新 Foo999Props。为了提供它,你需要更新调用者 foo567,并在那里添加它。依此类推,直到找到第一个已经拥有它的 fooX。这很烦人,噪音大(想想PR审核),等等。

          使用effect.website的效果类型,你可以将依赖项移动到返回类型中。因此,`fun foo1`现在将返回`Effect<string, Error, Dependencies>`,其中Dependencies就是Foo1Props——这意味着它不再需要props参数。(或者,我这样做的方式是,我只将 Dependencies 用于长期运行的服务,而不是一次性参数)

          在 fun1(或 funX)内部,你现在可以访问“Dependencies”。这基本上是一种“依赖倒置”,如果你愿意的话。

          看起来只是将所需的依赖项从参数移动到某种奇怪的复杂结果类型中。但,这里有一个很大的区别!你需要注释参数,但编译器可以推断出返回类型。

          因此,如果你有一个巨大的调用链/调用图,编译器会自动推断出所有函数的所有依赖项(感谢 TypeScript 拥有良好的联合类型,使这成为可能——许多其他语言无法做到这一点)。

          因此,你可以这样写:

          [预] def foo5(nonReleventProps: Props) =

          (props) => { const x1 = foo10(); const x2 = foo20(); return x1+x2; }

          [/预]

          其中 foo10 的类型为 `Props => ServiceA`,foo20 的类型为 `Props => ServiceB`,编译器会推断出 foo5 返回的效果需要同时使用 ServiceA 和 ServiceB——依此类推。(注意你需要以不同方式组合结果,我这里只用了 `const x = …` 以保持简洁)

          因此,如果你在调用图的较低层级进行修改,它会自动向上传播(只要你没有显式注释类型)。但你仍然拥有完整的类型安全,可以从推断出的类型中看到每个函数所需的依赖项,从而在测试时知道需要传递什么以及不需要传递什么。

          Lisp 是一个完全不同的世界,因为它是动态类型的。从某种意义上说,它从一开始就没有这个问题,但它为此牺牲了类型安全。

          > 乍一看,它们让我想起了 React Context。

          是的,基本意图是相同的,但 React Context 是…嗯,一个较差的版本,因为如果你在未提供上下文的地方使用它,它会在运行时崩溃,而没有人会保护你免受此影响。

  7. 认为 JavaScript 简单的想法是一个大错误。

    有多个构建链可供选择。多个包管理器,每个都有其独特之处。每个需求(例如 ORM、日志记录)都有多个选项可供选择,每个选项都有一些缺点,你直到深入其中才会发现。它有其他平台上不存在的漏洞类型。

    这门语言本身简单灵活,但若想开发企业级软件,整个平台及生态系统却是一团糟。当你意识到需要在后端定义元类型(如 Zod、class-validator 等)时,就是该开始转向运行时类型语言的时刻,因为你已意识到“其实运行时类型挺不错的”。

    许多团队意识到这一点时已经太晚,陷入了JS地狱,这种情况会影响团队的每一项工作,并在规模化时造成阻力。它会在CI中造成阻力,因为需要更多的检查,而工具本身也运行缓慢。它会在DX中造成阻力,因为我每天需要多次重启TS语言服务器。它会在平台配置中造成阻力,因为要让一切正常运行就像一团乱麻,而且在规模化时构建速度很慢。

    对于前端开发,这几乎无法避免。对于后端开发……既然你知道自己不是在开发一个玩具项目,为什么还要自找麻烦呢?

    是的,我确实会搭建 JavaScript 后端,但这只适合我的个人项目。甚至可以说很不错!但如果是真正的高价值工作?每次都是个错误,因为它需要比其他任何事情都多得多的维护工作。

  8. 这个服务器代码示例有些有趣的地方。

    [预] const fs = require(’fs’);

    const content = fs.readFileSync(’./data.txt’, ‘utf8’);

    [/预]

    这段代码实际上无法在Deno上运行,除非明确配置Deno以CommonJS格式加载JavaScript代码。而使用Bun时,这段代码无需任何配置即可运行。

    我猜作者故意省略了这些细节,因为文章结尾已对此进行了简要总结。JavaScript 代码确实需要大量关于周围开发环境的上下文信息。

    我记得去年读过一篇相关文章,声称“JavaScript 不存在”。遗憾的是,我找不到那篇文章,所以我将进行概括。该文章解释了现代开发栈如何由多个代码转换工具组成。我们实际上是在编写一种虚构的语言,并将其通过一系列转换,最终得到语法正确的 JavaScript。就我所知:前端开发者大多使用 React 和 TypeScript。TSX 文件至少要经过 TypeScript 编译器和插件将 JSX 转换为 JS。生成的 JS 文件可能包含诸如“CSS 导入”或打包器专属的“魔法注释”(如文章中 Preact 的示例)。这些内容必须由打包器(如 Vite)进行特殊处理,才能使 JS 文件成为语法正确的 JavaScript。

    1. 作者在此:这是一个很好的观点,感谢指出!也喜欢这个总结

      > 我们实际上是在用一种虚构的语言编写代码,并经过一系列转换,最终得到语法上有效的 JavaScript

      这篇文章听起来很有趣…

    2. 它将通过 ESM 导入一个 NPM 依赖项运行,Deno 只是不允许在顶级使用 commonjs

      1. 阅读文档后,似乎在以下三种情况下可以在顶级使用 `require`:

        (1) 文件扩展名为 “cjs”,

        (2) 存在一个 `package.json` 文件且 `type` 属性设置为 `commonjs`,或

        (3) 通过 `createRequire` 函数创建 `require` 函数

        https://docs.deno.com/runtime/fundamentals/node/#commonjs-su

        似乎文章中的代码在前两种情况下可以直接运行。无论如何,这就是我所说的 Deno 无法运行代码“除非显式配置”的意思。

    3. >”…现代开发堆栈涉及多个代码转换工具的组合。”

      或者不。例如,我们使用纯粹的浏览器端 JavaScript 进行业务前端开发。除了某些领域特定的库和 Web 组件外,我们并未使用任何框架如 React。这最终节省了大量时间。

  9. > 同一款普遍且标准化的脚本语言,我们都熟悉并喜爱:JavaScript。

    有时笑话就是这样自然而然地出现 :)

  10. 这就是任何抽象层的问题所在。我懂英语,但无法仅凭用英语撰写的法规条文提供专业的法律建议。除了“英语语言”这一约定外,我还需要学习另一层人类约定。

    我认为关键在于,调试一个库要比调试像 React 这样的框架(尽管他们坚持称其为库)简单得多,因为你的代码不再是入口点,而是逻辑中的某个中间步骤。

  11. 我认为这篇文章提到的“JavaScript 无处不在”现象,源于我们低估了学习新语言的难度。

    Python就是一个很好的例子(或者至少是我每天使用的语言)。为了能够在更大规模上继续使用Python,它已经成为了一种非常庞大、复杂的语言。

    有时我怀疑,对于许多需要类型化的Python用例来说,从一开始就使用一种类型化的语言(如Go)可能会更合适。

    从某种意义上说,学习一门新语言可能比学习一门日益复杂的语言中的大量功能更容易。

    1. 也许吧。但学习一门新语言(并在该语言中正确编程)的认知负荷是真实存在的。

      而更真实的是,当一个人被迫学习新事物而非选择熟悉的事物时,所面临的情感压力和心理困扰。当一个人面临截止日期、家庭生活、老板的严格要求、对构建管道的熟悉程度、一个可以随时求助的团队等情况时,往往通过坚持使用熟悉的事物来降低压力和困惑的程度会更容易。

      尤其是在像Python的类型系统或Java的Lombok这样的情况下,“再多做一件事”就能“修复”那些特定开发者遇到的粗糙边缘或困难场景。虽然这确实是“多做一步”,但很可能是小步骤,且他们无需颠覆整个思维模型就能理解(即使在实践中可能并不需要)。

  12. 该页面上似乎有一个指向RFC1918地址的链接:http://192.168.4.56:5001

    1. 而前三个链接都是http:而非https:。它们都应改为域名相对URL(/2019/jsx-like-…, 等等)。

    2. 作者在此,感谢指出这些问题!我当时正在本地主机上为博客编写一些 CSS,当我查找旧文章的引用以放入帖子时,我查看了本地运行的网站版本,却忘记更改链接,哈哈。哎呀。现在已修复——再次感谢!

  13. 我最喜欢的案例是调试一个部署到 iOS 的 Electron 应用的崩溃问题。结果发现,从一个点事件回调(深埋在应用代码中)抛出的异常,竟然传播到了设备的内核代码中。

    1. > 一个部署到 iOS 的 Electron 应用

      据我所知,这是不可能的。

      1. 也许是 React Native?它支持 iOS,而且与“又一个 Electron 应用程序”足够接近,可能存在思维上的重叠。

  14. TypeScript 不仅仅是 JavaScript;JavaScript 只是 TypeScript。

发表回复

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