这段来自EloquentJS的代码是如何判断主导书写方向的?

How does this code from Eloquent JS determine the dominant writing direction?

我正在接 Eloquent JavaScript this higher-order function exercise 的答案让我很困惑:

function characterScript(code) {
  for (let script of SCRIPTS) {
    if (script.ranges.some(([from, to]) => {
      return code >= from && code < to;
    })) {
      return script;
    }
  }
  return null;
}

// takes a test function and tells you whether that function
// returns true for any of the elements in the array

function countBy(items, groupName) {
  let counts = [];
  for (let item of items) {
    let name = groupName(item);
    let known = counts.findIndex(c => c.name == name);
    if (known == -1) {
      counts.push({name, count: 1});
    } else {
      counts[known].count++;
    }
  }
  return counts;
}

// returns an array of objects, each of which names a group
// and tells you the number of elements that were found in that group

function dominantDirection(text) {
  let scripts = countBy(text, char => {
    let script = characterScript(char.codePointAt(0));
    return script ? script.direction : "none";
  }).filter(({name}) => name != "none");
 
  if (scripts.length == 0) return "ltr";
  
  return scripts.reduce((a, b) => a.count > b.count ? a : b).name;
}

console.log(dominantDirection("Hello!"));
// → ltr
console.log(dominantDirection("Hey, مساء الخير"));
// → rtl 

此代码returns大型数据集中的主要书写方向,如下所示:

[
  {
    name: "Coptic",
    ranges: [[994, 1008], [11392, 11508], [11513, 11520]],
    direction: "ltr",
    year: -200,
    living: false,
    link: "https://en.wikipedia.org/wiki/Coptic_alphabet"
  },
  // …
]

我了解如何使用 some 方法的循环来查找包含字符代码 returns true 的任何数组。 我无法理解 countBy 函数或 dominantDirection 函数如何导致底部显示的结果。

我们将不胜感激这两个函数的分解以及它们如何导致正确的结果!

检查一些中间结果会更容易理解。 加个console.log看看scripts是什么return,去掉.name看看reduce调用的结果是什么:

function dominantDirection(text) {
  const scripts = countBy(text, (char) => {
      const script = characterScript(char.codePointAt(0));
    
      return (script
        ? script.direction
        : "none"
      );
    })
      .filter(({name}) => name !== "none");
 
  if(scripts.length === 0){
    return "ltr";
  }
  
  console.log(scripts); // What is the result of the `countBy` function?
  
  return scripts.reduce((a, b) => (a.count > b.count
    ? a
    : b)); // What is the object that the `name` property comes from?
}

现在 dominantDirection("Hello!") 会将 scripts 记录为

[
  { name: "ltr", count: 5 }
]

结果也将是

{ name: "ltr", count: 5 }

并且 dominantDirection("Hey, مساء الخير") 会将 scripts 记录为

[
  { name: "ltr", count: 3 },
  { name: "rtl", count: 9 }
]

结果

{ name: "rtl", count: 9 }

scripts 数组来自 countBy 调用,其中 return 是对每个脚本方向的字符串中有多少代码点的计数。 它试图通过比较 rangescodePoint 属于哪个 SCRIPTS 并获得相应的 direction 属性.

这个高阶函数 countBy 接受参数 itemsgroupNamedominantDirection 使用两个参数调用 countBy 并将其结果存储在 scripts.

  • items 是一个可迭代的值,在本例中是一个字符串(代码点):这只是输入字符串,例如"Hey, مساء الخير"。根据这个值,单个项目(代码点)将被分组到“桶”中并单独计数。
  • groupName 是一个函数,它 return 是单个代码点(例如字符)所属的“桶”的名称(基于代码点本身):在这个在这种情况下,它是箭头函数 char => {},它使用单个 char 的代码点和 return 调用 characterScript 相应的脚本对象(您说你明白)。然后它获取脚本的 direction,例如 "ltr" 用于您示例中的 { name: "Coptic",} 对象(如果找不到脚本对象,则为 "none") .

顺便说一句,groupName 不是一个好名字,因为它需要一个函数,但这个名字暗示了一个字符串。 也许 groupNameFromItem 更好。

countBy 遍历字符串 (for (let item of items)) 时,此函数(最初是 char => {})被调用并赋值给 namelet name = groupName(item);)。 由于 char => {} return 脚本的 directionname 变为 "ltr""rtl""none" —— 这就是“桶”的名字。 数组 counts 填充了像 { name: "ltr", count: 1 } 这样的对象。 如果下一个代码点也来自 ltr 脚本,则使用 findIndex 找到该对象,并使用 ++.

递增其 count

这个填充的数组然后被returned(这就是scriptsdominantDirection中所指的内容)。

reduce 很容易解释:ab 是来自 scripts 数组之一的对象。 如果a.count高于b.count,则a为returned,否则b为returned;然后 returned 对象用于下一次比较,或者,如果没有其他需要比较的对象,则 returned 作为结果。 因此 reduce 调用找到具有最大值 count 的对象。 在原始代码中,最后只有 name 被 return 编辑,而不是整个对象。


总结:

text 是由不同脚本的代码点组成的字符串。 countBy 采用 text,遍历代码点,调用 groupName 获取当前代码点的“桶名称”,填充 counts 数组(名为 scripts,在函数之外)和 { name, count } 条目告诉你 count 许多代码点来自 name 方向 的脚本。 然后 reduce 在这些条目中寻找最大值 count 并且它的 name 是 returned.


还有两件事:

  • I understand how a loop with the some method is used to find any arrays in which the character code returns true.

    字符编码本身没有returntrue。 如果代码点 code 落入 from(含)和 to(不含)之间的任何范围,则 some 调用 returns true ), 或 false, 否则.

  • 本章是关于高阶函数的,所以了解function countBy(items, groupName){}中的groupName参数是如何工作的很重要。 我不太确定你对这个概念有多熟悉,但这里有一个更简单的例子,其中计算奇数和偶数并附有一些解释性注释:

    const countOddAndEvenNumbers = (iterable) => {
        const oddOrEvenBucketFromNumber = (number) => (number % 2 === 0 ? "even" : "odd"); // This is the function that distinguishes odd and even numbers.
    
        return countGroups(iterable, oddOrEvenBucketFromNumber); // The distinguishing function is passed to `countGroups` to be used.
      },
      countGroups = (iterable, bucketNameFromItem) => {
        const result = {}; // Usually counting is done with hash maps, e.g. objects or Maps, instead of arrays.
    
        for(let item of iterable){
          const bucketName = bucketNameFromItem(item); // Generic way of `const bucketName = (item % 2 === 0 ? "even" : "odd")`; acts as `const bucketName = oddOrEvenBucketFromNumber(item)`, but with no own knowledge of what odd or even numbers are: it’s entirely separated and knows nothing about the implementation of the function.
    
          result[bucketName] = (result[bucketName] ?? 0) + 1; // Increment entry `bucketName` by one. If it doesn’t exist, initialize it to `0` first.
        }
    
        return result;
      };
    
    countOddAndEvenNumbers([0, 1, 1, 2, 3, 5, 8, 13]); // { "even": 3, "odd": 5 }