>  기사  >  웹 프론트엔드  >  작고 명확한 JavaScript 함수를 작성하는 방법을 가르쳐주세요.

작고 명확한 JavaScript 함수를 작성하는 방법을 가르쳐주세요.

怪我咯
怪我咯원래의
2017-04-05 14:57:36970검색

이 기사에서는 JavaScript를 예로 들어 함수를 명확하고 읽기 쉽고 효율적이고 안정적으로 만들기 위해 함수를 최적화하는 방법을 소개합니다.

소프트웨어의 복잡성은 계속해서 증가하고 있습니다. 코드 품질은 애플리케이션의 신뢰성과 확장성을 보장하는 데 매우 중요합니다.

그러나 저를 포함한 거의 모든 개발자는 경력을 쌓으면서 품질이 낮은 코드를 경험해 왔습니다. 이건 함정이에요. 품질이 낮은 코드는 다음과 같은 매우 위험한 특성을 가지고 있습니다.

  • 함수는 매우 길고 온갖 지저분한 함수로 가득 차 있습니다.

  • 함수에는 일반적으로 이해하기 어려울 뿐만 아니라 디버깅도 불가능한 부작용이 있습니다.

  • 함수 및 변수 이름이 모호합니다.

  • 취약한 코드: 작은 변경으로 인해 다른 애플리케이션 구성 요소가 예기치 않게 중단될 수 있습니다.

  • 코드 적용 범위가 누락되었습니다.

기본적으로 다음과 같이 들립니다. “이 코드가 어떻게 작동하는지 모르겠어요.”, “이 코드는 엉망입니다.”, “수정해야 합니다. 이 코드 조각 정말 어렵다” 등등.

저희 동료 중 한 명이 Ruby 기반 REST API를 계속 구축할 수 없어 사임한 적이 있습니다. 이 프로젝트는 이전 개발팀에서 인수되었습니다.

기존 버그 수정, 새 버그 도입, 새 기능 추가, 일련의 새 버그 추가 등(소위 깨지기 쉬운 코드). 고객은 더 나은 디자인으로 전체 애플리케이션을 리팩터링하고 싶지 않았고 개발자는 현 상태를 유지하기 위해 현명한 선택을 했습니다.

작고 명확한 JavaScript 함수를 작성하는 방법을 가르쳐주세요.

글쎄, 이런 일은 항상 일어나는데 꽤 안타깝습니다. 그럼 우리는 무엇을 할 수 있나요?

우선, 애플리케이션을 실행하는 것과 코드 품질을 보장하는 것은 완전히 다른 두 가지라는 점을 명심해야 합니다. 한편으로는 제품 요구사항을 구현해야 합니다. 그러나 다른 한편으로는 함수가 단순한지 확인하고, 읽을 수 있는 변수와 함수 이름을 사용하고, 함수 부작용을 방지하는 등의 작업을 수행해야 합니다.

함수(객체 메서드 포함)는 애플리케이션을 실행하는 톱니바퀴입니다. 먼저 구조와 전체 레이아웃에 집중해야 합니다. 이 문서에는 명확하고 이해하기 쉽고 테스트할 수 있는 함수를 작성하는 방법을 보여주는 몇 가지 훌륭한 예제가 포함되어 있습니다.

1. 함수는 작아야 합니다, 아주 작아야 합니다

많은 수의 함수를 포함하는 큰 함수를 사용하지 말고 해당 함수를 여러 개의 작은 함수로 나눕니다. 대형 블랙박스 기능은 이해도 수정도 어렵고 특히 테스트가 어렵습니다.

이러한 시나리오를 가정하면 배열, 맵 또는 일반 JavaScript 객체의 가중치를 계산하는 함수를 구현해야 합니다. 총 가중치는 각 멤버의 가중치를 계산하여 구할 수 있습니다.

  • null 또는 정의되지 않은 변수는 1점으로 계산됩니다.

  • 기본형은 2점으로 계산됩니다.

  • 객체나 함수는 4점으로 계산됩니다.

예를 들어 배열 [null, 'Hello World', {}]의 가중치는 다음과 같이 계산됩니다. 1(null) + 2(문자열이 기본 유형) + 4(객체) = 7 .

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 함수를 작성하는 방법을 가르쳐주세요.

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가 무엇을 의미하는지 짐작할 필요는 없습니다.

2단계: 계속해서 세분화하고 확장 가능하게 만듭니다.

그러나 이번 업그레이드 버전에는 여전히 단점이 있습니다. 세트나 다른 사용자 정의 세트에 대한 가중치 계산을 구현하려고 계획한다고 가정해 보겠습니다. getCollectionWeight()에는 가중치를 얻기 위한 특정 논리 세트가 포함되어 있기 때문에 빠르게 비대해질 수 있습니다.

지도의 가중치를 가져오는 코드를 getMapValues()에 추출하고, 기본 자바스크립트 객체의 가중치를 가져오는 코드를 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() 함수를 살펴보면 그 메커니즘을 이해하는 것이 더 쉽고 흥미로운 이야기처럼 보입니다.

각 기능은 간단하고 명확합니다. 작동 방식을 이해하기 위해 코드를 파헤치는 데 시간을 소비할 필요가 없습니다. 이것이 새로운 코드의 모습입니다.

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으로 문의하세요.