9.5 万 star 的「JS 整洁代码圣经」,终于有中文版了。
从 GitHub 顶流仓库 ryanmcdermott/clean-code-javascript(MIT © Ryan McDermott,9.5 万 star)翻译整理成 10 大类 46 条规范:每条都是「中文说明 + ❌ 坏例子 + ✅ 好例子」对照,团队代码规范与新人的第一本 JS 代码修养手册。
⭐ 如果对你有帮助,点个 Star 支持中文开源
- 10 大类 46 条规范:变量/函数/对象/类/SOLID/测试/并发/错误处理/格式化/注释,覆盖日常写码所有场景;
- 坏/好例子对照:每条规范都配一段「反例 vs 正例」JS 代码,一眼看懂差别;
- 中文讲解:每条规范 1-3 句原创中文说明,讲透「为什么重要、坏在哪、怎么改」;
- 学习提示:记忆口诀、常见违反场景、原则间关联,新人也能快速上手;
- 直接可用:规范代码示例原样保留源项目,可直接抄进团队 ESLint 规则与 Code Review 清单。
| 合计 | 11 个分类 | 46 条规范 | | | |
- 团队定规范:把 10 个分类过一遍,挑出你们最常违反的 10-15 条,写进 Code Review 检查单;
- 新人培训:按 01 变量 → 02/03 函数与对象 → 04/05 类与 SOLID 的顺序读,每读一条就用「坏例子 → 好例子」对照改自己的代码;
- 日常自查:写代码时对照「学习提示」,尤其是函数长度、副作用、命名这三类高频问题;
- 进阶:SOLID 五原则(05)值得反复读,是面向对象设计的骨架。
- 每条规范格式:中文规范名(英文原名) → 中文说明 → ❌ 坏例子(JS)→ ✅ 好例子(JS)→ 💡 学习提示;
- 代码示例原样保留自源项目(含英文注释),中文说明为原创翻译重述;
- 规范中的「坏/好」是约定俗成的代码风格判断,具体取舍按团队实际场景调整。
变量命名是代码与人沟通的第一道界面。本章围绕"名字起得对不对、能不能被搜索、读起来要不要猜"展开,帮你把命名从随手写变成团队共识。
变量名首先要让人一眼看懂它装的是什么,其次要能顺口读出来——因为团队讨论代码时经常需要口头交流。像 yyyymmdstr 这种缩写串,既读不顺,也得回头猜它到底是格式还是日期,换成 currentDate 就能直接表意。
❌ 坏例子
const yyyymmdstr = moment().format("YYYY/MM/DD");✅ 好例子
const currentDate = moment().format("YYYY/MM/DD");💡 学习提示:命名时先默念一遍——如果开会时你不愿意把这个词念出口,它八成不够好。
既然都是"获取用户",就不要在一个项目里同时出现 getUserInfo、getClientData、getCustomerRecord 三种写法。指代同一个概念时词汇不统一,会让读者误以为这是三种不同的数据来源,搜索和重构时也会漏改。
❌ 坏例子
getUserInfo();
getClientData();
getCustomerRecord();✅ 好例子
getUser();💡 学习提示:在团队词典里为每个核心概念选定一个词(比如统一用 user),所有函数、字段都围绕它命名。
我们读代码的时间远多于写代码的时间,所以写下的每个名字都要能被同事用编辑器搜索到。像 86400000 这种裸数字散落在代码里,下次想查它含义或调整它时根本无从下手;把它提成全大写命名常量,搜索、替换、加注释都变得直接。
❌ 坏例子
// What the heck is 86400000 for?
setTimeout(blastOff, 86400000);✅ 好例子
// Declare them as capitalized named constants.
const MILLISECONDS_PER_DAY = 60 * 60 * 24 * 1000; //86400000;
setTimeout(blastOff, MILLISECONDS_PER_DAY);💡 学习提示:口诀是"魔法数字必须有名"。buddy.js、ESLint 的 no-magic-numbers 规则可以帮你揪出未命名常量。
当一段表达式里嵌套着正则匹配、数组下标取值时,不要让读者现场心算第几个捕获组对应什么。把匹配结果先解构到 city、zipCode 这样的命名变量里,后续调用一眼就能看懂,也避免重复调用 match。
❌ 坏例子
const address = "One Infinite Loop, Cupertino 95014";
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
saveCityZipCode(
address.match(cityZipCodeRegex)[1],
address.match(cityZipCodeRegex)[2]
);✅ 好例子
const address = "One Infinite Loop, Cupertino 95014";
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
const [_, city, zipCode] = address.match(cityZipCodeRegex) || [];
saveCityZipCode(city, zipCode);💡 学习提示:显式永远优于隐式。多起一个局部变量的成本,远低于读者反复回溯表达式的成本。
短到 l、e、i 这种单字母变量,读者必须在脑子里"翻译"它到底代表 location 还是 line。循环次数用 i 是惯例可以接受,但回调参数、业务对象就应该写全称,让代码不用对照上下文也能读懂。
❌ 坏例子
const locations = ["Austin", "New York", "San Francisco"];
locations.forEach(l => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
// Wait, what is `l` for again?
dispatch(l);
});✅ 好例子
const locations = ["Austin", "New York", "San Francisco"];
locations.forEach(location => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
dispatch(location);
});💡 学习提示:单字母只留给短作用域里的循环下标;一旦作用域拉长或出现业务含义,立刻改成全称。
如果对象本身已经叫 Car,它的字段就不必再写成 carMake、carModel——car.make 和 make 语义相同,前缀纯属重复。类名或对象名已经提供了上下文时,字段名要瘦下来,调用处才不会啰嗦。
❌ 坏例子
const Car = {
carMake: "Honda",
carModel: "Accord",
carColor: "Blue"
};
function paintCar(car, color) {
car.carColor = color;
}✅ 好例子
const Car = {
make: "Honda",
model: "Accord",
color: "Blue"
};
function paintCar(car, color) {
car.color = color;
}💡 学习提示:写字段前先问自己——"如果把外层对象名念出来,这个前缀还成立吗?"不成立就是冗余。
name || "默认值" 这种短路写法看似省事,但它会把空字符串、0、false 这些"合法的假值"也一并替换掉,暗藏 bug。ES6 的默认参数只在参数为 undefined 时才生效,语义更精确,也把默认值直接写进了函数签名。
❌ 坏例子
function createMicrobrewery(name) {
const breweryName = name || "Hipster Brew Co.";
// ...
}✅ 好例子
function createMicrobrewery(name = "Hipster Brew Co.") {
// ...
}💡 学习提示:记住默认参数只认
undefined,不认''、0、false、null、NaN——这正是它比||安全的原因。
函数是程序执行的基本单元。本章从参数数量、单一职责、命名、抽象层次、去重、默认配置、标志位到副作用,层层拆解"什么样的函数才值得被测试和被复用"。
参数越多,测试要覆盖的组合就爆炸式增长。一两个参数最理想,三个就该警惕,再多通常说明函数承担了过多职责。JavaScript 可以随手构造对象,参数一多就用一个配置对象收拢;配合 ES6 解构,签名上一眼能看到用到哪些字段,还能模拟命名参数、让 lint 帮你发现未使用项。
❌ 坏例子
function createMenu(title, body, buttonText, cancellable) {
// ...
}
createMenu("Foo", "Bar", "Baz", true);✅ 好例子
function createMenu({ title, body, buttonText, cancellable }) {
// ...
}
createMenu({
title: "Foo",
body: "Bar",
buttonText: "Baz",
cancellable: true
});💡 学习提示:看到 3 个以上参数先别急着写函数签名,问自己一句"这是不是该用对象包起来了?"
这是软件工程里分量最重的一条。当一个函数既筛选活跃用户、又查库、又发邮件时,它就难以组合、难以测试、难以讲清楚。把"判断是否活跃"拆成独立函数后,主函数读起来像一行业务陈述,每个零件也能单独复用。
❌ 坏例子
function emailClients(clients) {
clients.forEach(client => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}✅ 好例子
function emailActiveClients(clients) {
clients.filter(isActiveClient).forEach(email);
}
function isActiveClient(client) {
const clientRecord = database.lookup(client);
return clientRecord.isActive();
}💡 学习提示:自检方法——函数名里如果出现"和""并""然后",往往就已经在做两件事了。
addToDate(date, 1) 这种名字让人猜:到底加的是天、月还是年?函数名必须把动作和对象讲清楚,调用处不用翻函数体就能知道发生了什么。好名字能让调用代码读起来像一句完整的话。
❌ 坏例子
function addToDate(date, month) {
// ...
}
const date = new Date();
// It's hard to tell from the function name what is added
addToDate(date, 1);✅ 好例子
function addMonthToDate(month, date) {
// ...
}
const date = new Date();
addMonthToDate(1, date);💡 学习提示:命名动词要精确(addMonthToDate 而不是 addToDate),参数顺序也顺手调整成"被加的量在前、作用对象在后"。
一个函数里如果同时出现正则拆分、词法分析、语法树遍历这种不同粒度的步骤,读者就要不断在高层语义和底层细节之间来回切换。正确做法是把每个层次拆成独立函数,主函数只做"调度",读起来就是一份清晰的大纲。
❌ 坏例子
function parseBetterJSAlternative(code) {
const REGEXES = [
// ...
];
const statements = code.split(" ");
const tokens = [];
REGEXES.forEach(REGEX => {
statements.forEach(statement => {
// ...
});
});
const ast = [];
tokens.forEach(token => {
// lex...
});
ast.forEach(node => {
// parse...
});
}✅ 好例子
function parseBetterJSAlternative(code) {
const tokens = tokenize(code);
const syntaxTree = parse(tokens);
syntaxTree.forEach(node => {
// parse...
});
}
function tokenize(code) {
const REGEXES = [
// ...
];
const statements = code.split(" ");
const tokens = [];
REGEXES.forEach(REGEX => {
statements.forEach(statement => {
tokens.push(/* ... */);
});
});
return tokens;
}
function parse(tokens) {
const syntaxTree = [];
tokens.forEach(token => {
syntaxTree.push(/* ... */);
});
return syntaxTree;
}💡 学习提示:和上一条"只做一件事"互为表里——拆出来的每个子函数本身也应该只在一个抽象层上活动。
重复代码意味着同一处逻辑散落在多个地方,下次改需求就得满世界找、漏改一处就是 bug。开发者和经理列表看起来差一点点,但大部分逻辑相同——把公共部分抽到一个函数,差异点用分支处理。不过要小心:糟糕的抽象比重复代码更难维护,抽象的分寸要靠 SOLID 原则来把握。
❌ 坏例子
function showDeveloperList(developers) {
developers.forEach(developer => {
const expectedSalary = developer.calculateExpectedSalary();
const experience = developer.getExperience();
const githubLink = developer.getGithubLink();
const data = {
expectedSalary,
experience,
githubLink
};
render(data);
});
}
function showManagerList(managers) {
managers.forEach(manager => {
const expectedSalary = manager.calculateExpectedSalary();
const experience = manager.getExperience();
const portfolio = manager.getMBAProjects();
const data = {
expectedSalary,
experience,
portfolio
};
render(data);
});
}✅ 好例子
function showEmployeeList(employees) {
employees.forEach(employee => {
const expectedSalary = employee.calculateExpectedSalary();
const experience = employee.getExperience();
const data = {
expectedSalary,
experience
};
switch (employee.type) {
case "manager":
data.portfolio = employee.getMBAProjects();
break;
case "developer":
data.githubLink = employee.getGithubLink();
break;
}
render(data);
});
}💡 学习提示:DRY(Don't Repeat Yourself)不是"凡是像就抽",而是先识别变化点,再把不变的部分收拢。
逐个属性写 config.title = config.title || "Foo" 又啰嗦又会污染入参对象。用 Object.assign(默认对象, 用户配置) 把默认值和传入配置合并成一个新对象,默认值集中在一处声明,调用方没传的字段自动补齐,还不会改写原始 config。
❌ 坏例子
const menuConfig = {
title: null,
body: "Bar",
buttonText: null,
cancellable: true
};
function createMenu(config) {
config.title = config.title || "Foo";
config.body = config.body || "Bar";
config.buttonText = config.buttonText || "Baz";
config.cancellable =
config.cancellable !== undefined ? config.cancellable : true;
}
createMenu(menuConfig);✅ 好例子
const menuConfig = {
title: "Order",
// User did not include 'body' key
buttonText: "Send",
cancellable: true
};
function createMenu(config) {
let finalConfig = Object.assign(
{
title: "Foo",
body: "Bar",
buttonText: "Baz",
cancellable: true
},
config
);
return finalConfig
// config now equals: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
// ...
}
createMenu(menuConfig);💡 学习提示:这是"函数参数不超过 2 个"里配置对象模式的标配——默认值放第一个参数,用户配置放第二个。
一个布尔参数往往在告诉调用方:"我里面根据这个值走了两条完全不同的路"——这等于一个函数干了两件事。与其让 createFile(name, true/false) 分叉,不如拆成 createFile 和 createTempFile 两个函数,调用处名字自解释,也免去了阅读 if (temp)。
❌ 坏例子
function createFile(name, temp) {
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}✅ 好例子
function createFile(name) {
fs.create(name);
}
function createTempFile(name) {
createFile(`./temp/${name}`);
}💡 学习提示:和"函数只做一件事"是亲兄弟——看到布尔参数分叉,第一反应就是拆函数。
函数除了"收一个值、吐一个值"之外,只要还改动了外部世界——写文件、改全局变量、改共享状态——就产生了副作用。完全禁止副作用不现实(程序总得和外部打交道),但要把它们集中到一处,别让多个函数偷偷改同一个全局变量。把全局变量改成函数参数和返回值,调用方的状态就不再被偷偷改写。
❌ 坏例子
// Global variable referenced by following function.
// If we had another function that used this name, now it'd be an array and it could break it.
let name = "Ryan McDermott";
function splitIntoFirstAndLastName() {
name = name.split(" ");
}
splitIntoFirstAndLastName();
console.log(name); // ['Ryan', 'McDermott'];✅ 好例子
function splitIntoFirstAndLastName(name) {
return name.split(" ");
}
const name = "Ryan McDermott";
const newName = splitIntoFirstAndLastName(name);
console.log(name); // 'Ryan McDermott';
console.log(newName); // ['Ryan', 'McDermott'];💡 学习提示:本节针对的是"偷偷改全局/共享变量";下一节继续讲"改传入的对象/数组"这个更隐蔽的副作用。
JavaScript 里对象和数组是引用传递,函数一改,所有持有同一引用的地方都会受影响。购物车场景最典型:下单请求还在重试,用户又点了一次加购,cart 被原地 push,请求就把误加的商品也发了出去。解法是函数内克隆一份再修改、返回新数组,老引用纹丝不动。真正必须修改入参的场景很少,而克隆大对象的性能问题已有 Immutable.js 之类的库兜底。
❌ 坏例子
const addItemToCart = (cart, item) => {
cart.push({ item, date: Date.now() });
};✅ 好例子
const addItemToCart = (cart, item) => {
return [...cart, { item, date: Date.now() }];
};💡 学习提示:口诀——"入参只读,返回新值"。展开运算符
[...arr, x]和{...obj, k: v}就是最轻量的克隆手段。
承接「函数(上)」,这一部分继续打磨函数的边界与表达方式:从不污染全局、改用函数式风格,到条件判断的封装与消除,再到类型检查、性能优化与死代码清理,目标是让函数既稳定又易读。
往全局对象(如 Array.prototype)上直接挂自定义方法看似省事,实则极易和第三方库撞名,而这种冲突往往要等到线上抛异常才会暴露。更稳妥的做法是用 ES6 类继承原生类型,在自己的子类里扩展方法,彼此井水不犯河水。
❌ 坏例子
Array.prototype.diff = function diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
};✅ 好例子
class SuperArray extends Array {
diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
}
}💡 学习提示:原型污染是前端历史上反复踩坑的重灾区——扩展原生原型之前,先想想“别人也可能用同一个名字”。
JavaScript 虽不是 Haskell 那样的纯函数式语言,但天生带着函数式味道:map/filter/reduce 这类高阶函数让代码更声明式、更好测试。能用函数式风格表达时,就别再手写带可变计数器的 for 循环。
❌ 坏例子
const programmerOutput = [
{
name: "Uncle Bobby",
linesOfCode: 500
},
{
name: "Suzie Q",
linesOfCode: 1500
},
{
name: "Jimmy Gosling",
linesOfCode: 150
},
{
name: "Gracie Hopper",
linesOfCode: 1000
}
];
let totalOutput = 0;
for (let i = 0; i < programmerOutput.length; i++) {
totalOutput += programmerOutput[i].linesOfCode;
}✅ 好例子
const programmerOutput = [
{
name: "Uncle Bobby",
linesOfCode: 500
},
{
name: "Suzie Q",
linesOfCode: 1500
},
{
name: "Jimmy Gosling",
linesOfCode: 150
},
{
name: "Gracie Hopper",
linesOfCode: 1000
}
];
const totalOutput = programmerOutput.reduce(
(totalLines, output) => totalLines + output.linesOfCode,
0
);💡 学习提示:
reduce的第二个参数(这里的初值0)千万别漏,否则空数组会直接抛错。
把一长串布尔表达式直接塞进 if,读代码的人得当场停下来拆解“这到底在判断什么”。把判断抽成一个见名知意的函数,调用处的语义立刻一目了然。
❌ 坏例子
if (fsm.state === "fetching" && isEmpty(listNode)) {
// ...
}✅ 好例子
function shouldShowSpinner(fsm, listNode) {
return fsm.state === "fetching" && isEmpty(listNode);
}
if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}💡 学习提示:好函数名就是最好的注释——
shouldShowSpinner本身就说清了这段条件的业务含义。
双重否定会额外消耗读者的脑力。把 isNotXxx 这类函数改写成肯定形式,调用处的 if 也能顺势去掉一个感叹号,读起来顺得多。
❌ 坏例子
function isDOMNodeNotPresent(node) {
// ...
}
if (!isDOMNodeNotPresent(node)) {
// ...
}✅ 好例子
function isDOMNodePresent(node) {
// ...
}
if (isDOMNodePresent(node)) {
// ...
}💡 学习提示:写完条件判断后反问自己一句——“这个判断能不能从正面描述?”
“不用 if 还怎么写代码?”答案是多态。当一个函数靠 switch/if 按类型分派做不同的事,往往意味着它承担了多个职责;把每种分支拆成独立子类、各自实现同一个方法,调用方就无需再关心具体类型。
❌ 坏例子
class Airplane {
// ...
getCruisingAltitude() {
switch (this.type) {
case "777":
return this.getMaxAltitude() - this.getPassengerCount();
case "Air Force One":
return this.getMaxAltitude();
case "Cessna":
return this.getMaxAltitude() - this.getFuelExpenditure();
}
}
}✅ 好例子
class Airplane {
// ...
}
class Boeing777 extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude() - this.getPassengerCount();
}
}
class AirForceOne extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude();
}
}
class Cessna extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude() - this.getFuelExpenditure();
}
}💡 学习提示:这条不是禁止所有
if,而是警惕“按类型分派”的大段分支;普通的判空、容错逻辑仍然合理。
JS 是动态类型语言,函数里用 instanceof 逐个分支判断入参类型,往往说明接口设计不统一。更好的做法是约定一个统一方法名(如 move),让不同类型各自实现,调用方一视同仁。
❌ 坏例子
function travelToTexas(vehicle) {
if (vehicle instanceof Bicycle) {
vehicle.pedal(this.currentLocation, new Location("texas"));
} else if (vehicle instanceof Car) {
vehicle.drive(this.currentLocation, new Location("texas"));
}
}✅ 好例子
function travelToTexas(vehicle) {
vehicle.move(this.currentLocation, new Location("texas"));
}💡 学习提示:统一 API 就是“面向接口编程”在鸭子类型世界里的落地方式。
当参数是字符串、数字这类基本类型、无法靠多态解决时,与其在运行时写一堆 typeof 判断和抛错,不如直接上 TypeScript 拿静态类型。手写的“伪类型安全”往往啰嗦得得不偿失,反倒牺牲了可读性。
❌ 坏例子
function combine(val1, val2) {
if (
(typeof val1 === "number" && typeof val2 === "number") ||
(typeof val1 === "string" && typeof val2 === "string")
) {
return val1 + val2;
}
throw new Error("Must be of type String or Number");
}✅ 好例子
function combine(val1, val2) {
return val1 + val2;
}💡 学习提示:如果项目暂时只能用纯 JS,就把精力放在好测试和 Code Review 上,而不是堆运行时类型断言。
现代浏览器在运行时已经自动做了大量优化,很多“经典优化技巧”在今天纯属白费功夫。先把代码写清晰,只有经过性能分析确认是瓶颈的地方,再去针对性优化。
❌ 坏例子
// On old browsers, each iteration with uncached `list.length` would be costly
// because of `list.length` recomputation. In modern browsers, this is optimized.
for (let i = 0, len = list.length; i < len; i++) {
// ...
}✅ 好例子
for (let i = 0; i < list.length; i++) {
// ...
}💡 学习提示:以今天的 JS 引擎,缓存
list.length这类老技巧早已是自动行为,别为二十年前的经验牺牲可读性。
不再被调用的代码和重复代码一样有害,留在仓库里只会增加阅读负担。放心删掉——版本控制历史里永远找得回来。
❌ 坏例子
function oldRequestModule(url) {
// ...
}
function newRequestModule(url) {
// ...
}
const req = newRequestModule;
inventoryTracker("apples", req, "www.inventory-awesome.io");✅ 好例子
function newRequestModule(url) {
// ...
}
const req = newRequestModule;
inventoryTracker("apples", req, "www.inventory-awesome.io");💡 学习提示:靠注释掉的旧代码“存档”是坏习惯,Git 历史才是真正的存档处。
对象不该是裸露的数据袋。这一部分只讲两件事:如何用 getter/setter 给属性访问加一层受控的壳,以及如何用闭包把内部成员真正藏起来,让外部无法随意篡改。
直接读写对象属性看似简单,但一旦将来要加校验、打日志或改成从服务器惰性加载,就得改动代码库里每一处访问点。通过 getter/setter 封装后,这些横切逻辑只需在一处实现,外面的调用代码保持原样。
❌ 坏例子
function makeBankAccount() {
// ...
return {
balance: 0
// ...
};
}
const account = makeBankAccount();
account.balance = 100;✅ 好例子
function makeBankAccount() {
// this one is private
let balance = 0;
// a "getter", made public via the returned object below
function getBalance() {
return balance;
}
// a "setter", made public via the returned object below
function setBalance(amount) {
// ... validate before updating the balance
balance = amount;
}
return {
// ...
getBalance,
setBalance
};
}
const account = makeBankAccount();
account.setBalance(100);💡 学习提示:今天更推荐用 ES6 class 的
get/set语法糖,思路等价、写法更现代。
把数据挂在 this 上其实是公开的,外部一句 delete 就能删掉,直接破坏对象状态。借助闭包——把变量封在工厂函数内部、只暴露访问方法——才能做到外部真正摸不到。
❌ 坏例子
const Employee = function(name) {
this.name = name;
};
Employee.prototype.getName = function getName() {
return this.name;
};
const employee = new Employee("John Doe");
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
delete employee.name;
console.log(`Employee name: ${employee.getName()}`); // Employee name: undefined✅ 好例子
function makeEmployee(name) {
return {
getName() {
return name;
}
};
}
const employee = makeEmployee("John Doe");
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
delete employee.name;
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe💡 学习提示:ES6 class 的
#私有字段是同一思路的语法糖,现在写新项目可以直接用它代替闭包。
类是把数据和行为打包在一起的手段,但"用不用类、怎么继承、怎么组织方法"直接决定代码的可读性与可扩展性。本章给出 3 条关于类的书写约定。
ES5 时代要手写继承:构造函数里判断 this instanceof、用 Animal.call(this, age) 借调父类构造、再用 Object.create 接上原型链、回头修 constructor,一步都不能少,啰嗦又容易写错。ES6 的 class / extends / super 把这些机制收进了语法糖,继承关系一眼可读。不过也要克制:能用小函数解决的就别急着上类,只有当对象确实变大、变复杂时再引入类。
❌ 坏例子
const Animal = function(age) {
if (!(this instanceof Animal)) {
throw new Error("Instantiate Animal with `new`");
}
this.age = age;
};
Animal.prototype.move = function move() {};
const Mammal = function(age, furColor) {
if (!(this instanceof Mammal)) {
throw new Error("Instantiate Mammal with `new`");
}
Animal.call(this, age);
this.furColor = furColor;
};
Mammal.prototype = Object.create(Animal.prototype);
Mammal.prototype.constructor = Mammal;
Mammal.prototype.liveBirth = function liveBirth() {};
const Human = function(age, furColor, languageSpoken) {
if (!(this instanceof Human)) {
throw new Error("Instantiate Human with `new`");
}
Mammal.call(this, age, furColor);
this.languageSpoken = languageSpoken;
};
Human.prototype = Object.create(Mammal.prototype);
Human.prototype.constructor = Human;
Human.prototype.speak = function speak() {};✅ 好例子
class Animal {
constructor(age) {
this.age = age;
}
move() {
/* ... */
}
}
class Mammal extends Animal {
constructor(age, furColor) {
super(age);
this.furColor = furColor;
}
liveBirth() {
/* ... */
}
}
class Human extends Mammal {
constructor(age, furColor, languageSpoken) {
super(age, furColor);
this.languageSpoken = languageSpoken;
}
speak() {
/* ... */
}
}💡 学习提示:看到一堆
prototype = Object.create(...)加constructor = ...的样板代码,就是在提醒你——该换成 ES6 class 了;但反过来,"能写 class"不等于"该写 class",先问自己是不是真需要一个对象。
在类的每个 setter / 操作方法末尾 return this,调用方就能把一连串方法像搭积木一样串在同一行里。jQuery、Lodash 都是靠这个让代码显得短小而达意。落地很简单:除了真正要返回数据的查询方法,其余修改自身状态的方法一律 return this。
❌ 坏例子
class Car {
constructor(make, model, color) {
this.make = make;
this.model = model;
this.color = color;
}
setMake(make) {
this.make = make;
}
setModel(model) {
this.model = model;
}
setColor(color) {
this.color = color;
}
save() {
console.log(this.make, this.model, this.color);
}
}
const car = new Car("Ford", "F-150", "red");
car.setColor("pink");
car.save();✅ 好例子
class Car {
constructor(make, model, color) {
this.make = make;
this.model = model;
this.color = color;
}
setMake(make) {
this.make = make;
// NOTE: Returning this for chaining
return this;
}
setModel(model) {
this.model = model;
// NOTE: Returning this for chaining
return this;
}
setColor(color) {
this.color = color;
// NOTE: Returning this for chaining
return this;
}
save() {
console.log(this.make, this.model, this.color);
// NOTE: Returning this for chaining
return this;
}
}
const car = new Car("Ford", "F-150", "red").setColor("pink").save();💡 学习提示:链式调用是"语法糖"而非"银弹"——它适合配置、构建这类对同一对象连续下指令的场景;如果一个方法需要返回有意义的数据,就别硬凑
return this。
四人帮《设计模式》的经典建议:能组合就别急着继承。继承真正合适的场景有限——两者是真"is-a"(人是动物)、要复用基类实现、想通过改基类统一影响所有子类。而当两个类只是"has-a"(员工"有"税务资料)时,让员工持有一个税务对象的实例,比让税务资料去继承员工要灵活得多,也不会被继承层级绑死。
❌ 坏例子
class Employee {
constructor(name, email) {
this.name = name;
this.email = email;
}
// ...
}
// Bad because Employees "have" tax data. EmployeeTaxData is not a type of Employee
class EmployeeTaxData extends Employee {
constructor(ssn, salary) {
super();
this.ssn = ssn;
this.salary = salary;
}
// ...
}✅ 好例子
class EmployeeTaxData {
constructor(ssn, salary) {
this.ssn = ssn;
this.salary = salary;
}
// ...
}
class Employee {
constructor(name, email) {
this.name = name;
this.email = email;
}
setTaxData(ssn, salary) {
this.taxData = new EmployeeTaxData(ssn, salary);
}
// ...
}💡 学习提示:判断该用继承还是组合,先在心里默念一句"X 是一个 Y"。读得通(正方形是一种形状)再考虑继承;读不通(税务资料是一种员工?)就果断改成组合持有。
SOLID 是面向对象设计的五条核心原则,缩写自 SRP / OCP / LSP / ISP / DIP。它们共同回答一个问题:怎么把类切小、切对,让代码好扩展、好替换、好维护。本章逐条给出可落地的判断标准。
《Clean Code》里那句话:"一个类存在的修改理由不应该超过一个。"给一个类塞太多功能,就像出差只带一个行李箱——全塞进去后概念上不再内聚,改任何一处都可能波及其它依赖模块。落地方法:按"变更原因"拆类,认证归认证、设置归设置,类之间通过组合协作。
❌ 坏例子
class UserSettings {
constructor(user) {
this.user = user;
}
changeSettings(settings) {
if (this.verifyCredentials()) {
// ...
}
}
verifyCredentials() {
// ...
}
}✅ 好例子
class UserAuth {
constructor(user) {
this.user = user;
}
verifyCredentials() {
// ...
}
}
class UserSettings {
constructor(user) {
this.user = user;
this.auth = new UserAuth(user);
}
changeSettings(settings) {
if (this.auth.verifyCredentials()) {
// ...
}
}
}💡 学习提示:问自己"这个类为什么要改?"——如果答案能列出两三个互不相关的理由(改字段校验、改登录逻辑、改存储方式),就说明该拆了。
Bertrand Meyer 提出:"软件实体应对扩展开放,对修改关闭。"意思是加新功能时应该是新增代码(新类、新实现),而不是回头去改老代码里的 if/else 分支。落地手段是面向抽象编程:让依赖方只认抽象约定,具体实现各写各的,新增一种适配器就新增一个类。
❌ 坏例子
class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = "ajaxAdapter";
}
}
class NodeAdapter extends Adapter {
constructor() {
super();
this.name = "nodeAdapter";
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
if (this.adapter.name === "ajaxAdapter") {
return makeAjaxCall(url).then(response => {
// transform response and return
});
} else if (this.adapter.name === "nodeAdapter") {
return makeHttpCall(url).then(response => {
// transform response and return
});
}
}
}
function makeAjaxCall(url) {
// request and return promise
}
function makeHttpCall(url) {
// request and return promise
}✅ 好例子
class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = "ajaxAdapter";
}
request(url) {
// request and return promise
}
}
class NodeAdapter extends Adapter {
constructor() {
super();
this.name = "nodeAdapter";
}
request(url) {
// request and return promise
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
return this.adapter.request(url).then(response => {
// transform response and return
});
}
}💡 学习提示:一旦在业务代码里看到
if (xxx.name === "A") ... else if (xxx.name === "B")这种按类型分支,多半就是 OCP 在报警——把分支收进每个实现类自己的request方法里。
名字吓人,意思很朴素:子类必须能在父类出现的任何地方原样替换,而不破坏程序的正确性。经典反例是"正方形继承长方形"——数学上正方形是长方形,但一旦让 Square extends Rectangle,setWidth / setHeight 就得互相牵连,外部按长方形的用法调用就会算出错误面积。正确做法是让两者共同继承一个更抽象的 Shape。
❌ 坏例子
class Rectangle {
constructor() {
this.width = 0;
this.height = 0;
}
setColor(color) {
// ...
}
render(area) {
// ...
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width) {
this.width = width;
this.height = width;
}
setHeight(height) {
this.width = height;
this.height = height;
}
}
function renderLargeRectangles(rectangles) {
rectangles.forEach(rectangle => {
rectangle.setWidth(4);
rectangle.setHeight(5);
const area = rectangle.getArea(); // BAD: Returns 25 for Square. Should be 20.
rectangle.render(area);
});
}
const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);✅ 好例子
class Shape {
setColor(color) {
// ...
}
render(area) {
// ...
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Shape {
constructor(length) {
super();
this.length = length;
}
getArea() {
return this.length * this.length;
}
}
function renderLargeShapes(shapes) {
shapes.forEach(shape => {
const area = shape.getArea();
shape.render(area);
});
}
const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);💡 学习提示:子类方法的行为契约必须"不弱于"父类——可以强化前置条件,但不能削弱后置条件。否则一旦把子类塞回父类列表里跑,结果就会悄悄出错。
"不应该强迫客户端依赖它用不到的方法。"JS 没有显式接口,但鸭子类型下接口是隐式约定。典型场景是大而全的配置对象:调用方明明只想遍历 DOM,却被迫要传一个动画模块进来。把配置拆细、把可选项做成"需要才配置",就能避免肥大接口。
❌ 坏例子
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.settings.animationModule.setup();
}
traverse() {
// ...
}
}
const $ = new DOMTraverser({
rootNode: document.getElementsByTagName("body"),
animationModule() {} // Most of the time, we won't need to animate when traversing.
// ...
});✅ 好例子
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.options = settings.options;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.setupOptions();
}
setupOptions() {
if (this.options.animationModule) {
// ...
}
}
traverse() {
// ...
}
}
const $ = new DOMTraverser({
rootNode: document.getElementsByTagName("body"),
options: {
animationModule() {}
}
});💡 学习提示:看构造函数要的参数列表——如果客户端经常要传
undefined或空函数占位,说明接口太胖,该按使用场景拆成多个小配置。
两句话:高层模块不该依赖低层模块,二者都该依赖抽象;抽象不该依赖细节,细节该依赖抽象。落到 JS 里就是——别在类内部 new 具体实现,而是把实现作为参数从外面注入进来(依赖注入)。这样换一种请求方式(HTTP 换 WebSocket)只需在外部换一个实例,业务代码一行都不用改。
❌ 坏例子
class InventoryRequester {
constructor() {
this.REQ_METHODS = ["HTTP"];
}
requestItem(item) {
// ...
}
}
class InventoryTracker {
constructor(items) {
this.items = items;
// BAD: We have created a dependency on a specific request implementation.
// We should just have requestItems depend on a request method: `request`
this.requester = new InventoryRequester();
}
requestItems() {
this.items.forEach(item => {
this.requester.requestItem(item);
});
}
}
const inventoryTracker = new InventoryTracker(["apples", "bananas"]);
inventoryTracker.requestItems();✅ 好例子
class InventoryTracker {
constructor(items, requester) {
this.items = items;
this.requester = requester;
}
requestItems() {
this.items.forEach(item => {
this.requester.requestItem(item);
});
}
}
class InventoryRequesterV1 {
constructor() {
this.REQ_METHODS = ["HTTP"];
}
requestItem(item) {
// ...
}
}
class InventoryRequesterV2 {
constructor() {
this.REQ_METHODS = ["WS"];
}
requestItem(item) {
// ...
}
}
// By constructing our dependencies externally and injecting them, we can easily
// substitute our request module for a fancy new one that uses WebSockets.
const inventoryTracker = new InventoryTracker(
["apples", "bananas"],
new InventoryRequesterV2()
);
inventoryTracker.requestItems();💡 学习提示:五原则记忆口诀——"一个类一件事(SRP)、加功能不改老代码(OCP)、子类随时替换父类(LSP)、接口小而专(ISP)、高层依赖抽象不依赖细节(DIP)"。
测试的意义在于让你每次发版都心里有底:它不是可选项,而是交付质量的底线。本章节用一条规范说明,如何让每个测试都只盯准一件事。
把多种场景塞进同一个测试用例,一旦其中一处断言失败,后面的断言根本不会执行,定位问题就像大海捞针。正确的做法是为每一种独立行为单独写一个用例,并用测试名把它在验证什么直接说出来。
❌ 坏例子
import assert from "assert";
describe("MomentJS", () => {
it("handles date boundaries", () => {
let date;
date = new MomentJS("1/1/2015");
date.addDays(30);
assert.equal("1/31/2015", date);
date = new MomentJS("2/1/2016");
date.addDays(28);
assert.equal("02/29/2016", date);
date = new MomentJS("2/1/2015");
date.addDays(28);
assert.equal("03/01/2015", date);
});
});✅ 好例子
import assert from "assert";
describe("MomentJS", () => {
it("handles 30-day months", () => {
const date = new MomentJS("1/1/2015");
date.addDays(30);
assert.equal("1/31/2015", date);
});
it("handles leap year", () => {
const date = new MomentJS("2/1/2016");
date.addDays(28);
assert.equal("02/29/2016", date);
});
it("handles non-leap year", () => {
const date = new MomentJS("2/1/2015");
date.addDays(28);
assert.equal("03/01/2015", date);
});
});💡 学习提示:测试名应当成为可执行的文档——看到哪个用例变红,你就能立刻知道是哪个月份、哪一年出了问题,而不必钻进测试体里逐行排查。
JavaScript 的异步写法从回调地狱一路演进到 Promise,再到 async/await,每一步都是为了让异步代码读起来更像同步代码。本章节两条规范带你走完这条演进路线。
层层嵌套的回调不仅缩进深、可读性差,错误处理还被拆散在每一层里。ES6 把 Promise 做成了内置全局类型,它能把异步流程拉平成一条链式调用,错误只需要在末尾统一捕获一次。
❌ 坏例子
import { get } from "request";
import { writeFile } from "fs";
get(
"https://en.wikipedia.org/wiki/Robert_Cecil_Martin",
(requestErr, response, body) => {
if (requestErr) {
console.error(requestErr);
} else {
writeFile("article.html", body, writeErr => {
if (writeErr) {
console.error(writeErr);
} else {
console.log("File written");
}
});
}
}
);✅ 好例子
import { get } from "request-promise";
import { writeFile } from "fs-extra";
get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin")
.then(body => {
return writeFile("article.html", body);
})
.then(() => {
console.log("File written");
})
.catch(err => {
console.error(err);
});💡 学习提示:如果维护的老代码里还留着回调风格,可以用 util.promisify 之类的工具逐步把它 Promise 化,一次迁移一个函数,风险可控。
Promise 的链式 then 已经比回调清爽不少,但一连串 .then 仍是函数式的串联写法。ES8 带来的 async/await 允许你用近乎同步的顺序语句描述异步流程,再配合 try/catch 集中处理错误,控制流一目了然。
❌ 坏例子
import { get } from "request-promise";
import { writeFile } from "fs-extra";
get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin")
.then(body => {
return writeFile("article.html", body);
})
.then(() => {
console.log("File written");
})
.catch(err => {
console.error(err);
});✅ 好例子
import { get } from "request-promise";
import { writeFile } from "fs-extra";
async function getCleanCodeArticle() {
try {
const body = await get(
"https://en.wikipedia.org/wiki/Robert_Cecil_Martin"
);
await writeFile("article.html", body);
console.log("File written");
} catch (err) {
console.error(err);
}
}
getCleanCodeArticle()💡 学习提示:await 只能写在 async 函数内部;遇到多个互不依赖的异步操作时,记得用 Promise.all 包起来,否则会把并行执行硬生生退化成串行等待。
抛错本身不是坏事——它意味着运行时及时发现了问题并通知了你。真正可怕的是:错误被你接住了,却被你悄悄忽略掉。本章节两条规范告诉你,接住错误之后究竟该做什么。
既然你主动用 try/catch 包住了一段代码,就说明你已经预见到这里可能出错。那就必须为这种情况准备好应对方案:打日志、提醒用户、上报监控系统,至少做一件。仅仅 console.log 一下,错误多半会淹没在控制台的输出海洋里,跟没处理几乎没区别。
❌ 坏例子
try {
functionThatMightThrow();
} catch (error) {
console.log(error);
}✅ 好例子
try {
functionThatMightThrow();
} catch (error) {
// One option (more noisy than console.log):
console.error(error);
// Another option:
notifyUserOfError(error);
// Another option:
reportErrorToService(error);
// OR do all three!
}💡 学习提示:生产环境别只盯着 console,接入 Sentry 一类的错误监控服务,线上偶发错误才不会在你眼皮底下溜走。
理由和上一条完全一致:只要你给 Promise 链加了 .catch,就说明你预期它可能失败,那就必须认真对待这个失败,而不是打印一行日志就翻篇。
❌ 坏例子
getdata()
.then(data => {
functionThatMightThrow(data);
})
.catch(error => {
console.log(error);
});✅ 好例子
getdata()
.then(data => {
functionThatMightThrow(data);
})
.catch(error => {
// One option (more noisy than console.log):
console.error(error);
// Another option:
notifyUserOfError(error);
// Another option:
reportErrorToService(error);
// OR do all three!
});💡 学习提示:漏掉 .catch 的 Promise 会触发 unhandledrejection 警告,现代浏览器和 Node 甚至会把它抛到进程级别——链式调用的每一环都要留好兜底出口。
代码格式本无绝对标准,但团队必须统一并交给工具自动执行。本章两条聚焦于:大小写约定保持一致、调用方与被调用方就近排布,让阅读代码像读报纸一样自上而下顺畅。
JavaScript 是弱类型语言,命名大小写本身就能传递语义:全大写常量、小驼峰函数与变量、大驼峰类名。具体选哪套风格由团队决定,真正的关键在于一旦定下来就全员一致,不混用。
❌ 坏例子
const DAYS_IN_WEEK = 7;
const daysInMonth = 30;
const songs = ["Back In Black", "Stairway to Heaven", "Hey Jude"];
const Artists = ["ACDC", "Led Zeppelin", "The Beatles"];
function eraseDatabase() {}
function restore_database() {}
class animal {}
class Alpaca {}✅ 好例子
const DAYS_IN_WEEK = 7;
const DAYS_IN_MONTH = 30;
const SONGS = ["Back In Black", "Stairway to Heaven", "Hey Jude"];
const ARTISTS = ["ACDC", "Led Zeppelin", "The Beatles"];
function eraseDatabase() {}
function restoreDatabase() {}
class Animal {}
class Alpaca {}💡 学习提示:大小写约定应写进 ESLint 等 lint 规则自动校验,靠人脑记忆必然会逐渐走样。
人读代码习惯自上而下,如同翻阅报纸。如果一个函数调用了另一个函数,就把两者在文件里纵向放得靠近些,理想情况下让调用者紧贴在被调用者上方,读者顺着读下去不必来回跳转。
❌ 坏例子
class PerformanceReview {
constructor(employee) {
this.employee = employee;
}
lookupPeers() {
return db.lookup(this.employee, "peers");
}
lookupManager() {
return db.lookup(this.employee, "manager");
}
getPeerReviews() {
const peers = this.lookupPeers();
// ...
}
perfReview() {
this.getPeerReviews();
this.getManagerReview();
this.getSelfReview();
}
getManagerReview() {
const manager = this.lookupManager();
}
getSelfReview() {
// ...
}
}
const review = new PerformanceReview(employee);
review.perfReview();✅ 好例子
class PerformanceReview {
constructor(employee) {
this.employee = employee;
}
perfReview() {
this.getPeerReviews();
this.getManagerReview();
this.getSelfReview();
}
getPeerReviews() {
const peers = this.lookupPeers();
// ...
}
lookupPeers() {
return db.lookup(this.employee, "peers");
}
getManagerReview() {
const manager = this.lookupManager();
}
lookupManager() {
return db.lookup(this.employee, "manager");
}
getSelfReview() {
// ...
}
}
const review = new PerformanceReview(employee);
review.perfReview();💡 学习提示:可把文件结构想象成一个“调用瀑布”——最顶层入口在前,向下逐层展开实现细节,读者不需要滚动鼠标就能看清整条调用链。
好代码本身就是最好的文档。注释不是用来解释代码在做什么,而是用来交代那些代码无法自我说明的复杂之处。本章告诉你什么时候该写注释、什么时候该把它们删掉。
注释更像是对代码不够清晰的道歉,而不是必需品。真正易读的代码几乎不需要逐行解释;只有当一段逻辑背后隐藏着不易直观看出的业务规则或算法技巧时,才值得落笔注释。
❌ 坏例子
function hashIt(data) {
// The hash
let hash = 0;
// Length of string
const length = data.length;
// Loop through every character in data
for (let i = 0; i < length; i++) {
// Get character code.
const char = data.charCodeAt(i);
// Make the hash
hash = (hash << 5) - hash + char;
// Convert to 32-bit integer
hash &= hash;
}
}✅ 好例子
function hashIt(data) {
let hash = 0;
const length = data.length;
for (let i = 0; i < length; i++) {
const char = data.charCodeAt(i);
hash = (hash << 5) - hash + char;
// Convert to 32-bit integer
hash &= hash;
}
}💡 学习提示:凡是“做什么”都能从代码一眼看出来的注释(如
// 循环每个字符),都是噪音;注释应解释“为什么这么做”。
版本控制系统存在的意义就是替你留存历史。旧代码真正需要时随时可以从 Git 历史里翻回来,而把它注释着留在源码里只会堆积垃圾、干扰阅读。
❌ 坏例子
doStuff();
// doOtherStuff();
// doSomeMoreStuff();
// doSoMuchStuff();✅ 好例子
doStuff();💡 学习提示:想临时停用某段逻辑,直接删掉并提交;需要找回时
git revert即可,比注释块干净得多。
在函数顶上贴一串“某年某月某人改了什么”的流水账是过时做法。版本控制工具(如 git log)已经完整记录了每次改动的作者、时间与原因,再在源码里手抄一份既冗余又会迅速失真。
❌ 坏例子
/**
* 2016-12-20: Removed monads, didn't understand them (RM)
* 2016-10-01: Improved using special monads (JP)
* 2016-02-03: Removed type-checking (LI)
* 2015-03-14: Added combine with type-checking (JR)
*/
function combine(a, b) {
return a + b;
}✅ 好例子
function combine(a, b) {
return a + b;
}💡 学习提示:提交信息(commit message)才是记录“谁、何时、为什么改”的正确场所,配合
git blame即可溯源。
像一长串斜杠划出的“===== 某段开始 =====”分隔线,除了制造视觉噪音并没有实际作用。恰当的命名、缩进与空行,本身就已经为代码提供了清晰的视觉结构。
❌ 坏例子
////////////////////////////////////////////////////////////////////////////////
// Scope Model Instantiation
////////////////////////////////////////////////////////////////////////////////
$scope.model = {
menu: "foo",
nav: "bar"
};
////////////////////////////////////////////////////////////////////////////////
// Action setup
////////////////////////////////////////////////////////////////////////////////
const actions = function() {
// ...
};✅ 好例子
$scope.model = {
menu: "foo",
nav: "bar"
};
const actions = function() {
// ...
};💡 学习提示:如果一段代码真的需要用横幅分隔才能看懂,往往说明它该被拆成命名清晰的函数或模块,而不是加装饰线。
- 为什么代码块里的注释是英文? 代码示例原样保留自源项目(MIT),保持逐字可对照;
- 与 ESLint 的关系? 多数规范可用 eslint 规则(如 max-params、no-else-return)自动兜底,规范是「人读」的标准,ESLint 是「机器」的标准,两者互补;
- 为什么没有翻译每一个单词? 规范名保留英文原名便于对照原文与搜索,说明文字全中文。
- 翻译有误、例子不贴切,欢迎提 Issue;
- 补充规范:请在
chapters/对应分类按同样格式追加(中文名/英文原名/中文说明/坏例子/好例子/学习提示); - 新增条目须与源项目或社区公认实践一致。
- 本仓库自身排版、配图与代码:MIT(见 LICENSE)
- 内容改编自 ryanmcdermott/clean-code-javascript(MIT © Ryan McDermott),署名与许可声明见 THIRD_PARTY_NOTICES.md
made with ❤️ by zieang88888 · 高星仓库中文解读系列第 13 弹
中文开源矩阵,一网打尽开发者的知识库:
