【翻译】Iterables and iterators in ECMAScript 6

本文翻译自Dr. Axel Rauschmayer的博客:http://www.2ality.com/2015/02/es6-iteration.html

本文是ES6中iteration的两篇博客:

  1. Iterables and iterators in ECMAScript 6

  2. ES6 generators in depth


ECMAScript 6对迭代/遍历(iteration)引入了一个新的接口: Iterable. 本文解释了它是如何工作,哪些语句通过它消费数据(例如for-of循环),哪些语句通过它提供数据源(例如arrays)。

Iterability

iterability的思想如下:

  • 数据消费者(Data consumers): JavaScript有一些语句消费数据。例如for-of 循环遍历 values,spread operator (...)把values插入arrays或function calls。

  • 数据源(Data sources): 数据消费者可以从许多源头得到values。例如想要对一个array的元素遍历,对一个map中key-value entries的遍历,或者对一个string中characters的遍历。

让每个数据消费者支持所有的数据源并不现实,尤其是可能会不断创建新的数据源和消费者,例如通过数据结构或采用新的处理数据方式的库来创建。因此ES6引入了接口Iterable,数据消费者使用它,数据源实现它:

由于JavaScript没有接口概念,Iterable 更多的是个约定:

  • Source: 一个value被视为iterable ,当它有一个key为Symbol.iterator 的方法返回一个iterator。这个iterator是一个对象,通过其方法 next() 返回值,每调用一次next( )返回一个item,那我们就说它枚举items

  • Consumption: 数据消费者使用iterator来获取消费的values。

我们来看如何消费一个array arr。首先通过key为Symbol.iterator的方法创建一个iterator: 

    > let arr = ['a', 'b', 'c'];
    > let iter = arr[Symbol.iterator]();

然后重复调用iterator的方法next()得到array内部的items:

    > iter.next()    { value: 'a', done: false }
    > iter.next()    { value: 'b', done: false }
    > iter.next()    { value: 'c', done: false }
    > iter.next()    { value: undefined, done: true }

next() 返回的每一项被封装在一个对象中, 该对象的属性value 对应 item 的value,布尔值属性done 表示是否到items序列末尾。

Iterable与iterators是遍历(iteration)的协议(使用遍历的方法和规则)。该协议的一个关键特点是它是序列化的:iterator一次返回一个值。这意味着如果一个iterable数据结构不是线性的(例如是一个树), 那么遍历将使之线性化。

Iterable数据源

下面使用for-of 循环(后面还会详细解释)来遍历各种iterable数据。

Arrays

Arrays (以及typed arrays)可以对其元素来遍历:

    for (let x of ['a', 'b']) {
        console.log(x);
    }    
    // Output:
    // 'a'
    // 'b'

Strings

Strings是可遍历的,但它们枚举的是Unicode编码点,每个Unicode编码点由1个或2个JavaScript “characters”组成:

    for (let x of 'a\uD83D\uDC0A') {
        console.log(x);
    }    
    // Output:
    // 'a'
    // '\uD83D\uDC0A' (crocodile emoji)

注意你已经看到了primitive values也是可遍历的。一个值不必是个对象才能遍历

Maps

Maps [3] 可以对其entries遍历。每个entry被编码为一个[key, value] pair,由两个元素组成的一个数组。这些entries总是可以被一一枚举,与它们被插入到map中的顺序相同。

    let map = new Map().set('a', 1).set('b', 2);    
    for (let pair of map) {
        console.log(pair);
    }    
    // Output:
    // ['a', 1]
    // ['b', 2]

注意WeakMaps [3]不可以遍历。

Sets

Sets [3] 可以对其entries遍历。它们被枚举的顺序与插入到set中的顺序相同。

    let set = new Set().add('a').add('b');    
    for (let x of set) {
        console.log(x);
    }    
    // Output:
    // 'a'
    // 'b'

注意WeakSets [3] 不可以遍历。

arguments

虽然特殊变量arguments 在ECMAScript 6中要被废弃 (由于rest parameters), 但它是可遍历的:

    function printArgs() {
        for (let x of arguments) {
            console.log(x);
        }
    }
    printArgs('a', 'b');    
    // Output:
    // 'a'
    // 'b'

DOM数据结构

大多数DOM数据结构是可遍历的:

    for (let node of document.querySelectorAll('···')) {
        ···
    }

注意这个功能的实现还在开发中。但实现相对容易,因为symbol Symbol.iterator 不能与现有的property keys [2]相抵触。

遍历计算出的数据

并非所有iterable的内容都来自于数据结构,也可以是计算中得出的。例如所有的major ES6数据结构(arrays, typed arrays, maps, sets)都有三个方法返回iterable对象:

  • entries() 返回一个可遍历的entries,编码为[key,value] arrays. 对于arrays, values是数组元素,keys是索引。对于sets, 每个key和value相等,都等于set元素。

  • keys() 返回一个可遍历的 entries keys。

  • values() 返回一个可遍历的 entries values。

下面看看. entries() 如何给出array元素及其索引:

    let arr = ['a', 'b', 'c'];    
    for (let pair of arr.entries()) {
        console.log(pair);
    }    
    // Output:
    // [0, 'a']
    // [1, 'b']
    // [2, 'c']

Plain objects不可以遍历

Plain objects (由object literals创建)不可以遍历:

    for (let x of {}) { // TypeError
        console.log(x);
    }

理由如下。下面两个活动不同:

  1. 检查一个程序的结构(reflection)

  2. 遍历数据

最好保持这两个活动分开。#1与所有的对象相关,#2仅与数据结构相关。可以向对象Object.prototype添加一个方法[Symbol.iterator]()来使对象可遍历,但在两种情况下会无效

  • 如果是通过 Object.create(null) 创建,那么 Object.prototype 不在对象的原型链中。

  • 如果它们是数据结构,那么就需要遍历数据。不仅不能对properties遍历,而且不能添加iterability到已有的类中,因为这将破坏对实例属性遍历的代码。

因此,使properties可遍历最安全的方式就是通过一个工具函数。例如通过objectEntries(), 其实现见后面 (未来的ECMAScript版本可能会内置类似的实现):

    let obj = { first: 'Jane', last: 'Doe' };    
    for (let [key,value] of objectEntries(obj)) {
        console.log(`${key}: ${value}`);
    }    
    // Output:
    // first: Jane
    // last: Doe

而且,重要的是记住针对对象properties的遍历主要当对象是maps [4]才有意义。但这仅在ES5中才这样做,因为别无他法。在ECMAScript 6中有Map.

Iterating language constructs

本章节列出ES6中内置的所有使用了iteration协议的编程结构。

通过array来分解结构模式

通过array来分解结构(Destructuring [5])模式可针对任意iterable:

    let set = new Set().add('a').add('b').add('c');    
    let [x,y] = set;        
    // x='a'; y='b'
    
    let [first, ...rest] = set;        
    // first='a'; rest=['b','c'];

for-of 循环

for-of 是ECMAScript 6中的新引入的一个循环. 它的一种用法是:

    for (let x of iterable) {
        ···
    }

这个循环遍历iterable, 将每个枚举项赋值给遍历变量 x ,然后在循环体内处理。x的范围是循环内,出了循环不再存在

注意iterable 需要能够遍历,否则for-of 不能做循环。这就意味着不可遍历的值必须转换为其它可遍历的。例如通过Array.from(), 可以将类似于array的值和iterables变为arrays:

    let arrayLike = { length: 2, 0: 'a', 1: 'b' };    
    
    for (let x of arrayLike) { // TypeError
        console.log(x);
    } 
       
    for (let x of Array.from(arrayLike)) { // OK
        console.log(x);
    }

我期待for-of最好能够替换Array.prototype.forEach()因为它更为通用,forEach() 只能用于类似于array的值,而且对于很长的项for-of将更快(参见末尾的FAQ)。

遍历变量:let 声明 vs. var 声明

如果用let来声明遍历的变量,那么对每个遍历将创建一个新的绑定(slot)。从下面的代码片段可以看出,通过一个箭头函数将当前的绑定elem保存起来以便后续使用。之后,会看到箭头函数没有共享同一个绑定elem, 每个有不同的elem。

    let arr = [];    
    for (let elem of [0, 1, 2]) {
        arr.push(() => elem); // save `elem` for later
    }
    console.log(arr.map(f => f())); // [0, 1, 2]
    
    // `elem`仅存在于循环内部:
    console.log(elem); // ReferenceError: elem is not defined

如果循环中使用var来声明遍历的变量看看发生了什么。现在所有的箭头函数指向同一个绑定elem。

    let arr = [];    
    for (var elem of [0, 1, 2]) {
        arr.push(() => elem);
    }
    console.log(arr.map(f => f())); // [2, 2, 2]
    
    // `elem` exists in the surrounding function:
    console.log(elem); // 2

当通过循环创建函数(例如添加event listeners)时,每次遍历有一个绑定就非常有帮助。

在for循环和for-in循环中用let声明的变量来遍历

对于for循环和for-in循环,如果用let来声明遍历的变量,那么每次遍历都会得到一个绑定。

先看看for循环中用let来声明遍历的遍历i:

    let arr = [];    
    for (let i=0; i<3; i++) {
        arr.push(() => i);
    }
    console.log(arr.map(f => f())); // [0, 1, 2]
    console.log(i); // ReferenceError: i is not defined

如果这里使用var来声明i,就会得到传统的行为:

    let arr = [];    
    for (var i=0; i<3; i++) {
        arr.push(() => i);
    }
    console.log(arr.map(f => f())); // [3, 3, 3]
    console.log(i); // 3


类似地,对于for-in循环,用let来声明遍历的遍历key会使得每次遍历得到一个绑定:

    let arr = [];    
    for (let key in ['a', 'b', 'c']) {
        arr.push(() => key);
    }
    console.log(arr.map(f => f())); // ['0', '1', '2']
    console.log(key); // ReferenceError: key is not defined

用var来声明key只会得到一个绑定:

    let arr = [];    
    for (var key in ['a', 'b', 'c']) {
        arr.push(() => key);
    }
    console.log(arr.map(f => f())); // ['2', '2', '2']
    console.log(key); // '2'

用先定义的变量、对象属性与数组元素来遍历

以上只看了for-of中使用一个声明了的变量来遍历,但还有其他几种形式。

可以用一个先定义的变量来遍历:

    let x;    
    for (x of ['a', 'b']) {
        console.log(x);
    }

也可以用一个对象属性来遍历:

    let obj = {};    
    for (obj.prop of ['a', 'b']) {
        console.log(obj.prop);
    }

还可以用一个array元素来遍历:

    let arr = [];    
    for (arr[0] of ['a', 'b']) {
        console.log(arr[0]);
    }

用解构模式来遍历

for-of循环与解构组合在一起,用于遍历key-value对(编码为arrays)非常有用。 下面以maps为例:

    let map = new Map().set(false, 'no').set(true, 'yes');    
    for (let [k,v] of map) {
        console.log(`key = ${k}, value = ${v}`);
    }    
    // Output:
    // key = false, value = no
    // key = true, value = yes

Array.prototype.entries() 也返回一个可遍历key-value对iterable:

    let arr = ['a', 'b', 'c'];    
    for (let [k,v] of arr.entries()) {
        console.log(`key = ${k}, value = ${v}`);
    }    
    // Output:
    // key = 0, value = a
    // key = 1, value = b
    // key = 2, value = c

因此entries() 可以根据枚举项的位置不同来做不同处理:

    /** Same as arr.join(', ') */
    function toString(arr) {
        let result = '';        
        for (let [i,elem] of arr.entries()) {            
            if (i > 0) {
                result += ', ';
            }
            result += String(elem);
        }        
        return result;
    }

这个函数的调用如下:

    > toString(['eeny', 'meeny', 'miny', 'moe'])    'eeny, meeny, miny, moe'

Array.from()

Array.from() [6] 将iterable和类似array的值转换为arrays.,也可以用于typed arrays.

    > Array.from(new Map().set(false, 'no').set(true, 'yes'))    
    [[false,'no'], [true,'yes']]
    
    > Array.from({ length: 2, 0: 'hello', 1: 'world' })    
    ['hello', 'world']

Array.from() 就像Array的子类一样 (继承类的方法) – 将iterables转换为子类实例。

Spread

spread操作符[5] 将一个iterable值插入到一个array中:

    > let arr = ['b', 'c'];
    > ['a', ...arr, 'd']
    ['a', 'b', 'c', 'd']

这就提供了一种将任意iterable转换为array的紧凑方法:

    let arr = [...iterable];

spread操作符还可以将一个iterable变为函数/方法或构造函数的参数:

    > Math.max(...[-1, 8, 3])    
    8

Maps与sets

map的构造函数将一个个由[键, 值]对组成的iterable变为map:

    > let map = new Map([['uno', 'one'], ['dos', 'two']]);
    > map.get('uno')    
    'one'
    > map.get('dos')    
    'two'

set的构造函数将一个个由元素组成的iterable变为set:

    > let set = new Set(['red', 'green', 'blue']);
    > set.has('red')    
    true
    > set.has('yellow')    
    false

WeakMap与WeakSet 的构造函数与上述类似。而且maps与sets本身就是iterable(WeakMaps与WeakSets不是),这意味着可以用它们的构造函数来克隆它们。

Promises

Promise.all()和Promise.race()接受iterables over promises [7]:

    Promise.all(iterableOverPromises).then(···);
    Promise.race(iterableOverPromises).then(···);

yield*

yield* [8] 会对一个iterable所有的枚举项让步.

    function* yieldAllValuesOf(iterable) {
        yield* iterable;
    }

yield*最重要的一个用法是递归调用一个generator [8] (这将产生某种iterable).

实现iterables

遍历协议类似于下面这样:

如果一个对象(拥有或继承)有key为Symbol.iterator的方法,那么该对象就是可遍历对象iterable (即实现Iterable接口) ,这个方法必须返回一个iterator, 通过其方法next()来枚举可遍历对象内部的每一项。

在TypeScript中,iterables和iterators的接口类似于下面这样(参见 [9]):

    interface Iterable {
        [System.iterator]() : Iterator;
    }
    interface IteratorResult {
        value: any;
        done: boolean;
    }
    interface Iterator {
        next(value? : any) : IteratorResult;        
        return?() : IteratorResult;
    }

return 是一个可选方法,在后面介绍 (throw()也是可选方法,但实际上也从不用于iterators,将在下一篇博客中讨论a follow-up blog post on generators). 首先实现一个dummy iterable来感受下iteration是如何工作的。

    let iterable = {
        [Symbol.iterator]() {            
            let step = 0;            
            let iterator = {
                next() {                    
                    if (step <= 2) {
                        step++;
                    }                    
                    switch (step) {                        
                        case 1:                            
                            return { value: 'hello', done: false }; 
                        case 2:                            
                            return { value: 'world', done: false }; 
                        default:                            
                            return { value: undefined, done: true };
                    }
                }
            };            
            return iterator;
        }
    };

现在来检查iterable:

    for (let x of iterable) {
        console.log(x);
    }    
    // Output:
    // hello
    // world

上面代码执行三步,用计数器step 来确保每一步输出对应的值。首先返回'hello',接下来返回'world',然后枚举项遍历结束。每一项都被封装在有下面属性的对象中:

  • value 对应实际的值

  • done 是个布尔标志, 表示是否遍历结束。

如果done 取值为 false或者value 是undefined可以忽略。重写上面的 switch 语句:

    switch (step) {        
        case 1:            
            return { value: 'hello' };        
        case 2:            
            return { value: 'world' };        
        default:            
            return { done: true };
    }

在下一篇博客中会解释 the follow-up blog post on generators, 有些情况下希望donetrue 时最后一项可以返回一个value. 否则的话,可以更简化next() 的实现,直接返回items而不用封装在对象中。可通过一个特殊值(例如一个符号symbol)来表示遍历结束。

下面再看一个iterable的实现,函数iterateOver() 返回一个对传入参数的遍历:

    function iterateOver(...args) {
        let index = 0;        
        let iterable = {
            [Symbol.iterator]() {                
                let iterator = {
                    next() {                        
                        if (index < args.length) {                            
                            return { value: args[index++] };
                        } else {                            
                            return { done: true };
                        }
                    }
                };                
                return iterator;
            }
        }        
        return iterable;
    }    
    // Using `iterateOver()`:
    for (let x of iterateOver('fee', 'fi', 'fo', 'fum')) {
        console.log(x);
    }    
    // Output:
    // fee
    // fi
    // fo
    // fum

Iterators是可遍历的(iterable)

如果iterable和iterator是同一个对象,那么前面的函数可以简化为:

    function iterateOver(...args) {
        let index = 0;        
        let iterable = {
            [Symbol.iterator]() {                
                return this;
            },
            next() {                
                if (index < args.length) {                    
                    return { value: args[index++] };
                } else {                    
                    return { done: true };
                }
            },
        };        
        return iterable;
    }

即使最初的iterable和iterator不是同一个对象,但如果iterator有下面方法(这也使得iterator是可遍历的),那么有时候也有用处:

    [Symbol.iterator]() {        
        return this;
    }

ES6中所有内置的iterators都遵循这个模式(通过一个公共prototype, 参见follow-up blog post on generators). 例如,arrays的缺省iterator:

    > let arr = [];
    > let iterator = arr[Symbol.iterator]();
    > iterator[Symbol.iterator]() === iterator    true

为什么当一个iterator是可遍历的(即是一个iterable)有用呢? for-of 仅用于iterables, 而不能用于iterators. 正是由于array iterators是iterable, 因此可在另一个循环中继续遍历:

    let arr = ['a', 'b'];    
    let iterator = arr[Symbol.iterator]();    
    for (let x of iterator) {
        console.log(x); // a
        break;
    }    
    // Continue with same iterator:
    for (let x of iterator) {
        console.log(x); // b
    }

另一种方式就是用方法返回一个iterable。例如Array.prototype.values() 返回结果与缺省遍历方式相同。因此前面的代码段等同于下面代码:

    let arr = ['a', 'b'];    
    let iterable = arr.values();    
    for (let x of iterable) {
        console.log(x); // a
        break;
    }    
    for (let x of iterable) {
        console.log(x); // b
    }

但使用iterable时,如果 for-of 调用了方法[Symbol.iterator]()不能确保不会重新开始遍历。例如Array 的实例是iterables,当调用这个方法时将从头开始遍历。

继续遍历的一个用况是在通过 for-of 处理实际内容前先去掉初始项(例如一个header)。

可选的iterator方法: return() 与 throw()

两个iterator方法是可选的:

  • return() 当遍历过早终止时让iterator有机会清理残局。

  • throw() 转发方法调用给一个通过yield*来遍历的generator,详细解释见the follow-up blog post on generators.

通过return()关闭iterators

前面提到,可选的iterator方法return() 就是还没有遍历结束时就让iterator停止,即关闭iterator。在for-of循环中,会由于以下原因过早终止premature(在语言规范中称中断abrupt) :

  • break

  • continue (如果在外部循环中继续遍历, continue行为类似于break)

  • throw

  • return

在上面四种情况下,for-of让iterator知道循环没有结束。下面看一个例子,函数readLinesSync 返回对一个文件每一文本行的遍历,不论发生什么都会关闭文件:

    function readLinesSync(fileName) {
        let file = ···;        
        return {
            ···
            next() {                
                if (file.isAtEndOfFile()) {
                    file.close();                    
                    return { done: true };
                }
                ···
            },            
            return() {
                file.close();                
                return { done: true };
            },
        };
    }

由于return(), 在下面循环中文件将被关闭:

    // Only print first line
    for (let line of readLinesSync(fileName)) {
        console.log(x);        
        break;
    }

return() 方法必须返回一个对象,这是由于generators处理return语句的方式,具体将在the follow-up blog post on generators解释。

下面的constructs会关闭还没有遍历到结尾的iterators:

  • for-of

  • yield*

  • Destructuring

  • Array.from()

  • Map(), Set(), WeakMap(), WeakSet()

  • Promise.all(), Promise.race()

iterables更多例子

在这一章节中来看更多iterables例子。这些iterables大多数通过generators更容易实现,详见The follow-up blog post on generators

返回iterables的工具函数

返回iterables的工具函数和方法就像iterable数据结构一样重要。下面是一个遍历对象属性的工具函数:

    function objectEntries(obj) {
        let index = 0;    
        // In ES6, you can use strings or symbols as property keys,
        // Reflect.ownKeys() retrieves both
        let propKeys = Reflect.ownKeys(obj);    
        return {
            [Symbol.iterator]() {                
                return this;
            },
            next() {                
                if (index < propKeys.length) {                    
                    let key = propKeys[index];
                    index++;                    
                    return { value: [key, obj[key]] };
                } else {                    
                    return { done: true };
                }
            }
        };
    }    
    let obj = { first: 'Jane', last: 'Doe' };    
    for (let [key,value] of objectEntries(obj)) {
        console.log(`${key}: ${value}`);
    }    
    // Output:
    // first: Jane
    // last: Doe

iterables组合函数

Combinators [10] 是组合已有iterables并创建新的iterable的函数。

先来看一个combinator函数take(n, iterable), 它返回一个iterable,由iterable前面 n 项组成。

    function take(n, iterable) {
        let iter = iterable[Symbol.iterator]();        
        return {
            [Symbol.iterator]() {                
                return this;
            },
            next() {                
                if (n > 0) {
                    n--;                    
                    return iter.next();
                } else {                    
                    return { done: true };
                }
            }
        };
    }    
    let arr = ['a', 'b', 'c', 'd'];    
    for (let x of take(2, arr)) {
        console.log(x);
    }    
    // Output:
    // a
    // b

zip 将n 个iterables组合为一个由n元数组(每个元数组tuple的编码是一个长度为n的数组)组成的iterable。 

    function zip(...iterables) {
        let iterators = iterables.map(i => i[Symbol.iterator]());        
        let done = false;        
        return {
            [Symbol.iterator]() {                
                return this;
            },
            next() {                
                if (!done) {                    
                    let items = iterators.map(i => i.next());
                    done = items.some(item => item.done);                    
                    if (!done) {                        
                        return { value: items.map(i => i.value) };
                    }                    
                    // Done for the first time: close all iterators
                    for (let iterator of iterators) {
                        iterator.return();
                    }
                }                
                // We are done
                return { done: true };
            }
        }
    }

可以看出,最短的iterable决定最终长度:

    let zipped = zip(['a', 'b', 'c'], ['d', 'e', 'f', 'g']);    
    for (let x of zipped) {
        console.log(x);
    }    
    // Output:
    // ['a', 'd']
    // ['b', 'e']
    // ['c', 'f']

无穷尽iterables

有些iterable可能永远不会结束done: 

    function naturalNumbers() {
        let n = 0;        
        return {
            [Symbol.iterator]() {                
                return this;
            },
            next() {                
                return { value: n++ };
            }
        }
    }

对无穷尽iterable,一定不能遍历所有元素。例如可从一个for-of循环跳出:

    for (let x of naturalNumbers()) {        
        if (x > 2) break;
        console.log(x);
    }

或者仅访问无穷尽iterable的前面几项:

    let [a, b, c] = naturalNumbers();        
    // a=0; b=1; c=2;

或者使用一个combinator函数。例如可以使用take()函数:

    for (let x of take(3, naturalNumbers())) {
        console.log(x);
    }    // Output:
    // 0
    // 1
    // 2

由zip() 返回的iterable的长度是由最短的iterable决定,即zip()和naturalNumbers() 就可以返回任意长度的iterable,包括无穷尽长度:

    let zipped = zip(['a', 'b', 'c'], naturalNumbers());    
    for (let x of zipped) {
        console.log(x);
    }    
    // Output:
    // ['a', 0]
    // ['b', 1]
    // ['c', 2]

常见问题(FAQ)

iteration协议慢吗?

你可能担心遍历协议很慢,因为每次调用next()需要创建一个新对象。但内存管理小对象对于现代引擎以及长时间运行是很快的,引擎可以优化遍历不需要为中间对象分配空间。更多信息可参见thread on es-discuss

结论

这篇博文虽然只涉及了ES6 iteration的基础,但内容已经很多了。Generators [8]以iterators的实现为基础。

JavaScript运行时库还没有与iterators工作的工具,Python有特性丰富的模块itertools, JavaScript最终也会有类似的模块。

进一步阅读

  1. Exploring ES6: Upgrade to the next version of JavaScript”, book by Axel

  2. Symbols in ECMAScript 6

  3. ECMAScript 6: maps and sets

  4. Pitfalls: Using an Object as a Map” in “Speaking JavaScript”

  5. Destructuring and parameter handling in ECMAScript 6 [includes an explanation of the spread operator (...)]

  6. ECMAScript 6’s new array methods

  7. ECMAScript 6 promises (2/2): the API

  8. ES6 generators in depth

  9. Closing iterators”, slides by David Herman

  10. Combinator” in HaskellWiki

转载于:https://my.oschina.net/1pei/blog/596510

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值