这段来自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 是对每个脚本方向的字符串中有多少代码点的计数。
它试图通过比较 ranges
和 codePoint
属于哪个 SCRIPTS
并获得相应的 direction
属性.
这个高阶函数 countBy
接受参数 items
和 groupName
。
dominantDirection
使用两个参数调用 countBy
并将其结果存储在 scripts
.
items
是一个可迭代的值,在本例中是一个字符串(代码点):这只是输入字符串,例如"Hey, مساء الخير"
。根据这个值,单个项目(代码点)将被分组到“桶”中并单独计数。
groupName
是一个函数,它 return 是单个代码点(例如字符)所属的“桶”的名称(基于代码点本身):在这个在这种情况下,它是箭头函数 char => {
…}
,它使用单个 char
的代码点和 return 调用 characterScript
相应的脚本对象(您说你明白)。然后它获取脚本的 direction
,例如 "ltr"
用于您示例中的 { name: "Coptic",
…}
对象(如果找不到脚本对象,则为 "none"
) .
顺便说一句,groupName
不是一个好名字,因为它需要一个函数,但这个名字暗示了一个字符串。
也许 groupNameFromItem
更好。
当 countBy
遍历字符串 (for (let item of items)
) 时,此函数(最初是 char => {
…}
)被调用并赋值给 name
( let name = groupName(item);
)。
由于 char => {
…}
return 脚本的 direction
、name
变为 "ltr"
、"rtl"
或 "none"
—— 这就是“桶”的名字。
数组 counts
填充了像 { name: "ltr", count: 1 }
这样的对象。
如果下一个代码点也来自 ltr
脚本,则使用 findIndex
找到该对象,并使用 ++
.
递增其 count
这个填充的数组然后被returned(这就是scripts
在dominantDirection
中所指的内容)。
reduce
很容易解释:a
和 b
是来自 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 }
我正在接 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 是对每个脚本方向的字符串中有多少代码点的计数。
它试图通过比较 ranges
和 codePoint
属于哪个 SCRIPTS
并获得相应的 direction
属性.
这个高阶函数 countBy
接受参数 items
和 groupName
。
dominantDirection
使用两个参数调用 countBy
并将其结果存储在 scripts
.
items
是一个可迭代的值,在本例中是一个字符串(代码点):这只是输入字符串,例如"Hey, مساء الخير"
。根据这个值,单个项目(代码点)将被分组到“桶”中并单独计数。groupName
是一个函数,它 return 是单个代码点(例如字符)所属的“桶”的名称(基于代码点本身):在这个在这种情况下,它是箭头函数char => {
…}
,它使用单个char
的代码点和 return 调用characterScript
相应的脚本对象(您说你明白)。然后它获取脚本的direction
,例如"ltr"
用于您示例中的{ name: "Coptic",
…}
对象(如果找不到脚本对象,则为"none"
) .
顺便说一句,groupName
不是一个好名字,因为它需要一个函数,但这个名字暗示了一个字符串。
也许 groupNameFromItem
更好。
当 countBy
遍历字符串 (for (let item of items)
) 时,此函数(最初是 char => {
…}
)被调用并赋值给 name
( let name = groupName(item);
)。
由于 char => {
…}
return 脚本的 direction
、name
变为 "ltr"
、"rtl"
或 "none"
—— 这就是“桶”的名字。
数组 counts
填充了像 { name: "ltr", count: 1 }
这样的对象。
如果下一个代码点也来自 ltr
脚本,则使用 findIndex
找到该对象,并使用 ++
.
count
这个填充的数组然后被returned(这就是scripts
在dominantDirection
中所指的内容)。
reduce
很容易解释:a
和 b
是来自 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 returnstrue
.字符编码本身没有return
true
。 如果代码点code
落入from
(含)和to
(不含)之间的任何范围,则some
调用 returnstrue
), 或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 }