6

2022年,到底如何写一个优雅的函数?来呀,看这里!

 2 years ago
source link: https://juejin.cn/post/7061842017487159333
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

2022年,到底如何写一个优雅的函数?来呀,看这里!

2022年02月07日 05:56 ·  阅读 4339
2022年,到底如何写一个优雅的函数?来呀,看这里!

转眼间,就是最后一天假期啦,大家在家玩的还开心吗?我呢,第一次在外面过年,好生无聊,想想假期以后,又要正式进入新一年的旅途啦,即期待又不想上班,哈哈哈,那不如就继续写文章吧,铛铛铛,第三篇:【代码整洁之道】函数篇 来啦。

对于程序员而言,相信大家曾经都有这样的经历,要去修改别人的代码,每次接到这样的任务,心里都是有苦说不出呀,于是乎硬着头皮上吧,但是看到没有任何注释,一个函数好几百行的代码时,内心更是趋于崩溃,心想还不如自己重写一遍呢。

之所以出现这样的原因,一方面是因为可能对原有的业务逻辑并不熟悉,另一方面其实更多是因为之前的代码写的太烂啦,业务逻辑不熟悉,我们找产品,找同事对一下,梳理一下就清楚啦,但是太烂的代码会成本的增加我们的工作量,而且修改完以后,内心还是一万个不放心,生怕又改出新的问题。

因此,如何写出更加优雅,更加可维护的代码,就变的十分重要,要想让自己的代码更加优雅和整洁,要从命名,函数,注释,格式等多个方面去养成良好习惯,因此,本专栏 代码整洁之道-理论与实践 就是从命名,函数,注释等多个方面从理论到实战进行总结,希望可以让大家有一个更加清晰的认识。

这里要强调两句话:

  1. 我们写代码是给人的,是给我们程序猿自己看的,不是给机器看的,因此,当我们写代码的时候,要经常思考,我们写下的这段代码,别人如果此时看了是否可以比较清晰的理解代码的含义,如果觉得不太好理解,是否意味着我们的代码可以进一步优化呢,是否意味着我们需要加一些注释呢。 总之,都是为了 代码能够让别人也看得懂。
  2. 代码写的好不好,和技术能力本身不是成正相关的,也就是说代码要写好,更多的还是要养成良好的习惯,并且从态度层面去重视这个事情,和技术能力本身没有强相关的关系,当然技术能力本身也很重要,它可以加成让我们的代码可以使用更好的设计模式等去组织代码。

该专栏文章目录如下:

  1. 【代码整洁之道】命名篇
  2. 【代码整洁之道】注释篇
  3. 【代码整洁之道】函数篇
  4. 【代码整洁之道】格式篇(待发布)
  5. 【代码整洁之道】对象和数据结构篇(待发布)
  6. 【代码整洁之道】错误处理篇(待发布)
  7. 【代码整洁之道】单元测试篇(待发布)
  8. 【代码整洁之道】迭代篇(待发布)

说明:该专栏【代码整洁之道 - 理论与实践】未来会持续的围绕代码整洁去展开更多更深入的内容,也欢迎大家点赞和关注哦~😊😊😊

我们平时项目开发的过程中,一定会写各种各样的函数,说到函数,可能第一时间想到的就是:函数名,函数参数,函数体,函数返回值。确实函数基本就包含以上四部分,但是,每一部分其实又包含了不少细节需要我们去注意,这就是本节需要去讨论的事情。

在此之前,自己首先想一下自己在平时写函数的时候,有没有想过以下这些问题:

  1. 函数名如何去定义才比较规范?
  2. 函数参数传几个最合适,参数的前后顺序有要求吗?还是说就随便写啦,哪个在前哪个在后无所谓?
  3. 函数体写多少行代码最合适?自己最多写过多长的函数?
  4. 返回值该什么时候加呢?到底什么样的函数需要返回数据?什么样的函数不需要返回数据?

之所以问这些问题,主要就是想强调一下,要真正写出一个优雅的函数,其实有很多细节需要去注意,而且很多细节是我们平时写的时候可能就从来没有思考过的注意点,至少我刚开始写代码的时候,是这样的,哈哈哈。

那老规矩,继续从以下三方面去阐述:

只做一件事儿

顾名思义,我们要保证我们的函数功能是拆分非常清晰的,每个函数都只做一件事儿,当发现该函数越来越大时,我们就需要考虑是否可以再进一步拆分出多个子函数,从而保证我们每个函数实现的功能都是只做了一件事儿,这样函数也会更加简洁和纯粹。

说到函数副作用,大家可能会想到函数式编程中的纯函数,即保证同样的输入每次都有相同的输出,不能有任何的副作用,纯函数固然是美好的,我们也不用担心有其他意想不到的结果出现。

但是在我们平时采用vue,react等框架开发时,完全使用纯函数那是不可能的,也做不到。不过这种思想我们是可以延续到我们平时的代码中的,即我们要尽可能保证一个函数是纯粹的,这里的纯粹不是指纯函数,而是只做一件事儿,尽可能去减少副作用。

例如:我们写一个读取文件的函数,正常思路也就是三步:读取-数据格式转换-输出。 但是我们却在该方法中又读取了数据库,很显然该函数的功能就不是只做一件事儿啦。

明确函数场景

什么是函数的场景?其实说白了就是用函数去做什么事情?从我们平时开发来说无外乎下面两种情况:

  1. 去执行某种操作:可能是是连续的几个动作的执行。
  2. 去获取数据:典型的就是从后端发送请求获取数据。

如果还不是特别理解,我们换个角度,从参数和返回值的角度来分析:

  1. 无参数,无返回值
  2. 无参数,有返回值
  3. 有参数,无返回值
  4. 有参数,有返回值

我们最后再两者结合起来看:

  1. 如果是执行某种操作,一般都是没有返回值的,参数可能有,也可能没有,要看该操作是否依赖其他数据。
  2. 如果是去获取数据:一般都是有返回值的,参数可能有,也可能没有.

为什么要说这些呢?其实就是我们写一个函数时,要明确去到底是属于哪种场景,不要混用,例如:下面的函数

function set(attr, val) {
  this[attr] = val;
  if (this['age'] > 30) {
    console.log('true')
    return true;
  } else {
    console.log('false')
    return false;
  }
}
let person = {};
person.set('name', 'kobe');
person.set('age', 41)
复制代码

上面的代码有什么问题呢? 我们一看到set函数,就会觉得该函数的大概功能是要为某个数据设置新的属性和值,正常是没有返回值的,结果我们却发现该函数体中,还有一部分代码是校验年龄,有返回值。很显然,这段代码犯了一下两个错误:

  1. 没有只做一件事儿
  2. 没有明确函数的场景,正常逻辑set函数一般是不会有返回值的,而这里却还返回true/false,这是很迷惑的。

那如何修改呢?

  1. 按照 函数只做一件事儿的思想,我们需要把校验年龄的逻辑单独抽成一个函数。
  2. 修改函数名,保证其名与其内部实现的功能是一致的。
function setAndCheckAge(attr, val) {
  this[attr] = val;
  return checkAge();
}
function checkAge(age) {
  if (age > 30) {
    console.log('true')
    return true;
  } else {
    console.log('false')
    return false;
  }
}
let person = {};
person.set('name', 'kobe');
person.set('age', 41)
复制代码

注意:上面的代码大家可以不用过多在意其实际逻辑是否合理哈,例如:怎么在set方法里校验年龄呀,是的,实际开发中,很可能不会有这样的业务逻辑,这里只是借助说明其思想。

最理想的情况是参数是零(零参数函数),其次是一(一参数函数),再次是二(二参数函数),应该尽量避免三(三参数函数),必须有足够的理由才可以使用三或者三个以上的参数。

因为参数越多,各种组合情况也就越多,那么也就意味着函数内部的逻辑会越复杂。

对于函数参数,总结了一下以下思想:

尽可能的减少函数参数的个数。 如果有多个参数,那就涉及到参数的顺序,我们就需要考虑哪些参数应该放在前面,哪些参数应该放在后面。 如果参数确实特别多,就要考虑是否可以把同类型的参数封装到一个参数对象中。

别重复自己

即我们一定要保证代码的可复用性,函数更是重中之重,如果有一些公共的函数,我们一定要单独抽象出来。千万别重复定义相同功能的函数。

规范篇,我们分别从以下几个方面去说明:

【可选】 使用命名的函数表达式代替函数声明 eslint: func-style

原因:使用函数声明的方式会存在生命提升,也就是说在函数声明之前调用也不会报错。虽然从语法层面是可以运行成功,但是从代码可读性以及可维护性等角度来考虑的话,这样的逻辑显然不符合正常思维,即先声明后调用的逻辑。

// bad case
function foo() {
  // ...
}
​
// good case 
const foo = () => {
  // ...
}
复制代码

【必须】 把立即执行函数包裹在圆括号里。

原因:主要也是从代码可读性的角度来考虑,函数立即调用属于一个相对独立的单元,外面统一用一层小括号包裹,更清晰。

// bad case
(function() {
  // ...
})();
​
// good case
(function() {
  // ...
}())
复制代码

【必须】 切记不要在非功能块中声明函数 (if, while, 等)。

// bad case
if (flag) {
  function foo() {
    console.log('foo')
  } 
}
​
// good case
let foo;
if (flag) {
  foo = () => {
    console.log('foo')
  }
}
复制代码

【推荐】 永远不要使用函数构造器来创建一个新函数。

// bad case
let foo = new Function('a', 'b', 'return a + b');
复制代码

【必须】 函数声明语句中需要空格

// bad case 
const a = function(){};
const b = function (){};
const c = function() {};
function d () {
  // ...
} 
​
// good
const a = function () {};
const b = function a() {};
function c() {
  // ...
}
复制代码

【必须】 永远不要给一个参数命名为 arguments。 这将会覆盖函数默认的 arguments 对象。

// bad case
function(arguments) {
  // ...
}
​
// good case
function(args) {
  // ...
}
复制代码

【推荐】 使用 rest 语法 ... 代替 arguments

这里,主要是说明,如何获取arguments的参数。

  1. Array.prototype.slice.call(arguments)
  2. ...arguments 【推荐】
// bad case
function foo() {
  const args = Array.prototype.slice.call(arguments);
  return args.join('');
}
​
// good case
function foo(...args) {
  return args.join('');
}
复制代码

【推荐】 使用默认的参数语法,而不是改变函数参数。

这里主要是想说明,如何给参数设置默认值,方法其实有很多种:

  1. 判断参数是否为空,然后手动赋值一个默认值
  2. 使用默认值语法【推荐】
// bad case
function foo(options) {
  if (!options) {
    options = {};
  }
}
// bad case
function foo(options) {
  options = options || {};
  // ...
}
​
// good case
function foo(options = {}) {
  // ...
}
复制代码

但是要注意:设置默认值的时候,一定要避免副作用。例如:

let opts = {};
function foo(options = opts) {
  // ...
}
opts.name = 'kobe';
opts.age = 41;
复制代码

说明:上面这个case就是说,虽然使用了参数默认值,但是该默认值引用的是外部的一个引用对象,很显然,这是存在副作用的,因为外部的对象随时可能会变化。一旦变化,就会导致我们的默认值也会改变。因此这些写法是有问题的,避免使用!

【推荐】 总是把默认参数放在最后。

// bad case
function foo(options = {}, name) {
  // ...
}
​
// good case
function foo(name, options = {}) {
  // ...
}
复制代码

【推荐】 不要改变入参,也不对参数进行重新赋值. eslint: no-param-reassign

原因:当我们把一个变量当作参数传入函数以后,如果在函数内部对该变量又重新赋值或者修改,会直接导致该变量发生变化,那其他地方如果引用了该变量,很可能造成意想不到的问题。(注意:这里的变量主要是指的是引用数据类型,因为基础数据类型当作函数参数时会直接copy一份)

// bad case
function foo(a) {
  a = 1;
}
​
// good case
function foo(a) {
  let b = a || 1;
}
复制代码

说明:因为我们在调用的时候,不确定传入的a是引用数据类型,还是基本数据类型,所以一律要求不对入参进行修改, 但是此时可能会有一个疑问?因为在js修改入参的场景还是挺多的,典型的就是:遍历一个列表,手动添加索引或者标识位等。例如:下面的代码:

const list = [];
list.forEach((item, index) => {
  item.index = index;
  item.isShow = index > 2;
})
复制代码

以上代码其实还是比较常见的,如果遇到这种情况,eslint会提示 no-param-reassign。怎么解决呢?

  1. 在当前代码出关闭该规则校验,注意:不是全局关闭,因为大多数场景下,还是不建议对入参进行修改的。
  2. 使用深克隆,先拷贝一份item出来,再对其进行修改,然后再return新的item。

【推荐】 优先使用扩展运算符 ... 来调用可变参数函数

// good case
console.log(...[1, 2, 3, 4]);
复制代码

【推荐】 当你必须使用匿名函数时 (当传递内联函数时), 使用箭头函数。

// bad case
[1, 2, 3].map(function (x) {
  const y = x + 1;
  return x * y;
});
​
// good case
[1, 2, 3].map((x) => {
  const y = x + 1;
  return x * y;
});
复制代码

【推荐】只有一个参数是可以不使用括号,超过一个参数使用括号

// bad case
[1, 2, 3].map((item) => item + 1);
​
// good case
[1, 2, 3].map(item => item + 1);
复制代码

【推荐】当函数体是一个没有副作用的表达式组成时,删除大括号 和return,否则保留。

// bad case
[1, 2, 3].map(item => {
  return item + 1;
})
​
// good case
[1, 2, 3].map(item => item + 1)
复制代码

同时,也要注意,如果表达式中包含>=,<=等比较运算符时,推荐使用圆括号隔离一下,因为他们和箭头函数符号=>容易混淆。

// bad case 
[1, 2, 3].map(item => item >= 1 ? 1 : 0)
​
// good case
[1, 2, 3].map(item => (item >= 1 ? 1 : 0));
复制代码

通过理论篇和规范篇,我们基本已经了解到了,写好一个函数,有哪些需要注意的地方,其中有一个点十分重要:明确函数场景,换句话说,明确函数什么时候该有参数,什么时候该有返回值?

针对这一点,这里再多强调一下,因为平时写代码的时候,确实会写很多函数,也遇到很多看起来不是特别整洁,清晰的函数,这里我们从一个实际例子出发再进一步说明一下:

例如:我们要从后端获取表格数据,渲染出来,但是,后端返回的数据不符合表格的格式,需要我们手动转换一下,于是,我们很容易写出下面这样的代码:

let rawData = [];
let tableData = [];
const transformRawData = () => {
    tableData = rawData.map(item => {
        // 经过一系列处理...
    });
}
const render = () => {
  rawData = await fetchRawData();
  transformRawData();
}
复制代码

以上代码,有什么问题呢?问题还是出在 transformRawData 方法上,顾名思义,该方法的主要作用就是转换数据,那么也就意味着应该有一个入参,同时转换之后的结果,也应该体现在返回值上,所以也应该有返回值。而我们现在没有这样处理,而是直接依赖全局变量,直接进行转换。

虽然功能上没有什么问题,但是从代码层面是可以进一步优化的。

let rawData = [];
let tableData = [];
const transformRawData = (data) => {
    return data.map(item => {
        // 经过一系列处理...
    });
}
const render = async () => {
  rawData = await fetchRawData();
  tableData = transformRawData(rawData);
}
复制代码

改成这样以后,transformRawData就变成了一个更加纯粹的函数。不依赖全局变量,它的作用就是对传入的参数进行数据格式转换,转换之后,返回新的数据。

相信通过本节的学习,大家对函数如何去写有了进一步的认识,最后,我们在强调两点:

保证函数只做一件事儿,减少其副作用 明确函数的使用场景【注意:这一点其实是平时写代码时常犯的错误】

同时,结合aslant,prettier等格式化工具,对函数定义的格式进行进一步的校验,希望大家一起在2022年,能够把函数写的越来越好呀,一起加油!

写作不易,欢迎点赞评论哦,有疑问随时可以反馈!😊😊😊


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK