深入javascript计划一:数据类型深入介绍

javascript有几种数据类型?

答:javascript有两种数据类型

基本数据类型:string、boolean、number、null、undefined、symbol

引用数据类型:对象Object(包括普通对象-object、数组对象-array、正则对象-regexp、日期对象-date、数学对象-math、函数对象-function)

小知识:&代表取址、*代表取值,不过JavaScript中是没有的,我们以go来做例子

 

基本数据类型

JavaScript

let a = 'a'
let b = a
let b = 'b'
console.log(a) // a
console.log(b) // b

go

package main

import "fmt"

func main()  {
    a:="a"
    b:=a
    b="b"
    fmt.Println(a, &a) // a 0xc00003e1f0
    fmt.Println(b, &b) // b 0xc00003e200
}

画解思路

所以javascript的基本数据类型,是自动会新建一个指针(内存地址)并把值拷贝过去,因为指针(内存地址)不同,所以例子中的b修改了不会影响到a

 

引用数据类型

正则对象-regexp、日期对象-date、数学对象-math、函数对象-function,JavaScript会自动会新建一个指针(内存地址)并把值拷贝过去,以日期对象-date为例子

let a = new Date()
let b = a
console.log(b) // 2020-06-15T15:18:59.026Z
b = 123
console.log(a) // 2020-06-15T15:18:59.026Z
console.log(b) // 123

主要注意:普通对象-object、数组对象-array

JavaScript

let a = {
    name: 'a',
    age: 13
}
let b = a
b.name = "b"
console.log(a) // { name: 'b', age: 13 }
console.log(b) // { name: 'b', age: 13 }

let a = new Object
a.name = 'a'
a.age = 12
let b = a
b.name = 'b'
console.log(a) // { name: 'b', age: 12 }
console.log(b) // { name: 'b', age: 12 }


// 分割线

let a = [ {name: "a", age: 13} ]
let b = a
b[0].name = 'b'
console.log(a) // [ { name: 'b', age: 13 } ]
console.log(b) // [ { name: 'b', age: 13 } ]

go

package main

import "fmt"

func main()  {
    var a map[string]interface{} // 声明变量不分配内存
    a = make(map[string]interface{}) // 分配内存
    a["name"] = "a"
    a["age"] = 12
    b:=a
    b["name"] = "b"
    fmt.Println(a) // map[age:12 name:b]
    fmt.Printf("%p", a) // 0xc000070330
    fmt.Println(b) // map[age:12 name:b]
    fmt.Printf("%p", b) // 0xc000070330
}

画解思路

所以JavaScript的引用数据类型(普通对象-object、数组对象-array),是指向同一个指针(内存地址),所以例子中的b修改了会影响到a

 

来谈谈深浅拷贝

浅拷贝

let a = [1, 2, 3]
let b = a.slice()
b[0] = 100
console.log(a) // [1, 2, 3]
console.log(b) // [100, 2, 3 ]

当修改b的时候,a的值并不会改变。why? 因为 b和a指向的指针(内存地址)已经不一样了,这就是浅拷贝

同时也是会有问题,看例子

let a = [1, 2, 3, {name: 'a'}]
let b = a.slice()
b[0] = 100
b[3].name = 'b'
console.log(a) // [ 1, 2, 3, { name: 'b' } ]
console.log(b) // [ 100, 2, 3, { name: 'b' } ]

明明指针(内存地址)指向都不一样了,为什么修改b[3].name,a[3].name的值也会跟着改变?

这就是浅拷贝的限制所在了。它只能拷贝一层对象。如果有对象的嵌套,那么浅拷贝将无能为力。所以深拷贝就是为了解决这个问题而生的,它能解决无限极的对象嵌套问题,实现彻底的拷贝。下面会介绍深拷贝,让我们先看看浅拷贝有多少种方法

Object.assign

小知识:object.assign(target, source1, source2, source3.....)方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象

let a = {name: "a", age: 13}
let b = Object.assign({}, a)
b.name = 'b'
b.age = 18
console.log(a) // { name: 'a', age: 13 }
console.log(b) // { name: 'b', age: 18 }

// 分割线

let a = {name: "a", age: 13}
let b = Object.assign({}, a, {name: 'b'})
console.log(a) // { name: 'a', age: 13 }
console.log(b) // { name: 'b', age: 13 }
b.age = 18
console.log(a) // { name: 'a', age: 13 }
console.log(b) // { name: 'b', age: 18 }

// 分割线

let a_name = { name: "a" }
let a_age = { age: 13 }

let b = Object.assign(a_name, a_age)
console.log(a_name) // { name: 'a', age: 13 }
console.log(a_age) // { age: 13 }
console.log(b) // { name: 'a', age: 13 }
b.name = "b"
b.age = 18
console.log(a_name) // { name: 'b', age: 18 }
console.log(a_age) // { age: 13 }
console.log(b) // { name: 'b', age: 18 }

// 分割线

let a_name = { name: "a" }
let a_age = { age: 13 }

let b = Object.assign({}, a_name, a_age)
console.log(a_name) // { name: 'a' }
console.log(a_age) // { age: 13 }
console.log(b) // { name: 'a', age: 13 }
b.name = "b"
b.age = 18
console.log(a_name) // { name: 'a' }
console.log(a_age) // { age: 13 }
console.log(b) // { name: 'b', age: 18 }

concat浅拷贝数组

let a = [1, 2, 3, 4]
let b = a.concat()
b[1] = "22"
console.log(a) // [ 1, 2, 3, 4 ]
console.log(b) // [ 1, '22', 3, 4 ]

slice浅拷贝数组

let a = [1, 2, 3]
let b = a.slice()
b[0] = 100
console.log(a) // [1, 2, 3]
console.log(b) // [100, 2, 3 ]

...展开运算符

let a = [1, 2, 3, 4]
let b = [...a]
b[1] = "22"
console.log(a) // [ 1, 2, 3, 4 ]
console.log(b) // [ 1, '22', 3, 4 ]

// 分割线

let a = { name: 'a', age: 13 }
let b = { ...a }
b.name = 'b'
b.age = 18
console.log(a) // { name: 'a', age: 13 }
console.log(b) // { name: 'b', age: 18 }

手写

function shallowClone (target) {
    if (typeof target === 'object' && target !== null) {
        let cloneTarget = Array.isArray(target) ? [] : {}
        for (let item in target) {
            if (target.hasOwnProperty(item)) {
                cloneTarget[item] = target[item]
            }
        }
        return cloneTarget
    } else {
        return
    }
}

let a = {name: "a", age: 13}
let b = shallowClone(a)
b.name = "b"
b.age = 18
console.log(a) // { name: 'a', age: 13 }
console.log(b) // { name: 'b', age: 18 }

好了,浅拷贝就讲到这里。

深拷贝

json序列化

let a = {
    name: 'a',
    age: 13
}
let b = JSON.parse(JSON.stringify(a))
b.name = "b"
console.log(a) // { name: 'a', age: 13 }
console.log(b) // { name: 'b', age: 13 }

json序列化能覆盖大多数的应用场景,但是实际上,对于一些严格的场景,这个方法是个坑,问题如下:

1.无法拷贝函数

let a = {
    name: "a",
    fn: (msg) => {
        console.log(msg)
    }
}
let b = JSON.parse(JSON.stringify(a));
a.fn("a") // a
b.fn("b") //  b.fn is not a function

2.无法解决循环引用问题

let a = {name: "a"}
a.info = a
console.log(a) // { name: 'a', info: [Circular] }
let b = JSON.parse(JSON.stringify(a)) // RangeError: Maximum call stack size exceeded

 拷贝a会出现系统栈溢出,因为出现了无限递归的情况

3.无法拷贝一些特殊对象,如:正则对象-regexp、日期对象-date等。

手写简易版深拷贝

let a = {
    name: 'a',
    age: 13
}
// 递归拷贝
function deepCopy(target) {
    if (typeof target === 'object' && target !== null) {
        let cloneTarget = Array.isArray(target) ? []: {};
        for (let item in target) {
          if (target.hasOwnProperty(item)) {
              cloneTarget[item] = deepCopy(target[item]);
          }
        }
        return cloneTarget;
    } else {
        return target;
    }
}

let b = deepCopy(a);
b.name = 'b'
console.log(a) // { name: 'a', age: 13 }
console.log(b) // { name: 'b', age: 13 }

这是一个递归拷贝,我们以刚刚发现的三个问题为导向,一步步来完善、优化我们的深拷贝代码

1.无法解决循环引用问题

对象a 中的某个属性指向 对象b 或者 对象b 中的某个属性指向 对象a 或者 对象a 中的某个属性指向 对象a,就会出现循环引用问题(当然不止这一种情况,不过原理是一样的)下面通过代码来说明一下。

let a = { name: 'a' }
let b = { b: a }
a.info = b
console.log(a) // { name: 'a', info: { b: [Circular] } }
console.log(b) // { b: { name: 'a', info: [Circular] } }

// 分割线

let a = {name: "a", age:14}
a.info = a
console.log(a) // { name: 'a', age: 14, info: [Circular] }

图。。。大概就这样吧

这时候就谈谈 JS 中垃圾回收策略

标记清除

JavaScript中最常见的垃圾收集方式是标记清除(mark-and-sweep)

当运行addTen()这个函数的时候,就是当变量进入环境时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”

function addTen(num){  
     var sum += num // 垃圾收集已将这个变量标记为“进入环境”
     return sum    // 垃圾收集已将这个变量标记为“离开环境”
}
addTen(10) // 输出20

 

可以使用任何方式来标记变量。比如,可以通过翻转某个特殊的位来记录一个变量何时进入环境, 或者使用一个“进入环境的”变量列表及一个“离开环境的”变量列表来跟踪哪个变量发生了变化。说到底,如何标记变量其实并不重要,关键在于采取什么策略。

let user = {name : 'scott', age : '21', gender : 'male'} // 在全局中定义变量,标记变量为“进入环境”
user = null // 最后定义为null,释放内存

 

垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间

引用计数

JavaScript中不太常见的垃圾收集策略叫做引用计数(reference counting)。引用计数的含义是跟踪记录每个值被引用的次数

当声明了一个变量并将一个引用类型值赋值该变量时,则这个值的引用次数就是1.

如果同一个值又被赋给另外一个变量,则该值得引用次数加1。

相反

如果包含对这个值引用的变量又取 得了另外一个值,则这个值的引用次数减 1。

当这个值的引用次数变成 0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。

这样,当垃圾收集器下次再运行时,它就会释放那 些引用次数为零的值所占用的内存。 

问题来了就是循环引用

function test() {
  let a = { name: 'a' }
  a.info = a
  a.info.name = "aa"
  console.log(a) // { name: 'aa', info: [Circular] }
}
test()

 

就用上面代码例子来说,a是引用类型,所以现在a的引用计算值为1,然后a.info又赋值为a,而a是引用类型所以a.info也是引用类型,所以现在a引用计算值为2

采用标记清除策略,由于函数执行之后,这两个对象都离开了作用域,因此这种相互引用不是个问题

采用引用计数策略,当函数执行完毕后,a还将继续存在,因为它们的引用次数永远不会是 0

假如这个函数被重复多次调用,就会导致大量内存得不到回收

小知识:所以要养成习惯,将变量设置为null意味着切断变量与它此前引用的值之间的连接。当垃圾收集器下次运行时,就会删除这些值并回收他们占用的内存。

回到 循环引用 那 解决方法有几种?

json序列化

let a = { name: 'a' }
let b = { b: JSON.parse(JSON.stringify(a)) }
a.info = JSON.parse(JSON.stringify(b))
console.log(a) // { name: 'a', info: { b: { name: 'a' } } }
console.log(b) // { b: { name: 'a' } }

// 分割线

let a = {name: "a", age:14}
a.info = JSON.parse(JSON.stringify(a))
console.log(a) // { name: 'a', age: 14, info: { name: 'a', age: 14 } }

还可以使用 json扩展包 JSON.decycle来去除循环引用。

不过我们还是以手写简易版为主,创建一个Map,记录下已经拷贝过的对象,如果说已经拷贝过,那直接返回它行了。

const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;
function deepCopy (target, map = new Map()) { 
  if(map.get(target)) return target
  if (isObject(target)) { 
    map.set(target, true)
    const cloneTarget = Array.isArray(target) ? []: {}
    for (let item in target) { 
      if (target.hasOwnProperty(item)) { 
          cloneTarget[item] = deepCopy(target[item],map)
      } 
    } 
    return cloneTarget
  } else { 
    return target
  } 
}
let a = {name: "a", age: 13}
a.info = a
let b = deepCopy(a)
b.name = "b"
b.age = 18
console.log(a) // { name: 'a', age: 13, info: [Circular] }
console.log(b) // { name: 'b', age: 18, info: { name: 'a', age: 13, info: [Circular] } }

好像是没有问题了, 拷贝也完成了。但还是有一个潜在的坑, 就是map 上的 key 和 map 构成了强引用关系,这是相当危险的。我给你解释一下与之相对的弱引用的概念你就明白了:

在计算机程序设计中,弱引用与强引用相对, 是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。 --百度百科

说的有一点绕,我用大白话解释一下,被弱引用的对象可以在 任何时候被回收,而对于强引用来说,只要这个强引用还在,那么对象无法被回收拿上面的例子说,map 和 a一直是强引用的关系, 在程序结束之前,a 所占的内存空间一直 不会被释放

怎么解决这个问题?

很简单,让 map 的 key 和 map 构成弱引用即可。ES6给我们提供了这样的数据结构,它的名字叫WeakMap,它是一种特殊的Map,其中的键是弱引用的。其键值必须是对象,而值可以是任意的。

function deepCopy (target, map = new WeakMap()) {
  //...
}

 

2.无法拷贝一些特殊对象

对于特殊的对象,我们使用以下方式来鉴别

Object.prototype.toString.call(obj);

梳理一下对于可遍历对象会有什么结果

["object Map"]
["object Set"]
["object Array"]
["object Object"]
["object Arguments"]

好,以这些不同的字符串为依据,我们就可以成功地鉴别这些对象。

const getType = Object.prototype.toString.call(obj);

const canTraverse = {
  '[object Map]': true,
  '[object Set]': true,
  '[object Array]': true,
  '[object Object]': true,
  '[object Arguments]': true,
};

const deepClone = (target, map = new Map()) => {
  if(!isObject(target)) 
    return target;
  let type = getType(target);
  let cloneTarget;
  if(!canTraverse[type]) {
    // 处理不能遍历的对象
    return;
  }else {
    // 这波操作相当关键,可以保证对象的原型不丢失!
    let ctor = target.prototype;
    cloneTarget = new ctor();
  }

  if(map.get(target)) 
    return target;
  map.put(target, true);

  if(type === mapTag) {
    //处理Map
    target.forEach((item, key) => {
      cloneTarget.set(deepClone(key), deepClone(item));
    })
  }
  
  if(type === setTag) {
    //处理Set
    target.forEach(item => {
      target.add(deepClone(item));
    })
  }

  // 处理数组和对象
  for (let prop in target) {
    if (target.hasOwnProperty(prop)) {
        cloneTarget[prop] = deepClone(target[prop]);
    }
  }
  return cloneTarget;
}

 不可遍历的对象

const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

对于不可遍历的对象,不同的对象有不同的处理

const handleRegExp = (target) => {
  const { source, flags } = target;
  return new target.constructor(source, flags);
}

const handleFunc = (target) => {
  // 待会的重点部分
}

const handleNotTraverse = (target, tag) => {
  const Ctor = targe.constructor;
  switch(tag) {
    case boolTag:
    case numberTag:
    case stringTag:
    case errorTag: 
    case dateTag:
      return new Ctor(target);
    case regexpTag:
      return handleRegExp(target);
    case funcTag:
      return handleFunc(target);
    default:
      return new Ctor(target);
  }
}

我们连拷贝函数一起写了,直接看总结果

3.无法拷贝函数

提到函数,在JS种有两种函数,一种是普通函数,另一种是箭头函数。每个普通函数都是 Function的实例,而箭头函数不是任何类的实例,每次调用都是不一样的引用。那我们只需要 处理普通函数的情况,箭头函数直接返回它本身就好了

那么如何来区分两者呢?

答案是: 利用原型。箭头函数是不存在原型的。

const handleFunc = (func) => {
  // 箭头函数直接返回自身
  if(!func.prototype) return func;
  const bodyReg = /(?<={)(.|\n)+(?=})/m;
  const paramReg = /(?<=\().+(?=\)\s+{)/;
  const funcString = func.toString();
  // 分别匹配 函数参数 和 函数体
  const param = paramReg.exec(funcString);
  const body = bodyReg.exec(funcString);
  if(!body) return null;
  if (param) {
    const paramArr = param[0].split(',');
    return new Function(...paramArr, body[0]);
  } else {
    return new Function(body[0]);
  }
}

到现在,我们的深拷贝就实现地比较完善了。不过在测试的过程中,我也发现了一个小小的bug

const target = new Boolean(false);
const Ctor = target.constructor;
new Ctor(target); // 结果为 Boolean {true} 而不是 false。

对于这样一个bug,我们可以对 Boolean 拷贝做最简单的修改, 调用valueOf: new target.constructor(target.valueOf())。

但实际上,这种写法是不推荐的。因为在ES6后不推荐使用【new 基本类型()】这 样的语法,所以es6中的新类型 Symbol 是不能直接 new 的,只能通过 new Object(SymbelType)。

因此我们接下来统一一下:

const handleNotTraverse = (target, tag) => {
  const Ctor = targe.constructor;
  switch(tag) {
    case boolTag:
      return new Object(Boolean.prototype.valueOf.call(target));
    case numberTag:
      return new Object(Number.prototype.valueOf.call(target));
    case stringTag:
      return new Object(String.prototype.valueOf.call(target));
    case errorTag: 
    case dateTag:
      return new Ctor(target);
    case regexpTag:
      return handleRegExp(target);
    case funcTag:
      return handleFunc(target);
    default:
      return new Ctor(target);
  }
}

OK!是时候给大家放出完整版的深拷贝啦

const getType = obj => Object.prototype.toString.call(obj);

const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;

const canTraverse = {
  '[object Map]': true,
  '[object Set]': true,
  '[object Array]': true,
  '[object Object]': true,
  '[object Arguments]': true,
};
const mapTag = '[object Map]';
const setTag = '[object Set]';
const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

const handleRegExp = (target) => {
  const { source, flags } = target;
  return new target.constructor(source, flags);
}

const handleFunc = (func) => {
  // 箭头函数直接返回自身
  if(!func.prototype) return func;
  const bodyReg = /(?<={)(.|\n)+(?=})/m;
  const paramReg = /(?<=\().+(?=\)\s+{)/;
  const funcString = func.toString();
  // 分别匹配 函数参数 和 函数体
  const param = paramReg.exec(funcString);
  const body = bodyReg.exec(funcString);
  if(!body) return null;
  if (param) {
    const paramArr = param[0].split(',');
    return new Function(...paramArr, body[0]);
  } else {
    return new Function(body[0]);
  }
}

const handleNotTraverse = (target, tag) => {
  const Ctor = target.constructor;
  switch(tag) {
    case boolTag:
      return new Object(Boolean.prototype.valueOf.call(target));
    case numberTag:
      return new Object(Number.prototype.valueOf.call(target));
    case stringTag:
      return new Object(String.prototype.valueOf.call(target));
    case symbolTag:
      return new Object(Symbol.prototype.valueOf.call(target));
    case errorTag: 
    case dateTag:
      return new Ctor(target);
    case regexpTag:
      return handleRegExp(target);
    case funcTag:
      return handleFunc(target);
    default:
      return new Ctor(target);
  }
}

const deepClone = (target, map = new WeakMap()) => {
  if(!isObject(target)) 
    return target;
  let type = getType(target);
  let cloneTarget;
  if(!canTraverse[type]) {
    // 处理不能遍历的对象
    return handleNotTraverse(target, type);
  }else {
    // 这波操作相当关键,可以保证对象的原型不丢失!
    let ctor = target.constructor;
    cloneTarget = new ctor();
  }

  if(map.get(target)) 
    return target;
  map.set(target, true);

  if(type === mapTag) {
    //处理Map
    target.forEach((item, key) => {
      cloneTarget.set(deepClone(key, map), deepClone(item, map));
    })
  }
  
  if(type === setTag) {
    //处理Set
    target.forEach(item => {
      cloneTarget.add(deepClone(item, map));
    })
  }

  // 处理数组和对象
  for (let prop in target) {
    if (target.hasOwnProperty(prop)) {
        cloneTarget[prop] = deepClone(target[prop], map);
    }
  }
  return cloneTarget;
}

手写深拷贝完毕!

在深入了解一下函数传参,这时候要先了解一下什么是形参、实参?

实参(argument):在调用有参函bai数时,函数名后面括号中的参数为“实际参数”

实参是:常量、变量、表达式、函数等,实参是任何类型的量,当开始在进行函数调用时,都必须有确定的值

形参(parameter):不是实际存在变量,又称虚拟变量

形参是:变量只有在被调用时才分配内存单元,在调用结束时,就会释放出所分配的内存单元。所以,形参只能在函数内部才有效

JavaScript

function test(obj) {
    obj.name = "b";
    return obj;
}
let a = {
    name: "a",
    age: 11
};
let b = test(a);
console.log(a); // { name: 'b', age: 11 }
console.log(b); // { name: 'b', age: 11 }

// 分割线

function test(obj) {
    obj = "b";
    return obj;
}
let a = 'a'
let b = test(a);
console.log(a); // a
console.log(b); // b

在函数传参也是一样。好了深浅拷贝就讲到这里!

 

null是对象吗?为什么?

结论: null不是对象。

解释: 虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象然而 null 表示为全零,所以将它错误的判断为 object 。

 

'1'.toString()为什么可以调用?

其实在这个语句运行的过程中做了这样几件事情:

var s = new String('1');
s.toString();
s = null;

第一步: 创建String类实例。

第二步: 调用实例方法。

第三步: 执行完方法立即销毁这个实例。

整个过程体现了基本包装类型的性质,而基本包装类型恰恰属于基本数据类型,包括Boolean, Number和String。

参考:《JavaScript高级程序设计(第三版)》P118

 

0.1+0.2为什么不等于0.3?

0.1和0.2在转换成二进制后会无限循环,由于标准位数的限制后面多余的位数会被截掉,此时就已经出现了精度的损失,相加后因浮点数小数位的限制而截断的二进制数字在转换为十进制就会变成0.30000000000000004。

 

参考:

https://juejin.im/post/5dbebbfa51882524c507fddb#heading-59

https://www.cnblogs.com/scottjeremy/p/6870729.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

An_s

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值