Cocos Creator_JavaScript权威指南(第六版)_第9章_类和模块

第6章详细介绍了JavaScript对象,每个JavaScript对象都是一个属性集合,相互之间没有任何联系。在JavaScript中也可以定义对象的类,让每个对象都共享某些属性,这种“共享”的特性是非常有用的。类的成员或实例都包含一些属性,用于存放或定义它们的状态,其中有些属性定义了他们的行为(通常称为方法)。这些行为通常是由类定义的,而且为所有实例所共享。例如,假设有一个名为Complex的类用来表示复数,同时还定义了一些复数运算。一个Complex实例应当包含复数的实部和虚部(状态),同样Complex类还会定义复数的加法和乘法操作(行为)。
在JavaScript中,类的实现是基于其原型继承机制的。如果两个实例都从同一个原型对象上继承了属性,我们说他们是同一个类的实例。JavaScript原型和继承在6.1.3和6.2.2中有详细讨论。
如果两个对象继承自同一个原型,往往意味着(但不是绝对)他们是由同一个构造函数创建并初始化的。我们已经于4.6、6.2和8.2.3中详细讲解了构造函数。
JavaScript中类的一个重要特性是“动态可继承”。9.4中详细解释这一特性。我们可以将类看做是类型。9.5讲解检测对象的类的几种方式,该节同样介绍一种编程哲学——“鸭式辩型”,他弱化了对象的类型,强化了对象的功能。
在讨论了JavaScript中所有基本的面向对象编程特性之后,我们将关注点从抽象的概念转向一些实例。9.6介绍两种非常重要的实现类的方法,包括很多实现面向对象的技术,这些技术可以很大程度上增强类的功能。9.7展示如果实现类的继承,包括如何在JavaScript中实现类的继承。9.8讲解如何使用ECMAScript 5中的新特性来实现类以及面向对象编程。
定义类是模块开发好重用代码的有效方式之一,最后一节集中讨论JavaScript中的模块。

9.1 类和原型
在JavaScript中,类的所有实例对象都从同一个原型对象上继承属性。因此,原型对象是类的核心。在例6-1中定义了inherit()函数,这个函数返回一个新创建的对象,后者继承自某个原型对象。如果定义一个原型对象,然后容通过inherit()函数创建一个继承自它的对象,这样就定义了一个JavaScript类。通常,类的实例还需要进一步的初始化,通常是通过定义一个函数来创建并初始化这个新对象,参照例9-1.例9-1给一个表示“值的范围”的类定义了原型对象,还定义了一个“工厂”函数用于创建并初始化类的实例。

9-1:一个简单的JavaScript类
      //这个工厂方法返回一个能表示值的范围的类。
      range:function (from,to) {
   
        //使用inherit()函数来创建对象,这个对象继承自在下面定义的原型对象。原型队形作为函数的一个属性存储,并定义所有“范围对象”所共享的方法(行为)
        var r = this.inherit(range.methods);
        //存储新的“范围对象”的起始位置和结束位置(状态)
        //这两个属性是不可继承的,每个对象都拥有唯一的属性
        r.from = from;
        r.to = to;
        //返回这个新创建的对象
        return r;
    },      

    //原型对象定义方法,这些方法为每个范围对象所继承
    this.range.methods ={
        //如果x在范围内,则返回true;否则返回false
        //这个方法可以比较数字范围,也可以比较字符串和日期范围
        includes:function (x) {
   return this.from <= x && x<= this.to;},          
        //对于范围内的每个整数都调用一个f
        //这个方法只可用做数字范围
        foreach:function (f) {
   
            for(var x = Math.ceil(this.from); x<=this.to;x++) f(x);},       
        //返回表示这个范围的字符串
        toString:function () {
    return "("+this.from+"..."+this.to +")";}
      };
      //这里是使用“范围对象”的一些例子
      var r = this.range(1,3); //创建一个范围对象
      r.includes(2); //true:2在这个范围内
      r.foreach(console.log); //输出 1 2 3 
      console.log(r);         //输出(Object {from: 1, to: 3})     
     },

这段代码定义了一个工厂方法range(),用来创建新的范围对象。我们注意到,这里给range()函数定义了一个属性range.methods,用于快捷地存放定义类的原型对象。把原型对象挂在函数上没什么大不了,但也不是惯用做法。再者,注意range()函数给每个范围对象都定义了from和to属性,用以定义范围的其实位置好结束位置,这两个属性是非共享的,当然也是不可继承的。最后,注意在range.methods中定义的那些可共享、可继承的方法都用到了from和to属性,而且使用了this关键字,为了指代它们,两者使用this关键字来指代调用了from和to属性,而且使用了this关键字,为了指代它们,两者使用this关键在来指代调用这个方法的对象。任何类的方法都可以通过this的这种基本用法来读取对象的属性。

9.2 类和构造函数
例9-1展示了在JavaScript中定义类的其中一种方法。但这种方法并不常用,毕竟他没有定义构造函数,构造函数是用来初始化新创建一个新对象,因此构造函数本身只需初始化这个新对象的状态即可。调用构造函数的一个重要特性是,构造函数的prototype属性被用做新对象的原型。这意味着通过同一个构造函数创建的所有对象都继承自一个相同的对象,因此它们都是同一个类的成员。例9-2对例9-1中的“范围类”做了修改,使用构造函数代替工厂函数:

//例9-2:使用构造函数来定义“范围类”
    //range2.js:表示值的范围的类的另一种实现
    //这是一个构造函数,用以初始化新创建的“范围对象”
    //注意,这里并没有创建并返回一个对象,仅仅是初始化
    Range:function (from ,to) {
   
        //存储“范围对象”的起始位置和结束位置(状态)
        //这两个属性是不可继承的,每个对象都拥有唯一的属性
        this.from = from;
        this.to = to;
    },

      //所有的“范围对象”都继承自这个对象
      //注意,属性的名字必须是“prototype”
      this.Range.prototype ={
          //如果x在范围内,则返回true;否则返回false
          //这个方法可以比较数字范围,也可以比较字符串和日期范围
          includes:function (x) {
   return this.from <=x && x<= this.to;},
          //对于范围内的每个整数都调用一个f
          //这个方法只可用于数字范围
          foreach:function (f) {
   
              for(var x = Math.ceil(this.from); x <= this.to;x++) f(x);
          },
          //返回表示这个范围的字符串
          toString:function () {
    return "("+this.from +"..."+this.to+")";}
      };
       var r = this.range(1,3); //创建一个范围对象
       console.log(r.includes(2));//true: 2在这个范围内
       r.foreach(console.log);  //输出1 2 3 
       console.log(r);          //输出(Object {from: 1, to: 3})

将例9-1和例9-2中的代码做一个仔细的对比,可以发现两种定义类的技术的差别。首先,注意当工厂函数range()转化为构造函数时被重命名为Range()。这里遵循了一个常见的编程约定:从某种意义上讲,定义构造函数即是定义类,并且类名首字母要大写。而普通的函数和方法都是首字母小写。
再者,注意Range()构造函数是通过new关键字调用的,而range()工厂函数则不必使用new。例9-1通过调用普通函数(8.2.1)来创建新对象,例9-2则使用构造函数调用(8.2.3)来创建新对象。由于Range()构造函数是通过new关键字调用的,因此不必调用inderit()或其他什么逻辑来创建新对象。在调用构造函数之前就已经创建了新对象,通过this关键字可以获取这个新对象。Range()构造函数只不过是初始化this而已。构造函数甚至不必返回这个新创建的对象,构造函数会自动创建对象,然后将构造函数作为这个对象的方法来调用一次,最后返回这个新对象。事实上,构造函数的命名规则(首字母大写)和普通函数是如此不同还有另外一个原因,构造函数调用和普通函数调用是不尽相同的。构造函数就是用来“构造新对象”的,它必须通过关键字new调用,如果将构造函数用做普通函数的话,往往不会正常工作。开发者可以通过命名约定来(构造函数首字母大写,普通方法首字母小写)判断是否应当在函数之前冠以关键字new。
例9-1和例9-2之间还有一个非常重要的区别,就是原型对象的命名。在第一段示例代码中的原型是rang.methods。这种命名方式很方便同时具有很好的语义,但又过于随意。在第二段示例代码中的原型是Rang.prototype,这是一个强制的命名。对Range()构造函数的调用会自动使用Range.prototype作为新Range对象的原型。
两者的范围方法定义和调用方式是完全一样的。

9.2.1 构造函数和类的标识
上文提到,原型对象是类的唯一标识:当且仅当两个对象继承自同一个原型对象时,他们才是属于同一个类的实例。而初始化对象的状态的构造函数则不能作为类的标识,两个构造函数的prototype属性可能指向同一个原型对象。那么这两个构造函数创建的实例是属于同一个类的。
尽管构造函数不像原型那样基础,但构造函数是类的“外在表现”。很明显的,构造函数的名字通常用做类名。比如,我们说Range()构造函数创建Range对象。然而,更根本地讲,当使用instanceof运算符来检测对象是否属于某个类时会用到构造函数。假设这里有一个对象r,我们想知道r是否是Range对象,我们这样写:

this.Range.toString();//如果r继承自Range.prototype,则返回true

实际上instanceof运算符并不会检查r是否是由Range()构造函数初始化而来,而会检查r是否继承自Range.prototype。不过,instanceof的语法则强化了“构造函数是类的公有标识”的概念。

9.2.2 constructor 属性
在例9-2中,将Range.prototype定义为一个新对象,这个对象包含类所需要的方法。其实没有必要新创建一个对象,用单个对象直接直接量的属性就可以方便地定义原型上的方法。任何JavaScript函数都可以用做构造函数,并且调用构造函数是需要用到一个prototype属性的。因此,每个JavaScript函数(ECMAScript 5中的Function.bind()方法返回的函数除外)都自动拥有一个prototype属性。这个属性的值是一个对象,这个对象包含唯一一个不可枚举属性constructor 。constructor 属性的值是一个函数对象:

       var F = function () {};
       var p = F.prototype;//这是f相关联的 原型对象
       var c = p.constructor;//这是与原型相关联的 函数
       console.log(c===F); //true:对于任意函数F.prototype.constructor ==F

可以看到构造函数的原型中存在预先定义好的constructor属性,这意味着对象通常继承的constructor均指代它们的构造函数。由于构造函数是类的“公共标识”,因此这个constructor属性为对象提供了类。

 var o = new F(); //创建类F的一个对象
 console.log(o.constructor ===F); //true,constructor  属性指代这个类  

如图9-1所示,图9-1展示了构造函数和原型对象之间的关系,包括原型到构造函数的反向引用以及构造函数创建的实例。
这里写图片描述
PS:图9-1用Range()构造函数作为示例,但实际上,例9-2中定义的Range类使用它自身的一个新对象重写预定义的Range.prototype对象。这个新定义的原型对象不含有constructor属性。因此Range类的实例也不含有constructor属性。我们可以通过补救措施来修正这个问题,显示给原型添加一个构造函数:

 this.Range.prototype ={
        constructor:Range,//显示设置构造函数反向引用
        includes:function (x) {
   return this.from <=x && x<= this.to;},      
        foreach:function (f) {
   
            for(var x = Math.ceil(this.from); x <= this.to;x++) f(x);
        },
        toString:function () {
    return "("+this.from +"..."+this.to+")"
};

9.3 JavaScript中Java式的类继承
JavaScript和Java的一个不同之处在于,JavaScript中的函数都是以值的形式出现的,方法和字段之间并没有太大的区别。如果属性值是函数,那么这个属性就定义一个方法;否则,它只是一个普通的属性或“字段”。
在JavaScript中定义类的步骤可以缩减为一个分三步的算法。第一步,先定义一个构造函数,并设置初始化新对象的实例属性。第二步,给构造函数的prototype对象定义实例的方法。第三步,给构造函数定义类字段和类属性。

9.4 类的扩充
JavaScript中基于原型的继承机制是动态的:对象从其原型继承属性,如果创建对象之后原型的属性发生改变,也会影响到继承这个原型的所有实例对象。这意味着我么你可以通过给原型对象添加新方法来扩充JavaScript类。

9.5 类和类型
JavaScript定义了少量的数据类型:null、undefined、布尔值、数字、字符串、函数和对象。typeof运算符(4.13.2)可以得出值的类型。然而,我们往往更希望将类作为类型对待,这样就可以根据对象所属的类来区分它们。JavaScript语言核心中的内置对象(通常是指客户端JavaScript的宿主对象)可以根据它们的class属性(6.8.2)来区分彼此,

9.5.1 instanceof 运算符
左操作数是待检测其类的对象,右操作数是定义类的构造函数。如果o继承自c.prototype,则表达式o instanceof c 值为true。这里的继承可以不是直接继承,如果o所继承的对象继承自另一个对象,后一个对象继承自c.prototype,这个表达式的运算符结果也是true。
构造函数是类的公共标识,但原型是唯一的标识。尽管instanceof运算符的右操作数是构造函数,但计算过程实际上是检测了对象的继承关系,而不是检测创建对象的构造函数。
如果你想检查对象的原型链上是否在某个特定的原型对象,可以使用isPrototypeOf()方法。

console.log(range.methods.isPrototypeOf(r));//range.methods是原型对象

instanceof运算符和isPrototypeOf()方法的缺点是,我们无法通过对象来获得类名,只能检测对象是否属于指定的类名。在客户端JavaScript中还有一个比较严重的不足,就是在多窗口和多框架子页面的Web应用中兼容性不佳。每个窗口和框架子页面都具有单独的执行上下文,每个上下文都包含独有的全局变量和一组构造函数。在两个不同框架页面中创建的两个数组继承自两个相同但相互独立的原型对象,其中一个框架页面中//的数组不是另一个框架页面的Array()构造函数的实例,instanceof运算结果是false

9.5.2 constructor属性
另一种识别对象是否属于某个类的方法是使用constructor属性。因为构造函数是类的公共标识,所以最直接的方法就是使用constructor属性,比如:

 typeAndValue:function (x) {
   
        if (x == null) return ""; //null和undefined没有构造函数
        switch (x.constructor) {             
            case Number: return "Number"+x+"'"; //处理原型类型
            case String: return "String:'"+x+"'";
            case Date: return "Date:"+x; //处理内置类型
            case RegExp: return "Regexp:"+x;
            case Complex: return "Complex:"+x;//处理自定义类型
        }
    },

在代码中关键字case后的表达式都是函数,如果改用typeof运算符或获取到对象的class属性的话,他们应当改为字符串。
使用constructor属性检测对象属于某个类的技术的不足之处和instanceof一样。在多个执行上下文的场景中它是无法正常工作的(比如在浏览器的多个框架子页面中)。在这种情况下,每个框架页面各自拥有独立的构造函数集合,一个框架页面中的Array构造函数和另一个框架页面的Array构造函数不是同一个构造函数。
同样,在JavaScript中也并非所有的对象都包含constructor属性。在每个新创建的函数原型上默认会有constructor属性,但我们常常会忽略原型上的constructor属性。例如9-1和9-2所定义的两个类中,他们的实例都没有constructor属性。

9.5.3 构造函数的名称
使用instanceof运算符和constructor属性来检测对象所属的类有一个主要的问题,在多个执行上下文中存在构造函数的多个副本的时候,这两种方法的检测结果会出错。多个执行上下文中的函数看起来是一模一样的,但它们时相互独立的对象,因此彼此也不相等。
一种可能的解决方案式使用构造函数的名字而不是构造函数本身作为类标识符。一个窗口里的Array构造函数和另一个窗口的Array构造函数是不相等的,但是他们的名字是一样的。在一些JavaScript的实现中为函数对象提供了一个非标准的属性name,用来表示函数的名称。对于那些没有name属性的JavaScript实现来说,可以将函数转换为字符串,然后从中提取出函数名。
例9-4定义的type()函数以字符串的形式返回对象的类型。他用typeof运算符来处理原始值和函数。对于对象来说,他要么返回class属性的值要么返回构造函数的名字。type()函数用到了例6-4中的classof()函数和9.4中的Function.getName()方法。

 //例9-4:可以判断值的类型的type()函数
    /*
     以字符串形式返回o的类型:如果o是null,返回“null”;如果o是NaN,返回“nan” 如果typeof所返回的值不是“object”,则返回这个值。(注意,有一些JavaScript的实现将正则表达式识别为函数)。如果o的类不是“Object”,则返回这个值。如果o包含构造函数并且这个构造函数具有名称,则返回这个名称。否则,;一律返回“Object”
    */
   type:function (o) {
   
       var t,c,n; //type,class,name
       //处理null值的特殊情形
       if (o===null) return "null";
       //另一种特殊情形:NaN和它自身不相等
       if (o !== o) return "nan";
       //如果typeof的值不是“object”,则使用这个值
       //这可以识别出原始值的类型和函数
       if ((t = typeof(o))!=="Object") return c;
       //如果对象构造函数的名字存在的话,则返回它
       if(o.constructor && typeof o.constructor === "function"&&(n = o.constructor.getName()))return n;
       //其他的类型都无法判别,一律返回“Object”
       return "Object";
   },
   //返回对象的类
   classof:function (o) {
   
       return Object.prototype.toString.call(o).slice(8,-1);
   },
    //返回函数的名字(可能是空字符串),不是函数的话返回null
    Function.prototype.getName = function () {
   
        if ("name" in this) return this.name;
        return this.name = this.toString().match(/function\s*([^(]*)\(/)[1];     
    };

这种使用构造函数名字来识别对象的类的做法和使用constructor属性一样有一个问题:并不是所有的对象都具有constructor属性。此外,并不是所有的函数都有名字。如果使用不带名字的函数定义表达式定义一个构造函数,getName()方法则会返回空字符串:

    //这个构造函数没有名字
    var Complex = function (x,y) {
   this.r = x;this.i = y;};
    //这个构造函数有名字
    var Range = function Range (f,t) {
   this.from = f;this.to = t;};

9.5.4 鸭式辩型
上述的检测对象的类和各种技术多少都会有些问题,至少在客户端JavaScript中是如此。解决办法就是规避掉这些问题:不要关注“对象的类是什么”,而是关注“对象能做什么”。这种思考问题的方式在Python和Ruby中非常普遍,称为“鸭式辩型”。
这句话可以理解为“如果一个对象可以像鸭子一样走路、游泳并且嘎嘎叫,就认为这个对象是鸭子,哪怕他并不是从鸭子类的原型对象继承而来的”。
拿例9-2中的Range类来举例。起初定义这个类用于描述数字的范围。但要注意,Range()构造函数并没有对实参进行类型检查以确保实参是数字类型。但却将参数使用“>”运算符进行比较运算,因为这里假定他们是可比较的。同样,includes()方法使用”<=”运算符进行比较,但没有对范围的结束点进行类似的假设。因为类并没有强制使用特定的类型,它的inlcudes()方法可以作用于任何结束点,只要结束点可以用关系运算符执行比较运算。

var lowercase = new Range("a","z");
var thisYear = new Range(new Date(2009,0,1),new Date(2010,0,1));

Range类的foreach()方法中也没有显式地检测表示范围的结束点的类型,但Math.ceil()和“++”运算符表明它只能对数字结束点进行操作。
另外一个例子,于7.11中讨论的类数组对象。在很多场景下,我们并不知道一个对象是否真的是Array的实例,当然是可以通过判断是否包含非负的length属性来得知是否是Array的实例。我们说“包含一个值是非负整数的length”是数组的一个特征——“会走路”,任何具有“会走路”这个特征的对象都可以当做数组来对待(大多情形下是合理的)。
然而,真正数组的length属性有一些独有的行为:当添加新的元素时,数组的长度会自动更新,并且当给length属性设置一个更小的整数时,数组会被自动截断。我们说这些特征是“会游泳”和“嘎嘎叫”。如果所实现的代码需要“会游泳”且能“嘎嘎叫”,则不能使用只“会走路”的类似数组的对象。
上文所讲到的鸭式辩型的例子提到了进行对象的“<”运算符的职责以及length属性的特殊行为。但当我们提到鸭式辩型时,往往是说检测对象是否实现了一个或多个方法。一个强类型的triathlon()函数所需要的参数必须是TriAthlete对象。而一种“鸭式辩型”式的做法是,只要对象包含walk()、swim()和bike()这三个方法就可以作为参数传入。同理,可以重新设计Rang类,使用结束点对象的compareTo()和succ()(successor)方法来代替“<”和“++”运算符。
鸭式辩型的实现方法让人感觉太“放任自流”:仅仅是假设输入对象实现了必要的方法,根本没有执行进一步的检查。如果输入对象没有遵循“假设”,那么当代码试图调用那些不存在的放法时就会报错。另一种实现方法是对输入对象进行检查。但不是检查它们的类,而是用适当的名字来检查它们所实现的方法。这样可以将非法输入尽可能早地拦截在外,并可给出更多提示信息的报错。
里9-5中按照鸭式辩型的理念定义了quacks()函数(函数名叫(第一个实参))是否实现了剩下的参数所表示的方法。对于除第一个参数外的每个参数,如果是字符串的话则直接检查是否存在以它命名的方法;如果是对象的话则检查第一个对象中的方法是否在这个对象中也具有同名的方法;如果参数是函数,则假定它是构造函数,函数将检测第一个对象实现的方法是否在构造函数的原型对象中也具有同名的方法。

 //例9-5:利用鸭式辩型实现的函数
   //如果o实现了除第一个参数之外的参数所表示的方法,则返回true
   quacks:function (o/*,...*/) {
   
       for(var i = 1; i<arguments.length; i++){ //遍历o之后的所有参数
        var arg = arguments[i];
        switch (typeof arg) {        //如果参数是:
            case 'string':           //string:直接用名字做检查
                if (typeof o[arg] !== "function") return false;
                continue;
            case 'function':         //function:检查函数的原型对象上的方法
                //如果实参是函数,则使用它的原型
                arg = arg.prototype; //进入下一个case
            case 'object':           //object:检查匹配的方法
                for(var m in arg){   //遍历对象的每个属性
                    if (typeof arg[m] !== "function") continue; //跳过不是方法的属性
                    if (typeof o[m] !== "function") return false; 
                }
            }
        }
        //如果程序能执行到这里,说明o实现了所有的方法
        return true;
   },

关于这个quacks()函数还有一些地方是需要尤为注意的。首先,这里只是通过特定的名称来检测对象是否含有一个或多个值为函数的属性。我们无法得知这些已经存在的属性的细节信息,比如,函数是干什么用的?他们需要多少参数?参数类型是什么?然而这是鸭式辩型的本质所在,如果使用鸭式辩型而不是强制的类型检测的方式定义API,那么创建的API应当更具有灵活性才可以,这样才能确保你提供给用户的API更加安全可靠。关于quacks()函数还有另一问题需要注意,就是他不能应用于内置类。比如,不能通过quacks(o,Array)来检测o是否实现了Array中所有同名的方法。原因是内置类的方法都是不可枚举的,quacks()中的for/in循环无法遍历到它们(在ECMAScript5中有一个补救办法,就是使用Object.getOwnPropertyNames()).

9.6 JavaScript中的面向对象技术

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

//例9-6:Set.js:值的任意集合
   Set :function () {
       //这是一个构造函数
      this.values = {};  //集合数据保存在对象的属性里
      this.n = 0;        //集合中值的个数
      this.add.apply(this,arguments);//把所有参数都添加进这个集合
   }
        //将每个参数都添加至集合中
        this.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;                              //支持链式方法调用
        };
       //从集合删除元素,这些元素由参数指定
       this.Set.prototype.remove = function () {
    
            for(var i = 0; i <arguments.length; i++){ //遍历每个参数
                var str = Set._v2s(arguments[i]);     //将字符串和值对应起来
                if (this
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值