警示作用,解释此处不能修改的原因。
// hack: 由于XXX历史原因,只能调度一下。 setTimeout(doSomething, 0)
TODO注释,记录下应该做但还没做的工作。另一个好处,提前写好命名,可以帮助后来者统一命名风格。
class Comment { // todo: 删除功能后期实现 delete() {} }
没用的代码直接删除,不要注释,反正git提交历史记录可以找回。
// bad: 如下,重写了一遍两数之和的实现方式 // const twoSum = function(nums, target) { // for(let i = 0;i<nums.length;i++){ // for(let j = i+1;j<nums.length;j++){ // if (nums[i] + nums[j] === target) { // return [i,j] // } // } // } // }; const twoSum = function(nums, target) { let map = new Map() for (let i = 0; i < nums.length; i++) { const item = nums[i]; const index = map.get(target - item) if (index !== undefined){ return [index, i] } map.set(item, i) } return [] };
避免循规式注释,不要求每个函数都要求jsdoc,jsdoc一般是用在公共代码上。
// bad or good? /** * @param {number[]} nums * @param {number} target * @return {number[]} */ const twoSum = function(nums, target) {}
对象
多使用getter和setter(getXXX和setXXX)。好处:
在set时方便验证。
可以添加埋点,和错误处理。
可以延时加载对象的属性。
// good function makeBankAccount() { let balance = 0; function getBalance() { return balance; } function setBalance(amount) { balance = amount; } return { getBalance, setBalance }; } const account = makeBankAccount(); account.setBalance(100);
使用私有成员。对外隐藏不必要的内容。
// bad const Employee = function(name) { this.name = name; }; Employee.prototype.getName = function getName() { return this.name; }; const employee = new Employee(John Doe); delete employee.name; console.log(employee.getName()); // undefined // good function makeEmployee(name) { return { getName() { return name; } }; }
类
solid
单一职责原则 (SRP) - 保证“每次改动只有一个修改理由”。因为如果一个类中有太多功能并且您修改了其中的一部分,则很难预期改动对其他功能的影响。
// bad:设置操作和验证权限放在一起了 class UserSettings { constructor(user) { this.user = user; } changeSettings(settings) { if (this.verifyCredentials()) { // ... } } verifyCredentials() { // ... } } // good: 拆出验证权限的类 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()) { // ... } } }
开闭原则 (OCP) - 对扩展放开,但是对修改关闭。在不更改现有代码的情况下添加新功能。比如一个方法因为有switch的语句,每次出现新增条件时就要修改原来的方法。这时候不如换成多态的特性。
// bad: 注意到fetch用条件语句了,不利于扩展 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 } // good 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 }); } }
里氏替换原则 (LSP)
如果S是T的子类,则T的对象可以替换为S的对象,而不会破坏程序。
所有引用其父类对象方法的地方,都可以透明的替换为其子类对象。
也就是,保证任何父类对象出现的地方,用其子类的对象来替换,不会出错。下面的例子是经典的正方形、长方形例子。
两个定义
// bad: 用正方形继承了长方形 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: 返回了25,其实应该是20 rectangle.render(area); }); } const rectangles = [new Rectangle(), new Rectangle(), new Square()];// 这里替换了 renderLargeRectangles(rectangles); // good: 取消正方形和长方形继承关系,都继承Shape 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);
接口隔离原则 (ISP) - 定义是客户不应被迫使用对其而言无用的方法或功能。常见的就是让一些参数变成可选的。
// bad class Dog { constructor(options) { this.options = options; } run() { this.options.run(); // 必须传入 run 方法,不然报错 } } const dog = new Dog({}); // Uncaught TypeError: this.options.run is not a function dog.run() // good class Dog { constructor(options) { this.options = options; } run() { if (this.options.run) { this.options.run(); return; } console.log('跑步'); } }
依赖倒置原则(DIP) - 程序要依赖于抽象接口(可以理解为入参),不要依赖于具体实现。这样可以减少耦合度。
// bad class OldReporter { report(info) { // ... } } class Message { constructor(options) { // ... // BAD: 这里依赖了一个实例,那你以后要换一个,就麻烦了 this.reporter = new OldReporter(); } share() { this.reporter.report('start share'); // ... } } // good class Message { constructor(options) { // reporter 作为选项,可以随意换了 this.reporter = this.options.reporter; } share() { this.reporter.report('start share'); // ... } } class NewReporter { report(info) { // ... } } new Message({ reporter: new NewReporter });
其他
优先使用 ES2015/ES6 类而不是 ES5 普通函数。
多使用方法链。
多使用组合而不是继承。
错误处理
不要忽略捕获的错误。而要充分对错误做出反应,比如console.error()到控制台,提交错误日志,提醒用户等操作。
不要漏了catch promise中的reject。
格式
可以使用eslint工具,这里就不展开说了。
最后
接受第一次愚弄
让程序一开始就做到整洁,并不是一件很容易的事情。不要强迫症一样地反复更改代码,因为工期有限,没那么多时间。等到下次需求更迭,你发现到代码存在的问题时,再改也不迟。
入乡随俗
每个公司、项目的代码风格是不一样的,会有与本文建议不同的地方。如果你接手了一个成熟的项目,建议按照此项目的风格继续写代码(不重构的话)。因为形成统一的代码风格也是一种代码整洁。