For AI agents: the complete documentation index is available at /tc39-atlas/llms.txt, the full documentation bundle is available at /tc39-atlas/llms-full.txt, and this page is available as Markdown at /tc39-atlas/proposals/year/pending/proposal-string-prototype-codepoints.md.
  • 简体中文
  • String.prototype.codePoints S1

    提案概览
    提案速览

    该提案为字符串添加一个 codePoints() 方法,该方法遍历码点并生成 { position, codePoint } 对象,结合了 Symbol.iterator 的便利性和 codePointAt 的位置信息。它旨在简化处理 0xFFFF 以上码点的词法分析器。

    Note

    以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。

    String.prototype.codePoints

    ECMAScript 提案:String.prototype.codePoints

    状态

    该提案处于 TC39 流程 的 stage 1。

    动机

    对于涉及 0xFFFF 以上码点的语言的词法分析器(例如 ECMAScript 语法本身),需要能够在自己的状态机处理之前将字符串分词为单独的码点。

    目前语言 API 提供了两种访问完整码点的方式:

    1. codePointAt 允许检索已知位置的码点。问题在于,如果你只是遍历字符串,位置通常是未知的,你需要手动使用 for(;;) 循环和类似魔法的表达式如 pos += currentCodePoint <= 0xFFFF ? 1 : 2 在每次迭代中计算它。
    2. String.prototype[Symbol.iterator] 允许无麻烦地遍历字符串码点,但生成的是字符串值,这对于性能关键的词法分析器来说效率低下,并且仍然缺乏位置信息。

    提出的解决方案

    我们建议添加一个 codePoints() 方法,功能上与 [@@iterator] 类似,但生成码点的位置和数值而不是仅生成字符串值,从而结合上述两种方法的优点,同时避免消费者代码中的相关陷阱。

    命名

    选择 codePoints 的名称和大小写是为了与现有的 codePointAt API 保持一致。

    示例

    测试是否为标识符

    function isIdent(input) {
        let codePoints = input.codePoints();
        let first = codePoints.next();
    
        if (first.done || !isIdentifierStart(first.value.codePoint)) {
            return false;
        }
    
        for (let { codePoint } of codePoints) {
            if (!isIdentifierContinue(codePoint)) {
                return false;
            }
        }
    
        return true;
    }

    完整的词法分析器

    function toDigit(cp) {
        return cp - /* '0' */ 48;
    }
    
    // 通用辅助类
    class LookaheadIterator {
        constructor(inner) {
            this[Symbol.iterator] = this;
            this.inner = inner;
            this.next();
        }
    
        next() {
            let next = this.lookahead;
            this.lookahead = this.inner.next();
            return next;
        }
    
        skipWhile(cond) {
            while (!this.lookahead.done && cond(this.lookahead.value.codePoint)) {
                this.next();
            }
            // 即使 `done == true`,返回的 `.value.position` 仍然有效
            // 并且表示字符串结束时的位置
            return this.lookahead.value.position;
        }
    }
    
    // 主词法分析器
    function* tokenise(input) {
        let iter = new LookaheadIterator(input.codePoints());
    
        for (let { position: start, codePoint } of iter) {
            if (isIdentifierStart(codePoint)) {
                yield {
                    type: 'Identifier',
                    start,
                    end: iter.skipWhile(isIdentifierContinue)
                };
            } else if (isDigit(codePoint)) {
                yield {
                    type: 'Number',
                    start,
                    end: iter.skipWhile(isDigit)
                };
            } else {
                throw new SyntaxError(`Expected an identifier or digit at ${start}`);
            }
        }
    }

    常见问题

    1. 为什么迭代器发出对象而不是像其他键值迭代器那样的数组?

      [key, value] 格式通常用于集合的条目,这些集合可以直接用 key 索引。

      与这些集合不同,ECMAScript 中的字符串是以 UTF-16 文本的 16 位单元而不是码点进行索引的,因此发出的对象不会有连续的索引,而是位置,这些位置可能彼此相隔 1 或 2 个 16 位单元。

      为了明确它们代表不同的测量单位和字符串表示,我们决定采用 { position, codePoint } 对象格式。

      更多细节见 #1

    2. 不同字符串表示(代码单元、字素簇等)的迭代呢?

      这些不在本提案的范围内,但应该容易作为单独的方法或 API 添加。特别是,语言特定的表示正在作为 Intl.Segmenter 提案 进行工作。

    规范

    你可以查看渲染的规范 这里

    实现