面向对象JavaScript开发实战

面向对象JavaScript开发实战


一. JavaScript高级部分

1.1 函数

1.1.1 函数的内存分布
        JavaScript中定义的每个函数在被解析的时候都将分配一个prototype属性,该prototype引用一个对象,而这个对象的constructor属性又引用到原函数,如下代码所示:
function Person(_name) {
	this.name = _name;
	this.sayHi = function() {
		console.log('hi, I am '+this.name);
	};
}

Person.prototype.age = 25;
Person.prototype.get = function(properName) {
	return this[properName];
};


        函数的prototype对象可以对函数进行方法的扩展,其用法非常灵活。由于JavaScript语言缺乏很多静态语言所拥有的特性,如对象类、继承和多态等,要想在JavaScript中模拟实现这些特性,prototype对象起着非常关键的作用,在后面章节中将会详细提及。

1.1.2 函数的声明方式
        函数的声明有如下几种方式。
方式一:new Function(arg1,arg2,arg3,body) 
var func = new Function("name","age", 
	"console.log('name:'+name+', age:'+age);");

func('Peter', 23);
注意:函数也是对象,由Function实例化的,通常,可以通过在Function.prototype对象上定义通用方法,使得所有函数均持有该方法。

方式二:var func = function() {}
var func = function(name, age) {
	console.log('name:'+name+', age:'+age);
};

func('Peter', 23);
方式三:function func() {} 
function func(name, age) {
	console.log('name:'+name+', age:'+age);
}

func('Peter', 23);
1.1.3 函数的调用方式
        函数被调用的方式有多种,不同的调用方式适合不同的场景。JavaScript最常见的函数调用方式有如下几种:
方式一,作为函数直接调用
function hello() {
	console.log('hello, world');
}

hello();
方式二,作为对象的构造函数调用
function Hello() {
	console.log('create a Hello object.');
}

var hello = new Hello();
方式三,作为对象方法调用
var peter = {
	name: 'peter',
	age: 25,
	showProfile: function() {
		console.log('name:'+this.name+', age:'+this.age);
	}
};

peter.showProfile();
方式四,采用call或者apply进行调用,用法如下:
method.call(obj, arg1, arg2…);
method.apply(obj, [arg1, arg2, arg3]);
var showProfile = function(other) {
	console.log('name:'+this.name+', age:'+this.age+', other:'+other);
};

var peter = {
	name: 'peter',
	age: 25
};

showProfile.call(peter, 'call');
showProfile.apply(peter, ['apply']);
运行结果:
name:peter, age:25, other:call
name:peter, age:25, other:apply
       由此可见,JavaScript函数能够以非常灵活的方式使用,而正因为这种灵活性导致开发人员不易于掌握他们,这要求开发人员要经常实践才能深刻理解他们的精妙。

1.1.4 this的用法
        JavaScript中this的用法非常灵活,也是非常难以熟练掌握的。由于其运行期绑定的特性,this含义非常丰富,它可以是全局对象、当前对象或者任意对象,这完全取决于函数的调用方式。JavaScript 中函数的调用有以下几种方式:作为对象方法调用,作为函数调用,作为构造函数调用和使用 apply 或 call 调用。下面我们将按照调用方式的不同,分别讨论 this 的含义。

当作为对象方法被调用的时候,this指的是该对象本身,见下面例子:
var point = {
	x: 0,
	y: 0,
	move: function(witdh, heigh) {
		this.x = this.x + width;
		this.y = this.y + height;
	}
};

point.move(5, 10);//this绑定到当前对象,即point对象
当作为函数直接被调用时,this指的是当前上下文:
function makeNoSense(x) {
	this.x = x;
}

makeNoSense(5);//相当于window.makeNoSense(5);
console.log(x);//输出为:5
当作为构造方法调用的时候,this绑定到新创建的对象上:
function Point(x, y) {
	this.x = x;
	this.y = y;
}

var point = new Point(10, 20);
console.log(point.x);//输出:10
console.log(point.y);//输出:20
当用call或apply方式调用时,this绑定到第一个参数对象:
function Point(x, y) {
	this.x = x;
	this.y = y;

	this.moveTo = function(x, y) {
		this.x = x;
		this.y = y;
	};
}

var p1 = new Point(0, 0);
var p2 = {x: 0, y: 0};

p1.moveTo(1, 1);
p1.moveTo.apply(p2, [10, 10]);//this绑定到p2对象

console.log(p1.x+', '+p1.y);//输出:1, 1
console.log(p2.x+', '+p2.y);//输出:10, 10
最后来看一个jQuery绑定事件的例子
HTML代码
<div class="ui-btn ui-active" data-action="showPageFromMenu" data-rel="pageHome">
	<img class="ui-btn-tit-pic" src="img/home.png"/>首页
</div>
<div class="ui-btn" data-action="showPageFromMenu" data-rel="pageNotice">
	<img class="ui-btn-tit-pic" src="img/message.png"/>业务通知
</div>
<div class="ui-btn" data-action="showPageFromMenu" data-rel="pageArticle">
	<img class="ui-btn-tit-pic" src="img/book.png"/>最新发文
</div>
JavaScript代码
$('body').on('tap click', '.ui-btn', function() {
	//this指的是触发事件的dom元素
	var action = $(this).attr('data-action');
	app[action](this);
});
        这块代码的目的是要在具有相同class样式的div上面注册click事件,其传入的回调函数最终将会这样被调用:div.onclick = callback;,因此,回调函数中的this便是指被触发事件的div元素,总而言之一句话: this指的是函数的调用者
1.1.5 callback回调函数
        JavaScript回调函数是指将一个函数作为参数传入到另一个函数(主函数),该函数将在主函数内有机会被执行,这样函数称之为回调函数(callback)。callback可以用来模拟实现静态语言中的多态特性,也可以用来实现异步功能。常见的使用到回调函数的情形有setTimeout、Ajax和Event等。
$.ajax({
	url: 'test.html',
	context: document.body
}).done(function() {
	$(this).addClass('done');
}).fail(function() {
	alert('error');
}).always(function() {
	alert('complete');
});

1.2 对象

1.2.1 对象的内存分布
        对象定义:属性的无序集合,每个属性存放一个原始值、对象或函数。每个对象都是由类定义的,通过类实例化对象,在JavaScript中并没有正式的类,开发人员通过函数作为类来定义对象的属性和方法,与其他静态语言中的类的概念相比,两者是等价的。

1.2.2 对象的创建
       我们可以通过以下两种方式创建一个对象:字面量和构造函数。这两种方式分别用于不同的场景。
字面量创建对象
var obj = {
	attr1: 'attr1',
	attr2: 20,

	print: function() {
		console.log('attr1:'+this.attr1+', attr2:'+this.attr2);
	}
};
        这种方式适合于封装复杂数据结构,创建普通对象,并且将该对象作为全局变量使用,由于这种方式创建对象非常简单明了,是比较常见的创建对象的方式。
构造函数创建对象
function Person() {
	this.name = 'Peter';
	this.sayHello = function() {
		console.log('hello, '+this.name);
	};
}

var person = new Person();
person.sayHello();//输出:hello, Peter

        通过构造函数创建对象,这种用关键字new的方式跟其他静态语言几乎一样,只不过JavaScript没有原生的对类继承的支持,因此需要我们模拟实现类继承的特性,这将在后面介绍。

        构造函数创建对象适合于需要按照类创建多个实例的场景,在这种场景下,每个对象实例都拥有自己独立的属性和方法,为了节省更多的资源,我们往往把对象方法提取到类的prototype中去,这样多个对象实例便可以重用同一个方法,这也是我们常常所推荐的用来定义对象的方式。
function Person(name) {
	this.name = name;
}

Person.prototype.sayHello = function() {
	console.log('hello, '+this.name);
};

var p1 = new Person('Peter');
p1.sayHello();//输出:hello, Peter

var p2 = new Person('Hans');
p2.sayHello();//输出:hello, Hans


1.3 事件

1.3.1 JavaScript单线程
        JavaScript是单线程的,因此当一个程序需要处理多个任务的时候,程序只能在前一个任务执行完毕后才开始执行后一个任务,如果前面的任务执行需要花费很长时间,这将导致后面的任务阻塞和浏览器假死,为了避免这种情况,我们需要应用异步模式对程序进行优化,将耗时比较多的任务放到事件队列,等到主体函数执行完后再执行事件队列里面的任务,常见的异步模式有:回调函数、事件监听、发布订阅和Promises对象等(相关知识点请上网查询)。
1.3.2 JavaScript事件机制
var startTime = new Date();

setTimeout(function() {
	var endTime = new Date();
	console.log(endTime - startTime);
}, 500);

while(new Date() - startTime < 1000) {
	
}
        以上代码执行的结果并不是预期的500毫秒,而是1000毫秒(可能存在一点误差),这是因为setTimeout参数中的回调函数并未在500毫秒后立即执行,而是被放到一个事件队列中去了,等到主体函数执行完毕之后才开始执行该回调函数。同样的,在DOM元素上触发事件也不是立即执行事件绑定的函数,而是等待DOM所有事件触发后才开始执行绑定到事件的函数。

下面的例子将使用setTimeout和callback来实现异步模式的处理方式。
function asyn(callback) {
	setTimeout(callback, 0);
}

function readFile() {
	var startTime = new Date();
	while(new Date() - startTime < 1000) {
		//read file will cost 1 second
	}
	console.log('finished reading file.');
}

console.log('start...');

asyn(readFile);

console.log('finished...');
输出结果:
start...
finished...
finished reading file.
        上面的代码中假设读取文件将花费1秒钟时间,那么我们将读取文件的操作放到事件队列中,等到主体函数执行完毕之后再执行文件读取操作,这样便可以防止程序阻塞的问题。读者有兴趣可以研究一下为什么NodeJS并发处理能力要优于其他静态语言程序(如Java),以便更深入了解JavaScript单线程和事件队列机制。

1.4 继承

1.4.1 创建类
        在JavaScript语言中,函数可以作为类进行对象实例化,但是JavaScript中没有类继承的概念,类继承可以大大降低重复代码,子类继承父类后便拥有父类的方法和属性,我们只能通过编程的方法去模拟实现类继承,这样一来类的创建就不仅仅是定义一个函数那么简单了,创建类可以这样写:
var Parent = new Class();
var Child = Parent.extend();//Child类继承Parent类

var parent = new Parent();
var child = new Child();
1.4.2 类继承
        继承即子类继承父类的属性和方法,通过前面的介绍可知,在定义类(函数)的时候,一个类方法既可以声明在类内部,也可以声明在prototype对象里面,那么要实现一个子类,就需要把父类和prototype的属性和方法复制到子类,这才是关键所在。前面已经提到可以通过编程的方式模拟实现类继承的特性,下面介绍几种具体方法。

方式一:prototype链接,即子类的prototype对象链接到父类的prototype对象,这样子类便拥有了父类的prototype所定义的方法:


方式二:类抄写,即子类抄写父类的属性和方法,其实就是在子类构造方法内调用父类的构造方法来实现的,如下示例:
//declare parent class
function Parent(name) {
	this.name = name;
	this.showName = function() {
		console.log('name:'+this.name);
	};
}

Parent.extend = function() {
	var Child = function() {
		Parent.apply(this, arguments);//继承父类构造方法
	};

	var F = function() {
		this.constructor = Child;
	};
	F.prototype = Parent.prototype;
	Child.prototype = new F();//间接方法,防止污染父类prototype对象

	return Child;
};

Parent.prototype.sayHello = function() {
	console.log('hello, '+this.name);
};

var parent = new Parent('Peter');
parent.showName();
parent.sayHello();

var Child = Parent.extend();
var child = new Child('Hans');
child.showName();//该方法继承Person函数内定义的方法
child.sayHello();//该方法继承Person原型对象内定义的方法
输出结果:
name:Peter
hello, Peter
name:Hans
hello, Hans
        读者可以思考一下为什么上面要引用一个中间对象F,其实理由很简单,如果不建立一个中间对象F,让子类prototype直接引用父类的prototype,这样便可以通过修改子类prototype来修改父类prototype,显然是不符合面向对象程序设计原则,建立中间对象是为了防止子类污染父类。这里请读自行描画出上面Parent和Child的内存分布图,以加深对其的理解。

方式三:prototype属性复制,某些时候你需要扩展子类的属性和方法,你可以在创建子类的时候定义新的属性和方法,写法如下:
var Child = Parent.extend({
	age: 27,
	showAge: function() {
		console.log('age:'+this.age);
	}
});
extend方法传入的参数即为子类扩展的属性和方法,具体实现如下代码:
function Parent(name){
	this.name = name;
	this.sayHi = function() {
		console.log('hello, '+this.name);
	};
}

Parent.extend = function(options) {
	var Child = function() {
		Parent.apply(this, arguments);
	};

	for(var prop in options) {
		Child.prototype[prop] = options[prop];
	}

	return Child;
};

var Child = Parent.extend({
	age: 27,
	showAge: function() {
		console.log('age:'+this.age);
	}
});

var child = new Child('bxiao');
child.sayHi();
child.showAge();
输出结果:
hello, bxiao
age:27

方式四:圣杯模式,以上的几种继承手段各有其用处,但是都不能单独使用,如果将上面介绍的几种方式合并起来使用,相互弥补各自的缺陷,则可以达到真正的继承效果,具体实现请参考下面代码:
var Class = function(){};
Class.new = function() {
	var klass = function() {
		//约定所有函数均申明了init方法,用于初始化
		if(typeof this.init === 'function') {
			this.init.apply(this, arguments);
		}
	};

	klass.extend = function(options) {
		var Parent = this;
		var Child = function(){
			Parent.apply(this, arguments);//继承父类的属性和方法
		};
		var F = function(){
			this.constructor = Child;
		};
		F.prototype = Parent.prototype;
		Child.prototype = new F();//间接方法,防止父类被污染

		for(var prop in options){//扩展属性和方法
			Child.prototype[prop] = options[prop];
		}
        Child.extend = klass.extend;//让子类可以继续扩展子类

		return Child;
	};

	return klass;
}

var Parent = Class.new();//创建Parent类
Parent.prototype.init = function(name) {//定义初始化方法
	this.name = name;
	this.showName = function() {
		console.log('name:'+this.name);
	};
};
Parent.prototype.sayHello = function() {//定义方法
	console.log('hello, '+this.name);
};

var parent = new Parent('Peter');//实例化Parent类
parent.sayHello();//输出:hello, Peter
parent.showName();//输出:name:Peter

var Child = Parent.extend({//创建Child类,继承Parent并扩展属性和方法
	age : 35,
	showAge : function() {
		console.log('age:'+this.age);
	}
});

var child = new Child('Hans');//实例化Child类
child.sayHello();//输出:hello, Hans
child.showName();//输出:name:Hans
child.showAge();//输出:age:35
该继承模式综合了上述几种模式,已经较为健壮了,读者可以直接仿照该继承模式来编写自己项目中的框架代码,相信能够给项目带来许多改善。
1.4.3 对象继承
        由于JavaScript动态扩展的特性,可以直接给对象添加和删除属性,在很多情况下需要让一个对象拥有另一个对象的方法和属性,这种做法我们称之为对象继承,其实叫对象扩展更加贴切。ECMAScript5制定了一系列新API,其中一个创建扩展对象的API是Object.create(prototype, descriptors),可以用来扩展对象,这种方式原型链干净,但是浏览器支持有限制。在JQuery中扩展一个对象可以调用如下方法:

extend(dest,src1,src2,src3...srcN)

其中dest为目标对象,该方法将 src1、src2...对象的属性和方法逐一复制给dest对象。对象扩展已经被很多JavaScript库做得很好了,所以我们无须重复发明轮子,放心地使用这些工具库,将大大提高前端开发效率。

1.5 模块

1.5.1 命名空间
        命名空间可以用来防止函数命名冲突,大多数静态语言都支持命名空间,虽然JavaScript不支持,但我们可以通过编程的手段来模拟实现命名空间,见如下代码示例:
var cn = cn || {};
cn.hunnu = cn.hunnu || {};
cn.hunnu.edu = cn.hunnu.edu || {};

var Class = cn.hunnu.edu.Class = function(){};

1.5.2 模块创建
        前面讲了命名空间可以防止函数命名冲突,那么模块化则可以防止全局变量冲突,模块化编程要求一个js文件为一个模块,程序模块化可以使得系统设计变得优良,使得代码管理和维护变得轻松。下面通过代码简单地演示如何创建和使用模块:

编写cn.hunnu.edu.util.js文件
(function() {
	var name = 'Peter';
	var sayHello = function() {
		console.log('hello, '+this.name);
	};

	this.cn = this.cn || {};
	this.cn.hunnu = this.cn.hunnu || {};
	this.cn.hunnu.edu = this.cn.hunnu.edu || {};
	this.cn.hunnu.edu.util = {
		name: name,
		sayHello: sayHello
	};

}).call(global);//在NodeJS环境中传入global全局对象,如果在浏览器中运行则传入window对象

编写app.js文件
console.log(cn.hunnu.edu.util.name);//输出:Peter
cn.hunnu.edu.util.sayHello();//输出:hello, Peter

        上述代码为什么这么写,这样写有什么优点?由于这些涉及到JavaScript作用域和闭包相关问题,请读者自行研究相关知识点来加深理解。读者不妨参考一些主流的JavaScript框架,几乎都会采用上述的方式进行设计。
1.5.3 AMD和CMD

        前面简单介绍了模块创建的基本原理,在实际应用中要使用模块化编程还需要考虑许多问题,如模块书写规范、模块依赖、异步加载等等,要解决这一系列问题就需要有一个统一的规范,目前已形成的两大规范分别是CMD和AMD。

        CMD是"Common Module Definition"的缩写,该规范明确了模块的基本书写格式和基本交互规则,该规范是Sea.js推广过程中形成的。

        AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行,该规范是在推广RequireJS库形成的。下面将简单介绍一下RequireJS的用法。

        首先到相关网站下载最新的require.js和其他js库,然后搭建一个基本工程目录,如下图所示,js/app目录放置应用程序模块,js/lib目录放置第三方js库,js/main.js为程序初始化入口:

- www/

     - index.html

     - js/

          - app/

                - util.js

                - a.js

          - lib/

                - jquery.js

                - backbone.js

          - main.js

在index.html文件中添加:

<script data-main="js/main" src="js/require.js"></script>
data-main属性告诉require.js在加载完require.js之后再加载js/main.js,因此一般把main.js作为程序入口,对整个程序进行初始化操作。

编辑main.js,添加如下内容:

requirejs.config({
    baseUrl: 'js/lib',//定义模块根路径
    paths: {
        app: '../app'//定义模块子路径,相对于根路径
    }
});
//开始主函数逻辑
requirejs(['jquery', 'backbone', 'app/util', 'js/app/a.js'], 
    function ($,  backbone, util) {
        //jQuery, backbone 和app/util模块都已全部加载
        //并且可以直接在这里使用
});

在util.js中定义模块:

/**
 * 定义模块
 * define函数第一个参数为依赖模块列表,依赖模块加载后作为参数分别传入到回调函数
 * define函数第二个参数为模块定义函数,将返回模块对象
 */
define(["jquery", "backbone"], function($, Backbone) {
        //返回一个util模块对象.
        return {
            extend: function(dest, src) {
                return $.extend(dest, src);
            },
            createModel : function() {
            	return Backbone.Model.extend({});
            }
        };
    }
);
        上面介绍了如何在html页面引入require.js,如何定义一个module和如何使用module,这些都非常容易上手,当然,要想了解require.js的更多用法,还请参考官方网站:http://requirejs.org

        至此你已经可以构建一个结构良好的前端应用程序了。前面已经对类继承、对象扩展和模块化编程的具体实现原理做了简单分析,由于目前已有大量的js库已经实现了这些功能,所以我们只需直接拿来用即可。对于JavaScript编程热爱者,不仅要学会如何使用主流的JavaScript框架,还要理解其中设计原理,方能知其然知其所以然,在具体项目中遇到问题时,才不至于手足无措。

二. NodeJS入门

2.1 开发环境搭建

        Node.js是一个基于Chrome JavaScript引擎的运行平台,该平台用来构建高性能、伸缩性强的服务器端应用程序,Node.js是跨平台的轻量级引擎,它采用事件驱动和非阻塞I/O模型使得其适合编写高性能的、数据密集型的和实时性强的应用程序。

       安装node.js:windows平台直接访问官网http://nodejs.org/直接下载安装,linux和mac平台下可以通过git下载源码编译安装,见下面安装步骤:

git clone git://github.com/joyent//node.git
cd node
./configure
make
sudo make install

        安装通用插件。Node.js安装后便可以通过包管理器npm安装其他插件,其官网上https://npmjs.org/有大量的插件供开发者选择,下面将介绍几款比较常用的插件的安装。

  • mocha 单元测试框架

> npm install -g mocha

  • node-inspector 代码调试器

> npm install -g node-inspector

  • supervisor 热部署插件

> npm install -g supervisor

  • express 网站构建插件,详细请访问:http://expressjs.jser.us/

> npm install -g express

  • 前端应用构建工具Yeoman,详细请访问:http://yeoman.io/

> npm install -g yo

  • Karma测试驱动器(Test Runner),详情请访问:https://github.com/vojtajina/karma/

> npm install -g karma

        Nodejs的插件非常丰富,如数据库支持、集群、MVC框架、模板工具、图片处理、邮件服务等等,这些插件都可以在互联网获取。

2.2 hello world开始

        打开命令行窗口,输入node命令,回车进入运行窗口,输入JavaScript代码回车执行,返回结果,如下所示:
>node
>console.log(‘hello, world’);
hello, world

>

        两次按下ctrl+c即可退出node窗口运行模式。当然,node不限于此,我们可以把JavaScript逻辑写在js文件中,然后通过node命令来运行。

        编写hello.js:

function Hello() {
   this.sayHello = function() {
      console.log('hello, world');
   };
}

var hello = new Hello();
hello.sayHello();
        运行hello.js

>node hello.js

       运行结果:
       hello, world

2.3 NodeJS应用模块化

2.4.1 引用外部模块

        node既然作为一种服务脚本语言,跟其他服务器静态语言一样,需要一套完善函数库(类库),而组织类库或者编写第三方api均需要一个标准规范,node使用CommonJS规范来具体实施类库编写。CommonJS规范包括模块(module)、包(package)、单元测试等等内容。

        下面演示如何引用一个模块:

        编写foo.js

var circle = require('./circle.js');
console.log( 'The area of a circle of radius 4 is '+ circle.area(4));
        上面代码通过require引入circle模块,并在后面直接调用circle的area方法计算半径为4的圆形面积。
2.4.2 构建自定义模块

        接着上面的例子,我们来看看circle模块如何定义,编写circle.js文件:

var PI = Math.PI;

exports.area = function (r) {
  return PI * r * r;
};

exports.circumference = function (r) {
  return 2 * PI * r;
};

       代码中exports向外面公开一个模块,即为circle模块,该模块申明了两个方法分别是area和circumference,一旦模块定义好,便可以在其他模块中通过require方法引用它。模块名称即为js文件名。

2.4 NodeJS常用API

2.5.1 File System

文件系统,即针对系统文件操作所提供的一套函数库,下面简单演示一下文件读取api:

var fs = require('fs');
var fileName = 'D:/git/nodejs_workspace/oojs/test.js';
fs.readFile(fileName, 'utf-8', function(err, fc) {
    console.log(fc);
});
以上程序将读取文件D:/git/nodejs_workspace/oojs/test.js的内容,并将其打印到控制台,注意,readFile方法是异步方法,由于文件读取是需要消耗磁盘I/O时间的,所以上面打印操作放在回调函数中执行,不会造成主体函数的阻塞等待问题,这个特性是其他静态语言无法相比的。

2.5.2 Http

一个简单的服务器应用,对所有HTTP请求均返回“hello, world”,编写server.js:

var http = require('http');

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
}).listen(1337, '127.0.0.1');

console.log('Server running at http://127.0.0.1:1337/');
运行该应用:>node server.js

打开浏览器并输入http://127.0.0.1:1337,可以看到返回hello, world字符串。更多关于node服务器端编程接口,请访问:http://nodejs.org/api/http.html

三. JavaScript书籍推荐


《JavaScript权威指南》弗兰纳根(David Flanagan)


《JavaScript语言精粹(修订版)》道格拉斯•克罗克福德


《JavaScript高级程序设计(第3版)》尼古拉斯•泽卡斯


作者:肖波
日期:2014-06-13
QQ:691546285   欢迎交流~~
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值