js高级总结(数据类型 闭包 原型链 继承)

分享自己对于js的一些知识总结,部分内容为转载,链接如下:

js原始数据类型和引用数据类型=>callback数据传输原理 - 黑白kn - 博客园

Object.create()和setPrototypeof和Child.prototype = Parent.prototype和Child.prototype = new Parent()的区别 - 乐乐学长 - 博客园

(建议收藏)原生JS灵魂之问, 请问你能接得住几个?(上)_杨兴元的博客-CSDN博客

一: JS数据类型

1.JS原始数据类型和引用数据类型详解

  • 1、基本类型
  • Boolean.  布尔值,true 和 false.
  • null. 一个表明 null 值的特殊关键字。 JavaScript 是大小写敏感的,因此 null 与 NullNULL或其他变量完全不同。
  • undefined.  变量未定义时的属性。
  • Number.  表示数字,例如: 42 或者 3.14159
  • String.  表示字符串,例如:"Howdy"
  • Symbol ( 在 ECMAScript 6 中新添加的类型).。一种数据类型,它的实例是唯一且不可改变的。
  • Bigint

2、复杂类型(引用数据类型):

对象Object(包含普通对象-Object,数组对象-Array,正则对象-RegExp,日期对象-Date,数学函数-Math,函数对象-Function)

引用类型的值是同时保存在栈内存和堆内存中的对象

javascript和其他语言不同,其不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间,那我们操作啥呢? 实际上,是操作对象的引用
所以引用类型的值是按引用访问的。
准确地说,引用类型的存储需要内存的栈区和堆区(堆区是指内存里的堆内存)共同完成,栈区内存保存变量标识符和指向堆内存中该对象的指针
也可以说是该对象在堆内存的地址。


假如有以下几个对象:

var person1 = {name:'jozo'};
var person2 = {name:'xiaom'};
var person3 = {name:'xiaoq'};

则这三个对象的在内存中保存的情况如下图:

var person1 = {};

var person2 = {};

console.log(person1 == person2); // false

引用类型时按引用访问的,换句话说就是比较两个对象的堆内存中的地址是否相同,那很明显,person1和person2在堆内存中地址是不同的。

不可变(immutable)性质

基本类型是不可变的(immutable),只有对象是可变的(mutable). 有时我们会尝试“改变”字符串的内容,但在JS中,任何看似对string值的”修改”操作,实际都是创建新的string值。任何方法都无法改变一个基本类型的值 

   var str = "abc";
   str[0]; // "a"
   str[0] = "d";
   console.log(str); //abc

   var name = 'jozo';
   var upName=name.toUpperCase(); 
   console.log(upName,name); // 输出 'JOZO' 'jozo'

方法操作无法改变一个基本类型的值 

  var str = "abc";
  str[0]; // "a"
  str[0] = "d";
  console.log(str); //abc

  var name = 'jozo';
  var upName=name.toUpperCase(); 
  console.log(upName,name); // 输出 'JOZO' 'jozo'

上面代码可知,我们不能给基本类型添加属性和方法

引用类型的值是可变的

1     var obj = {x : 0};
2     obj.x = 100;
3     var o = obj;
4     o.x = 1;
5     console.log(obj.x)// 1, 被修改
6     o = {x:100};  //等同于重新赋值,重新开辟内存,不是修改
7     console.log(JSON.stringify(obj),JSON.stringify(o))//{"x":1} {"x":100}
8     obj.x; // 1, 不会因o = {"x":100}改变

两种数据类型在实参和形参中的区别

按值传递 VS. 按引用传递

按值传递(call by value)是最常用的求值策略:函数的形参是被调用时所传实参的副本。修改形参的值并不会影响实参。

按引用传递(call by reference)时,函数的形参接收实参的隐式引用,而不再是副本。这意味着函数形参的值如果被修改,实参也会被修改。同时两者指向相同的值

按引用传递会使函数调用的追踪更加困难,有时也会引起一些微妙的BUG。

按值传递由于每次都需要克隆副本,对一些复杂类型,性能较低。两种传值方式都有各自的问题。

demo实例

typeof 运算符,返回一个字符串,指示未计算的操作数的类型

    var person,name;
    person = 'kn';

    name=person;
    person='黑白';
    console.log(person,name,typeof person)
    //黑白 kn string

person的改变没有改变name,说明 string 是按值传递的。赋值时创建一块新的内存空间

  • 实参:可以是常量、变量、表达式、函数等, 无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值, 以便把这些值传送给形参。 因此应预先用赋值,输入等办法使实参获得确定值。
  • 形参:全称为形式参数是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传递的参数。
    形参的作用是实现主调函数与被调函数之间的联系,通常将函数所处理的数据,影响函数功能的因素或者函数处理的结果作为形参。
function addNum(param)  //相对于addNum方法来说 param是形参
{ 
    param+=10; 
     return param; 
} 
var num=10;  
var result=addNum(num); //相对于addNum方法来说 num是实参
console.log(num);  //10  
console.log(result);//20

上面的例子中,当将 num作为实参传入方法 addNum是 param作为形参接受 并在方法体内使用,且num在全局中没用改变

但当实参为引用类型时

1 function fun(param)  //相对于fun方法来说 param是形参
2 { 
3     param[0]=99; 
4      return param; 
5 } 
6 var num=[10];  
7 var result=fun(num); //相对于fun方法来说 num是实参
8 console.log(num[0]);  //99 
9 console.log(result);//[99]

在方法体内改变 形参将同时改变实参。这个是其他语言中不会存在的

 思考: 正因为js中 function的形参若为引用类型时,可以影响实参! 推断 callback中参数原理

​
 1     function fun(data,callback){
 2         var json=[1,2,3];
 3         callback(json)
 4     }
 5 
 6     var data=[];
 7     fun(data,function(result){
 8         data=result;
 9     })
10     console.log(data)//[1, 2, 3]

​

如上例子 在回调函数中修改了 变量 data

知识点1、 function 是一种数据类型,可以当做参数传递 2、数组是引用类型 3、引用类型的形参会影响实参

1 <body>
 2     <button onclick='log()'>ajax</button>
 3 </body>
 4 <script>
 5     function fun(data,callback){
 6         setTimeout(function(){
 7             var json=[1,2,3];
 8             callback(json)
 9         },4000)
10     
11     }
12 
13     var data=[];
14     fun(data,function(res){
15         data=res;
16     })
17     console.log(data)//[]
18     function log(){
19         console.log(data)//[1, 2, 3]  4秒后输出 
20     }
21 
22 </script>

使用 setTimeout 模拟ajax请求! 

2. JS中类型转换有哪几种?

JS中,类型转换只有三种:

  • 转换成数字
  • 转换成布尔值
  • 转换成字符串

转换具体规则如下:

 注意"Boolean 转字符串"这行结果指的是 true 转字符串的例子

二: 谈谈你对闭包的理解

作用域、作用域链、执行环境、闭包

变量的作用域:全局变量和局部变量。 

全局作用域: 最外层函数定义的变量拥有全局作用域,即对任何内部函数来说,都是可以访问的

局部作用域: 和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,而对于函数外部是无法访问的。

注意:函数内部声明变量的时候,一定要使用var命令。如果不用的话,实际上声明了一个全局变量。

执行环境

每个函数运行时都会产生一个执行环境。

js为每一个执行环境关联了一个变量对象。环境中定义的所有变量和函数都保存在这个对象中。 

全局执行环境是最外围的执行环境,全局执行环境被认为是window对象,因此所有的全局变量和函数都作为window对象的属性和方法创建的。 

js的执行顺序是根据函数的调用来决定的,当一个函数被调用时,该函数环境的变量对象就被压入一个环境栈中。而在函数执行之后,栈将该函数的变量对象弹出,把控制权交给之前的执行环境变量对象。 

举个例子:

 <script>
      var scope = "global"; 
      function fn1(){
         return scope; 
      }
      function fn2(){
         return scope;
      }
      fn1();
      fn2();
</script>

作用域链

每个执行环境都有一个与之关联的变量对象,环境中所有的变量和函数都保存在这个变量对象里面。这个变量对象虽然我们无法访问,但是解释器会在后台使用。

当代码在一个环境中执行时,会创建变量对象的一个作用域链,作用域链的前端始终是当前执行的代码所在环境的变量对象,下一个变量对象来自外部包含环境,一直延伸到全局执行环境。

每个函数都有自己的执行环境,当执行到一个函数时,该函数的环境被推入一个环境栈,函数执行后将其弹出。

闭包

  • 通常解释为:有权访问另一个函数作用域中变量的函数。本质上,闭包是将函数内部和函数外部连接起来的桥梁。
  • 准确来说:在内部函数的作用域链中保留了(原)外部函数的活动对象的现象。    (闭包是基于正常的垃圾回收处理机制下的。也就是说,一般情况一个函数(函数作用域)执行完毕,里面声明的变量会全部释放,被垃圾回收器回收。但闭包利用一个技巧,让作用域里面的变量,在函数执行完之后依旧保存没有被垃圾回收处理掉。)
  • 作用: 1:可以读取自身函数外部的变量(沿着作用域链寻找)     2:让这些外部变量始终保存在内存中 
  • 优点:1:变量长期驻扎在内存中;
               2:避免全局变量的污染;
               3:私有成员的存在 ;
  • 特性:1:函数套函数;
               2:内部函数可以直接使用外部函数的局部变量或参数;
               3:变量或参数不会被垃圾回收机制回收;
  • 缺点:常驻内存 会增大内存的使用量 使用不当会造成内存泄露,详解:

(1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

(2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法,把内部变量当作它的私有属性,这时一定要小心,不要随便改变父函数内部变量的值。

  • 闭包常见的创建方式是在一个函数中返回另一个函数。
function foo(x) {
    var tmp = 3;
    return function (y) {
        alert(x + y + (++tmp));
    }
   }
       
var bar = foo(2); // bar 现在是一个闭包
        bar(10)  //16

执行var bar = foo(2)时; foo就执行了,参数2也传进去了,但是执行完毕之后,因为返回值里面还等待使用tmp变量以及参数x,所以此时,foo虽然执行了,但是foo的变量并没有被释放,在return在等待继续使用这些变量了,这个时候bar就是一个闭包;然后我们再执行bar,结果是16,这就是闭包的神奇之处,它改变了JS的内存机制。 

function foo(x) {
    var tmp = 3;
    function bar(y) {
        alert(x + y + (++tmp));
    }
    bar(10);
}
     
 foo(2)


foo在全局中执行,执行过程中访问到了局部变量,但他不是闭包。函数foo执行完毕之后没有阻止foo作用域中的变量被垃圾回收,所有不是闭包。

什么是闭包?

  • 红宝书(p178)上对于闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数
  • MDN 对闭包的定义为:闭包是指那些能够访问自由变量的函数。(其中自由变量,指在函数中使用的,但既不是函数参数arguments也不是函数的局部变量的变量,其实就是另外一个函数作用域中的变量。)

闭包产生的原因?

首先要明白作用域链的概念,其实很简单,在ES5中只存在两种作用域————全局作用域和函数作用域, 当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链,值得注意的是,每一个子函数都会拷贝上级的作用域,形成一个作用域的链条。

比如:

 var a = 1;
function f1() {
 var a = 2 
function f2() { 
var a = 3; console.log(a);//3
 }
}

在这段代码中,f1的作用域指向有全局作用域(window)和它本身,而f2的作用域指向全局作用域(window)、f1和它本身。而且作用域是从最底层向上找,直到找到全局作用域window为止,如果全局还没有的话就会报错。就这么简单一件事情!

闭包产生的本质就是,当前环境中存在指向父级作用域的引用。还是举上面的例子:

function f1() { 
var a = 2 
function f2() { 
console.log(a);//2 
}
 return f2;
}
var x = f1();
x();

这里x会拿到父级作用域中的变量,输出2。因为在当前环境中,含有对f2的引用,f2恰恰引用了window、f1和f2的作用域。因此f2可以访问到f1的作用域的变量。

那是不是只有返回函数才算是产生了闭包呢?、

回到闭包的本质,我们只需要让父级作用域的引用存在即可,因此我们还可以这么做:

var f3;

function f1() { 
var a = 2 
f3 = function() {
 console.log(a); 
}
}

f1();
f3();

让f1执行,给f3赋值后,等于说现在 f3拥有了window、f1和f3本身这几个作用域的访问权限,还是自底向上查找, 最近是在f1中找到了a,因此输出2。

在这里是外面的变量 f3存在着父级作用域的引用,因此产生了闭包,形式变了,本质没有改变。

闭包有哪些表现形式?

明白了本质之后,我们就来看看,在真实的场景中,究竟在哪些地方能体现闭包的存在?

  • 返回一个函数。刚刚已经举例。
  • 作为函数参数传递
var a = 1;

function foo(){ 
var a = 2; 
function baz(){ 
console.log(a);
 } 
bar(baz);
}

function bar(fn){ 
// 这就是闭包
 fn();
}
// 输出2,而不是1
foo();
  • 在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。

以下的闭包保存的仅仅是window和当前作用域。

// 定时器

setTimeout(

function timeHandler(){

console.log('111');},100)

// 事件监听

$('#app').click(function(){ console.log('DOM Listener');})
  • IIFE(立即执行函数表达式)创建闭包, 保存了 全局作用域window和 当前函数的作用域,因此可以全局的变量。
 var a = 2;

(function IIFE(){console.log(a); // 输出2})();

如何解决下面的循环输出问题?

 for(var i = 1; i <= 5; i ++){
 setTimeout(function timer(){ 
console.log(i) 
}, 0)}

为什么会全部输出6?如何改进,让它输出1,2,3,4,5?(方法越多越好)

因为setTimeout为宏任务,由于JS中单线程eventLoop机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后setTimeout中的回调才依次执行,但输出i的时候当前作用域没有,往上一级再找,发现了i,此时循环已经结束,i变成了6。因此会全部输出6。

解决方法:

1、利用IIFE(立即执行函数表达式)当每次for循环时,把此时的i变量传递到定时器中

for(var i = 1;i <= 5;i++){ 
(function(j){ 
setTimeout(
function timer(){ 
console.log(j) }, 0) 
})(i)}

2、给定时器传入第三个参数, 作为timer函数的第一个函数参数

for(var i=1;i<=5;i++){

setTimeout(

function timer(j){

 console.log(j)

}, 0, i)}

3、使用ES6中的let

 for(let i = 1; i <= 5; i++){

setTimeout(

function timer(){

console.log(i)

},0)}

let使JS发生革命性的变化,让JS有函数作用域变为了块级作用域,用let后作用域链不复存在。代码的作用域以块级为单位,以上面代码为例:

   // i = 1{ setTimeout(function timer(){ console.log(1) },0)}// i = 2{ setTimeout(function timer(){ console.log(2) },0)}// i = 3...

因此能输出正确的结果。

三: 谈谈你对原型链的理解

1.构造函数

每一个构造函数都有一个protype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的所拥有。

这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在protype对象上。

function Person (name, age) {
  this.name = name
  this.age = age
}

Person.prototype = {
  constructor: Person, // => 手动将 constructor 指向正确的构造函数
  type: 'human',
  sayHello: function () {
    console.log('我叫' + this.name + ',我今年' + this.age + '岁了')
  }
}
var p1 = new Person(...)
var p2 = new Person(...)
console.log(p1.sayName === p2.sayName) // => true

这时所有实例的type属性和sayname方法,其实都是同一个内存地址,指向ptotype对象,因此就提高了运行效率。

2.构造函数、实例、原型三者之间的关系

  • 任何函数都具有一个 prototype 属性,该属性是一个对象(原型对象)
  • 构造函数的 prototype 对象默认都有一个 constructor 属性,指向 prototype 对象所在函数
  • 通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype 对象的指针 __proto__
  • 所有实例都直接或间接继承了原型对象的成员

3.原型链

实例对象可以访问原型对象中的成员

每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性

  • 搜索首先从对象实例本身开始
  • 如果在实例中找到了具有给定名字的属性,则返回该属性的值
  • 如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性
  • 如果在原型对象中找到了这个属性,则返回该属性的值
  • 先在自己身上找,找到即返回
  • 自己身上找不到,则沿着原型链向上查找,找到即返回
  • 如果一直到原型链的末端还没有找到,则返回 undefined
  • JavaScript对象通过prototype指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条, 即原型链。

  • 对象的 hasOwnProperty() 来检查对象自身中是否含有该属性
  • 使用 in 检查对象中是否含有某个属性时,如果对象中没有但是原型链中有,也会返回 true

四: JS如何实现继承?

第一种: 借助call

function Parent1(){ 
this.name = 'parent1'; 
} 
function Child1(){
 Parent1.call(this); 
this.type = 'child1'
 } 
console.log(new Child1);

这样写的时候子类虽然能够拿到父类的属性值,但是问题是父类原型对象中一旦存在方法那么子类无法继承。那么引出下面的方法。

第二种: 借助原型链

function Parent2() { 
this.name = 'parent2';
 this.play = [1, 2, 3]
 } 
function Child2() { 
this.type = 'child2'; } 
Child2.prototype =new Parent2(); 
console.log(new Child2());

看似没有问题,父类的方法和属性都能够访问,但实际上有一个潜在的不足。举个例子:

var s1 = new Child2(); 
var s2 = new Child2();
 s1.play.push(4); 
console.log(s1.play, s2.play);

可以看到控制台:

明明我只改变了s1的play属性,为什么s2也跟着变了呢?很简单,因为两个实例使用的是同一个原型对象。

那么还有更好的方式么?

第三种:将前两种组合

function Parent3 () { 
this.name = 'parent3';
 this.play = [1, 2, 3]; 
}
function Child3() { 
Parent3.call(this); 
this.type = 'child3'; 
} 
Child3.prototype = new Parent3(); 
var s3 = new Child3(); 
var s4 = new Child3(); 
s3.play.push(4); 
console.log(s3.play, s4.play);

可以看到控制台:

之前的问题都得以解决。但是这里又徒增了一个新问题,那就是Parent3的构造函数会多执行了一次(Child3.prototype = new Parent3();)。这是我们不愿看到的。那么如何解决这个问题?

第四种: 组合继承的优化1

function Parent4 () { 
this.name = 'parent4';
 this.play = [1, 2, 3]; 
} 
function Child4() {
Parent4.call(this); 
this.type = 'child4'; 
} 
Child4.prototype = Parent4.prototype;

这里让将父类原型对象直接给到子类,父类构造函数只执行一次,而且父类属性和方法均能访问,但是我们来测试一下:

var s3 = new Child4(); 
var s4 = new Child4(); 
console.log(s3)

子类实例的构造函数是Parent4,显然这是不对的,应该是Child4。

第五种(最推荐使用): 组合继承的优化1

function Parent5 () { 
this.name = 'parent5'; 
this.play = [1, 2, 3];
 } 
function Child5() { 
Parent5.call(this); 
this.type = 'child5'; 
} 
Child5.prototype = Object.create(Parent5.prototype); 
Child5.prototype.constructor = Child5;

这是最推荐的一种方式,接近完美的继承,它的名字也叫做寄生组合继承。

Object.create()和setPrototypeof和Child.prototype = Parent.prototype和Child.prototype = new Parent()的区别 - 乐乐学长 - 博客园



Object.create()和setPrototypeof和Child.prototype = Parent.prototype和Child.prototype = new Parent()的区别

Child.prototype = Parent.prototype和Child.prototype = new Parent()的区别

1.Child.prototype = new Parent() 会导致Child多个实例指向同一个引用,因为new Parent返回的是一个引用类型,此时修改Child的某个实例的属性,其它实例的属性也会被修改。

//父类
function Parent(){
    this.name = "P_Name";
}
Parent.prototype.func=function(){
    console.log("Parent Func");
}
//子类
function Child(){
}
Child.prototype=new Parent();

Child.prototype.func = function(){
    console.log("Child Func");
}
var child = new Child;
var parent = new Parent;

child.func();//Child Func
parent.func();//Parent Func

console.log(child.name);//P_Name
console.log(Child.prototype.constructor) // Parent

2、Child.prototype = Parent.prototype取不到Parent的构造函数属性,constructor指向Parent,而且对子类的原型方法重写会使父类的原型方法被重写

function Child2(){
}

# 以下赋值将覆盖Parent.prototype.func方法
Child2.prototype=Parent.prototype;

Child2.prototype.func = function(){
    console.log("Child2 Func");
}

var child2 = new Child2;

child2.func();//Child2 Func
parent.func();//Child2 Func

console.log(child2.name);//undefined
console.log(Child2.prototype.constructor) // Parent

setPrototypeOf 与 Object.create区别

  • 可以用prototype访问的只有function类型,其他类型只能用getPrototypeOf或者proto,其他类型也都是通过function生成的(String,Number……涉及到隐式创建对象)
  • 对象的原型只是一个引用,指向另外一个对象。对象原型之间的嵌套组成了原型链,原型链的作用是维护访问对象属性的查询,确定访问权限。

若存在A和B俩个函数,让A的原型指向B

1.setPrortotypeOf

Object.setPrototypeOf(A.prototype,B.prototype)

2.Create

A.prototype = Object.create(B.prototype)

俩者都可以达到设置对象原型的功能,但是具体表现上有一些区别。

代码如下

使用Object.create

Animal.prototype = Object.create(Plants.prototype)
    console.log(Animal.prototype)
    /*
    Plants {}
        __proto__:
            shout: ƒ (xssss)
            genO2: ƒ ()
            constructor: ƒ Plants()
            __proto__: Object
    */
    let cat = new Animal('mimi','miao~miao~')
    
    dog.shout() // pipi wang!wang!
    cat.shout() // mimi miao~miao~ plants tag
    cat.genO2() // mimi 生成氧气。

使用setPrototypeOf

Object.setPrototypeOf(Animal.prototype,Plants.prototype)
    console.log(Animal.prototype)
    /*
    Plants {shout: ƒ, constructor: ƒ}
        shout: ƒ (xssss)
        constructor: ƒ Animal(name,sound)
        __proto__:
        shout: ƒ ()
        genO2: ƒ ()
        constructor: ƒ Plants()
        __proto__: Object
    */
    dog.shout() // pipi wang!wang!
    cat.shout() // mimi miao~miao~
    cat.genO2() // mimi 生成氧气。

总结

  • 使用Object.create,Animal.prototype将会指向一个空对象,空对象的原型属性指向Plants的prototytpe。所以我们不能再访问Animal的原有prototypoe中的属性。Object.create的使用方式也凸显了直接重新赋值。
  • 使用Object.setPrototypeOf则会将Animal.prototype将会指向Animal原有的prototype,然后这个prototype的prototype再指向Plants的prototytpe。所以我们优先访问的Animal,然后再是plants
  • 在进行俩个原型之间的委托时使用setPrototype更好,Object.create更适和直接对一个无原生原型的对象快速进行委托。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值