面向 JavaScript / TypeScript 开发者:裁剪非必要解释,对比 Python 与 JS/TS 函数与模块的核心差异、特殊语法与踩坑点。
函数定义与类型签名
Python
python
from collections.abc import Callable
# 函数类型签名:Callable[[参数类型列表], 返回类型]
Formatter = Callable[[str, bool], str]
def format_task(title: str, done: bool = False) -> str:
# 格式化任务文本
status = "done" if done else "todo"
return f"[{status}] {title}"
formatter: Formatter = format_taskPython 使用
Callable[[入参类型列表], 返回类型]声明函数类型
TypeScript
typescript
// 函数类型签名
type Formatter = (title: string, done?: boolean) => string;
function formatTask(title: string, done: boolean = false): string {
// 格式化任务文本
const status = done ? "done" : "todo";
return `[${status}] ${title}`;
}
const formatter: Formatter = formatTask;TS 使用
(name: Type) => ReturnType声明函数类型;对比Python的Callable,TS 可为参数命名并用?标注可选参数。
补充
Callable与 TS 函数类型对比:- Python
Callable[[str, bool], str]仅按顺序约束参数类型,无法命名形参和标注可选; - TS
(title: string, done?: boolean) => string支持形参命名与?可选标记。
- Python
- 类型注解纯属约束:
- Python 的类型提示在运行时会被解释器完全忽略,不会做强类型校验。
- 一等公民:
- 两者的函数均为一等公民,可作为参数或返回值传递。
参数默认值陷阱
Python
python
# 错误写法:
def add_item(item: str, target: list[str] = []):
# 正确写法:默认值使用 None
def add_item(item: str, target: list[str] | None = None) -> list[str]:
# 内部动态初始化空列表
if target is None:
target = []
target.append(item)
return target默认参数只在函数定义时创建一次。用
[]或{}做默认值,多次调用会共享同一个对象。 即引用一个
JavaScript
javascript
// JS 默认参数在每次调用时独立求值
function addItem(item, target = []) {
// 每次未传参时自动创建新 Array
target.push(item);
return target;
}JS 默认参数在每次函数调用时独立
新建一个变量。
补充
- 坑:可变默认值:默认参数只在函数定义时创建一次。
- 如果用
[]或{}当默认值,多次调用会修改同一个对象,导致数据污染。
- 如果用
- 惯用法:
- Python 统一用
None作为可变参数默认值,在函数内部判断并初始化。
- Python 统一用
- 差异:
- JS 默认参数在每次函数调用时重新求值,不存在多调用共享可变对象问题。
① 位置参数与 ② 关键字参数约束
Python
python
# / 左侧必须按位置传参
# * 右侧必须按关键字传参
def configure(mode: str, /, *, verbose: bool = False) -> None:
print(mode, verbose)
# 正确调用
configure("prod", verbose=True)
# configure(mode="prod") # 报错:/ 左侧不能写参数名
# configure("prod", True) # 报错:* 右侧必须写参数名
/左侧参数强制只能按位置传递*右侧参数强制只能按关键字传递
补充
/与*作用:/强制位置传参(防止库作者改形参名破坏调用);*强制显式写出参数名(提升调用易读性)。
变长参数与解包
Python
python
# *args 收集多余位置参数 (tuple)
# **kwargs 收集多余关键字参数 (dict)
def create_node(name: str, *tags: str, **meta: str) -> dict:
return {"name": name, "tags": tags, "meta": meta}
tag_list = ("python", "fastapi")
meta_dict = {"env": "prod"}
# * 解包列表/元组,** 解包字典
res = create_node("app", *tag_list, **meta_dict)
# 输出: {"name": "app", "tags": ("python", "fastapi"), "meta": {"env": "prod"}}
*args收集多余位置实参为tuple,**kwargs收集关键字实参为dict;
调用时*解包序列**解包字典
补充
*argsvs...rest:- Python
*args收集到的参数是不可变元组tuple; - JS
...rest收集到的是数组Array。
- Python
**kwargs特性:- Python 原生支持
**kwargs捕获任意命名参数; - JS 无对应语法,需要手动传递配置对象。
- Python 原生支持
- 解包区别:
- Python 用
*解包位置序列 **解包字典关键字;- JS 统一用
...处理数组解包与对象展开。
- Python 用
返回值与空值
Python
python
# return 多值本质上是返回 tuple
def parse_coordinate(text: str) -> tuple[int, int] | None:
if "," not in text:
# 显式返回 None
return None
x, y = text.split(",")
# 返回多值(元组)
return int(x), int(y)
# 解包赋值
result = parse_coordinate("10,20")
if result is not None:
x, y = result
return a, b本质是返回tuple 元组;未显式 return 时返回None,判空须用is None。
补充
- 多返回值:Python 的
return a, b语法糖本质是返回一个tuple;JS 必须用return [a, b]或return {a, b}。 - 隐式空值:Python 函数无显式 return 时返回
None;JS 返回undefined。 - 判空坑:Python 判空必须使用
is None或is not None。若使用if not res:,当返回值为0、""或[]时会被误判为假值。
作用域与变量赋值
Python
python
# 变量只有 def/class/lambda 作用域,没有块级作用域
def scope_demo():
if True:
temp = "hello"
print(temp) # 输出: "hello"(if 块外部可见)
scope_demo()
# 修改全局变量须声明 global
count = 0
def increment():
global count
count += 1
print(count) # 输出: 0
increment()
print(count) # 输出: 1Python 仅有
def/class/lambda划分作用域(无块级作用域);在函数内部修改全局变量须显式声明 global。
JavaScript
JS 的 let/const 具备 {} 块级作用域,内部可直接沿作用域链修改外层变量。
补充
- 块级作用域:
- Python 没有
{}块级作用域。- 在
if或for中定义的变量在外层函数内部依然能被访问;
- 在
- JS 的
let/const则受{}作用域限制。
- Python 没有
- 赋值陷阱:
- 在 Python 函数内部给变量赋值(如
x = 1),默认创建局部变量并遮蔽外层同名变量。若要修改全局变量,必须显式声明global。
- 在 Python 函数内部给变量赋值(如
- 按引用传递:
- Python 与 JS 传参语义一致,都是传递“对象引用的副本”。
- 修改可变对象(如
list.append())外部可见,重新给形参赋值(如items = [])不影响外部。
模块与入口保护
Python
python
# module.py
def main() -> None:
print("模块主逻辑")
# 入口保护
if __name__ == "__main__":
# 仅当直接运行 python module.py 时执行
# 被 import 时忽略
main()一个 .py 文件即一个模块;
if __name__ == "__main__":用于区分是直接运行脚本还是被 import 导入。
JavaScript
javascript
// module.js (CommonJS / Node.js)
function main() {
console.log("模块主逻辑");
}
// 判断是否为主模块直接运行
if (require.main === module) {
main();
}
export { main };ES Module 使用 import/export 导出;Node.js 中通常使用
require.main === module判断直接运行。
补充
- 模块定义:
- Python 中每一个
.py文件就是一个模块,使用import module_name导入。
- Python 中每一个
- 入口保护:
- Python 依靠
if __name__ == "__main__":区分“直接运行脚本”与“作为模块导入”,防止导入时误触发测试或运行逻辑。
- Python 依靠
- JS 对照:
- ES Module 规范中无该机制,在 Node.js 环境中常使用
require.main === module达到相同效果。
- ES Module 规范中无该机制,在 Node.js 环境中常使用
补充
- 参数定义顺序(硬规则,写反就语法错):
- 位置参数 →
/→ 带默认值参数 →*args→ 仅关键字参数 →**kwargs - 例:
def f(a, /, b, c=1, *args, d, e=2, **kwargs): ... - JS 没有这套分隔符;TS 可选参数也只能“后面可选”,不能用
/、*强制传参方式。
- 位置参数 →
lambdavs箭头函数:lambda x: x * 2只能写单个表达式,不能写语句块、不能多行return。- 需要语句、分支、多行逻辑时一律用
def;- 别把
() => { ... }的习惯硬搬过来。
- 别把
- 嵌套函数改外层变量:用
nonlocal:- 函数里赋值默认当局部;要改上一层函数的变量,写
nonlocal name。 - 改模块级全局才用
global;JS 闭包直接改外层let,没有这两个关键字。
- 函数里赋值默认当局部;要改上一层函数的变量,写
import常见写法(相对 JS 的对照):import os≈import * as os from "os"(整模块命名空间)from pathlib import Path≈import { Path } from "pathlib"from pathlib import Path as P≈import { Path as P } from "pathlib"- 避免
from x import *:污染当前命名空间,且静态分析难跟踪。
- 包与模块:
- 单个
.py是模块;含__init__.py的目录是包(现代 Python 也支持无__init__.py的命名空间包,日常项目仍常见显式包)。 - 包内相对导入用
from . import sibling/from ..pkg import name,对应前端monorepo里的相对路径,但语法是包语义不是文件路径字符串。
- 单个
- 空函数体:
- Python 不能写空缩进块;
- JS 可以
function f() {}。