JS代码脏乱差?你须要知道这些优化技巧

JS代码脏乱差?你须要知道这些优化技巧


image.png




做者 | Ilya Suzdalnitski译者 | 王强编辑 | Yoniehtml

JavaScript 是万众瞩目的力量。它是世界上最流行的编程语言。它容易理解,有丰富的学习资源,对初学者很是友好。JavaScript 有着庞大的资源库,对小公司和大企业都颇具吸引力。庞大的 JS 工具和库生态系统为开发者的生产力带来了福音。只用 JS 一种语言就能统一前端和后端,因而就能够在整个技术栈中使用同一套技能组合。前端

JavaScript 的力量就像核能

JavaScript 提供了许多工具和选项,但它对开发者几乎没有任何限制。让没有经验的人使用 JavaScript,就像是给一个两岁的孩子一盒火柴和一罐汽油同样......java

JavaScript 的力量就像核能——既能够用来为城市供电,也能够用来摧毁一切。用 JavaScript 构建东西很容易。但构建既可靠又易维护的软件就不是什么轻松的事情了。ios

代码可靠性

在建造大坝时,工程师首先关注的是可靠性。在没有任何规划或安全措施的前提下修建大坝是很危险的。建造桥梁、生产飞机、汽车...... 都是一回事。若是汽车不安全不可靠,那么像马力、引擎响声,仍是内饰中使用的皮革类型这些都可有可无了。git

一样,每位软件开发者的目标都是编写可靠的软件。若是代码有缺陷且不可靠,那么其余问题都是小巫见大巫了。编写可靠代码的最佳方法是什么?那就是写出简洁的代码。简单的反面是复杂。所以软件开发者首要的责任应该是下降代码的复杂度程序员

开发者经验丰富的标志就是能编写可靠的软件。可靠性还包括可维护性——只有可维护的代码库才是可靠的。es6

虽然我是函数式编程的坚决信徒,但我不会安利什么内容。我只是会从函数式编程中引用一些概念,并演示如何在 JavaScript 中应用它们。github

咱们真的须要软件可靠性吗?这取决于你本身。有些人认为客户能凑合用软件就好了,我不敢苟同。事实上,若是软件不可靠且难以维护,那么其余问题根本就不重要了。谁会购买一辆随机刹车和加速的汽车呢?谁会但愿本身的手机天天断线几回,随机重启呢?面试

内存不足

咱们怎样开发可靠的软件?数据库

首先考虑可用内存的大小。咱们的程序应该尽可能节约内存,永远不会耗尽全部可用内存,以免性能降低。

这和编写可靠的软件有什么关系?人类的大脑也有本身的内存,叫作工做记忆。咱们的大脑是宇宙中已知最强大的机器,但它有本身的一套限制——咱们只能在工做记忆中保存大约五条信息。

对于编程工做来讲,这意味着简单的代码消耗的脑力资源更少,进而提高咱们的效率,并产出更可靠的软件。本文和一些 JavaScript 工具将帮助你实现这一目标!

初学者的注意事项

本文中我将大量使用 ES6 函数。简单回顾一下:

// ---------------------------------------------
// lambda (fat arrow) anonymous functions
// ---------------------------------------------

const doStuff = (a, b, c) => {...}

// same as:
function doStuff(a, b, c) {
 ...
}


// ---------------------------------------------
// object destructuring
// ---------------------------------------------

const doStuff = ({a, b, c}) => {
 console.log(a);
}

// same as:
const doStuff = (params) => {
 const {a, b, c} = params;

 console.log(a);
}

// same as:
const doStuff = (params) => {
 console.log(params.a);
}


// ---------------------------------------------
// array destructuring
// ---------------------------------------------

const [a, b] = [1, 2];

// same as:
const array = [1, 2];
const a = array[0];
const b = array[1];
    工具   

JavaScript 的最大优点之一是丰富的可用工具。没有其余哪一种编程语言有如此庞大的工具和库生态系统。

咱们应该充分利用这些工具,尤为是 ESLint(https://eslint.org/)。ESLint 是静态代码分析工具,能够找到代码库中潜在的问题,维持代码库的高质量。并且 linting 是一个彻底自动化的过程,能够防止低质量代码进入代码库。

不少人没能充分利用 ESLint——他们只用了预建配置,如 eslint-config-airbnb 而已。很惋惜这只是 ESlint 的皮毛。JavaScript 是一种没有限制的语言。而 linting 设置不当会带来深远的影响。

熟练的开发者不只知道该用哪些函数,还会知道不该该使用哪些 JS 函数。JavaScript 是一种古老的语言,有不少包袱,因此区分好坏是很重要的。

ESLint 配置 你能够按以下方式设置 ESLint。 我建议逐一熟悉这些建议,并将 ESLint 规则逐一归入你的项目中。 先将它们配置为 warn,习惯了能够将一些规则转为 error。

在项目的根目录中运行:

npm i -D eslint
npm i -D eslint-plugin-fp

而后在项目的根目录中建立一个.eslintrc.yml 文件:

env:
 es6: true

plugins:
 fp

rules:
 # rules will go in here

若是你使用的是像 VSCode 这样的 IDE,请安装 ESLint 插件。

你还能够从命令行手动运行 ESLint:

npx eslint .
重构的重要性

重构是下降现有代码复杂度的过程。若是使用得当,它将成为咱们对付可怕的技术债务怪物的最佳武器。若是没有持续的重构,技术债务将不断积累,反过来又会拖累开发者。

重构就是清理现有代码,同时确保代码仍能正常运行的过程。重构是软件开发中的良好实践,是健康组织中开发流程的一部分。

须要注意的是,在重构以前最好将代码归入自动化测试。重构时很容易在无心中破坏现有功能,全面的测试套件是预防潜在风险的好办法。

复杂度的最大源头

这可能听起来很奇怪,但代码自己就是复杂度的最大源头。实际上NoCode就是编写安全可靠软件的最佳途径。但不少时候咱们作不到NoCode,因此备选答案就是减小代码量。更少的代码意味着更少的复杂度,也意味着产生错误的潜在区域更少。有人说初级开发者编写代码,而高级开发者删除代码——不能赞成更多。

长文件

人类是懒惰的。懒惰是一种短时间生存策略,舍弃对生存不重要的事物来节省能量。

有些人很懒,不守规矩。人们将愈来愈多的代码放入同一个文件中...... 若是文件的长度没有限制,那么这些文件每每会无限增加下去。根据个人经验,超过 200 行代码的文件就太难理解、太难维护了。长文件还意味着程序可能处理的工做太多了,违反了单一责任原则。

怎么解决这个问题?只需将大文件分解为更细粒度的模块便可。

建议的 ESLint 配置:

rules:
 max-lines:
 - warn
 - 200
长函数

复杂度的另外一大来源是漫长而复杂的函数,很难推理;并且函数的职责太多,很难测试。

例以下面的 express.js 代码片断是用来更新博客条目的:

router.put('/api/blog/posts/:id', (req, res) => {
 if (!req.body.title) {
   return res.status(400).json({
     error: 'title is required',
   });
 }

 if (!req.body.text) {
   return res.status(400).json({
     error: 'text is required',
   });
 }

 const postId = parseInt(req.params.id);

 let blogPost;
 let postIndex;
 blogPosts.forEach((post, i) => {
   if (post.id === postId) {
     blogPost = post;
     postIndex = i;
   }
 });

 if (!blogPost) {
   return res.status(404).json({
     error: 'post not found',
   });
 }

 const updatedBlogPost = {
   id: postId,
   title: req.body.title,
   text: req.body.text
 };

 blogPosts.splice(postIndex, 1, updatedBlogPost);

 return res.json({
   updatedBlogPost,
 });
});

函数体长度为 38 行,执行如下操做:分析 post id、查找现有博客帖子、验证用户输入、在输入无效的状况下返回验证错误、更新帖子集合,并返回更新的博客帖子。

显然它能够重构为一些较小的函数。路由处理程序可能看起来像这样:

router.put("/api/blog/posts/:id", (req, res) => {
 const { error: validationError } = validateInput(req.body);
 if (validationError) return errorResponse(res, validationError, 400);

 const { blogPost } = findBlogPost(blogPosts, req.params.id);

 const { error: postError } = validateBlogPost(blogPost);
 if (postError) return errorResponse(res, postError, 404);

 const updatedBlogPost = buildUpdatedBlogPost(req.body);

 updateBlogPosts(blogPosts, updatedBlogPost);

 return res.json({updatedBlogPost});
});

推荐的 ESLint 配置:

rules:
 max-lines-per-function:
 - warn
 - 20
复杂函数

复杂函数每每就是长函数,反之亦然。函数之因此变复杂可能有不少因素,但其中嵌套回调和圈复杂度较高都是比较容易解决的。

嵌套回调每每致使回调地狱。能够用 promise 处理回调,而后使用 async-await 就能削弱其影响。

来看一个带有深度嵌套回调的函数:

fs.readdir(source, function (err, files) {
 if (err) {
   console.error('Error finding files: ' + err)
 } else {
   files.forEach(function (filename, fileIndex) {
     gm(source + filename).size(function (err, values) {
       if (err) {
         console.error('Error identifying file size: ' + err)
       } else {
         aspect = (values.width / values.height)
         widths.forEach(function (width, widthIndex) {
           height = Math.round(width / aspect)
           this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
             if (err) console.error('Error writing file: ' + err)
           })
         }.bind(this))
       }
     })
   })
 }
})
 圈复杂度

函数复杂度的另外一大来源是圈复杂度。它指的是给定函数中的语句(逻辑)数,诸如 if 语句、循环和 switch 语句。这些函数很难推理,要尽可能避免使用。这是一个例子:

if (conditionA) {
 if (conditionB) {
   while (conditionC) {
     if (conditionD && conditionE || conditionF) {
       ...
     }
   }
 }
}

推荐的 ESLint 配置:

rules:
 complexity:
 - warn
 - 5

 max-nested-callbacks:
 - warn
 - 2
 max-depth:
 - warn
 - 3

另外一个下降代码复杂度的方法是声明式代码,稍后会具体展开。

可变状态

状态是存储在内存中的临时数据,例如对象中的变量或字面量。状态自己是无害的,但可变状态是软件复杂度的最大源头之一,与面向对象结合时尤为如此(稍后将详细介绍)。

 人脑的局限

如前所述,人类大脑是宇宙中已知最强大的机器。然而咱们的大脑很难应付状态,由于咱们在工做记忆中一次只能容纳五件事情。咱们很容易推理一段代码自己的做用,但涉及到它对代码库中变量的影响时就会糊涂了。

使用可变状态编程容易让人精神错乱。只要放弃可变状态,咱们的代码就能变得更加可靠。

 可变状态的问题

举个例子:

const increasePrice = (item, increaseBy) => {
 // never ever do this
 item.price += increaseBy;

 return item;
};

const oldItem = { price: 10 };

const newItem = increasePrice(oldItem, 3);

// prints newItem.price 13
console.log('newItem.price', newItem.price);

// prints oldItem.price 13
// unexpected?
console.log('oldItem.price', oldItem.price);

错误很难看出来:咱们改变函数参数时不当心修改了原始项目的价格。原本应该是 10,实际上改为了 13。

咱们构造和返回一个不可变的新对象来解决这个问题:

const increasePrice = (item, increaseBy) => ({
 ...item,
 price: item.price + increaseBy
});

const oldItem = { price: 10 };

const newItem = increasePrice(oldItem, 3);

// prints newItem.price 13
console.log('newItem.price', newItem.price);

// prints oldItem.price 10
// as expected!
console.log('oldItem.price', oldItem.price);

请记住,使用 ES6 spread 等运算符复制时会生成浅拷贝,而不是深拷贝——它不会复制任何嵌套属性。若是上面的 item 具备 item.seller.id 这样的内容,则新 item 的 seller 仍将引用旧 item,这是不行的。在 JavaScript 中使用不可变状态时,一些较为稳健的方法包括 immutable.js 和 Ramda lense 等。我将在另外一篇文章中介绍这些选项。

建议的 ESLint 配置:

rules:
 fp/no-mutation: warn
 no-param-reassign: warn
不要 push 数组

在数组突变中使用像 push 这样的方法也存在一样的问题:

const a = ['apple', 'orange'];
const b = a;

a.push('microsoft')

// ['apple', 'orange', 'microsoft']
console.log(a);

// ['apple', 'orange', 'microsoft']
// unexpected?
console.log(b);

数组 b 本应保持不变的。咱们建立一个新数组而不是调用 push 就能够了。

构造新数组来避免问题:

const newArray = [...a, 'microsoft'];
 不肯定性

不肯定性是说程序在输入不变的状况下输出却没法肯定。明明 2 + 2 == 4,但不肯定性程序不必定得出这个结果。

虽然可变状态自己并非不肯定性的,但它会使代码更容易出现不肯定性(如上所示)。讽刺的是最流行的编程范式(OOP 和命令式编程)特别容易产生不肯定性。

不变性

想要避免可变性的缺陷,最好的方法就是改用不变性。不变性一个很大的话题,我可能会专门撰文讨论它。

建议的 ESLint 配置:

rules:
 fp/no-mutating-assign: warn
 fp/no-mutating-methods: warn
 fp/no-mutation: warn
 避免使用 Let 关键字

咱们不该该用 var 在 JavaScript 中声明变量,一样咱们也应该避免使用 let 关键字。用 let 声明的变量能够被从新分配,让代码更难推理。本质上这也是人脑工做记忆的一种限制。使用 let 关键字编程时,咱们必须记住全部反作用和潜在的极端状况。咱们可能不当心为变量分配一个不正确的值,结果就得浪费时间来调试了。

单元测试受其影响最严重。多数测试都是并行开展的,因此在多个测试之间共享可变状态是一种灾难。

let 关键字的替代方案固然是 const 关键字。虽然它不能保证不变性,但它会禁止从新分配,使代码更易推理。大多数状况下,从新给变量赋值的代码能够被提取到一个单独的函数中。来看一个例子:

let discount;

if (isLoggedIn) {
 if (cartTotal > 100  && !isFriday) {
   discount = 30;
 } else if (!isValuedCustomer) {
   discount = 20;
 } else {
   discount = 10;
 }
} else {
 discount = 0;
}

将同一个示例提取到一个函数中:

const getDiscount = ({isLoggedIn, cartTotal, isValuedCustomer}) => {
 if (!isLoggedIn) {
   return 0;
 }

 if (cartTotal > 100  && !isFriday()) {
   return 30;
 }

 if (!isValuedCustomer) {
   return 20;
 }

 return 10;
}

一开始不用 let 可能会不习惯,但这样代码会更简洁易懂。我好久没用 let 了,一点都不想它。

养成不使用 let 关键字编程的习惯可让你更有条理。你必须将代码分解为更小,更易于管理的函数组合,进而让函数的职责更清晰,更好地分离关注点,并使代码库更具可读性和可维护性。

建议的 ESLint 配置:

rules:
 fp/no-let: warn
面向对象编程

Java 是自 MS-DOS 以来计算行业最使人痛苦的事情。”

—— 名 Alan Kay,面向对象编程的发明者

面向对象编程是一种用来组织代码的流行编程范例。本节会讨论 Java、C#、JavaScript、TypeScript 等语言中使用的主流 OOP 的局限。我不会批判正确的 OOP(例如 SmallTalk)。

若是你认为在开发软件时必须使用 OOP,则能够跳过本节。

 优秀程序员和普通程序员

优秀的程序员会编写好的代码,普通的程序员编写错误的代码,不管什么编程范式都是如此。编程范式要作的是防止普通的程序员搞出太多破坏。无论你愿不肯意,你都会和普通的程序员共事。惋惜 OOP 没有足够的约束力来防止他们形成巨大的伤害。

OOP 的初衷是帮助程序员打理代码库。讽刺的是人们认为 OOP 能够下降复杂度,但它提供的工具彷佛只是在增长复杂度而已。

 OOP 不肯定性

OOP 代码容易出现不肯定性——它严重依赖可变状态,不像函数式编程那样能够保证输出不变,让代码更难推理。涉及并发时这种问题更为严重。

 共享可变状态

“我以为用可变对象构建大型对象图会让面向对象的大型程序愈来愈复杂。你得试着理解并记住你在调用一种方法时会发生什么,反作用会是什么。“——Rich Hickey,Clojure 的创造者

可变状态很棘手,而 OOP 共享可变状态的引用(而非值)的作法让这个问题更严重了。这意味着几乎任何东西均可以改变给定对象的状态。开发者必须牢记与当前对象交互的每一个对象的状态,很快就会超过人脑工做记忆的上限。人脑要推理这种复杂的可变对象是极为困难的。它消耗了宝贵且有限的认知资源,而且不可避免地会致使大量缺陷。

共享可变对象的引用是为了提升效率而作出的权衡,过去这可能还很合理。但现在硬件性能飞速提高,咱们应该更加关注开发者的效率而不是代码的执行效率。并且有了现代工具的支持,不变性几乎不会影响性能。

OOP 说全局状态是万恶之源。但讽刺的是 OOP 程序基本上就是一个大型全局状态(由于一切都是可变的而且经过引用共享)。

最小知识原则没什么用途,只是鸵鸟政策而已——无论你怎样访问或改变一个状态,共享的可变状态仍然是共享的可变状态。领域驱动设计是一种有用的设计方法,能解决一些复杂度问题。但它仍然没有解决不肯定性这个根本问题。

 信噪比

不少人都在关注 OOP 程序的不肯定性引入的复杂度。他们提出了许多设计模式试图解决这些问题。但这只是自欺欺人,并引入了更加没必要要的复杂度。

正如我以前所说,代码自己是复杂度的最大来源,代码老是越少越好。OOP 程序一般带有大量的样板代码,以及设计模式提供的“创可贴”,这些都会下降信噪比。这意味着代码变得更加冗长,人们更难看到程序的原始意图,使代码库变得很是复杂,不太可靠。

我坚信现代 OOP 是软件复杂度的最大来源之一。的确有使用 OOP 构建的成功项目,但这并不意味着此类项目不会受无谓的复杂度影响。

JavaScript 中的 OOP 尤为糟糕,由于这种语言缺乏静态类型检查、泛型和接口等。JavaScript 中的 this 关键字至关不可靠。

若是咱们的目标是编写可靠的软件,那么咱们应该努力下降复杂度,理想状况下应该避免使用 OOP。若是你有兴趣了解更多信息,请务必阅读个人另外一篇文 “OOP,万亿美圆的灾难”: https://medium.com/@ilyasz/object-oriented-programming-the-trillion-dollar-disaster-%EF%B8%8F-92a4b666c7c7

This 关键字

this 关键字的行为老是飘忽不定。它很挑剔,在不一样的环境中可能搞出来彻底不一样的东西。它的行为甚至取决于谁调用了一个给定的函数。使用 this 关键字常常会致使细小而奇怪的错误,很难调试。

拿它作面试问题可能颇有意思,但关于 this 关键字的知识其实也没什么意义,只能说明应聘者花了几个小时研究过最多见的 JavaScript 面试问题。

真实世界的代码不该该那么容易出错,应该是可读的,不让人感到莫名其妙。This 是一个明显的语言设计缺陷,别再用它了。

建议的 ESLint 配置:

rules:
 fp/no-this: warn
声明式代码

声明式编程是一个流行术语,咱们来看看它的实质和优势。

若是你是编程老手,可能你一直在用命令式的编程风格,这种风格描述了一系列实现结果所需的步骤。相比之下声明式风格是描述指望的结果,而不是具体的步骤。

典型的声明式语言有 SQL 和 HTML。甚至包括 React 中的 JSX!

咱们不会指定具体的步骤来告诉数据库如何获取数据,而是使用 SQL 来描述要获取的内容:

SELECT * FROM Users WHERE Country='USA';

在命令式 JavaScript 中这样表示:

let user = null;

for (const u of users) {
 if (u.country === 'USA') {
   user = u;
   break;
 }
}

在声明式 JavaScript 中使用实验性流水线运算符(https://github.com/tc39/proposal-pipeline-operator):

import { filter, first } from 'lodash/fp';

const filterByCountry =
 country => filter( user => user.country === country );

const user =
 users
 |> filterByCountry('USA')
 |> first;

我以为第二种方法看起来更简洁,更具可读性。

 优先使用表达式而非语句

编写声明式代码时应优先使用表达式而非语句。表达式始终返回一个值,而语句是用来执行操做的,不返回任何结果。这在函数式编程中也称为“反作用”。顺便说一句,前面讨论的状态突变也是反作用。

经常使用的语句有 if、return、switch、for、while。

来看一个简单的例子:

const calculateStuff = input => {
 if (input.x) {
   return superCalculator(input.x);
 }

 return dumbCalculator(input.y);
};

这能够很容易地重写为三元表达式(这是声明式的):

const calculateStuff = input => {
 return input.x
         ? superCalculator(input.x)
         : dumbCalculator(input.y);
};

若是 lambda 函数中只有 return 语句,那么 JavaScript 也容许咱们不用 lambda 语句:

const calculateStuff = input =>
 input.x ? superCalculator(input.x) : dumbCalculator(input.y);

函数长度从六行减到了一行。声明式编程太有用了!

语句还会引发反作用和突变,进而产生不肯定性,下降代码的可读性和可靠性。从新排序语句是不安全的,由于它们的执行依赖编写的顺序。语句(包括循环)难以并行化,由于它们在其做用域以外突变状态。使用语句会带来更多复杂度,进而产生额外的头脑负担。

相比之下,表达式能够安全地从新排序,不会产生反作用,易于并行化。

声明式编程须要努力才能熟练

学习声明式编程不是一蹴而就的,尤为是多数人学的都是命令式编程。声明式编程须要全新的思惟模式。要熟悉声明式编程,学习使用没有可变状态的程序是一个好的开始——既不用 let 关键字,也不改变状态。我能够确定,熟悉声明式编程后你的代码会变得美观优雅。

建议的 ESLint 配置:

rules:
 fp/no-let: warn
 fp/no-loops: warn
 fp/no-mutating-assign: warn
 fp/no-mutating-methods: warn
 fp/no-mutation: warn
 fp/no-delete: warn
避免将多个参数传递给函数

JavaScript 不是静态类型语言,没法保证函数使用正确和符合预期的参数来调用。ES6 引入了许多出色的功能,解构对象就是其中之一,它也可用于函数参数。

下面的代码很直观吗?你能马上说出参数是什么吗?我反正不能。

const total = computeShoppingCartTotal(itemList, 10.0, 'USD');

下面的例子呢?

const computeShoppingCartTotal = ({ itemList, discount, currency }) => {...};

const total = computeShoppingCartTotal({ itemList, discount: 10.0, currency: 'USD' });

显而后者比前者更具可读性。从不一样模块发起的函数调用尤为符合这种状况。使用参数对象还能让参数不受编写顺序的影响。

建议的 ESLint 配置:

rules:
 max-params:
 - warn
 - 2
优先从函数返回对象

下面这段代码的函数签名是什么?它返回了什么?是返回用户对象?用户 ID?操做状态?不看上下文很难回答。

const result = saveUser(...);

从函数返回一个对象能明确开发者的意图,使代码更易读:

const { user, status } = saveUser(...);

...

const saveUser = user => {
  ...

  return {
    user: savedUser,
    status: "ok"
  };
};
控制执行流程中的异常

咱们常常会遇到莫名其妙的错误,错误信息什么细节都没有。虽然说老师教咱们在发生意外状况时抛出异常,但这并非处理错误的最佳方法。

 异常破坏了类型安全

即便在静态类型语言中,异常也会破坏类型安全性。根据其签名所示,函数 fetchUser(id: number): User 应该返回一个用户。函数签名没说若是找不到用户就抛出异常。若是须要异常,那么更合适的函数签名是:fetchUser(...): User|throws UserNotFoundError。固然这种语法在任何语言中都是无效的。

推理程序的异常是很难的——人们可能永远不会知道函数是否会抛出异常。咱们是能够把函数调用都包装在 try/catch 块中,但这不怎么实用,而且会严重影响代码的可读性。

 异常破坏了函数组合

异常使函数组合难以利用。下面的例子中若是某篇帖子没法获取,服务器将返回 500 内部服务器错误。

const fetchBlogPost = id => {
 const post = api.fetch(`/api/post/${id}`);

 if (!post) throw new Error(`Post with id ${id} not found`);

 return post;
};

const html = postIds |> map(fetchBlogPost) |> renderHTMLTemplate;

若是其中一个帖子被删除,但因为一些模糊的 bug,用户仍然试图访问它怎么办?这将显著下降用户体验。

 用元组处理错误

一种简单的错误处理方法是返回包含结果和错误的元组,而不是抛出异常。JavaScript 的确不支持元组,但可使用 [error,result] 形式的双值数组很容易地模拟它们。顺便说一下,这也是 Go 中错误处理的默认方法:

const fetchBlogPost = id => {
 const post = api.fetch(`/api/post/${id}`);

 return post
     // null for error if post was found
   ?  [null, post]
     // null for result if post was not found
   :  [`Post with id ${id} not found`, null];
};

const blogPosts = postIds |> map(fetchBlogPost);

const errors =
 blogPosts
 |> filter(([err]) => !!err)  // keep only items with errors
 |> map(([err]) => err); // destructure the tuple and return the error

const html =
 blogPosts
 |> filter(([err]) => !err)  // keep only items with no errors
 |> map(([_, result]) => result)  // destructure the tuple and return the result
 |> renderHTML;
 有时异常也有用途

异常仍然在开发中占有一席之地。一个简单的原则是你来问本身一个问题——我是否能接受程序崩溃?抛出的任何异常均可能摧毁整个流程。就算咱们仔细考虑了全部极端状况,异常仍然是不安全的,早晚让程序崩溃。只有在你能接受程序崩溃时才抛出异常,好比说开发者错误或数据库链接失败等。

所谓异常,只应该用在出现例外状况,程序别无选择只能崩溃的时候。为了控制执行流程应该尽可能避免抛出和捕获异常。

 让它崩溃——避免捕获异常

因而就能够总结出处理错误的终极规则——避免捕获异常。若是咱们打算让程序崩溃就能够抛出错误,但永远不该该捕获这些错误。这也是 Haskell 和 Elixir 等函数式语言推荐的方法。

惟一例外是使用第三方 API 的状况。即便在这种状况下也最好仍是使用包装函数的辅助函数来返回 [error,result] 元组代替异常。你可使用像 saferr 这样的工具。

问问本身谁应该对错误负责。若是答案是用户,则应该正常处理错误。咱们应该向用户显示友好的消息,而不是什么 500 内部服务器错误。

惋惜这里没有 no-try-catch ESLint 规则。最接近的是 no-throw 规则。出现特殊状况时,你抛出异常就应该预料到程序的崩溃。

建议的 ESLint 配置:

rules:
 fp/no-throw: warn
部分应用函数

部分应用函数(Partial function application)多是史上最佳的代码共享机制之一。它摆脱了 OOP 依赖注入。你无需使用典型的 OOP 样板也能在代码中注入依赖项。

如下示例包装了因抛出异常(而不是返回失败的响应)而臭名昭著的 Axios 库)。这些库根本不必,尤为是在使用 async/await 时。

下面的例子中咱们使用 currying 和部分应用函数来保证一个不安全函数的安全性。

// Wrapping axios to safely call the api without throwing exceptions
const safeApiCall = ({ url, method }) => data =>
 axios({ url, method, data })
   .then( result => ([null, result]) )
   .catch( error => ([error, null]) );

// Partially applying the generic function above to work with the users api
const createUser = safeApiCall({
   url: '/api/users',
   method: 'post'
 });

// Safely calling the api without worrying about exceptions.
const [error, user] = await createUser({
 email: 'ilya@suzdalnitski.com',
 password: 'Password'
});

注意,safeApiCall 函数写为 func = (params) => (data) => {...}。这是函数式编程中的经常使用技术,称为 currying;它与部分应用函数关系密切。使用 params 调用时,func 函数返回另外一个实际执行做业的函数。换句话说,该函数部分应用了 params。

它也能够写成:

const func = (params) => (
  (data) => {...}
);

请注意,依赖项(params)做为第一个参数传递,实际数据做为第二个参数传递。

为了简化操做你可使用 saferr npm 包,它也适用于 promise 和 async/await:

import saferr from "saferr";
import axios from "axios";

const safeGet = saferr(axios.get);

const testAsync = async url => {
 const [err, result] = await safeGet(url);

 if (err) {
   console.error(err.message);
   return;
 }

 console.log(result.data.results[0].email);
};


// prints: zdenka.dieckmann@example.com
testAsync("https://randomuser.me/api/?results=1");

// prints: Network Error
testAsync("https://shmoogle.com");
几个小技巧

列举一些方便的小技巧。它们不必定让代码更可靠,但可让咱们的工做更轻松。有些技巧广为人知,有些否则。

 来一点类型安全

JavaScript 不是静态类型语言。但咱们能够按需标记函数参数来使代码更加健壮。下面的代码中,所需的值没能传入时将抛出错误。请注意它不适用于空值,但仍然能够很好地防范未定义的值。

const req = name => {
 throw new Error(`The value ${name} is required.`);
};

const doStuff = ( stuff = req('stuff') ) => {
 ...
}
 短路条件和评估

你们都熟悉短路条件,它能用来访问嵌套对象中的值。

const getUserCity = user =>
 user && user.address && user.address.city;

const user = {
 address: {
   city: "San Francisco"
 }
};

// Returns "San Francisco"
getUserCity(user);

// Both return undefined
getUserCity({});
getUserCity();

若是值为虚值(falsey),那么短路评估能够用来提供替代值:

const userCity = getUserCity(user) || "Detroit";
 赋值两次

给值赋值两次能够将任何值转换为布尔值。请注意,任何虚值都将转换为 false,这可能并不老是你想要的。数字毫不能这样作,由于 0 也将被转换为 false。

const shouldShowTooltip = text => !!text;

// returns true
shouldShowTooltip('JavaScript rocks');

// all return false
shouldShowTooltip('');
shouldShowTooltip(null);
shouldShowTooltip();
 使用现场日志来调试

咱们能够利用短路和 console.log 的虚值输出来调试函数代码,甚至 React 组件:

const add = (a, b) =>
 console.log('add', a, b)
 || (a + b);

const User = ({email, name}) => (
 <>
   <Email value={console.log('email', email) || email} />
   <Name value={console.log('name', name) || name} />
 </>
);
    总结   

你真的须要代码可靠性吗?答案取决于你本身的决定。你的组织是否定为开发者的效率取决于完成的 JIRA 故事?大家是否是所谓的函数工厂,工做只是生产更多的函数?若是是这样的话仍是换个工做吧。

本文的内容在实践中很是有用,值得反复阅读。好好看看这些技巧,ESLint 规则也都试一试吧。

英文原文: https://medium.com/better-programming/js-reliable-fdea261012ee

 活动推荐

GMTC 全球大前端技术大会首次落地华南,走入大湾区深圳。

往届咱们请到了来自 Google、Twitter、Ins、阿里、腾讯、字节跳动、百度、京东、美团等国内外一线公司的顶级前端技术专家,分享了关于小程序、Flutter、Node、RN、前端框架、前端安全、前端工程化等 50 多场技术干货。今年深圳大会 7 折售票通道已经开启,详情咨询:13269078023(同微信)。

阅读原文阅读 7904分享收藏在看35写下你的留言精选留言

  • 07aa5766a4411b3e98840dfffe1be502.jpeg楓梓2Go 那样的错误反回?Oh 饶了我吧!
  • 40c9954943317e02563c67440e69b6a9.jpeg这一年1对于let的疑问,做者初衷应该是想养成一个良好的声明习惯,方便在不当心篡改变量时形成对程序的影响,相似在java中不常修改的变量声明时尽可能采用finally同样
  • de6b865001894e872ffb87dd28acd710.jpegMamba1好文!尽可能不使用let让我大开眼界
  • 4bab94efdebc42e251ec5710f121d295.jpeg小鑫1不一样意,不要 push 数组。 应该在拷贝时候 const b = [...a]; 而不是在修改的时候
  • 9239b80c8c31e59a38089c4ef1bff9a6.jpegRyn1let 的意义何在。没有场景吗
相关文章
相关标签/搜索