面向 Python 开发者的 JavaScript

你已经会编程了。真正的危险,是以为 JavaScript 只是加了花括号的 Python。并非如此:它有两种“缺失值”,数组按引用传递,Promise 本身也是普通值,而且运行时和语法同样重要。下面这些,是我会在一位 Python 队友提交第一个 JavaScript 合并请求前讲清楚的事。

🎙️ 发布并录制于: · 更新于 ·

01别照着标点翻译

写出糟糕 JavaScript 的最快办法,就是把 Python 文件逐句改写。该翻译的是意图,不是表面形式。Python 字典常常对应对象,但键值任意的字典可能更适合用 Map。列表推导式往往能改成先过滤再映射,不过普通循环经常更清楚。JavaScript 还有两种“没有值”:null 和 undefined。可以把 undefined 理解成“没有提供或没有找到”,把 null 留给有意设置的空值。

# Python
names = [u["name"] for u in users if u.get("active")]
first = next((u for u in users if u["id"] == wanted_id), None)
// JavaScript: the question mark is optional chaining
const names = users.filter(u => u.active).map(u => u.name);
const first = users.find(u => u.id === wantedId); // undefined if absent
const city = first?.address?.city ?? "unknown";
迁移原则
===,不要用 ==。宽松相等会先转换类型再比较,所以 0 == false 的结果是 true。Python 让你习惯了平淡无奇的比较;严格相等能让它继续保持平淡。

02先用 const,避开 var

我的规则很直接:声明变量时先用 const。只有变量名本身需要重新赋值时,才改用 let。不要用 var;它的函数作用域和变量提升都是历史遗留问题,没必要继承。也别把常量绑定误解成不可变的值。const 声明的数组照样能添加元素,就像 Python 的变量名仍可指向一份已经修改过的列表。

const user = { name: "Ada" };
user.name = "Grace";       // valid: object mutated
user = { name: "Lin" };     // TypeError: Assignment to constant variable.

let retries = 3;
retries -= 1;               // reassignment, so let is honest

// Do not write new code with var.
真实错误:ReferenceError
ReferenceError: Cannot access 'total' before initialization
你在声明之前读取了一个用 letconst 定义的变量。把声明移到首次读取之前。不要把它改成 var 来“修复”;那只会把一个明显的错误变成更隐蔽的 undefined 错误。

03真假值规则已经变了

Python 开发者最容易栽在两个值上:空数组和空对象在 JavaScript 里都是真值。所以,判断 items 是否为真,回答的是“这个变量里有没有一个数组”,而不是“数组里有没有内容”。字符串的表现仍然符合你的预期。数字零、空字符串、null、undefined、非数字值 NaN 和 false 都是假值,其余都是真值。

# Python
if not items:
    print("empty")

// JavaScript
if (items.length === 0) console.log("empty");
if (Object.keys(options).length === 0) console.log("no options");

const shown = count ?? 10; // preserves a legitimate 0
const wrong = count || 10; // replaces 0 with 10
线上静默错误
把 Python 的 value or default 直接写成 value || default,会误伤有效的零和空字符串。只有 nullundefined 才表示缺失时,请用 ??

04数组像列表,但更容易伤人

JavaScript 数组和 Python 列表一样,是可变的引用,但数组方法分成两类。map、filter、slice 和 toSorted 会返回新数组。push、pop、splice 和 sort 会修改原数组。最后这个 sort 特别容易制造问题,因为它默认按文本排序,所以十会排在二前面。

# Python
prices = [10, 2, 30]
ordered = sorted(prices)               # [2, 10, 30]
last = prices[-1]

// JavaScript
const prices = [10, 2, 30];
const ordered = prices.toSorted((a, b) => a - b);
const last = prices.at(-1);
const doubled = prices.map(n => n * 2);
真实错误:undefined 没有属性
TypeError: Cannot read properties of undefined (reading 'name')
索引或 find() 没找到值,下一行却继续读取了结果的属性。先输出或检查出错点号前的值,再明确选择处理方式:用 if (!user) 做保护并提前返回;对确实可选的数据使用 user?.name;或者抛出更清楚的错误。必需数据不要到处用可选链掩盖。

05对象不完全等于字典

如果一条记录有已知的字符串键,比如用户、配置或接口负载,对象就很合适。如果键是任意值,特别是对象,应当使用 Map。属性既能用点号访问,也能用方括号访问;键名保存在变量里时,必须用方括号。解构可以替代很多在 Python 里反复写的字典拆包,展开语法则能创建浅拷贝。注意是浅拷贝:嵌套对象仍然共享。

# Python
name = user["name"]
role = user.get("role", "reader")
updated = {**user, "active": True}

// JavaScript
const { name, role = "reader" } = user;
const updated = { ...user, active: true };
const field = "name";
console.log(user[field]);               // variable key

const byElement = new Map();          // arbitrary keys
byElement.set(button, { clicks: 1 });
迁移陷阱:拷贝是浅层的
执行 const copy = {...original} 后,修改 copy.address.city 也会改变 original.address.city。确实需要深拷贝的类 JSON 数据可用 structuredClone(original)。更好的做法,是只复制准备修改的那一层嵌套分支。

06函数:值、回调与参数

两个语言都把函数当作值,但 JavaScript 的各种 API 大量依赖回调。较完整且有名字的行为适合用函数声明,简短回调适合用箭头函数。JavaScript 没有 Python 那样的关键字参数;位置参数开始难懂时,就改用选项对象。可以直接在参数列表里解构,并在那里给出默认值。

# Python
def fetch_users(active=True, limit=20):
    ...
fetch_users(limit=5)

// JavaScript: options object substitutes for keyword arguments
function fetchUsers({ active = true, limit = 20 } = {}) {
  return client.get("/users", { active, limit });
}
fetchUsers({ limit: 5 });

const labels = users.map(user => user.name.toUpperCase());
真实错误:不可调用
TypeError: formatUser is not a function
在失败的调用处,用 console.log(typeof formatUser, formatUser) 检查 formatUser。如果它是 undefined,核对导出和导入名称。如果它是对象,你很可能错误地导入了模块命名空间或默认导出。如果误写成 onClick={formatUser()},应当传入函数本身:onClick={formatUser}

07尽量避开 this

Python 的 self 明确而稳定。JavaScript 的 this 却取决于函数怎样被调用,迁移时一直记着这件事很累。我的建议是尽量避开 this,优先使用普通函数、闭包,以及通过参数传入的数据。在类和旧库里,你仍然需要看懂 this,但不必让新应用围着它设计。

// Fragile when passed as a callback: the receiver is lost.
const counter = {
  value: 0,
  increment() { this.value += 1; }
};
setTimeout(counter.increment, 100);

// Boring and portable: state is explicit.
function increment(state) {
  return { ...state, value: state.value + 1 };
}
let state = { value: 0 };
state = increment(state);
现有代码必须使用它时
把一个方法交给另一个 API 时,可用 counter.increment.bind(counter) 保留接收者,也可以包一层调用:() => counter.increment()。不要不加判断地把所有函数都改成箭头函数;先弄清楚这次调用应当属于哪个对象。

08asyncawait:语法熟悉,运行方式不同

由于 JavaScript 使用事件循环,异步函数会立即返回一个 Promise。await 只会暂停当前异步函数,不会暂停整个进程。fetch 还有第二个陷阱:像 404 这样的 HTTP 错误,不会让 Promise 进入拒绝状态,必须主动检查 response.ok。彼此独立的任务应使用 Promise.all;如果任务没有依赖关系,在循环里逐个等待虽然结果正确,却会白白变慢。

async function fetchJson(url) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`Request failed: ${response.status}`);
  }
  return response.json();
}

const [user, teams] = await Promise.all([
  fetchJson("/api/users/42"),
  fetchJson("/api/teams")
]);
真实错误:Promise 尚未完成
Promise { <pending> }
你输出的是 Promise,而不是它最终产生的值。把代码放进 async 函数,再写 const user = await loadUser(42)。现代 ES 模块允许在模块顶层使用 await。较旧的 CommonJS 代码中,应定义一个异步入口函数,并在调用末尾接上 .catch(console.error)
forEach 陷阱
await items.forEach(async item => save(item)) 不会等待其中的回调。并行任务应使用 await Promise.all(items.map(save));顺序重要时,则用带 awaitfor...of 循环。

09模块:只选一套系统

Python 主流的导入方式只有一套。JavaScript 则有现代的 ES 模块系统,还有 Node.js 上较旧的 CommonJS 系统。新代码应当使用 ES 模块,也就是 export 和 import。在 Node 里,可在 package.json 中把 type 声明为 module,或者改用 .mjs 扩展名。在浏览器里,应以 module 类型加载入口脚本。混用两套系统时,报错看似是语法问题,实际却是配置问题。

// math.js
export function mean(values) {
  return values.reduce((sum, n) => sum + n, 0) / values.length;
}
export const version = "1.0";

// report.js — relative imports need the extension in browsers
import { mean, version } from "./math.js";
console.log(mean([2, 4, 9]), version);
真实错误:模块模式不对
SyntaxError: Unexpected token 'export'
运行时把 ES 模块当成了普通脚本或 CommonJS 文件。在 Node 中,给最近的 package.json 添加 "type": "module",或把文件扩展名改成 .mjs。在 HTML 中,使用 <script type="module" src="app.js"></script>。之后统一使用 ESM 的导入和导出;不要随机把几行改成 require 来糊住问题。

10迁移调试地图

JavaScript 的调用栈很嘈杂,但有效的排查步骤和 Python 一样:先找到你自己的代码中第一个相关栈帧,检查边界上的值,再用最小输入复现。浏览器控制台不能替代你对类型的理解。检查 typeof 的结果,检查一个值是不是数组,也检查 Promise 是否已经等待。最后,要修正产生坏值的源头,而不是逐个修补下游消费者。

// Keep this five-question checklist beside the debugger.
value === undefined                 // missing property, index, return?
value === null                      // deliberate empty value?
typeof value                         // "function", "object", "string"...
Array.isArray(value)                   // typeof [] is misleadingly "object"
value instanceof Promise             // forgot await?

// JSON is stricter than a JS object literal:
JSON.parse("{'name': 'Ada'}");
再看一个真实错误
SyntaxError: Unexpected token ' in JSON at position 1
JSON 要求键名和字符串值都使用双引号。把负载改成 {"name":"Ada"}。更好的办法是不要手写 JSON:先构造 JavaScript 对象,再调用 JSON.stringify(data);读取响应时,则在调用 response.json() 前确认内容类型。

实际迁移时,请坚持这套做法:使用 const、严格相等、明确的空值检查、普通函数、ES 模块,并等待每一个需要取值的 Promise。避开 var,尽量避开 this,也别再强迫 JavaScript 模仿 Python。接受它的运行时模型后,这门语言就没那么出人意料了。

Tell me what missed

A correction is more useful than a compliment. This goes straight to the person who writes SwiftGrasp.

Was this page useful?
0/1000

Please do not include passwords, private keys, or personal information.