[ES6]Symbol Iterator Generator Async使用小结

前言

由于这些几个知识点都是相关且层层递进的,所以一起总结学习了。

Symbol

ES5中对象的属性名都是字符串,容易造成重名,污染环境。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法,新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入Symbol的原因。

ES6中的添加了一种原始数据类型symbol, 表示独一无二的值。(已有的原始数据类型:String, Number, boolean, null, undefined, 对象)。

Symbol 值通过Symbol函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型的属性,就都是独一无二的,可以保证不会与其他属性名产生冲突。

特点:

​ 1、Symbol属性对应的值是唯一的,解决命名冲突问题
​ 2、Symbol值不能与其他数据进行计算,包括同字符串拼串
​ 3、for in, for of遍历时不会遍历symbol属性。

使用:

​ 1、调用Symbol函数得到symbol值,Symbol类型属于ES6中新增的基本数据类型之一,内部没有construtor构造器,不能使用new关键字创建

let symbol = Symbol(); // 创建一个symbol值
let obj = {};
obj[symbol] = 'hello'; // 将symbol作为一个对象的属性

​ 2、传参标识

let symbol = Symbol('one');
let symbol2 = Symbol('two');
console.log(symbol);// Symbol('one')
console.log(symbol2);// Symbol('two')

​ 3、内置Symbol值:除了定义自己使用的Symbol值以外,ES6还提供了11个内置的Symbol值,指向语言内部使用的方法。

举例应用:

<script type="text/javascript">
  //创建symbol属性值
  let symbol = Symbol();
  console.log(symbol);
  let obj = {username:"chenjie",age:33};
  obj.sex = '女';
    
  //symbol可以作为对象的属性时 不能用obj.symbol的形式赋值
  obj[symbol] = 'hello';
  console.log(obj);
    
  console.log("---------for in for of 不能遍历symnol属性--------");
  for (let i in obj) {
    console.log(i);
  }
    
  console.log("---------symbol属性值是唯一的------------");
  let symbol2 = Symbol();
  let symbol3 = Symbol();
  console.log(symbol2==symbol3);//false
  console.log(symbol2,symbol3);//Symbol() Symbol()
  let symbol4 = Symbol('one');
  let symbol5 = Symbol('two');
  console.log(symbol4==symbol5);//false
  console.log(symbol4,symbol5);//Symbol(one) Symbol(two)
    
  console.log("----------可以利用Symbol定义常量---------");
  const Person_key = Symbol('person_key');
  console.log(Person_key);
</script>

Iterator

介绍

概念: iterator是一种接口机制,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作。

作用:

1、为各种数据结构,提供一个统一的、简便的访问接口;
2、使得数据结构的成员能够按某种次序排列
3、ES6创造了一种新的遍历命令for…of循环,Iterator接口主要供for…of消费。

工作原理:

  • 创建一个指针对象(迭代器对象**),指向数据结构的起始位置。

  • 第一次调用next方法,指针自动指向数据结构的第一个成员

  • 接下来不断调用next方法,指针会一直往后移动,直到指向最后一个成员

  • 每调用next方法返回的是一个包含value属性和done属性的对象,{value: 当前成员的值, done: 布尔值}

    ​ * value表示当前成员的值; done对应的布尔值表示当前的数据的结构是否遍历结束,即是否还有必要再一次调用next方法。

    ​ * 当遍历结束的时候返回的value值是undefined,done值为true

原生具备iterator接口的数据结构:

 1、Array
 2、arguments
 3set容器
 4、map容器
 5、String

举例

手动实现Iterator

<script>
  console.log('--------------模拟iterator指针对象(遍历器对象)-------------');
  function myIterator(arr) {
    let nextIndex = 0; //记录指针的位置
    return {
      next: function () {
        return nextIndex < arr.length ? { value: arr[nextIndex++], done: false } : { value: undefined, done: true };
      },
    };
  }
    
  // 准备一个数据来测试
  let arr = [1, 4, 65, 'abc'];
  let iteratorObj = myIterator(arr); // iterator对象
  console.log(iteratorObj.next()); // {value: 1, done: false}
  console.log(iteratorObj.next()); // {value: 4, done: false}
  console.log(iteratorObj.next()); // {value: 65, done: false}
  console.log(iteratorObj.next()); // {value: "abc", done: false}
  console.log(iteratorObj.next()); // {value: undefined, done: true}
  console.log(iteratorObj.next()); // {value: undefined, done: true}
  //将iterator接口部署到指定的数据类型上,就可以使用for of去循环遍历  
</script>

用for…of遍历数据结构

console.log('----------用for of 遍历Array-------------------------');
let arr = [1, 4, 65, 'abc']; 
for (let i of arr) {
    console.log(i);
}

console.log('----------用for of 遍历String-------------------------');
let str = 'abcd';
for (let i of str) {
    console.log(i);
}

console.log('----------用for of 遍历arguments--------------');
function fun() {
    for (let i of arguments) {
        console.log(i);
    }
}
fun(1, 2, 'abc');

Symbol.iterator属性返回迭代器

ES6 规定,默认的 Iterator 接口是部署在数据结构的Symbol.iterator属性上的,或者说,一个对象只要具有Symbol.iterator属性,就说明他具备iterator接口。Symbol.iterator属性值是一个函数,这个函数是当前对象默认的遍历器生成函数,执行这个函数,就会返回一个遍历器。

/*
    Array,String等对象,身上都有一个Symbol.iteration
    当使用for of去遍历某一个数据结构的时候,首先去找Symbol.iteration属性,
    找到了就去遍历,没有找到的话就不能遍历 xxx is not iterable
    实际就是下面这套原理
  */

// 下面的语句在targetData对象身上部署了iterator接口 那么他和数组Array对象一样可以用for of来遍历
let targetData = {
    [Symbol.iterator]: function () {
        let nextIndex = 0; //记录指针的位置
        return {
            next: function () {
                return nextIndex < this.length ? { value: this[nextIndex++], done: false } : { value: undefined, done: true };
            },
        };
    },
};

ES6 的有些数据结构原生具备 Iterator 接口(比如数组),即不用任何处理,就可以被for…of循环遍历。原因在于,这些数据结构原生部署了Symbol.iterator属性,另外一些数据结构没有(比如对象)。凡是部署了Symbol.iterator属性的数据结构,就称为部署了遍历器接口。

数组对象的Symbol.iterator属性

let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator](); // 返回该数组的迭代器对象

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

上面代码中,变量arr是一个数组,原生就具有遍历器接口,部署在arr的Symbol.iterator属性上面。所以,调用这个属性,就得到遍历器对象。

对于原生部署 Iterator 接口的数据结构,不用自己写遍历器生成函数,for…of循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的 Iterator 接口,都需要自己在Symbol.iterator属性上面部署,这样才会被for…of循环遍历。

iterator接口的应用

有些语法操作会静悄悄的调用迭代接口,比如:

  • 解构赋值:对数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator方法。
  • 扩展运算符:扩展运算符(…)也会调用默认的 Iterator 接口。
console.log('--------使用三点运算符和解构赋值默认都去调用了iterator接口----');
let arr3 = [2, 3, 4, 5];
arr2 = [1, ...arr3, 6]; //三点运算符
console.log(arr2); // [1,2,3,4,5,6]
let [a, b] = arr2; //解构赋值
console.log(a, b); // 1 2

Generator

概念

1、ES6提供的解决异步编程的方案之一
2、Generator函数是一个状态机,内部封装了不同状态的数据,
3、用来生成遍历器对象

使用方法

  1. function 与函数名之间有一个星号

  2. 内部用yield表达式来定义不同的状态

    // 下面代码定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield表达式(hello和world),即该函数有三个状态:hello,world 和 return 语句(结束执行)。
    function* helloWorldGenerator() {
      yield 'hello';
      yield 'world';
      return 'ending';
    }
    var hw = helloWorldGenerator();
    
  3. Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象。调用遍历器对象的next方法,会使指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止,next方法的返回值是这样的一个对象:{value: yield表达式后的表达式结果/yield表达式后面的语句的执行返回值, done: false/true}

    function* helloWorldGenerator() { // 包含三个状态 hello world ending
      yield 'hello';
      yield 'world';
      return 'ending';
    }
    var hw = helloWorldGenerator(); // 返回迭代器对象
    hw.next() // 返回{ value: 'hello', done: false }
    hw.next() // 返回{ value: 'world', done: false }
    hw.next() // 返回{ value: 'ending', done: true }
    hw.next() // 返回{ value: undefined, done: true }
    // 第一次调用从函数头部执行到yield 'hello'为止
    // 第二次调用从函数头部指定到yield 'world'为止
    // 第二次调用从函数头部指定到return 'ending'为止
    
    function* helloWorldGenerator() { // 包含四个状态 hello world ending undefined
        yield 'hello';
        yield 'world';
        yield 'ending';
    }
    var hw = helloWorldGenerator();
    hw.next() // 返回{ value: 'hello', done: false }
    hw.next() // 返回{ value: 'world', done: false }
    hw.next() // 返回{ value: 'ending', done: false }
    hw.next() // 返回{ value: undefined, done: true }
    
  4. yield表达式本身没有返回值,或者说总是返回undefinednext方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。

    这个功能有很重要的语法意义。Generator 函数从暂停状态到恢复运行,它的上下文状态是不变的。通过next方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。

    对比下面的例子可以理解:

    function* foo(x) {
      var y = 2 * (yield (x + 1)); // y -> 2 * undefined -> NaN
      var z = yield (y / 3); // z -> undefined
      return (x + y + z); // 5 + NaN + undefined 
    }
    
    var a = foo(5);
    a.next() // Object{value:6, done:false}
    a.next() // Object{value:NaN, done:false}
    a.next() // Object{value:NaN, done:true}
    
    function* foo(x) {
      var y = 2 * (yield (x + 1)); // y -> 2 * 12
      var z = yield (y / 3); // z -> 13
      return (x + y + z); // 5+24+13=42
    }
    
    var b = foo(5);
    b.next() // { value:6, done:false }
    b.next(12) // { value:8, done:false }  这个12会被当作上一个yield表达式的返回值
    b.next(13) // { value:42, done:true }
    
  5. Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。

    function* f() {
      console.log('执行了!')
    }
    var generator = f();
    setTimeout(function () {
      generator.next()
    }, 2000);
    
  6. 注意,yield表达式如果用在另一个表达式之中,必须放在圆括号里面。

    function* demo() {
      console.log('Hello' + yield); // SyntaxError
      console.log('Hello' + yield 123); // SyntaxError
    
      console.log('Hello' + (yield)); // OK
      console.log('Hello' + (yield 123)); // OK
    }
    
  7. for…of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法。

    function* foo() {
      yield 1;
      yield 2;
      yield 3;
      yield 4;
      yield 5;
      return 6;
    }
    for (let v of foo()) {
      console.log(v);
    }
    // 打印1 2 3 4 5
    

    上面代码使用for…of循环,依次显示 5 个yield表达式的值。这里需要注意,一旦next方法的返回对象的done属性为true,for…of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for…of循环之中。

    function* foo() {
      yield 1;
      yield 2;
      yield 3;
      yield 4;
      yield 5;
      yield 6;
    }
    for (let v of foo()) {
      console.log(v);
    }
    // 打印1 2 3 4 5 6
    

利用Generator为对象部署迭代器

任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。

// 原生实现
Object.prototype[Symbol.iterator] = (function (){
    // Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。
    let keys = Object.keys(this)
    let index = 0
    return {
        next: () => {
            if (index < keys.length) {
                return { value: [ keys[index++], this[keys[index]] ], done: false }
            } else {
                return { value: undefined, done: true }
            }
        }
    }
})
let a = {name:1,age:2,gender:0};
// for...of会调用 对象的 Symbol.iterator 接口实现迭代 
for(let [key, value] of a){
    console.log(key,value)
}
// Generator实现
Object.prototype[Symbol.iterator] = (function* (){
    // Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。
    let keys = Object.keys(this)
    // (yield* 表达式)用于委托给另一个generator 或可迭代对象。
    yield* keys.map(key=>([key, this[key]]))
})
let a = {name:1,age:2,gender:0};
// for...of会调用 对象的 Symbol.iterator 接口实现迭代 
for(let [key, value] of a){
    console.log(key,value)
}

Generator在异步函数中的应用

异步任务的封装

var fetch = require('node-fetch');

function* gen(){
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result.bio);
}

上面代码中,Generator 函数封装了一个异步操作,该操作先读取一个远程接口,然后从 JSON 格式的数据解析信息。就像前面说过的,这段代码非常像同步操作,除了加上了yield命令。

执行这段代码的方法如下。

var g = gen();
var result = g.next(); // {value: promise对象, done: false}

result.value.then(function(data){ // 等网络操作完成后再对返回数据进行处理
  return data.json();
}).then(function(data){
  g.next(data);
});

上面代码中,首先执行 Generator 函数,获取遍历器对象,然后使用next方法(第二行),执行异步任务的第一阶段。由于Fetch模块返回的是一个 Promise 对象,因此要用then方法调用下一个next方法。

可以看到,虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。

async函数

介绍

  • 概念: 真正意义上去解决异步回调的问题,同步流程表达异步操作
  • 本质: Generator的语法糖
  • 语法:
async function foo() {
    await 异步操作;
    await 异步操作;
}
  • 特点:

    1、不需要像Generator去手动调用next方法,遇到await等待,当前的异步操作完成就往下执行

    2、async函数返回的总是Promise对象,可以用then方法进行下一步操作

    3、async取代Generator函数的星号*,await取代Generator的yield

    4、语意上更为明确,使用简单

与Generator区别

const fs = require('fs');

// 异步操作 读取文件
const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};

// Generator的异步应用
const gen = function* () {
  const f1 = yield readFile('/etc/fstab');
  const f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

// async的异步应用
const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

async函数对 Generator 函数的改进,体现在以下四点。

(1)内置执行器。
上面的代码调用了asyncReadFile函数,然后它就会自动执行,输出最后结果。这完全不像 Generator 函数,需要调用next方法。
(2)更好的语义。
async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
(3)返回值是 Promise。
async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

基本使用

下面的代码展示了async的基本用法。async函数是使用async关键字声明的函数,在async函数中可以使用await关键字。async和await关键字让我们可以用一种更简洁的方式写出基于Promise的异步行为,而无需刻意地链式调用promise。

// async的异步应用
const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

async函数返回一个 Promise 对象。当函数执行的时候,一旦遇到await语句,就会暂停当前async函数的执行,async函数会先返回一个等待状态的promise对象(记为A),然后等待await后的语句的执行结果。

await后面一般跟一个返回Promise对象(记为B)的异步任务,若 Promise对象(B) 正常处理(fulfilled),其回调的resolve函数参数作为 await 表达式的值,继续执行 async 函数。
若 Promise 对象(B)处理异常(rejected),await 表达式会把 Promise 的异常原因抛出。

async函数返回的 Promise 对象(A),必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。

async基本使用

// async基本使用
function foo () {
  console.log('foo代码执行了')
  return new Promise(resolve => {
    setTimeout(resolve,2000);
  });
}
async function test () {
  console.log('开始执行',new Date().toTimeString());
  await foo(); // 遇到await之后test函数会先返回 等待await后面的异步操作完成后,再继续就往下执行
  console.log('执行完毕',new Date().toTimeString());
}
test();
console.log('hhh')
// 打印结果
// 开始执行 15:09:03 GMT+0800 (中国标准时间)
// foo代码执行了
// hhh
// 执行完毕 15:09:05 GMT+0800 (中国标准时间)
// 或者写成
// async基本使用
async function foo () {
  console.log('foo代码执行了')
  return new Promise(resolve => {
    setTimeout(resolve,2000);
  });
}
async function test () {
  console.log('开始执行',new Date().toTimeString());
  await foo(); // 遇到await 等待后面的异步操作完成,当前的异步操作完成就往下执行
  console.log('执行完毕',new Date().toTimeString());
}
test();
console.log('hhh')
// 打印结果
// 开始执行 15:09:03 GMT+0800 (中国标准时间)
// foo代码执行了
// hhh
// 执行完毕 15:09:05 GMT+0800 (中国标准时间)

async函数一定会返回一个promise对象。

async函数一定会返回一个promise对象。如果一个async函数的返回值看起来不是promise,那么它将会被隐式地包装在一个promise中。

async function foo() {
   return 1
}
// 等价于
function foo() {
   return Promise.resolve(1)
}

async函数内部抛出错误

async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。

async function f() {
  throw new Error('出错了');
}

f().then(
  v => console.log('resolve', v),
  e => console.log('reject', e)
)
//reject Error: 出错了

await操作符的返回值

语法:[返回值] = await 表达式;

await 操作符用于等待一个Promise对象,它只能在异步函数中使用。表达式可以是一个Promise对象或任何其他类型的值(数值、字符串、函数、布尔值…,但这时会自动转成立即 resolved 的 Promise 对象)。
如果表达式是一个Promise对象,那么返回值是该Promise对象的处理结果;如果表达式的不是 Promise 对象,则返回该值本身。

function test2() {
  return 'xxx';
}
async function asyncPrint() {
  let result = await test2();
  console.log(result);//xxx
  let result2 = await Promise.resolve('成功了');
  console.log(result2);//成功了
  let result3 = await Promise.reject('失败了');
  console.log(result3);//VM119:11 Uncaught (in promise) 失败了
}
asyncPrint();
async function f() {
  return await 123;
  // 等同于
  // return 123;
}

f().then(v => console.log(v))
// 123

await操作符后的 Promise 对象如果变为reject状态,则reject的参数会被catch方法的回调函数接收到。如下所示:

async function f() {
  await Promise.reject('出错了');
  console.log('hhh')
  await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出错了

注意,在async函数中,任何一个await操作符后的 Promise 对象变为reject状态,那么整个async函数都会中断执行。有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await放在try…catch里面,这样不管这个异步操作是否成功,第二个await都会执行。

async function f() {
  try {
    await Promise.reject('出错了');
    console.log('hhh')
  } catch(e) {
  }
  return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// hello world

或者这样

async function f() {
  await Promise.reject('出错了')
    .catch(e => console.log(e));
  console.log('hhh')
  return await Promise.resolve('hello world');
}

f()
.then(v => console.log(v))
// 出错了
// hhh
// hello world

例:await操作符后面跟一个promise对象

function foo1 () {
  return new Promise(resolve => {
    setTimeout(function () {
      console.log('foo1')
      resolve('hhhfoo1');
    },1000);
  });
}
function foo2 () {
  return new Promise(resolve => {
    setTimeout(function () {
      console.log('foo2')
      resolve();
    },1000);
  });
}
function foo3 () {
  return new Promise(resolve => {
    setTimeout(function () {
      console.log('foo3')
      resolve();
    },1000);
  });
}
async function test () {
  console.log( await foo1() );
  console.log('foo1执行完了')
  await foo2();
  console.log('foo2执行完了')
  await foo3();
  console.log('foo3执行完了')
}
test();
// 打印结果
// 一秒后打印 foo1 hhhfoo1 foo1执行完了
// 又一秒后打印 foo2 foo2执行完了
// 又一秒后打印 foo3 foo3执行完了

如果await后跟的是promise,会等promise的状态变为成功,才会继续往下执行。

例:await操作符后面跟一个普通函数

function foo1() {
  console.log('foo1执行啦')
  setTimeout(function () {
    console.log('foo1');
  }, 1000);
}
function foo2() {
  console.log('foo2执行啦')
  setTimeout(function () {
    console.log('foo2');
  }, 1000);
}
function foo3() {
  console.log('foo3执行啦')
  setTimeout(function () {
    console.log('foo3');
  }, 1000);
}
async function test() {
  await foo1();
  console.log('foo1执行完了');
  await foo2();
  console.log('foo2执行完了');
  await foo3();
  console.log('foo3执行完了');
}
test();
// 打印结果
// 立刻打印 foo1执行拉 foo1执行完了 foo2执行拉 foo2执行完了 foo3执行拉 foo3执行完了
// 1秒后打印 foo1 foo2 foo3

如果await后面跟的是普通函数,普通函数回立即返回,不会中断async函数。

参考

http://www.ruanyifeng.com/blog/2015/05/async.html 大部分来源于该博客

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值