本文并不是代码风格指南,而是关于代码的 ==可读性==、==复用性==、==扩展性== 探讨
Bad:
const yyyymmdstr=moment.format('YYYY/MM/DD');
Good:
const currentDate = moment().format('YYYY/MM/DD');
当一个功能在项目中不同文件夹中出现时,此时我们应该保证该命名的统一。
Bad:
getUserInfo();getClientData();getCustomerRecord();
Good:
getUser();
可以使用buddy.js或者 ESLint检测代码中未命名的常量
Bad:
setTimeout(blastOff, 864000000);
Good:
const MILLISECONDS_IN_A_DAY = 86400000;setTimeout(blastOff, MILLISECONDS_IN_A_DAY);
Bad:
const address = 'One Infinite Loop, Cupertino 95014';const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;saveCityZipCode(address.match(cityZipCodeRegex)[1],address.match(cityZipCodeRegex)[2],);
Good:
const address = 'One Infinite Loop, Cupertino 95014';const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;const [, city, zipCode] = address.match(cityZipCodeRegex) || [];saveCity;
避免需要通过上下文来推断的变量,显式优于隐式 Bad:
const locations = ['Austin', 'New York', 'San Francisco'];locations.forEach((l) => {doStuff();doSomeOtherStuff();// ...// ...// ...// 1 是什么?dispatch(l);});
Good:
const locations = ['Austin', 'New York', 'San Francisco'];locations.forEach((location) => {doStuff();doSomeOtherStuff();// ...// ...// ...dispatch(location);});
当类/对象名已经有意义时,对其变量进行命名不需要再次重复。
Bad:
var Car = {carMake: 'Honda',carModel: 'Accord',carColor: 'Blue',};function painCar(car) {car.carColor = 'red';}
Good:
var Car = {make: 'Honda',model: 'Accord',color: 'Blue',};function paintCar(car) {car.color = 'Red';}
Bad:
function createMicrobrewery(name) {const breweryName = name || 'Hipster Brew Co.';// ...}
Good:
function createMicrobrewery(name = 'Hipster Brew Co.') {// ...}
理想情况下函数参数不应该超过 3 个,如果超过 3 个以上,此时我们应该通过es6 解构或者将这些参数封装成一个对象 Bad:
function createMenu(title, body, buttonText, cancellable) {//....}
Good:
function createMenu({ title, body, buttonText, cancellable }) {//....}createMenu({title: 'Foo',body: 'Bar',buttonText: 'Baz',cancelable: true,});//参数封装对象形式var menuConfig = {title: 'Foo',body: 'Bar',buttonText: 'Baz',cancelable: true,};function createMenu(menuConfig) {//...}
这是软件功能中最重要的原则之一。
功能不单一的函数将导致难以重构、测试和理解。功能单一的函数易于重构,并使代码更加干净。 Bad:
function emailClients(clients) {clients.forEach((client) => {let clientRecord = database.lookup(client);if (clientRecord.isActive()) {email(client);}});}
Good:
function emailClients(clients) {clients.forEach((client) => {emailClientIfNeeded(client);});}function emailClientIfNeeded(client) {if (isClientActive(client)) {email(client);}}function isClientActive(client) {let clientRecord = database.lookup(client);return clientRecord.isActive();}
Bad:
function addToDate(date, month) {// ...}const date = new Date();// It's hard to tell from the function name what is addedaddToDate(date, 1);
Good:
function addMonthToDate(month, date) {// ...}const date = new Date();addMonthToDate(1, date);
当函数需要的抽象多于一层时通常意味着函数功能过于复杂,需要将其进行分解以提高其可重用性和可测试性。
Bad:
function parseBatterJsAlternative(code) {const REGEXES = [//...];const statements = code.split(' ');const tokens = [];REGEXES.forEach((REGEX) => {statements.forEach((statements) => {//...});});const ast = [];tokens.forEach((token) => {//let...});ast.forEach((node) => {//...});}
Good:
function parseBetterJSAlternative(code) {const tokens = tokenize(code);const ast = lexer(tokens);ast.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 lexer(tokens) {const ast = [];tokens.forEach((token) => {ast.push(/* ... */);});return ast;}
很多时候在一个函数中由于一两点的不同,让我们去重新写一个函数。要想优化重复代码需要有较强的抽象能力,错误的抽象还不如重复代码。所以在抽象过程中必须要遵循 SOLID 原则 Bad:
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);});}
Good:
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);});}
Bad:
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);
Good:
const menuConfig={title:'Order',//body key 缺失buttonText:'Send',cancellable:true};unction createMenu(config){config=Object.assign({title: 'Foo',body: 'Bar',buttonText: 'Baz',cancellable: true},config);//config 变成:{title:'Order',body:'Bar',buttonText:'Send',cancelable:true}};createMenu(menuConfig);
通过 flag 的 true 或 false,来判读执行逻辑,违反了一个函数干一件事的原则,因此要拆分该函数。
Bad:
function createFile(name, temp) {if (temp) {fs.create(`./temp/${name}`);} else {fs.create(name);}}
Good:
function createFile(name) {fs.create(name);}function createTempFile(name) {createFile(`./temp/${name}`);}
函数接受一个值返回一个新值,除此之外的行为我们都称为副作用,比如修改全局变量、对文件进行 IO 操作等。
当函数确定需要副作用时,请不要用多个函数/类进行文件操作,有且仅用一个函数/类来处理。
副作用的三大天坑:随意修改可变数据类型、随意分享没有数据结构的状态、没有在统一地方处理副作用。
Bad:
// 全局变量被一个函数引用// 现在这个变量从字符串变成了数组,如果有其他的函数引用,会发生无法预见的错误。let name = 'Ryan McDermott';function splitIntoFirstAndLastName() {//变成了数组,此时会有不可预测的错误name = name.split(' ');}splitIntoFirstAndLastName();console.log(name); // ['Ryan', 'McDermott'];
Good:
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 中,基本类型通过赋值传递,对象和数组通过引用传递.以引用传递为例:假如我们写一个购物车,通过 addItemToCart()
方法添加商品到购物车,修改cart
数组。此时调用purchase()
方法购买,由于引用传递,获取的购物车数组 正好是最新的数据。
那么我们来看一种坏情况: 用户点击购买时,该按钮调用purchase()
功能产生网络请求并将cart
数组数据发送到服务器。因为糟糕的网络连接,purchase()
功能必须继续重试请求。现在用户又继续添加新的商品,这时网络恢复,那么purchase()
方法获取到的cart
数组就是错误的。
为了避免这种问题,我们需要每次新增商品时,克隆cart
数组并返回新的数组。 Bad:
const addItemToCart = (cart, item) => {cart.push({ item, date: Date.now() });};
Good:
const addItemToCart = (cart, item) => {return [...cart, { item, date: Date.now() }];};
在 JS 中污染全局是一个非常不好的实践,这么做可能和其他库起冲突。
想象以下例子:如果你想扩展 JS 中的Array
,为其添加一个 diff
函数显示两个数组间的差异,此时应如何去做?你可以将diff
写入 Array.prototype
,但这么做会和其他有类似需求的库造成冲突。如果另一个库对 diff
的需求为比较一个数组中首尾元素间的差异呢?
使用 ES6 中的 class 对全局的 Array 做简单的扩展显然是一个更棒的选择。
Bad:
Array.prototype.diff = function diff(comparisonArray) {const hash = new Set(comparisonArray);return this.filter((elem) => !hash.has(elem));};
Good:
class SuperArray extends Array {diff(comparisonArray) {const hash = new Set(comparisonArray);return this.filter((elem) => !hash.has(elem));}}
函数式的编程具有更干净且便于测试的特点,尽可能使用这种风格。
Bad:
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;}
Good:
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.map((output) => output.linesOfCode).reduce((totalLines, lines) => totalLines + lines, 0);
Bad:
if (fsm.state === 'fetching' && isEmpty(listNode)) {// ...}
Good:
function shouldShowSpinner(fsm, listNode) {return fsm.state === 'fetching' && isEmpty(listNode);}if (shouldShowSpinner(fsmInstance, listNodeInstance)) {// ...}
Bad:
function isDOMNodeNotPresent(node) {// ...}if (!isDOMNodeNotPresent(node)) {// ...}
Good:
function isDOMNodePresent(node) {// ...}if (isDOMNodePresent(node)) {// ...}
这看起来似乎不太可能。
大多人听到这的第一反应是:“怎么可能不用 if 完成其他功能呢?”许多情况下通过使用多态(polymorphism)可以达到同样的目的。
第二个问题在于采用这种方式的原因是什么。答案是我们之前提到过的:保持函数功能的单一性。 Bad:
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();}}}
Good:
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();}}
JS 是弱类型语言,这意味着函数可接受任意类型的参数。有时候会给我们带来麻烦,我们就会用类型判读类避免这些情况发生。仔细想想是你真的需要检查类型还是你的 API 设计有问题?
Bad:
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'));}}
Good:
function travelToTexas(vehicle) {vehicle.move(this.currentLocation, new Location('texas'));}
如果你需要做静态类型检查,比如字符串、整数等,推荐使用 TypeScript,不然你的代码会变得又臭又长。 Bad:
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');}
Good:
function combine(val1, val2) {return val1 + val2;}
现代的浏览器在运行时会对代码自动进行优化。有时人为对代码进行优化可能是在浪费时间。这里可以找到许多真正需要优化的地方点击这里 Bad:
// 这里使用变量len是因为在老式浏览器中,// 直接使用正例中的方式会导致每次循环均重复计算list.length的值,// 现代浏览器已对此做了优化。for (let i = 0, len = list.length; i < len; i++) {// ...}
Good:
for (let i = 0; i < list.length; i++) {// ...}
不再被调用的代码应及时删除。
Bad:
function oldRequestModule(url) {// ...}function newRequestModule(url) {// ...}const req = newRequestModule;inventoryTracker('apples', req, 'www.inventory-awesome.io');
Good:
function newRequestModule(url) {// ...}const req = newRequestModule;inventoryTracker('apples', req, 'www.inventory-awesome.io');
完成进度