ES6笔记

一、let和const

为什么新增let声明变量的关键字
a. var可以重复声明
b. var无法限制修改
c. var没有块级作用域
块级作用域:{}、if() {}、 for() {}
只有在function(){} 中有作用域

let

  1. 块级作用域
var name = "edu"
    if (true) {
        var name = "hei";
        console.log(name);      //输出hei
    }
    console.log(name);      //输出hei

解决上面的作用域问题要采用闭包

var name = "edu"
    if (true) {
        (function (name){
            var name = "hei";
            console.log(name);       //hei
        })("lmonkey")

    }
    console.log(name);         //edu

ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。

// 第一种写法,报错
if (true) let x = 1;

// 第二种写法,不报错
if (true) {
  let x = 1;
}

应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。

// 块级作用域内部的函数声明语句,建议不要使用
{
  let a = 'secret';
  function f() {
    return a;
  }
}

// 块级作用域内部,优先使用函数表达式
{
  let a = 'secret';
  let f = function () {
    return a;
  };
}
  1. for循环计数
var buts = document.getElementsByTagName("button")
    for (var i = 0; i<5; i++) {
        buts[i].addEventListener("click",function (){
            console.log(i)               //不管哪个按钮都是5
        })
    }

变量i是全局范围有效,每一次循环,变量i的值都会发送该斌,而console.log(i),里的i指向全局的i
若解决这个问题,可以采用let,老办法就是采用闭包

var buts = document.getElementsByTagName("button")
    for (var i = 0; i<5; i++) {
        (function (i){            //闭包
            buts[i].addEventListener("click",function (){
                console.log(i)
            })
        })(i);
    }

或是

var buts = document.getElementsByTagName("button")
    for (let i = 0; i<5; i++) {
         buts[i].addEventListener("click",function (){
              console.log(i)
          })
    }
  1. 不存在变量提升
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;

// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
  1. let不可以重复声明
// 报错
function func() {
  let a = 10;
  var a = 1;
}

// 报错
function func() {
  let a = 10;
  let a = 1;
}
  1. 暂时性死区
    代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)
    ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
if (true) {
  // TDZ开始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // TDZ结束
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}

“暂时性死区”也意味着typeof不再是一个百分之百安全的操作。

typeof x; // ReferenceError
let x;

变量x使用let命令声明,所以在声明之前,都属于x的“死区”,只要用到该变量就会报错。因此,typeof运行时就会抛出一个ReferenceError。
作为比较,如果一个变量根本没有被声明,使用typeof反而不会报错。

typeof undeclared_variable // "undefined"

const

const 命令声明一个只读的常量。一旦声明,常量的值就不能改变
const 命令声明的常量不得改变值。即一旦声明,就必须立即初始化。
const 命令声明的常量,只在声明所在的块级作用域内有效

if (true) {
  const MAX = 5;
}
MAX // Uncaught ReferenceError: MAX is not defined

const 命令声明的常量不提升,只能在声明的位置后使用

if (true) {
  console.log(MAX); // ReferenceError
  const MAX = 5;
}

const 命令声明的常量,与 let 一样不可重复声明

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。
const 命令声明的复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。

const foo = {};

// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123

// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

常量foo储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址即不能把foo指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。

const a = [];
a.push('Hello'); // 可执行
a.length = 0;    // 可执行
a = ['Dave'];    // 报错

常量a是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给a,就会报错。

二、函数

函数默认值

ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法。

unction log(x, y) {
  y = y || 'World';
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World

这种写法的缺点在于,如果参数y赋值了,但是对应的布尔值为false,则该赋值不起作用。就像上面代码的最后一行,参数y等于空字符,结果被改为默认值。
为了避免这个问题,通常需要先判断一下参数y是否被赋值,如果没有,再等于默认值。

if (typeof y === 'undefined') {
  y = 'World';
}

ES6允许为函数参数设置默认值

function log(x, y = 'World') {
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello

但参数变量是默认声明的,所以不能用let或const再次声明。

function foo(x = 5) {
  let x = 1; // error
  const x = 2; // error
}

箭头函数

ES6 允许使用“箭头”(=>)定义
常规方法

function fun(x) {
        return x*x;
    }
const fun = function (x) {
    return x*x;
}
//等同于
//箭头函数写法
const fun = x => x*x;

如果箭头函数不需要参数或需要多个参数,就用圆括号代表参数部分

var f = () => 5;
// 等同于
var f = function () { return 5 };

var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
  return num1 + num2;
};

代码块部分多于一条语句,就用大括号括起来,并且用return返回

var sum = (num1, num2) => { return num1 + num2; }

箭头函数返回对象时,必须在对象外面加上括号

// 报错
let getTempItem = id => { id: id, name: "Temp" };

// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });

如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了。

let fn = () => void doesNotReturn();

箭头函数使得表达更加简洁

const isEven = n => n % 2 === 0;
const square = n => n * n;

箭头函数能够简化回调函数

let arr = [1,5,8,9,4,99,2];
let narr = arr.sort((a,b) => a-b);
console.log(narr)
  
 // 正常函数写法
    [1,2,3].map(function (x) {
        return x * x;
    });                           //[1,4,9]

    // 箭头函数写法
    [1,2,3].map(x => x * x);

箭头函数注意点

(1)关于箭头函数中的this的指向

  • 普通函数的this:指向它的调用者,如果没有调用者则默认指向window.
  • 箭头函数的this: 指向箭头函数定义时所处的对象,而不是箭头函数使用时所在的对象,默认使用父级的this.
//普通函数
const obj = {
     fun: function (){console.log(this);}
 }
 obj.fun()
//箭头函数
 const obj2 = {
     fun: () => {console.log(this);}
 }
 obj2.fun()

在这里插入图片描述
*this对象的指向是可变的,但在箭头函数中,它是固定的

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        #box{
            width: 200px;
            height: 200px;
            background-color: red;
        }
        #box.bgcolor{
            background-color: yellow;
        }
    </style>
</head>
<body>
<div id="box"></div>
</body>
<script>
    const box = document.getElementById("box");
    box.onclick = function () {
        console.log(this)               //box对象
        const obj = this
        //普通函数
        setTimeout(function () {
            console.log(this)           //window对象
            obj.className = 'bgcolor'    
            //this.className = 'bgcolor'     //不会改变颜色
        },3000)
    }
</script>
</html>

在这里插入图片描述
若在setTimeout()使用箭头函数,则箭头函数里的this是继承了外层代码块的this

const box = document.getElementById("box");
    box.onclick = function () {
        console.log(this)               //box
        //箭头函数
        setTimeout(() => {
            console.log(this)          //box
        //箭头函数导致this总是指向函数定义生效时所在的对象
            this.className = 'bgcolor'
        },3000)
    }

在这里插入图片描述
相当于箭头函数转成ES5的代码如下:

// ES6
function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

// ES5
function foo() {
  var _this = this;

  setTimeout(function () {
    console.log('id:', _this.id);
  }, 100);
}

*箭头函数可以让setTimeout里面的this,绑定定义时所在的作用域,而不是指向运行时所在的作用域。下面是另一个例子。

function Timer() {
        this.s1 = 0;
        this.s2 = 0;
        // 箭头函数
        setInterval(() => {
            console.log(this);         //Timer函数
            this.s1++;
        }, 1000);
        // 普通函数
        setInterval(function () {
            console.log(this);        //window全局对象
            this.s2++;
        }, 1000);
    }

    var timer = new Timer();

    setTimeout(() => console.log('s1: ', timer.s1), 3100);
    setTimeout(() => console.log('s2: ', timer.s2), 3100);
    // s1: 3
	// s2: 0

上面代码中,Timer函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this绑定定义时所在的作用域(即Timer函数),后者的this指向运行时所在的作用域(即全局对象)。所以,3100 毫秒之后,timer.s1被更新了 3 次,而timer.s2一次都没更新。执行结果如下
在这里插入图片描述
*箭头函数可以让this指向固定化,这种特性很有利于封装回调函数。下面是一个例子,DOM 事件的回调函数封装在一个对象里面。

var handler = {
  id: '123456',

  init: function() {
    document.addEventListener('click',
      event => this.doSomething(event.type), false);
  },

  doSomething: function(type) {
    console.log('Handling ' + type  + ' for ' + this.id);
  }
};

上面代码的init方法中,使用了箭头函数,这导致这个箭头函数里面的this,总是指向handler对象。否则,回调函数运行时,this.doSomething这一行会报错,因为此时this指向document对象。

this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数,且不能用call()、apply()、bind()这些方法去改变this的指向。

综上:箭头函数没有自己的this,它的this是继承而来,默认指向在定义它时所处的对象(宿主对象)。

不适用的场合

  • 第一个场合是定义对象的方法,且该方法内部包括this。
const cat = {
  lives: 9,
  jumps: () => {
    this.lives--;
  }
}

上面代码中,cat.jumps()方法是一个箭头函数,这是错误的。调用cat.jumps()时,如果是普通函数,该方法内部的this指向cat;如果写成上面那样的箭头函数,使得this指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致jumps箭头函数定义时的作用域就是全局作用域。

globalThis.s = 21;

const obj = {
  s: 42,
  m: () => console.log(this.s)
};

obj.m() // 21

JavaScript 引擎的处理方法是,先在全局空间生成这个箭头函数,然后赋值给obj.m,这导致箭头函数内部的this指向全局对象,所以obj.m()输出的是全局空间的21,而不是对象内部的42。上面的代码实际上等同于下面的代码。

globalThis.s = 21;
globalThis.m = () => console.log(this.s);

const obj = {
  s: 42,
  m: globalThis.m
};

obj.m() // 21
  • 第二个场合是需要动态this的时候,也不应使用箭头函数
var button = document.getElementById('press');
button.addEventListener('click', () => {
  this.classList.toggle('on');
});

上面代码运行时,点击按钮会报错,因为button的监听函数是一个箭头函数,导致里面的this就是全局对象。如果改成普通函数,this就会动态指向被点击的按钮对象。

(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

三、Set和Map数据结构

Set

1. Set基本用法

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

Set本身是一个构造函数,用来生成 Set 数据结构。

const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
  console.log(i);
}
// 2 3 5 4

上面代码通过add()方法向 Set 结构加入成员,结果表明 Set 结构不会添加重复的值。

Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
去除重复数组
在这里插入图片描述
在这里插入图片描述

去除字符串里的重复字符
在这里插入图片描述
Set内部,两个对象总是不相等的
在这里插入图片描述
Set 加入值时不会发生类型转换,5和’5’是两个不同的值,Set加入值时认为NAN等于自身,所以Set内部,两个NAN是相等的。
在这里插入图片描述

2. Set实例的属性和方法

Set 结构的实例有以下属性。
Set.prototype.constructor:构造函数,默认就是Set函数。
Set.prototype.size:返回Set实例的成员总数。

Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。
Set.prototype.add(value):添加某个值,返回 Set 结构本身。
Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。
Set.prototype.clear():清除所有成员,没有返回值。
在这里插入图片描述
Array.from方法可以将Set结构转为数组
在这里插入图片描述
去除数组重复成员的另一种方法
在这里插入图片描述

3. Set的遍历操作

Set 结构的实例有四个遍历方法,可以用于遍历成员。

Set.prototype.keys():返回键名的遍历器
Set.prototype.values():返回键值的遍历器
Set.prototype.entries():返回键值对的遍历器
Set.prototype.forEach():使用回调函数遍历每个成员

Set的遍历顺序就是插入顺序。这个特性有时非常有用,比如使用 Set 保存一个回调函数列表,调用时就能保证按照添加顺序调用。

(1)keys(),values(),entries()
keys方法、values方法、entries方法返回的都是遍历器对象。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。
在这里插入图片描述
也可以
在这里插入图片描述
(2)forEach()
在这里插入图片描述
(3)遍历的应用
扩展运算符(…)内部使用for…of循环,所以也可以用于 Set 结构。
在这里插入图片描述扩展运算符和 Set 结构相结合,就可以去除数组的重复成员。
在这里插入图片描述
数组的map和filter方法也可以间接用于 Set 了。
在这里插入图片描述
使用 Set 可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference)。
在这里插入图片描述
如果想在遍历操作中,同步改变原来的 Set 结构,目前没有直接的方法,但有两种变通方法。一种是利用原 Set 结构映射出一个新的结构,然后赋值给原来的 Set 结构;另一种是利用Array.from方法。

// 方法一
let set = new Set([1, 2, 3]);
set = new Set([...set].map(val => val * 2));
// set的值是2, 4, 6

// 方法二
let set = new Set([1, 2, 3]);
set = new Set(Array.from(set, val => val * 2));
// set的值是2, 4, 6

Map

1. Map基本用法

ES6提供Map数据结构,是为解决只能用字符串当键的问题。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。
在这里插入图片描述
Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。
在这里插入图片描述
在这里插入图片描述
Set和Map都可以用来生成新得Map
在这里插入图片描述
如果对同一个键多次赋值,后面的值将覆盖前面的值。
在这里插入图片描述
如果读取一个未知的键,则返回undefined。
在这里插入图片描述
注意,只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心。
在这里插入图片描述
上面代码的set和get方法,表面是针对同一个键,但实际上这是两个不同的数组实例,内存地址是不一样的,因此get方法无法读取该键,返回undefined。

同理,同样的值的两个实例,在 Map 结构中被视为两个键。
在这里插入图片描述
由此可知,Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。
在这里插入图片描述
如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0和-0就是一个键,布尔值true和字符串true则是两个不同的键。另外,undefined和null也是两个不同的键。虽然NaN不严格相等于自身,但 Map 将其视为同一个键。

2. Map实例的属性和操作方法

(1)属性和操作方法,操作跟Set一样
size 属性,返回 Map 结构的成员总数
set(key,value)方法,设置set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该
get(key) 方法,读取key对应的键值,如果找不到key,返回undefined
has(key) 方法,返回一个布尔值,表示某个键是否在当前 Map 对象之中。
delete(key) 方法,删除某个键,返回true。如果删除失败,返回false。
clear(key) 方法,清除所有成员,没有返回值。

(2)遍历方法
Map 的遍历顺序就是插入顺序。

keys() 返回键名的遍历器
values() 返回键值的遍历器
entries()返回所有成员的遍历器
forEach()遍历Map的所有成员

const map = new Map([
  ['F', 'no'],
  ['T',  'yes'],
]);

for (let key of map.keys()) {
  console.log(key);
}
// "F"
// "T"

for (let value of map.values()) {
  console.log(value);
}
// "no"
// "yes"

for (let item of map.entries()) {
  console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"

// 或者
for (let [key, value] of map.entries()) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

// 等同于使用map.entries()
for (let [key, value] of map) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

Map 结构转为数组结构,比较快速的方法是使用扩展运算符(…)

const map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

[...map.keys()]
// [1, 2, 3]

[...map.values()]
// ['one', 'two', 'three']

[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]

[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]

结合数组的map方法、filter方法,可以实现 Map 的遍历和过滤(Map 本身没有map和filter方法)。

const map0 = new Map()
  .set(1, 'a')
  .set(2, 'b')
  .set(3, 'c');

const map1 = new Map(
  [...map0].filter(([k, v]) => k < 3)
);
// 产生 Map 结构 {1 => 'a', 2 => 'b'}

const map2 = new Map(
  [...map0].map(([k, v]) => [k * 2, '_' + v])
    );
// 产生 Map 结构 {2 => '_a', 4 => '_b', 6 => '_c'}

Map 还有一个forEach方法,与数组的forEach方法类似,也可以实现遍历。

map.forEach(function(value, key, map) {
  console.log("Key: %s, Value: %s", key, value);
});

四、解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。

数组解构
本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。下面是一些使用嵌套数组进行解构的例子。

let [a, b, c] = [1, 2, 3];

let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3

let [ , , third] = ["foo", "bar", "baz"];
third // "baz"

let [x, , y] = [1, 2, 3];
x // 1
y // 3

let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]

let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []


let [{a,b}, [x,y,z], n , q] = [{a:10, b:20}, [1,2,3], 8, 'hello']
one // {a:10, b:20}
two // [1,2,3]

let [{a,b}, [x,y,z], n , q] = [{a:10, b:20}, [1,2,3], 8, 'hello']
a // 10
b // 20
x //1

let [foo] = [];
foo  //undefined

let [bar,foo] = [1];
bar //1
foo //undefind

另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功。

let [x, y] = [1, 2, 3];
x // 1
y // 2

let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4

//等号右边不是数组,会报错
// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};

对于 Set 结构,也可以使用数组的解构赋值。

let [x, y, z] = new Set(['a', 'b', 'c']);
x // "a"
y // "b"
z // "c"

解构赋值允许指定默认值

let [foo = true] = [];
foo // true

let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'

ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined,默认值才会生效。

let [x = 1] = [undefined];
x // 1

let [x = 1] = [null];
x // null

如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。

function f() {
  console.log('aaa');
}

let [x = f()] = [1];
x  //1

let [y = f()] = [];
//aaa
y // undefind

默认值可以引用解构赋值的其他变量,但该变量必须已经声明。

let [x = 1, y = x] = [];     // x=1; y=1
let [x = 1, y = x] = [2];    // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = [];     // ReferenceError: y is not defined

对象解构

let {name,age,grade} = {name:'zs',age:20,grade:98}
name // zs
age  //20
grade //98

let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
l
et { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined

对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量。
在这里插入图片描述
上面代码将Math对象的对数、正弦、余弦三个方法,赋值到对应的变量上,使用起来就会方便很多
在这里插入图片描述
将console.log赋值到log变量。

如果变量名与属性名不一致,必须写成下面这样。

let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"

let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'

这实际上说明,对象的解构赋值是下面形式的简写

let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };

let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined

也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值