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/proposal-slice-notation.md.
  • 简体中文
  • Slice notation S1

    中文标题:切片表示法

    提案概览
    提案速览

    该提案为数组和类型化数组引入了切片表示法语法(例如 arr\[1:3\]),为现有的切片方法提供了更符合人体工程学且歧义更少的替代方案。该提案概述了默认边界、负索引处理和越界行为,同时明确排除了步长参数和视图等功能,以保持范围最小化。

    Note

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

    切片表示法

    本仓库包含一个向 JavaScript 添加切片表示法语法提案。该提案目前处于 TC39 流程 的第 1 阶段。

    提案发起人:

    • Sathya Gunasekaran (@gsathya)
    • HE Shi-Jun (@hax)

    引言

    切片表示法为 Array.prototype、TypedArray.prototype 等上存在的各种切片方法提供了一种符合人体工程学的替代方案。

    const arr = ['a', 'b', 'c', 'd'];
    
    arr[1:3];
    // → ['b', 'c']
    
    arr.slice(1, 3);
    // → ['b', 'c']

    此表示法可用于类似 Array 和 TypedArray 等原始类型的切片操作。

    动机

    const arr = ['a', 'b', 'c', 'd'];
    arr.slice(3);
    // → ['a', 'b', 'c'] 还是 ['d'] ?

    在上面的例子中,并不清楚新创建的数组是范围 03 的切片,还是从 3len(arr) 的切片。

    const arr = ['a', 'b', 'c', 'd'];
    arr.slice(1, 3);
    // → ['b', 'c'] 还是 ['b', 'c', 'd'] ?

    添加第二个参数也同样有歧义,因为不清楚第二个参数是指定上界还是新切片的长度。

    像 Ruby 和 C++ 这样的编程语言将新切片的长度作为第二个参数,但 JavaScript 的切片方法将上界作为第二个参数。

    const arr = ['a', 'b', 'c', 'd'];
    arr[3:];
    // → ['d']
    
    arr[1:3];
    // → ['b', 'c']

    使用新的切片语法,可以立即明确下界是 3,上界是 len(arr)。它使意图变得明确。

    该语法也比函数调用更短且更符合人体工程学。

    示例

    在下面的文字中,“对象的长度”指的是对象的 length 属性。

    默认值

    下界和上界都是可选的。

    下界的默认值是 0。

    const arr = ['a', 'b', 'c', 'd'];
    
    arr[:3];
    // → ['a', 'b', 'c']

    上界的默认值是对象的长度。

    const arr = ['a', 'b', 'c', 'd'];
    arr[1:];
    // → ['b', 'c', 'd']

    省略所有下界和上界值,会产生对象的新副本。

    const arr = ['a', 'b', 'c', 'd'];
    
    arr[:];
    // → ['a', 'b', 'c', 'd']

    负索引

    如果下界为负,则起始索引计算如下:

    start = max(lowerBound + len, 0)

    其中 len 是对象的长度。

    const arr = ['a', 'b', 'c', 'd'];
    
    arr[-2:];
    // → ['c', 'd']

    在上面的例子中,start = max((-2 + 4), 0) = max(2, 0) = 2

    const arr = ['a', 'b', 'c', 'd'];
    
    arr[-10:];
    // → ['a', 'b', 'c', 'd']

    在上面的例子中,start = max((-10 + 4), 0) = max(-6, 0) = 0

    类似地,如果上界为负,则结束索引计算如下:

    end = max(upperBound + len, 0)
    const arr = ['a', 'b', 'c', 'd'];
    
    arr[:-2];
    // → ['a', 'b']
    
    arr[:-10];
    // → []

    这些语义与现有切片操作的行为完全匹配。

    越界索引

    下界和上界都被限制在对象的长度内。

    const arr = ['a', 'b', 'c', 'd'];
    
    arr[100:];
    // → []
    
    arr[:100];
    // → ['a', 'b', 'c', 'd']

    这些语义与现有切片操作的行为完全匹配。

    先前艺术

    Python

    本提案深受 Python 的启发。不出所料,Python 的切片表示法语法惊人地相似:

    slicing      ::=  primary "[" slice_list "]"
    slice_list   ::=  slice_item ("," slice_item)* [","]
    slice_item   ::=  expression | proper_slice
    proper_slice ::=  [lower_bound] ":" [upper_bound] [ ":" [stride] ]
    lower_bound  ::=  expression
    upper_bound  ::=  expression
    stride       ::=  expression

    示例:

    arr = [1, 2, 3, 4];
    
    arr[1:3];
    // → [2, 3]
    
    arr[1:4:2]
    // → [2, 4]

    CoffeeScript

    CoffeeScript 提供了一个 Range 运算符,该运算符对上界是 包含 的。

    arr = [1, 2, 3, 4];
    arr[1..3];
    // → [2, 3, 4]

    CoffeeScript 还提供了 Range 运算符的另一种形式,该形式对上界是 排他 的。

    arr = [1, 2, 3, 4];
    arr[1...3];
    // → [2, 3]

    Go

    Go 提供 切片

    arr := []int{1,2,3,4};
    arr[1:3]
    // → [2, 3]

    还可以提供下界或上界:

    arr := []int{1,2,3,4};
    arr[1:]
    // → [2, 3, 4]
    
    arr := []int{1,2,3,4};
    arr[:3]
    // → [1, 2, 3]

    Ruby

    Ruby 似乎有两种不同的方式来获取切片:

    • 使用 Range:
    arr = [1, 2, 3, 4];
    arr[1..3];
    // → [2, 3, 4]

    这类似于 CoffeeScript。1..3 会生成一个 Range 对象,该对象定义了要切出的索引集合。

    • 使用逗号运算符:
    arr = [1, 2, 3, 4];
    arr[1, 3];
    // → [2, 3, 4]

    这里的区别在于第二个参数实际上新切片的长度,而不是上界索引。

    这目前是有效的 ECMAScript 语法,这使得此方案无法继续。

    const s = 'foobar'
    s[1, 3]
    // → 'b'

    常见问题解答

    为什么选择 Python 语法而不是 Ruby/CoffeeScript 语法?

    Python 语法排除了上界索引,这与 JavaScript 中现有的切片方法类似。

    我们可以使用 CoffeeScript 的排他 Range 运算符(...),但这并不适用于所有情况,因为它与展开语法有歧义。来自 getify 的示例代码:

    Object.defineProperty(Number.prototype,Symbol.iterator,{
      *value({ start = 0, step = 1 } = {}) {
         var inc = this > 0 ? step : -step;
         for (let i = start; Math.abs(i) <= Math.abs(this); i += inc) {
            yield i;
         }
      },
      enumerable: false,
      writable: true,
      configurable: true
    });
    
    const range = [ ...8 ];
    // → [0, 1, 2, 3, 4, 5, 6, 7, 8]

    为什么这不使用迭代器协议?

    迭代器协议不限于索引查找,这使得它与这种仅适用于索引的切片表示法不兼容。

    例如,Map 和 Set 有迭代器,但我们不应该对它们进行切片,因为它们没有索引。

    splice 呢?

    CoffeeScript 允许在 AssignmentExpression 的左侧使用类似的语法,从而实现拼接操作。

    numbers = [1, 2, 3, 4]
    numbers[2..4] = [7, 8]
    // → [1, 2, 7, 8]

    此功能目前被省略以限制提案的范围,但可以在后续提案中纳入。

    为什么这不像 Python 那样包含步长参数?

    步长参数会使切片表示法与绑定运算符产生歧义。

    const x = [2];
    const arr = [1, 2, 3, 4];
    arr[::x[0]];

    上面是创建一个值为 [1, 3] 的新数组还是创建一个绑定方法?

    这是否应该在数组上创建一个 view,而不是创建一个新数组?

    Go 在底层数组上创建一个 slice,而不是分配一个新数组。

    arr := []int{1,2,3,4};
    v = arr[1:3];
    // → [2, 3]

    在这里,v 只是一个保存对原始数组 arr 引用的描述符。没有执行新的数组分配。更多细节请参阅 这篇博客文章

    这不对应于 JavaScript 中的任何现有构造,这将背离 JavaScript 中方法的工作方式。为了使此语法在 JavaScript 模型中良好工作,此提案中不包含这样的 view 数据结构。

    切片表示法应该作用于字符串吗?

    String.prototype.slice 方法对 unicode 字符处理不佳。Mathias Bynens 的 这篇博客文章 解释了这个问题。

    鉴于现有方法处理不佳,此提案不会将 @@slice 添加到 String.prototype

    将其与 + 结合用于追加怎么样?

    const arr = [1, 2, 3, 4] + [5, 6];
    // → [1, 2, 3, 4, 5, 6]

    为了保持提案的范围尽可能最小,此功能未被包含在内。

    运算符重载提案 可能更适合于此。

    你能使用此语法创建 Range 对象吗?

    切片表示法仅提供一种执行切片操作的人体工程学语法。

    当前的切片表示法并不排除将来创建 range 原始类型。

    这里正在讨论一个新的 Range 原始类型: https://github.com/tc39/proposal-Number.range/issues/22

    这不是在进行属性查找,难道不令人困惑吗?

    这实际上通过 [[Get]] 在底层对象上进行属性查找。例如,

    const arr = [1, 2, 3, 4];
    
    arr[1:3];
    // → [2, 3]

    这是对键 12 进行属性查找。

    但是,它不应该查找字符串 '1:3' 吗?

    const arr = [1, 2, 3, 4];
    
    arr['1:3'];
    // → undefined

    不。切片表示法使其与键控查找的工作方式类似。键首先被计算为一个值,然后使用该值进行查找。

    const arr = [1, 2, 3, 4];
    const x = 0;
    
    arr[x] !== arr['x'];
    // → true

    切片表示法的工作方式类似。表示法首先被计算为一个值的范围,然后查找每个值。

    ':' 已经有许多模式表示不同含义了。这不令人困惑吗?

    根据上下文,a:b 可以表示:

    • LabelledStatement,其中 a 是标签
    • 对象字面量中值为 b 的属性 a:{a: b }
    • ConditionalExpression:confused ? a : b
    • 未来可能进入 JavaScript 的潜在类型系统(如 TypeScript 和 Flow)。

    通过上下文消除这些模式之间的歧义是否开销过大?像 Python 这样的主流主流编程语言都有所有这些模式,并且被用作教学编程的主要工具。