String.normalize() 有什么意义?

What's the point of String.normalize()?

在回顾 JavaScript 个概念时,我发现了 String.normalize()。这不是出现在 W3School 的“JavaScript String Reference”中的东西,因此,这就是我之前可能错过它的原因。

我在 HackerRank 中找到了更多关于它的信息,其中指出:

Returns a string containing the Unicode Normalization Form of the calling string's value.

以例:

var s = "HackerRank";
console.log(s.normalize());
console.log(s.normalize("NFKC"));

作为输出:

HackerRank
HackerRank

此外,在 GeeksForGeeks 中:

The string.normalize() is an inbuilt function in javascript which is used to return a Unicode normalisation form of a given input string.

举例:

<script> 
  
  // Taking a string as input. 
  var a = "GeeksForGeeks"; 
    
  // calling normalize function. 
  b = a.normalize('NFC') 
  c = a.normalize('NFD') 
  d = a.normalize('NFKC') 
  e = a.normalize('NFKD') 
    
  // Printing normalised form. 
  document.write(b +"<br>"); 
  document.write(c +"<br>"); 
  document.write(d +"<br>"); 
  document.write(e); 
    
</script> 

作为输出:

GeeksForGeeks
GeeksForGeeks
GeeksForGeeks
GeeksForGeeks

也许给出的例子真的很糟糕,因为它们不允许我看到任何变化。

我想知道...这个方法有什么意义?

MDN documentation 所述,String.prototype.normalize() return 字符串的 Unicode 规范化形式。这是因为在 Unicode 中,一些字符可以有不同的表示代码。

这是示例(取自 MDN):

const name1 = '\u0041\u006d\u00e9\u006c\u0069\u0065';
const name2 = '\u0041\u006d\u0065\u0301\u006c\u0069\u0065';

console.log(`${name1}, ${name2}`);
// expected output: "Amélie, Amélie"
console.log(name1 === name2);
// expected output: false
console.log(name1.length === name2.length);
// expected output: false

const name1NFC = name1.normalize('NFC');
const name2NFC = name2.normalize('NFC');

console.log(`${name1NFC}, ${name2NFC}`);
// expected output: "Amélie, Amélie"
console.log(name1NFC === name2NFC);
// expected output: true
console.log(name1NFC.length === name2NFC.length);
// expected output: true

如您所见,字符串 Amélie 作为两种不同的 Unicode 表示形式。通过归一化,我们可以将两种形式简化为同一个字符串。

这取决于将如何处理字符串:通常您不需要它(如果您只是从用户那里获取输入,然后将其提供给用户)。但要 check/search/use 为 key/etc。这样的字符串,您可能需要一种独特的方式来识别相同的字符串(从语义上讲)。

主要问题是您可能有两个语义相同但具有两种不同表示的字符串:例如一种带有重音字符[一个代码点],一种带有字符与重音组合[一个代码点用于字符,一个用于组合重音]。用户可能无法控制输入文本的发送方式,因此您可能有两个不同的用户名或两个不同的密码。但是如果你破坏数据,你可能会得到不同的结果,这取决于初始字符串。用户不喜欢。

另一个问题是关于组合字符的唯一顺序。你可能有一个重音和一个较低的尾巴(例如 cedilla):你可以用几种组合来表达:“pure char, tail, accent”,“pure char, accent, tail”,“char+tail, accent”,“字符+重音,变音符。

并且您可能遇到退化的情况(尤其是如果您从键盘输入):您可能会得到应该删除的代码点(您可能有一个无限长的字符串,它可能相当于几个字节。

无论如何,为了对字符串进行排序,您(或您的图书馆)需要一个规范化的形式:如果您已经提供了权利,图书馆将不需要再次转换它。

因此:您希望相同(从语义上讲)的字符串具有相同的 unicode 代码点序列。

注意:如果你直接在 UTF-8 上做,你还应该关心 UTF-8 的特殊情况:相同的代码点可以用不同的方式编写 [使用更多字节]。这也可能是一个安全问题。

K 通常用于“搜索”和类似的任务:CO2 和 CO₂ 将以相同的方式解释,但这可能会改变文本的含义,因此通常只在内部使用, 用于临时任务,但保留原文。

字符串规范化不排除 JavaScript - see for instances in Python. The values valid for the arguments are defined by the Unicode (more on Unicode normalization).

当谈到 JavaScript 时,请注意 String.normalize() and String.prototype.normalize() 有文档。正如@ChrisG 提到的

String.prototype.normalize() is correct in a technical sense, because normalize() is a dynamic method you call on instances, not the class itself. The point of normalize() is to be able to compare Strings that look the same but don't consist of the same characters, as shown in the example code on MDN.

然后,说到它的用法,发现一个great example of the usage of String.normalize()

let s1 = 'sabiá';
let s2 = 'sabiá';

// one is in NFC, the other in NFD, so they're different
console.log(s1 == s2); // false

// with normalization, they become the same
console.log(s1.normalize('NFC') === s2.normalize('NFC')); // true

// transform string into array of codepoints
function codepoints(s) { return Array.from(s).map(c => c.codePointAt(0).toString(16)); }

// printing the codepoints you can see the difference
console.log(codepoints(s1)); // [ "73", "61", "62", "69", "e1" ]
console.log(codepoints(s2)); // [ "73", "61", "62", "69", "61", "301" ]

所以虽然 saibá e saibá 在这个例子中在人眼看来是一样的,或者即使我们使用 console.log(),我们也可以看到,在比较它们时我们没有归一化d 得到不同的结果。然后,通过分析代码点,我们发现它们是不同的。

这里解释得非常漂亮 --> https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize

简答:关键是,字符是通过 ascii、utf-8 等编码方案表示的(我们主要使用 UTF-8)。有些字符有不止一种表示。所以 2 个字符串可能呈现类似,但它们的 unicode 可能不同!所以这里的字符串比较可能会失败!所以我们使用 normaize 来 return 单一类型的表示

// source from MDN

let string1 = '\u00F1';                           // ñ
let string2 = '\u006E\u0303';                     // ñ

string1 = string1.normalize('NFC');
string2 = string2.normalize('NFC');

console.log(string1 === string2);                 // true
console.log(string1.length);                      // 1
console.log(string2.length);                      // 1

这里已经有一些很好的答案,但我想举一个实际的例子。

我喜欢翻译圣经作为一种爱好。在我的价格范围内(免费),我对外面的抽认卡选项不太感兴趣,所以我自己制作了。问题是,在 Unicode 中使用希伯来语和希腊语来获得完全相同的东西的方法不止一种。例如:

בָּא
בָּא

这些在您的屏幕上看起来应该是相同的,并且对于所有实际用途而言,它们是相同的。然而,第一个是在 dagesh(字母中间的点)之前用 qamats(它下面的小 t 形的东西)打字的,第二个是在 qamats 之前用 dagesh 打字的。现在,既然你只是在读这个,你不在乎。而您的网络浏览器不在乎。但是当我的抽认卡比较两者时,它们就不一样了。对于幕后代码来说,无异于说“center”和“centre”是一样的

同样,在希腊语中:

ἀ
ἀ

这两个看起来应该几乎相同,但最上面的是一个 Unicode 字符,第二个是两个 Unicode 字符。哪一个最终会出现在我的抽认卡中取决于我坐在哪个键盘上。

当我添加抽认卡时,不管你信不信,我并不总是输入 100 个单词的词汇表。这就是为什么上帝给了我们电子表格。有时我从中导入列表的地方以一种方式进行,有时他们以另一种方式进行,有时他们混合使用。但是当我打字时,我并不是要记住 dagesh 或 quamats 出现的顺序,或者重音符号是否作为单独的字符输入。不管我是否记得先输入 dagesh,我都想得到正确的答案,因为无论哪种方式,在任何实际意义上都是相同的答案。

所以我在保存抽认卡之前对顺序进行了规范化,在检查之前对顺序进行了规范化,结果是无论我以何种方式输入,它都是正确的!

如果您想查看结果:

https://sthelenskungfu.com/flashcards/

您需要 Google 或 Facebook 帐户才能登录,以便它可以跟踪进度等。据我所知(或关心)目前只有我和我的女儿在使用它。

它是免费的,但永远处于测试阶段。