目录
1. 函数应当很小,非常小" >1. 函数应当很小,非常小
Step 0: 最初的大函数" >Step 0: 最初的大函数
Step 1:根据类型计算权重,抛弃那些“迷之数字”。" >Step 1:根据类型计算权重,抛弃那些“迷之数字”。
Step 2: 继续切分,使之具备扩展性" >Step 2: 继续切分,使之具备扩展性
Step 3: 优化永无止境" >Step 3: 优化永无止境
2. 函数应当是简单的" >2. 函数应当是简单的
3. 使用简练的函数名称" >3. 使用简练的函数名称
4. 总结" >4. 总结
首页 web前端 js教程 教你如何写出小而清晰的JavaScript函数

教你如何写出小而清晰的JavaScript函数

Apr 05, 2017 pm 02:57 PM

本文以 JavaScript 为例,介绍了该如何优化函数,使函数清晰易读,且更加高效稳定。

软件的复杂度一直在持续增长。代码质量对于保证应用的可靠性、易扩展性非常重要。

然而,几乎每一个开发者,包括我自己,在职业生涯中都见过低质量的代码。这东西就是个坑。低质量代码具备以下极具杀伤力的特点:

  • 函数超级长,而且塞满了各种乱七八糟的功能。

  • 函数通常有一些副作用,不仅难以理解,甚至根本没法调试。

  • 含糊的函数、变量命名。

  • 脆弱的代码:一个小的变更,就有可能出乎意料的破坏其他应用组件。

  • 代码覆盖率缺失。

它们听起来基本都是: “我根本没法理解这段代码是如何工作的”,“这段代码就是一堆乱麻”,“要修改这一段代码实在太难了” 等等。

我就曾遇到过这样的情况,我的一个同事由于无法继续将一个基于Ruby 的 REST API 做下去,继而离职。这个项目是他从之前的开发团队接手的。

修复现有的 bug ,然后引入了新的 bug,添加新的特性,就增加了一连串新 bug,如此循环(所谓的脆弱代码)。客户不希望以更好的设计重构整个应用,开发人员也做出明智的选择——维持现状。

教你如何写出小而清晰的JavaScript函数

好吧,这种事儿经常发生,而且挺糟糕的。那我们能做点什么呢?

首先,需要谨记于心:只是让应用运转起来,和尽心保证代码质量是两个完全不同的事。一方面,你需要实现产品需求。但是另一方面,你应该花点时间,确保函数功能简单、使用易读的变量和函数命名,避免函数的副作用等等。

函数(包括对象方法)是让应用运转起来的齿轮。首先你应当将注意力集中在他们的结构和整体布局上。这篇文章包括了一些非常好的示例,展示如何编写清晰、易于理解和测试的函数。

1. 函数应当很小,非常小

避免使用包含大量的功能的大函数,应当将其功能分割为若干较小的函数。大的黑盒函数难于理解、修改,特别是很难测试。

假设这样一个场景,需要实现一个函数,用于计算 array、map 或 普通 JavaScript 对象的权重。总权重可通过计算各成员权重获得:

  • null 或者 未定义变量计 1 点。

  • 基本类型计 2 点。

  • 对象或函数计 4 点。

例如,数组 [null, ‘Hello World’, {}] 的权重这样计算:1(null) + 2(string 是基本类型) + 4(对象) = 7。

Step 0: 最初的大函数

我们从最糟的实例开始。所有的逻辑都被编码在函数 getCollectionWeight() 中:

 

function getCollectionWeight(collection) {  
  letcollectionValues;
  if (collectioninstanceof Array) {
    collectionValues = collection;
  } else if (collectioninstanceof Map) {
    collectionValues = [...collection.values()];
  } else {
    collectionValues = Object.keys(collection).map(function (key) {
      return collection[key];
    });
  }
  return collectionValues.reduce(function(sum, item) {
    if (item == null) {
      return sum + 1;
    } 
    if (typeof item === 'object' || typeof item === 'function') {
      return sum + 4;
    }
    return sum + 2;
  }, 0);
}
letmyArray = [null, { }, 15];  
letmyMap = new Map([ ['functionKey', function() {}] ]);  
letmyObject = { 'stringKey': 'Hello world' };  
getCollectionWeight(myArray);  // => 7 (1 + 4 + 2)   getCollectionWeight(myMap);    // => 4   getCollectionWeight(myObject); // => 2
登录后复制

问题显而易见,getCollectionWeight() 函数超级长,而且看起来像一个装满“意外”的黑盒子。可能你也发现了,第一眼根本就搞不明白它要干什么。再试想一下,应用里有大把这样的函数。

在工作中遇到这样的代码,就是在浪费你的时间和精力。反之,高质量的代码不会令人不适。高质量代码中,那些精巧、自文档极好的函数非常易于阅读和理解。

教你如何写出小而清晰的JavaScript函数

Step 1:根据类型计算权重,抛弃那些“迷之数字”。

现在,我们的目标是:把这个巨型函数,拆分为较小的、独立的、可重用的一组函数。第一步,将根据类型计算权重的代码提取出来。这个新的函数命名为 getWeight()。

我们再看看这几个“迷之数字”: 1, 2, 4。在不知道整个故事背景的前提下,仅靠这几个数字提供不了任何有用的信息。幸好 ES2015 允许定义静态只读引用,那你就能简单的创造几个常量,用有意义的名称,替换掉那几个“迷之数字”。(我特别喜欢“迷之数字”这个说法:D)

我们来新建一个较小的函数 getWeightByType(),并用它来改进 getCollectionWeight():

// Code extracted into getWeightByType() function getWeightByType(value) {  
  const WEIGHT_NULL_UNDEFINED  = 1;
  const WEIGHT_PRIMITIVE      = 2;
  const WEIGHT_OBJECT_FUNCTION = 4;
  if (value == null) {
    return WEIGHT_NULL_UNDEFINED;
  } 
  if (typeof value === 'object' || typeof value === 'function') {
    return WEIGHT_OBJECT_FUNCTION;
  }
  return WEIGHT_PRIMITIVE;
} function getCollectionWeight(collection) {  
  letcollectionValues;
  if (collectioninstanceof Array) {
    collectionValues = collection;
  } else if (collectioninstanceof Map) {
    collectionValues = [...collection.values()];
  } else {
    collectionValues = Object.keys(collection).map(function (key) {
      return collection[key];
    });
  }
  return collectionValues.reduce(function(sum, item) {
    return sum + getWeightByType(item);
  }, 0);
}
letmyArray = [null, { }, 15];  
letmyMap = new Map([ ['functionKey', function() {}] ]);  
letmyObject = { 'stringKey': 'Hello world' };  
getCollectionWeight(myArray);  // => 7 (1 + 4 + 2)   getCollectionWeight(myMap);    // => 4   getCollectionWeight(myObject); // => 2
登录后复制

看起来好多了,对吧? getWeightByType() 函数是一个独立的组件,仅仅用于决定各类型的权重值。而且它是可复用的,你可以在其他任何函数中使用它。

getCollectionWeight() 稍微瘦了点身。

WEIGHT_NULL_UNDEFINED , WEIGHT_PRIMITIVE 还有 WEIGHT_OBJECT_FUNCTION 都是具备自文档能力的常量,通过它们的名字就可以看出各类型的权重。你就不需要猜测 1、2、4 这些数字的意义。

Step 2: 继续切分,使之具备扩展性

然而,这个升级版依然有不足的地方。假如你打算对一个 Set,甚至其他用户自定义集合来实现权值计算。getCollectionWeight() 会快速膨胀,因为它包含了一组获得权值的具体逻辑。

让我们将获得 maps 权重的代码提取到 getMapValues() ,将获得基本 JavaScript 对象权值的代码则放到 getPlainObjectValues() 中。看看改进后的版本吧。

function getWeightByType(value) {  
  const WEIGHT_NULL_UNDEFINED = 1;
  const WEIGHT_PRIMITIVE = 2;
  const WEIGHT_OBJECT_FUNCTION = 4;
  if (value == null) {
    return WEIGHT_NULL_UNDEFINED;
  } 
  if (typeof value === 'object' || typeof value === 'function') {
    return WEIGHT_OBJECT_FUNCTION;
  }
  return WEIGHT_PRIMITIVE;
} // Code extracted into getMapValues() function getMapValues(map) {  
  return [...map.values()];
} // Code extracted into getPlainObjectValues() function getPlainObjectValues(object) {  
  return Object.keys(object).map(function (key) {
    return object[key];
  });
} function getCollectionWeight(collection) {  
  letcollectionValues;
  if (collectioninstanceof Array) {
    collectionValues = collection;
  } else if (collectioninstanceof Map) {
    collectionValues = getMapValues(collection);
  } else {
    collectionValues = getPlainObjectValues(collection);
  }
  return collectionValues.reduce(function(sum, item) {
    return sum + getWeightByType(item);
  }, 0);
}
letmyArray = [null, { }, 15];  
letmyMap = new Map([ ['functionKey', function() {}] ]);  
letmyObject = { 'stringKey': 'Hello world' };  
getCollectionWeight(myArray);  // => 7 (1 + 4 + 2)   getCollectionWeight(myMap);    // => 4   getCollectionWeight(myObject); // => 2
登录后复制

现在再来看 getCollectionWeight() 函数,你会发现已经比较容易明白它的机理,看起来就像一段有趣的故事。

每一个函数的简单明了。你不需要花费时间去挖掘代码,理解代码的工作。这就是清新版代码该有的样子。

Step 3: 优化永无止境

就算到了现在这种程度,依然有很大优化的空间!

你可以创建一个独立的函数 getCollectionValues() ,使用 if/else 语句区分集合中的类型:

function getCollectionValues(collection) {  
  if (collectioninstanceof Array) {
    return collection;
  }
  if (collectioninstanceof Map) {
    return getMapValues(collection);
  }
  return getPlainObjectValues(collection);
}
登录后复制

那么, getCollectionWeight() 应该会变得异常纯粹,因为它唯一的工作:用 getCollectionValues() 获得集合中的值,然后依次调用求和累加器。

你也可以创建一个独立的累加器函数:

function reduceWeightSum(sum, item) {  
  return sum + getWeightByType(item);
}
登录后复制

理想情况下 getCollectionWeight() 函数中不应该定义函数。

最后,最初的巨型函数,已经被转换为如下一组小函数:

function getWeightByType(value) {  
  const WEIGHT_NULL_UNDEFINED = 1;
  const WEIGHT_PRIMITIVE = 2;
  const WEIGHT_OBJECT_FUNCTION = 4;
  if (value == null) {
    return WEIGHT_NULL_UNDEFINED;
  } 
  if (typeof value === 'object' || typeof value === 'function') {
    return WEIGHT_OBJECT_FUNCTION;
  }
  return WEIGHT_PRIMITIVE;
} function getMapValues(map) {  
  return [...map.values()];
} function getPlainObjectValues(object) {  
  return Object.keys(object).map(function (key) {
    return object[key];
  });
} function getCollectionValues(collection) {  
  if (collectioninstanceof Array) {
    return collection;
  }
  if (collectioninstanceof Map) {
    return getMapValues(collection);
  }
  return getPlainObjectValues(collection);
} function reduceWeightSum(sum, item) {  
  return sum + getWeightByType(item);
} function getCollectionWeight(collection) {  
  return getCollectionValues(collection).reduce(reduceWeightSum, 0);
}
letmyArray = [null, { }, 15];  
letmyMap = new Map([ ['functionKey', function() {}] ]);  
letmyObject = { 'stringKey': 'Hello world' };  
getCollectionWeight(myArray);  // => 7 (1 + 4 + 2)   getCollectionWeight(myMap);    // => 4   getCollectionWeight(myObject); // => 2
登录后复制

这就是编写简单精美函数的艺术!

除了这些代码质量上的优化之外,你也得到不少其他的好处:

  • 通过代码自文档,getCollectionWeight() 函数的可读性得到很大提升。

  • getCollectionWeight() 函数的长度大幅减少。

  • 如果你打算计算其他类型的权重值,getCollectionWeight() 的代码不会再剧烈膨胀了。

  • 这些拆分出来的函数都是低耦合、高可复用的组件,你的同事可能希望将他们导入其他项目中,而你可以轻而易举的实现这个要求。

  • 当函数偶发错误的时候,调用栈会更加详细,因为栈中包含函数的名称,甚至你可以立马发现出错的函数。

  • 这些小函数更简单、易测试,可以达到很高的代码覆盖率。与其穷尽各种场景来测试一个大函数,你可以进行结构化测试,分别测试每一个小函数。

  • 你可以参照 CommonJS 或 ES2015 模块格式,将拆分出的函数创建为独立的模块。这将使得你的项目文件更轻、更结构化。

这些建议可以帮助你,战胜应用的复杂性。

教你如何写出小而清晰的JavaScript函数

原则上,你的函数不应当超过 20 行——越小越好。

现在,我觉得你可能会问我这样的问题:“我可不想将每一行代码都写为函数。有没有什么准则,告诉我何时应当停止拆分?”。这就是接下来的议题了。

2. 函数应当是简单的

让我们稍微放松一下,思考下应用的定义到底是什么?

每一个应用都需要实现一系列需求。开发人员的准则在于,将这些需求拆分为一些列较小的可执行组件(命名空间、类、函数、代码块等),分别完成指定的工作。

一个组件又由其他更小的组件构成。如果你希望编写一个组件,你只能从抽象层中低一级的组件中,选取需要的组件用于创建自己的组件。

换言之,你需要将一个函数分解为若干较小的步骤,并且保证这些步骤都在抽象上,处于同一级别,而且只向下抽象一级。这非常重要,因为这将使得函数变得简单,做到“做且只做好一件事”。

为什么这是必要的?因为简单的函数非常清晰。清晰就意味着易于理解和修改。

我们来举个例子。假设你需要实现一个函数,使数组仅保留 素数 (2, 3, 5, 7, 11 等等),移除非素数(1, 4, 6, 8 等等)。函数的调用方式如下:

getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
登录后复制

如何用低一级抽象的若干步骤实现 getOnlyPrime() 函数呢?我们这样做:

为了实现 getOnlyPrime() 函数, 我们用 isPrime() 函数来过滤数组中的数字。

非常简单,只需要对数字数组执行一个过滤函数 isPrime() 即可。

你需要在当前抽象层实现 isPrime() 的细节吗?不,因为 getOnlyPrime() 函数会在不同的抽象层实现一些列步骤。否则,getOnlyPrime() 会包含过多的功能。

在头脑中谨记简单函数的理念,我们来实现 getOnlyPrime() 函数的函数体:

function getOnlyPrime(numbers) {  
  return numbers.filter(isPrime);
}
getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
登录后复制

如你所见, getOnlyPrime() 非常简单,它仅仅包含低一级抽象层的步骤:数组的 .filter() 方法和 isPrime() 函数。

现在该进入下一级抽象。

数组的 .filter() 方法由 JavaScript 引擎提供,我们直接使用即可。当然,标准已经 准确描述 了它的行为。

现在你可以深入如何实现 isPrime() 的细节中了:

为了实现 isPrime() 函数检查一个数字 n 是否为素数,只需要检查 2 到 Math.sqrt(n) 之间的所有整数是否均不能整除n。

有了这个算法(不算高效,但是为了简单起见,就用这个吧),我们来为 isPrime() 函数编码:

function isPrime(number) {  
  if (number === 3 || number === 2) {
    return true;
  }
  if (number === 1) {
    return false;
  }
  for (letpisor = 2; pisor <= Math.sqrt(number); pisor++) {
    if (number % pisor === 0) {
      return false;
    }
  }
  return true;
} function getOnlyPrime(numbers) {  
  return numbers.filter(isPrime);
}
getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
登录后复制

getOnlyPrime() 很小也很清晰。它只从更低一级抽象中获得必要的一组步骤。

只要你按照这些规则,将函数变的简洁清晰,复杂函数的可读性将得到很大提升。将代码进行精确的抽象分级,可以避免出现大块的、难以维护的代码。

3. 使用简练的函数名称

函数名称应该非常简练:长短适中。理想情况下,名称应当清楚的概括函数的功用,而不需要读者深入了解函数的实现细节。

对于使用 骆驼风格 的函数名称,以小写字母开始:  addItem(), saveToStore() 或者  getFirstName() 之类。

由于函数都是某种操作,因此名称中至少应当包含一个动词。例如 deletePage(), verifyCredentials() 。需要 get 或 set 属性的时候,请使用 标准的  set 和 get 前缀: getLastName() 或  setLastName() 。

避免在生产代码中出现有误导性的名称,例如 foo(), bar(), a(), fun() 等等。这样的名称没有任何意义。

如果函数都短小清晰,命名简练:代码读起来就会像诗一样迷人。

4. 总结

当然了,这里假定的例子都非常简单。现实中的代码更加复杂。你可能要抱怨,编写清晰的函数,只在抽象上一级一级下降,实在太没劲了。但是如果从项目一开始就开始你的实践,就远没有想象中复杂。

如果应用中已经存在一些功能繁杂的函数,希望对它们进行重构,你可能会发现困难重重。而且在很多情况下,在合理的时间内是不可能完成的。但千里之行始于足下:在力所能及的前提下,先拆分一部分出来。

当然,最正确的解决方案应该是,从项目一开始就以正确的方式实现应用。除了花一些时间在实现上,也应该花一些精力在组建合理的函数结构上:如我们所建议的——让它们保持短小、清晰。

教你如何写出小而清晰的JavaScript函数

ES2015 实现了一个非常棒的模块系统,它明确建议,小函数是优秀的工程实践。

记住,干净、组织良好的代码通常 需要投入大量时间 。你会发现这做起来有难度。可能需要很多尝试,可能会迭代、修改一个函数很多次。

然而,没有什么比乱麻一样的代码更让人痛心的了,那么这一切都是值得的!

以上是教你如何写出小而清晰的JavaScript函数的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

golang函数动态创建新函数的技巧 golang函数动态创建新函数的技巧 Apr 25, 2024 pm 02:39 PM

Go语言提供了两种动态函数创建技术:closures和反射。closures允许访问闭包作用域内的变量,而反射可使用FuncOf函数创建新函数。这些技术在自定义HTTP路由器、实现高度可定制的系统和构建可插拔的组件方面非常有用。

C++ 函数命名中参数顺序的考虑 C++ 函数命名中参数顺序的考虑 Apr 24, 2024 pm 04:21 PM

在C++函数命名中,考虑参数顺序至关重要,可提高可读性、减少错误并促进重构。常见的参数顺序约定包括:动作-对象、对象-动作、语义意义和遵循标准库。最佳顺序取决于函数目的、参数类型、潜在混淆和语言惯例。

如何在Java中写出高效和可维护的函数? 如何在Java中写出高效和可维护的函数? Apr 24, 2024 am 11:33 AM

编写高效和可维护的Java函数的关键在于:保持简洁。使用有意义的命名。处理特殊情况。使用适当的可见性。

excel函数公式大全 excel函数公式大全 May 07, 2024 pm 12:04 PM

1、 SUM函数,用于对一列或一组单元格中的数字进行求和,例如:=SUM(A1:J10)。2、AVERAGE函数,用于计算一列或一组单元格中的数字的平均值,例如:=AVERAGE(A1:A10)。3、COUNT函数,用于计算一列或一组单元格中的数字或文本的数量,例如:=COUNT(A1:A10)4、IF函数,用于根据指定的条件进行逻辑判断,并返回相应的结果。

C++ 函数默认参数与可变参数的优缺点比较 C++ 函数默认参数与可变参数的优缺点比较 Apr 21, 2024 am 10:21 AM

C++函数中默认参数的优点包括简化调用、增强可读性、避免错误。缺点是限制灵活性、命名限制。可变参数的优点包括无限灵活性、动态绑定。缺点包括复杂性更高、隐式类型转换、调试困难。

C++ 函数返回引用类型有什么好处? C++ 函数返回引用类型有什么好处? Apr 20, 2024 pm 09:12 PM

C++中的函数返回引用类型的好处包括:性能提升:引用传递避免了对象复制,从而节省了内存和时间。直接修改:调用方可以直接修改返回的引用对象,而无需重新赋值。代码简洁:引用传递简化了代码,无需额外的赋值操作。

自定义 PHP 函数和预定义函数之间有什么区别? 自定义 PHP 函数和预定义函数之间有什么区别? Apr 22, 2024 pm 02:21 PM

自定义PHP函数与预定义函数的区别在于:作用域:自定义函数仅限于其定义范围,而预定义函数可在整个脚本中访问。定义方式:自定义函数使用function关键字定义,而预定义函数由PHP内核定义。参数传递:自定义函数接收参数,而预定义函数可能不需要参数。扩展性:自定义函数可以根据需要创建,而预定义函数是内置的且无法修改。

C++ 函数中引用参数和指针参数的高级用法 C++ 函数中引用参数和指针参数的高级用法 Apr 21, 2024 am 09:39 AM

C++函数中的引用参数(本质为变量别名,修改引用修改原始变量)和指针参数(存储原始变量的内存地址,通过解引用指针修改变量)在传递和修改变量时有着不同的用法。引用参数常用于修改原始变量(尤其是大型结构体),传递给构造函数或赋值运算符时避免复制开销。指针参数则用于灵活指向内存位置,实现动态数据结构或传递空指针表示可选参数。

See all articles