首页 > 代码库 > 代码整洁之道——2、函数
代码整洁之道——2、函数
一、函数参数(两个或者更少)
限制函数的参数至关重要,因为这样测试函数会更简单。有超过三个以上的参数,测试的时候就要使用不同参数测无数的场景。
一个或者两个参数是理想情况。如果可能的话避免三个参数。三个以上的参数应该杜绝。通常,如果有两个以上的参数,说明这个函数做的太多了。大多数情况下,一个高质量的对象就足够充当一个参数了,当你发现你需要多个参数的时候,你可以使用一个对象来代替。
可以用ES6解构赋值,使函数期望的参数清晰化。这样做有几个好处:
1、他人阅读的时候,一下子就看明白了使用了哪些属性
2、解构赋值同时也克隆了对象中的同名值。注:从参数对象解构的对象和数组没有被克隆。(没看明白注释啥意思)
3、检测工具可以提示你哪些属性没有用到,不使用解构赋值就不能了。
Bad: //传入了四个参数 function createMenu(title, body, buttonText, cancellable) { // ... } Good: //将四个参数放入到一个对象中 function createMenu({ title, body, buttonText, cancellable }) { // ... } //通过解构赋值给函数传参 createMenu({ title: ‘Foo‘, body: ‘Bar‘, buttonText: ‘Baz‘, cancellable: true });
二、一个函数只做一件事
这是目前为止软件工程领域最重要的一条法则。当一个函数做了不止一件事情的时候,它们就变得难以创作、测试和解释了。当你可以把一个函数拆解成只做一件事情的时候,你的代码将变得非常清晰并且容易重构。如果这篇文章你只学会了这一条,那你也比很多开发者优秀了。
Bad: //函数共做了两件事情:1、判断是否为活动客户 2、给客户发邮件 function emailClients(clients) { clients.forEach((client) => { const clientRecord = database.lookup(client); if (clientRecord.isActive()) { email(client); } }); } Good: //给客户发邮件 function emailActiveClients(clients) { clients .filter(isActiveClient) .forEach(email); } //判断是否为活动客户 function isActiveClient(client) { const clientRecord = database.lookup(client); return clientRecord.isActive(); }
三、函数名应该说明这个函数做的事情
Bad: function addToDate(date, month) { // ... } const date = new Date(); // 从函数名很难看出把什么添加到date中 addToDate(date, 1); Good: function addMonthToDate(month, date) { // ... } const date = new Date(); addMonthToDate(1, date);
四、函数应该只是一个维度的抽象
当你的函数有超过一个维度的抽象,那么通常是你的函数做了太多的事情。把他们拆分开,可以使复用和测试更加简单。(感觉跟上一条一个意思呢)
Bad: //在一个函数中,先得到token,再得到ast,最后对ast处理 function parseBetterJSAlternative(code) { const REGEXES = [ // ... ]; const statements = code.split(‘ ‘); const tokens = []; REGEXES.forEach((REGEX) => { statements.forEach((statement) => { tokens.push( /* ... */ ); }); }); const ast = []; tokens.forEach((token) => { ast.push( /* ... */ ); }); ast.forEach((node) => { // parse... }); }
Good: //获取token function tokenize(code) { const REGEXES = [ // ... ]; const statements = code.split(‘ ‘); const tokens = []; REGEXES.forEach((REGEX) => { statements.forEach((statement) => { tokens.push( /* ... */ ); }); }); return tokens; } //获取ast function lexer(tokens) { const ast = []; tokens.forEach((token) => { ast.push( /* ... */ ); }); return ast; } //整合,对ast处理 function parseBetterJSAlternative(code) { const tokens = tokenize(code); const ast = lexer(tokens); ast.forEach((node) => { // parse... }); }
五、删除重复的代码
尽最大的努力避免重复代码。重复的代码很糟糕,因为它意味着,如果你需要改变一些逻辑,则要更改多个地方。
设想一下如果你经营了一家餐馆,你需要跟踪库存,包括所有的土豆、洋葱、大蒜、调料等。如果你有多分列表记录这些,那么当你提供了一盘土豆的时候,你就需要更新多份列表。如果你只有一份列表,那么你只需要更新这一份就可以了。
通常有重复的代码是因为,两个或多个事情有些细微的不同但是大部分是相同的,迫使你使用两个或多个函数来做这些相同的事情。删除重复的代码意味着,抽象出一个函数/模块/类来处理这些不同的事情。
正确的抽象是至关重要的,这就是为什么在类模块你需要遵循SOLID原则(follow the SOLID principles laid out in the Classes section)?不好的抽象比重复代码还要糟糕,所以小心点。如果你做了个很好的抽象,一定要抽出来。别重复,否则你会发现,更新一个内容需要更改多个地方。
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(); let data = { expectedSalary, experience }; switch (employee.type) { case ‘manager‘: data.portfolio = employee.getMBAProjects(); break; case ‘developer‘: data.githubLink = employee.getGithubLink(); break; } render(data); }); }
六、用Object.assign设置默认对象值
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 buttonText: ‘Send‘, cancellable: true }; function createMenu(config) { config = Object.assign({ title: ‘Foo‘, body: ‘Bar‘, buttonText: ‘Baz‘, cancellable: true }, config); // config : {title: "Order", body: "Bar", buttonText: "Send", cancellable: true} // ... } createMenu(menuConfig);
七、别用标识(flags)作为函数参数
flags告诉用户,这个函数做了不止一件事。函数应该只做一件事,如果是按照布尔值来区分不同代码,那么拆开函数。
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}`); }
八、避免副作用
(1)
一个函数如果不是只做一个值进返回另一个值,那么这个函数产生了副作用,副作用可能是写入一个文件,修改一些全局变量或者是意外的把你的钱给了陌生人。
有时候代码中不得不存在副作用。像之前的例子,你可能需要写入一个文件。你想做什么控制着你在哪里做这件事。别用多个函数或者类去写一个特定的文件,用且只用一个函数来做这件事。
主要就是要避免常见陷阱如:不结构化就在两个对象之间共享状态,使用可被重写的可变数据类型,并且不关注副作用可能发生的地方。(The main point is to avoid common pitfalls like sharing state between objects without any structure, using mutable data types that can be written to by anything, and not centralizing where your side effects occur)。如果你可以做到这样,你将会比绝大多数的程序员开心。
Bad: //声明了一个全局变量name,在splitIntoFirstAndLastName函数中改变了name值,如果有其他函数调用,name已经变成了一个数组 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‘];
(2)在JS中,数字、字符串等按值传递,数组和对象按照引用传递。对于数组和对象,如果你的函数中改变了一份购物列表,比如添加了一项到购物清单中,那么其他的函数如果用到了这个列表(cart)就会受到影响。这可能会很严重很糟糕,让我们想象下糟糕的场景:
用户点击了购买,按钮出发了购买函数(purchase),发起了网络请求,将购买列表发送到服务器端。由于网络不好,购买函数需要重新发起请求。现在。如果同时用户碰巧点击了一个内容的"添加到购物车"按钮,但这个东西用户在发起网络请求的时候并不是真的想买。如果这种情况发生了,那个购物函数(purchase)将会发送意外添加的那个条目,因为它传递的是引用,这个购物清单在添加到购物车(addItemToCart)函数中被修改了。
一个很好的解决方案就是,永远都克隆一份购物清单(cart)。这样确保了其他的函数不能直接通过引用意外的修改原始清单。
这个方案的两个注意事项:
1、可能有场景是你真的想改变对象的引用,但是当你接受了这个编程习惯,你将会发现这样的场景是非常罕见的。大多数事情可以没有副作用的解决。
2、克隆大的对象在程序中的代价是非常昂贵的。幸运的是,在实际生产中这不是个大问题,因为有大量的库支持这样的操作,快速且不占内存,对你来说就只是克隆对象和数组。(Luckily, this isn‘t a big issue in practice because there are great libraries that allow this kind of programming approach to be fast and not as memory intensive as it would be for you to manually clone objects and arrays.)
Bad: //直接对原数组操作 const addItemToCart = (cart, item) => { cart.push({ item, date: Date.now() }); }; Good: //返回新数组,原数组不变 const addItemToCart = (cart, item) => { return [...cart, { item, date : Date.now() }]; };
九、别写全局函数
全局变量污染在JS中是一个糟糕的实践,因为你可能与其他库冲突,并且使用你API的用户会一无所知,直到他们在生产环境中捕获到异常。让我们想一个例子:如果你想扩展JS原生库,给数组增加一个diff方法来比较两个数组的差异。你可以给Array.prototype添加一个新方法,但这样会与另一个企图做一样事情的库冲突。如果另一个库只是使用diff来找到一个数组首尾元素的差异呢?这就是为什么使用ES6的类去简单的扩展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 INITIAL_VALUE = 0; //函数式 const totalOutput = programmerOutput .map((programmer) => programmer.linesOfCode) .reduce((acc, linesOfCode) => acc + linesOfCode, INITIAL_VALUE);
十一、封装判断条件
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我能做什么呢"。答案是,在大多数场景下,可以使用多态来完成相同的任务。第二个问题是,“好吧,那我为什么要这样做呢?”答案是我们之前学到的:一个函数应该只做一件事。当你的类或者函数中使用if,你在告诉用户,你的函数做了不止一件事。记住:只做一件事。
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(); } }
十四、避免类型检测 (1)
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‘)); }
(2)
如果你用的是字符串、整形、数组等基本数据类型,你不可以使用多态,但仍然需要类型检查,你应该考虑TypeScript。它比起传统的JS是一个很好的选择,与传统JS语法相比,它提供静态类型检查。手动JS类型检查会写大量的代码,而失去了代码的可读性。保持你的代码干净、易于测试、便于code review。否则,用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; }
十五、避免过度优化
现代浏览器在运行时会进行很多代码优化。很多情况下的优化是浪费你的时间。这里有些很好的资源可以看到那些真正需要优化的地方There are good resources 。
Bad: //老的浏览器中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‘);
代码整洁之道——2、函数