JavaScript中的面向对象

  • 一个例子:集合类

集合(set)是一种数据结构,用以表示非重复值的无序集合。集合的基础方法包括添加值、检测值是否在集合中,这种集合需要一种通用的实现,以保证操作效率。JavaScript的对象是属性名以及与之对应的值的基本集合。因此将对象只用做字符串的集合是大材小用。例子9-6用JavaScript实现了一个更加通用的Set类,它实现了从JavaScript值到唯一字符串的映射,然后将字符串用做属性名。对象和函数都不具备如此简明可靠的唯一字符串表示。因此集合类必须给集合中的每一个对象或函数定义一个唯一的属性标识。

例9-6:Set.js:值的任意集合

function Set(){//这是一个构造函数
    this.values={};//集合数据保存在对象的属性里
    this.n=0;//集合中值的个数
    this.add.apply(this,arguments);//把所有参数都添加进这个集合
}

//将每个参数都添加至集合中
Set.prototype.add=function(){
    for(var i=0;i<arguments.length;i++){//遍历每个参数
        var val=arguments[i];//待添加到集合中的值
        var str=Set._v2s(val);//把它转换为字符串
        if(!this.values.hasOwnProperty(str)){//如果不在集合中
            this.values[str]=val;//将字符串和值对应起来
            this.n++;//集合中值的计数加一
        }
    }
    return this;//支持链式方法调用
};

//从集合删除元素,这些元素由参数指定
Set.prototype.remove=function(){
    for(var i=0;i<arguments.length;i++){//遍历每个参数
        var str=Set._v2s(arguments[i]);//将字符串和值对应起来
        if(this.values.hasOwnProperty(str)){//如果它在集合中
            delete this.values[str];//删除它
            this.n--;//集合中值的计数减一
        }
    }
    return this;//支持链式方法调用
};

//如果集合包含这个值,则返回true;否则,返回false
Set.prototype.contains=function(value){
    return this.values.hasOwnProperty(Set._v2s(value));
};

//返回集合的大小
Set.prototype.size=function(){
    return this.n;
};

//遍历集合中的所有元素,在指定的上下文中调用f
Set.prototype.foreach=function(f,context){
    for(var s in this.values)//遍历集合中的所有字符串
        if(this.values.hasOwnProperty(s))//忽略继承的属性
            f.call(context,this.values[s]);//调用f,传入value
};

//这是一个内部函数,用以将任意JavaScript值和唯一的字符串对应起来
Set._v2s=function(val){
    switch(val){
        case undefined:return'u';//特殊的原始值
        case null:return'n';//值只有一个字母
        case true:return't';//代码
        case false:return'f';
        default:switch(typeof val){
            case'number':return'#'+val;//数字都带有#前缀
            case'string':return'"'+val;//字符串都带有"前缀
            default:return'@'+objectId(val);//Objs and funcs get@
        }
    }


//对任意对象来说,都会返回一个字符串
//针对不同的对象,这个函数会返回不同的字符串
//对于同一个对象的多次调用,总是返回相同的字符串
//为了做到这一点,它给o创建了一个属性,在ES5中,这个属性是不可枚举且是只读的

    function objectId(o){
        var prop="|**objectid**|";//私有属性,用以存放id
        if(!o.hasOwnProperty(prop))//如果对象没有id
            o[prop]=Set._v2s.next++;//将下一个值赋给它
        return o[prop];//返回这个id
    }
};

Set._v2s.next=100;//设置初始id的值
  • 一个例子:枚举类型

枚举类型(enumerated type)是一种类型,它是值的有限集合,如果值定义为这个类型则该值是可列出(或“可枚举”)的。在C及其派生语言中,枚举类型是通过关键字enum声明的。Enum是ECMAScript 5中的保留字(还未使用),很有可能在将来JavaScript就会内置支持枚举类型。到那时,例9-7展示了如何在JavaScript中定义枚举类型的数据。需要注意的是,这里用到了例6-1中的inherit()函数。

例9-7包含一个单独函数enumeration()。但它不是构造函数,它并没有定义一个名叫"enumeration"的类。相反,它是一个工厂方法,每次调用它都会创建并返回一个新的类,比如:

//使用4个值创建新的Coin类:Coin.Penny,Coin.Nickel等
var Coin=enumeration({Penny:1,Nickel:5,Dime:10,Quarter:25});

var c=Coin.Dime;//这是新类的实例

c instanceof Coin//=>true:instanceof正常工作

c.constructor==Coin//=>true:构造函数的属性正常工作

Coin.Quarter+3*Coin.Nickel//=>40:将值转换为数字

Coin.Dime==10//=>true:更多转换为数字的例子

Coin.Dime>Coin.Nickel//=>true:关系运算符正常工作

String(Coin.Dime)+":"+Coin.Dime//=>"Dime:10":强制转换为字符串

这个例子清楚地展示了JavaScript类的灵活性,JavaScript的类要比C++和Java语言中的静态类要更加灵活。

例9-7:JavaScript中的枚举类型

//这个函数创建一个新的枚举类型,实参对象表示类的每个实例的名字和值
//返回值是一个构造函数,它标识这个新类
//注意,这个构造函数也会抛出异常:不能使用它来创建该类型的新实例
//返回的构造函数包含名/值对的映射表
//包括由值组成的数组,以及一个foreach()迭代器函数

function enumeration(namesToValues){//这个虚拟的构造函数是返回值
    var enumeration=function(){throw"Can't Instantiate Enumerations";};//枚举值继承自这个对象
    var proto=enumeration.prototype={
        constructor:enumeration,//标识类型
        toString:function(){return this.name;},//返回名字
        valueOf:function(){return this.value;},//返回值
        toJSON:function(){return this.name;}//转换为JSON
    };
    enumeration.values=[];//用以存放枚举对象的数组

//现在创建新类型的实例
    for(name in namesToValues){//遍历每个值
        var e=inherit(proto);//创建一个代表它的对象
        e.name=name;//给它一个名字
        e.value=namesToValues[name];//给它一个值
        enumeration[name]=e;//将它设置为构造函数的属性
        enumeration.values.push(e);//将它存储到值数组中
    }

//一个类方法,用来对类的实例进行迭代
    enumeration.foreach=function(f,c){
        for(var i=0;i<this.values.length;i++)f.call(c,this.values[i]);
    };//返回标识这个新类型的构造函数
    return enumeration;
}

 

  • 标准转换方法

有一些方法是在需要做类型转换时由JavaScript解释器自动调用的。不需要为定义的每个类都实现这些方法,但这些方法的确非常重要。

最重要的方法首当toString()。这个方法的作用是返回一个可以表示这个对象的字符串。在希望使用字符串的地方用到对象的话(比如将对象用做属性名或使用“+”运算符来进行字符串连接运算),JavaScript会自动调用这个方法。如果没有实现这个方法,类会默认从Object.prototype中继承toString()方法,这个方法的运算结果是"[object Object]",这个字符串用处不大。toString()方法应当返回一个可读的字符串,这样最终用户才能将这个输出值利用起来,然而有时候并不一定非要如此,不管怎样,可以返回可读字符串的toString()方法也会让程序调试变得更加轻松。

toLocaleString()和toString()极为类似:toLocaleStri ng()是以本地敏感性(locale-sensitive)的方式来将对象转换为字符串。默认情况下,对象所继承的toLocaleString()方法只是简单地调用toString()方法。有一些内置类型包含有用的toLocaleString()方法用以实际上返回本地化相关的字符串。如果需要为对象到字符串的转换定义toString()方法,那么同样需要定义toLocaleString()方法用以处理本地化的对象到字符串的转换。下面的Set类的定义中会有相关代码。

第三个方法是valueOf(),它用来将对象转换为原始值。比如,当数学运算符(除了“+”运算符)和关系运算符作用于数字文本表示的对象时,会自动调用valueOf()方法。大多数对象都没有合适的原始值来表示它们,也没有定义这个方法。但在例9-7中的枚举类型的实现则说明valueOf()方法是非常重要的。

第四个方法是toJSON(),这个方法是由JSON.stringify()自动调用的。JSON格式用于序列化良好的数据结构,而且可以处理JavaScript原始值、数组和纯对象。它和类无关,当对一个对象执行序列化操作时,它会忽略对象的原型和构造函数。比如将Range对象或Complex对象作为参数传入JSON.stringify(),将会返回诸如{"form":1,"to":3}或{"r":1,"i":-1}这种字符串。如果将这些字符串传入JSON.parse(),则会得到一个和Range对象和Complex对象具有相同属性的纯对象,但这个对象不会包含从Range和Complex继承来的方法。

这种序列化操作非常适用于诸如Range和Complex这种类,但对于其他一些类则必须自定义toJSON()方法来定制个性化的序列化格式。如果一个对象有toJSON()方法,JSON.stringify()并不会对传入的对象做序列化操作,而会调用toJSON()来执行序列化操作(序列化的值可能是原始值也可能是对象)。比如,Date对象的toJSON()方法可以返回一个表示日期的字符串。例9-7中的枚举类型也是如此:它们的toJSON()方法和toString()方法完全一样。如果要模拟一个集合,最接近JSON的表示方法就是数组,因此在下面的例子中将定义toJSON()方法用以将集合对象转换为值数组。

例9-6中的Set类并没有定义上述方法中的任何一个。JavaScript中没有哪个原始值可以表示集合,因此也没必要定义valueOf()方法,但该类应当包含toString()、toLocaleString()和toJSON()方法。可以用如下代码来实现。注意extend()函数的用法,这里使用extend()来向Set.prototype来添加方法:

//将这些方法添加至Set类的原型对象中
extend(Set.prototype,{//将集合转换为字符串
    toString:function(){
        var s="{",
            i=0;
        this.foreach(function(v){s+=((i++>0)?",":"")+v;});
        return s+"}";
    },

//类似toString,但是对于所有的值都将调用toLocaleString()
    toLocaleString:function(){
        var s="{",i=0;
        this.foreach(function(v){
            if(i++>0)s+=",";
            if(v==null)s+=v;//null和undefined
            else s+=v.toLocaleString();//其他情况
        });
        return s+"}";
    },

//将集合转换为值数组
    toArray:function(){
        var a=[];
        this.foreach(function(v){a.push(v);});
        return a;
    }
});

//对于要从JSON转换为字符串的集合都被当做数组来对待
Set.prototype.toJSON=Set.prototype.toArray;
  • 比较方法

JavaScript的相等运算符比较对象时,比较的是引用而不是值。也就是说,给定两个对象引用,如果要看它们是否指向同一个对象,不是检查这两个对象是否具有相同的属性名和相同的属性值,而是直接比较这两个单独的对象是否相等,或者比较它们的顺序(就像“<”和“>”运算符进行的比较一样)。如果定义一个类,并且希望比较类的实例,应该定义合适的方法来执行比较操作。

Java编程语言有很多用于对象比较的方法,将Java中的这些方法借用到JavaScript中是一个不错的主意。为了能让自定义类的实例具备比较的功能,定义一个名叫equals()实例方法。这个方法只能接收一个实参,如果这个实参和调用此方法的对象相等的话则返回true。当然,这里所说的“相等”的含义是根据类的上下文来决定的。对于简单的类,可以通过简单地比较它们的constructor属性来确保两个对象是相同类型,然后比较两个对象的实例属性以保证它们的值相等。例9-3中的Complex类就实现了这样的equals()方法,我们可以轻易地为Range类也实现类似的方法:

//Range类重写它的constructor属性,现在将它添加进去
Range.prototype.constructor=Range;//一个Range对象和其他不是Range的对象均不相等
//当且仅当两个范围的端点相等,它们才相等
Range.prototype.equals=function(that){
    if(that==null)return false;//处理null和undefined
    if(that.constructor!==Range)return false;//处理非Range对象
//当且仅当两个端点相等,才返回true
    return this.from==that.from&&this.to==that.to;
}

给Set类定义equals()方法稍微有些复杂。不能简单地比较两个集合的values属性,还要进行更深层次的比较:

Set.prototype.equals=function(that){//一些次要情况的快捷处理
    if(this===that)return true;//如果that对象不是一个集合,它和this不相等
//我们用到了instanceof,使得这个方法可以用于Set的任何子类
//如果希望采用鸭式辩型的方法,可以降低检查的严格程度
//或者可以通过this.constructor==that.constructor来加强检查的严格程度
//注意,null和undefined两个值是无法用于instanceof运算的
    if(!(that instanceof Set))return false;//如果两个集合的大小不一样,则它们不相等
    if(this.size()!=that.size())return false;//现在检查两个集合中的元素是否完全一样
//如果两个集合不相等,则通过抛出异常来终止foreach循环
    try{
        this.foreach(function(v){if(!that.contains(v))throw false;});
        return true;//所有的元素都匹配:两个集合相等
    }catch(x){
        if(x===false)return false;//如果集合中有元素在另外一个集合中不存在
        throw x;//重新抛出异常
    }
};

按照我们需要的方式比较对象是否相等常常是很有用的。对于某些类来说,往往需要比较一个实例“大于”或者“小于”另外一个示例。比如,你可能会基于Range对象的下边界来定义实例的大小关系。枚举类型可以根据名字的字母表顺序来定义实例的大小,也可以根据它包含的数值(假设它包含的都是数字)来定义大小。另一方面,Set对象其实是无法排序的。

如果将对象用于JavaScript的关系比较运算符,比如“<”和“<=”,JavaScript会首先调用对象的valueOf()方法,如果这个方法返回一个原始值,则直接比较原始值。例9-7中由enumeration()方法所返回的枚举类型包含valueOf()方法,因此可以使用关系运算符对它们做有意义的比较。但大多数类并没有valueOf()方法,为了按照显式定义的规则来比较这些类型的对象,可以定义一个名叫compareTo()的方法(同样,这里遵照Java中的命名约定)。

compareTo()方法应当只能接收一个参数,这个方法将这个参数和调用它的对象进行比较。如果this对象小于参数对象,compareTo()应当返回比0小的值。如果this对象大于参数对象,应当返回比0大的值。如果两个对象相等,应当返回0。这些关于返回值的约定非常重要,这样我们可以用下面的表达式替换掉关系比较和相等性运算符:

例9-8中的Card类定义了该类的compareTo()方法,可以给Range类添加一个类似的方法,用以比较它们的下边界:

Range.prototype.compareTo=function(that){
return this.from-that.from;
};

需要注意的是,这个方法中的减法操作根据两个Range对象的关系正确地返回了小于0、等于0和大于0的值。例9-8中的Card.Rank枚举值包含valueOf()方法,其实也可以给Card类实现类似的compareTo()方法。

上文所提到的equals()方法对其参数执行了类型检查,如果参数类型不合法则返回false。compareTo()方法并没有返回一个表示“这两个值不能比较”的值,由于compareTo()没有对参数做任何类型检查,因此如果给compareTo()方法传入错误类型的参数,往往会抛出异常。

注意,如果两个范围对象的下边界相等,为Range类定义的compareTo()方法会返回0。这意味着就compareTo()而言,任何两个起始点相同[7]的Range对象都相等。这个相等概念的定义和equals()方法定义的相等概念是相背的,equals()要求两个端点均相等才算相等。这种相等概念上的差异性会造成很多bug,最好将Range类的equals()和compareTo()方法中处理相等的逻辑保持一致。这里是Range类修正后的compareTo()方法,它的比较逻辑和equals()保持一致,但当传入不可比较的值时仍然会报错:

//根据下边界来对Range对象排序,如果下边界相等则比较上边界
//如果传入非Range值,则抛出异常
//当且仅当this.equals(that)时,才返回0
Range.prototype.compareTo=function(that){
    if(!(that instanceof Range))
        throw new Error("Can't compare a Range with"+that);
    var diff=this.from-that.from;//比较下边界
    if(diff==0)diff=this.to-that.to;//如果相等,比较上边界
    return diff;
};

给类定义了compareTo()方法,这样就可以对类的实例组成的数组进行排序了。Array.sor t()方法可以接收一个可选的参数,这个参数是一个函数,用来比较两个值的大小,这个函数返回值的约定和compareTo()方法保持一致。假定有了上文提到的compareTo()方法,就可以很方便地对Range对象组成的数组进行排序了:

ranges.sort(function(a,b){return a.compareTo(b);});
排序运算非常重要,如果已经为类定义了实例方法compareTo(),还应当参照这个方法定义一个可传入两个参数的比较函数。使用compareTo()方法可以非常轻松地定义这个函数,比如:

Range.byLowerBound=function(a,b){return a.compareTo(b);};
使用这个方法可以让数组排序的操作变得非常简单:

ranges.sort(Range.byLowerBound);
有些类可以有很多方法进行排序。比如Card类,可以定义两个方法分别按照花色排序和按照点数排序。

  • 方法借用

JavaScript中的方法没有什么特别:无非是一些简单的函数,赋值给了对象的属性,可以通过对象来调用它。一个函数可以赋值给两个属性,然后作为两个方法来调用它。比如,我们在Set类中就这样做了,将toArray()方法创建了一个副本,并让它可以和toJSON()方法一样完成同样的功能。

多个类中的方法可以共用一个单独的函数。比如,Array类通常定义了一些内置方法,如果定义了一个类,它的实例是类数组的对象,则可以从Array.prototype中将函数复制至所定义的类的原型对象中。如果以经典的面向对象语言的视角来看JavaScript的话,把一个类的方法用到其他的类中的做法也称做“多重继承”(multiple inheritance)。然而,JavaScript并不是经典的面向对象语言,我更倾向于将这种方法重用更正式地称为“方法借用”(borrowing)。

不仅Array的方法可以借用,还可以自定义泛型方法(generic method)。例9-9定义了泛型方法toString()和equals(),可以被Range、Complex和Card这些简单的类使用。如果Range类没有定义equals()方法,可以这样借用泛型方法equals():

Range.prototype.equals=generic.equals;
注意,generic.equals()只会执行浅比较,因此这个方法并不适用于其实例太复杂的类,它们的实例属性通过其equals()方法指代对象。同样需要注意,这个方法包含一些特殊情况的程序逻辑,以处理新增至Set对象中的属性(见例9-6)。

例9-9:方法借用的泛型实现

var generic={//返回一个字符串,这个字符串包含构造函数的名字(如果构造函数包含名字)
//以及所有非继承来的、非函数属性的名字和值
    toString:function(){
        var s='[';//如果这个对象包含构造函数,且构造函数包含名字
//这个名字会作为返回字符串的一部分
//需要注意的是,函数的名字属性是非标准的,并不是在所有的环境中都可用
        if(this.constructor&&this.constructor.name)
        s+=this.constructor.name+":";//枚举所有的非继承的、非函数属性
        var n=0;
        for(var name in this){
            if(!this.hasOwnProperty(name))continue;//跳过继承来的属性
            var value=this[name];
            if(typeof value==="function")continue;//跳过方法
            if(n++)s+=",";
            s+=name+'='+value;
        }
        return s+']';
    },//通过比较this和that的构造函数和实例属性来判断它们是否相等
//这种方法只适合于那些实例属性是原始值的情况,原始值可以通过"==="来比较
//这里还处理一种特殊情况,就是忽略由Set类添加的特殊属性
    equals:function(that){
        if(that==null)return false;
        if(this.constructor!==that.constructor)return false;
        for(var name in this){
            if(name==="|**objectid**|")continue;//跳过特殊属性
            if(!this.hasOwnProperty(name))continue;//跳过继承来的属性
            if(this[name]!==that[name])return false;//比较是否相等
        }
        return true;//如果所有属性都匹配,两个对象相等
    }
};
  • 私有状态

在经典的面向对象编程中,经常需要将对象的某个状态封装或隐藏在对象内,只有通过对象的方法才能访问这些状态,对外只暴露一些重要的状态变量可以直接读写。为了实现这个目的,类似Java的编程语言允许声明类的“私有”实例字段,这些私有实例字段只能被类的实例方法访问,且在类的外部是不可见的。

我们可以通过将变量(或参数)闭包在一个构造函数内来模拟实现私有实例字段,调用构造函数会创建一个实例。为了做到这一点,需要在构造函数内部定义一个函数(因此这个函数可以访问构造函数内部的参数和变量),并将这个函数赋值给新创建对象的属性。例9-10展示了对Range类的另一种封装,新版的类的实例包含from()和to()方法用以返回范围的端点,而不是用from和to属性来获取端点。这里的from()和t o()方法是定义在每个Range对象上的,而不是从原型中继承来的。其他的Range方法还是和之前一样定义在原型中,但获取端点的方式从之前直接从属性读取变成了通过from()和to()方法来读取。

例9-10:对Range类的读取端点方法的简单封装

function Range(from,to){//不要将端点保存为对象的属性,相反
//定义存取器函数来返回端点的值
//这些值都保存在闭包中
    this.from=function(){return from;};
    this.to=function(){return to;};
}
//原型上的方法无法直接操作端点
//它们必须调用存取器方法
Range.prototype={
    constructor:Range,
    includes:function(x){return this.from()<=x&&x<=this.to();},
    foreach:function(f){
        for(var x=Math.ceil(this.from()),max=this.to();x<=max;x++)f(x);
    },
    toString:function(){return"("+this.from()+"..."+this.to()+")";}
};


这个新的Range类定义了用以读取范围端点的方法,但没有定义设置端点的方法或属性。这让类的实例看起来是不可修改的,如果使用正确的话,一旦创建Range对象,端点数据就不可修改了。除非使用ECMAScript 5(参照9.3节)中的某些特性,但from和to属性依然是可写的,并且Range对象实际上并不是真正不可修改的:

var r=new Range(1,5);//一个不可修改的范围
r.from=function(){return 0;};//通过方法替换来修改它

但需要注意的是,这种封装技术造成了更多系统开销。使用闭包来封装类的状态的类一定会比不使用封装的状态变量的等价类运行速度更慢,并占用更多内存。

  • 构造函数的重载和工厂方法

有时候,我们希望对象的初始化有多种方式。比如,我们想通过半径和角度(极坐标)来初始化一个Complex对象,而不是通过实部和虚部来初始化,或者通过元素组成的数组来初始化一个Set对象,而不是通过传入构造函数的参数来初始化它。

有一个方法可以实现,通过重载(overload)这个构造函数让它根据传入参数的不同来执行不同的初始化方法。下面这段代码就是重载Set()构造函数的例子:

function Set(){
    this.values={};//用这个对象的属性来保存这个集合
    this.n=0;//集合中值的个数
//如果传入一个类数组的对象,将这个元素添加至集合中
//否则,将所有的参数都添加至集合中
    if(arguments.length==1&&isArrayLike(arguments[0]))
    this.add.apply(this,arguments[0]);
else if(arguments.length>0)
    this.add.apply(this,arguments);
}

这段代码所定义的Set()构造函数可以显式将一组元素作为参数列表传入,也可以传入元素组成的数组。但是这个构造函数有多义性,如果集合的某个成员是一个数组就无法通过这个构造函数来创建这个集合了(为了做到这一点,需要首先创建一个空集合,然后显式调用add()方法)。

在使用极坐标来初始化复数的例子中,实际上并没有看到有函数重载。代表复数两个维度的数字都是浮点数,除非给构造函数传入第三个参数,否则构造函数无法识别到底传入的是极坐标参数还是直角坐标参数。相反,可以写一个工厂方法——一个类的方法用以返回类的一个实例。下面的例子即是使用工厂方法来返回一个使用极坐标初始化的Complex对象:

Complex.polar=function(r,theta){
return new Complex(r*Math.cos(theta),r*Math.sin(theta));
};

下面这个工厂方法用来通过数组初始化Set对象:

Set.fromArray=function(a){
s=new Set();//创建一个空集合
s.add.apply(s,a);//将数组a的成员作为参数传入add()方法
return s;//返回这个新集合
};

可以给工厂方法定义任意的名字,不同名字的工厂方法用以执行不同的初始化。但由于构造函数是类的公有标识,因此每个类只能有一个构造函数。但这并不是一个“必须遵守”的规则。在JavaScript中是可以定义多个构造函数继承自一个原型对象的,如果这样做的话,由这些构造函数的任意一个所创建的对象都属于同一类型。并不推荐这种技术,但下面的示例代码使用这种技术定义了该类型的一个辅助构造函数:

//Set类的一个辅助构造函数
function SetFromArray(a){//通过以函数的形式调用Set()来初始化这个新对象
//将a的元素作为参数传入[8]
Set.apply(this,a);
}
//设置原型,以便SetFromArray能创建Set的实例
SetFromArray.prototype=Set.prototype;
var s=new SetFromArray([1,2,3]);
s instanceof Set//=>true

 

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值