继承

在JavaScript中继承是一个非常复杂的话题,比其他任何面向对象的语言中的继承都复杂得多。在大多数其他面向对象语言中,继承一个类只需使用一个关键字即可。但JavaScript中要想达到传承公用成员的目的,需要采取一系列措施。得益于语言的灵活性,你既可使用标准的基于类的继承,也可使用更微妙一些的原型式继承。
本章将讨论在JavaScript中创建子类的各种技术以及他们的适用场合。

4.1 为什么需要继承

借助继承,可以在现有类的基础上进行设计并充分利用它们已经具备的各种方法,而对设计进行修改也更为轻松。这样可以减少重复性的代码
但这样可能会导致二者产生强耦合,也即一个类依赖于另一个类的内部实现。

4.2 类式继承

用函数来声明类,其函数名就是类名,首字母应大写,该函数也叫构造函数,在构造函数中创建实例属性要使用关键字this,类的方法则被添加到其prototype对象中。
用关键字new来创建实例。

JavaScript中简单的类声明与实例创建:

function Person(name) {
	this.name = name;
}
Person.prototype.getName = function () {
	return this.name;
}
var render = new Person('John Smith');
render.getName();

4.2.1 原型链

创建继承Person的类

function Author(name, books){
	Person.call(this,name);//在Author的this上调用父类的构造函数
	this.books = books;// 为Author添加属性
}
// 设置原型链,不然Author实例访问不到Person原型上的方法,只能访问Person上的属性
Author.prototype = new Person();
// 重设constructor属性为Author,不设的话,其constructor是指向Person的
Author.prototype.constructor = Author;
Author.prototype.getBooks = function() {
	return this.books;
}

要点:

  • 在构造函数中,调用超类的构造函数。
  • 设置原型链。JavaScript中每个对象都有一个名为prototype的属性,这个属性要么指向另一个对象,要么是null。在访问对象的某个成员时,如果这个成员不在当前对象,那么回在prototype属性指向的对象中查找。如果在那个对象中也没有找到,那么JavaScript会沿着原型链向上逐一访问每个原型对象,直到到达原型链的最顶端的Object.prototype对象。
  • 重设prototype的constructor属性为Author。因为把prototype属性设置为Person的实例时,其constructor属性被抹除了。

4.2.2 extend函数

为了简化类的声明,可以把派生子类的整个过程包装在一个名为extend的函数中。它的作用于其他语言中的extend关键字类似,即基于一个给定的类结构创建一个新的类:

function extend(subClass, superClass) {
	var F = function() {};
	F.prototype = superClass.prototype;
	subClass.prototype = new F();
	subClass.prototype.constructor = subClass;
}

作为一项改进,它添加了一个空函数F,并将用它创建的对象实例插入原型链中。这样做可以避免创建超类的新实例,因为它可能比较庞大,而且有时超类的构造函数有一些副作用,或者会执行一些需要进行大量计算的任务。

使用extend函数后,前面那个Person/Author例子变成了:

/* Person 类*/
function Person(name) {
	this.name = name;
}
Person.prototype.getName = function () {
	return this.name;
}
/* Author类 */
function Author(name, books){
	Person.call(this,name);
	this.books = books;
}
extend(Author, Person);
Author.prototype.getBooks = function() {
	return this.books;
}

唯一的问题是超类(Person)的名称被固化在了Author类的声明之中,看一下下面这个版本:

/* Extend function, improved*/
function extend(subClass, superClass) {
	var F = function() {};
	F.prototype = superClass.prototype;
	subClass.prototype = new F();
	subClass.prototype.constructor = subClass;

	subClass.superclass = superClass.prototype;
	if(superClass.prototype.constructor == Object.prototype.constructor){
		superClass.prototype.constructor = superClass;
	}
}

这个版本为子类Author类提供了superclass属性,这个属性可以用来弱化Author与Person之间的耦合,最后3行代码 确保超类的constructor属性被正确设置,因为在用这个新的superclass属性调用超类的构造函数时这个问题很重要,如下:

function Author(name, books) {
	Author.superclass.constructor.call(this, name);
	this.books = books;
}
extend(Author, Person);

Author.prototype.getBooks = function() {
	return this.books;
}

有了superclass属性,就可以直接调用超类中的方法。这在既要重定义超类的某个方法,又想访问其在超类中的实现时可以派上用场。例如:

Author.prototype.getName = function (){
	var name = Author.superclass.getName.call(this);
	return name + ', Author of' + this.getBooks().join(', ');
}

4.3 原型式继承

原型式继承,只从对象的角度来思考。
使用原型式继承时,并不需要用类来定义对象的结构,只需直接创建一个对象即可。这个对象随后可以被新的对象重用,这得益于原型链查找的工作机制。该对象被称为原型对象,这是因为它为其他对象提供了一个原型。

下面我们将使用原型式继承来重新设计Person和Author:

var Person = {
	name: 'default name',
	getName: function() {
		return this.name;
	}
};

var render = clone(Person);
render.getName();// default name
render.name = 'John Smith';
render.getName();// John Smith

clone函数(见4.3.2节)可以用来创建新的类Person对象。它会创建一个空对象,而该空对象的原型设置为Person。这意味着在这个新对象中查找某个方法或属性时,如果找不到,那么查找过程会在其原型对象中继续进行。

你不必为创建Author而定义一个Person类,只要执行一次克隆即可:

var Author = clone(Person);
Author.books = [];//default value
Author.getBooks = function () {
	return this.books;
}

var author = [];
author[0] = clone(Author);
author[0].name = 'Dustin Diaz';
author[0].books = ['JavaScript Design Patterns'];

author[1] = clone(Author);
author[1].name = 'Ross Harmes';
author[1].books = ['JavaScript Design Patterns'];

author[1].getName();
author[1].getBooks();

4.3.1 对继承而来的成员的读和写的不对等性

在类式继承中,Author的每一个实例都有一份自己的books数组副本。但是对于使用原型式继承方式创建的类Author来说,一个克隆并非其原型对象的一份完全独立的副本,它只是一个以那个对象为原型对象的空对象而已。克隆刚创建时,author[1].name其实是一个指向最初的Person.name的链接。对于从原型对象继承而来的成员,其读和写具有内在的不对等性。其表现在:

  • 在读取author[1].name的值时,如果你还没有直接为author[1]实例定义name属性的话,那么所得到的是其原型对象的同名属性值。
  • 而在写入author[1].name的值时,你是直接为author[1]对象定义一个新属性。
var authorClone = clone(Author);
alert(authorClone.name);//指向Person.name "default name"

authorClone.name = 'new name';//直接为authorClone对象定义name属性
alert(authorClone.name);//指向authorClone.name, "new name"

authorClone.books.push('new book');//指向Author.books,
								   //现在已经修改了Author.books的默认值了
								   //所有的Author子类的books将有新的默认值
authorClone.books = [];//为authorClone添加一个新的属性books
authorClone.books.push('new book');//修改的是authorClone.books

var anotherAuthor = clone(Author);
alert(anotherAuthor.books);//['new book']

上面的代码也说明了为什么必须为通过引用传递的数据类型的属性创建新副本。在这类场合中,可以使用hasOwnProperty方法来区分对象的实际成员和它继承而来的成员。

var CompoundObject = {
	string1: 'default value',
	childObject: {
		bool: true,
		num: 10
	}
}
var compoundObjectClone = clone(CompoundObject);

//Bad!改变了CompoundObject.childObject.num的值
compoundObjectClone.childObject.num = 5;
//Better
compoundObjectClone.childObject = {
	bool: true,
	num: 5
}

4.3.2 clone函数

function clone(object) {
	function F() {}//创建了一个新的空函数F
	F.prototype = object;//将object设置为F的prototype属性
	return new F();
}

由此可以体会到JavaScript设计者最初的用意。prototype属性就是用来指向原型对象的,通过原型链接机制,它提供了到所有继承而来的成员的链接。
函数所返回的新对象是一个以给定对象为原型对象的空对象。

4.4 类式继承和原型链继承的对比

如果你设计的是一个供众人使用的API,或者可能会有不熟悉原型式继承的其他程序员基于你的代码进行设计,那么最好还是使用类式继承。
原型式继承更能节约内存。克隆出来对象都共享每个属性和方法的唯一一份实例,只有在直接设置了某个克隆出来的对象的属性和方法时,情况才会有所变化。而类式继承方式创建的每一个对象在内存中都有自己的一套属性的副本。
原型式继承也比类式继承显得更为简练。
该使用哪种继承主要还是取决于你更喜欢哪种范型。本书介绍的每一种设计模式中都可以使用这两种继承范型。为了便于理解,在后面的设计模式中,我们主要使用类式继承。

4.5 继承与封装

从现有的类派生出一个子类时,只有公用和特权成员会被承袭下来。由于这个原因,门户大开型类是最适合于派生子类的。它们的所有成员都是公开的,因此可以被遗传给子类。如果某个成员需要稍加隐藏,你可以使用下划线符号规范。
在派生具有真正的私用成员的类时,因为其特权方法是公用的,所以它们会被遗传下来。借此可以在子类中间接访问父类的私用属性,但子类自身的实例方法都不能直接访问这些私用属性。父类的私用成员只能通过这些既有的特权方法进行访问,你不能在子类中添加能够直接访问它们的新的特权方法。

4.6 掺元类

如果你想把一个函数用到多个类中,可以通过扩充的方式让这些类共享该函数。
实际做法大体为:
先创建一个包含各种通用方法的类,在用它扩充其他类。
这种包含通用方法的类称为掺元类。它们通常不会被实例化或直接调用。其存在的目的只是向其他类提供自己的方法。

/* Mixin : 掺元类 */
var Mixin = function() {};
Mixin.prototype = {
	serialize: function() {
		var output = [];
		for(key in this) {
			output.push(key+ ':' + this[key]);
		}
		return output.join(', ');
	}
};
/* augment方法用来扩充receivingClass类 */
function augment(receivingClass, givingClass) {
	for(methodName in givingClass.prototype) {
		if(!receivingClass.prototype[methodName]) {
			receivingClass.prototype[methodName] = givingClass.prototype[methodName];
		}
	}
}

augment(Author, Mixin);
var author = new Author('Ross Harmes', ['JavaScript Design Patterns']);
var serializedString = author.serialize();

用一些方法来扩充一个类有时比让这个类继承另一个类更合适。

4.7 示例:就地编辑

什么是就地编辑?
就地编辑是指网页上的一段普通文本被点击后就变成一个配有一些按钮的表单域,以便用户就地对这段文本进行编辑

有一个任务:编写一个用于创建和管理就地编辑域的可重用的模块化API。使用这个API,用户应该能够为对象分配一个唯一的ID值,能够为它提供一个默认值,并且能够指定其在页面上的目标位置。用户还应该在任何时候都可以访问到这个域的当前值,并且可以选择具体使用的编辑域(比如多行文本框或单行文本框)。

本节将提供这个任务的三种解决方案:类式继承、原型式继承、掺元类的使用。

4.7.1 类式继承解决方案

实现如下:

function EditInPlaceField(id, parent, value) {
	this.id = id;
	this.value = value || 'default value';
	this.parentElement = parent;

	this.createElements(this.id);
	this.attachEvents();
};

EditInPlaceField.prototype = {
	createElements: function(id){
		this.containerElement = document.createElement('div');
		this.parentElement.appendChild(this.containerElement);

		this.staticElement = document.createElement('span');
		this.containerElement.appendChild(this.staticElement);
		this.staticElement.innerHTML = this.value;

		this.fieldElement = document.createElement('input');
		this.fieldElement.type = 'text';
		this.fieldElement.value = this.value;
		this.containerElement.appendChild(this.fieldElement);

		this.saveButton = document.createElement('input');
		this.saveButton.type = 'button';
		this.saveButton.value = 'Save';
		this.containerElement.appendChild(this.saveButton);

		this.cancelButton = document.createElement('input');
		this.cancelButton.type = 'button';
		this.cancelButton.value = 'Cancel';
		this.containerElement.appendChild(this.cancelButton);
		this.convertToText();
	},
	attachEvents: function() {
		var that = this;
		addEvent(this.staticElement, 'click', function() { that.convertToEditable();});
		addEvent(this.saveButton, 'click', function() { that.save();});
		addEvent(this.cancelButton, 'click', function() { that.cancel();});
	},
	convertToEditable: function() {
		this.staticElement.style.display = 'none';
		this.fieldElement.style.display = 'inline';
		this.saveButton.style.display = 'inline';
		this.cancelButton.style.display = 'inline';

		this.setValue(this.value);
	},
	save: function() {
		this.value = this.getValue();
		var that = this;
		var callback = {
			success: function() { that.convertToText(); },
			failure: function() { alert('Error saving value.');}
		};
		ajaxRequest('GET', 'save.php?id=' + this.id + '&value=' + this.value, callback);
	},
	cancel: function() {
		this.convertToText();
	},
	convertToText: function() {
		this.fieldElement.style.display = 'none';
		this.saveButton.style.display = 'none';
		this.cancelButton.style.display = 'none';
		this.staticElement.style.display = 'inline';

		this.setValue(this.value);
	},
	setValue: function(value) {
		this.fieldElement.value = value;
		this.staticElement.innerHTML = value;
	},
	getValue: function() {
		return this.fieldElement.value;
	}
};
// 创建一个就地编辑域
var titleClassical = new EditInPlaceField('titleClassical',$('doc'),'Title Here');
var currentTitleText = titleClassical.getValue();

接下来创建一个使用多行文本框的类。这个EditInPlaceArea类与EditInPlaceField类有很多共同之处,将EditInPlaceField作为父类来处理:

function EditInPlaceArea(id, parent, value) {
	EditInPlaceArea.superClass.constructor.call(this, id, parent, value);
};
extend(EditInPlaceArea, EditInPlaceField);

// Override certain methods
EditInPlaceArea.prototype.createElements = function(id) {
	this.containerElement = document.createElement('div');
	this.parentElement.appendChild(this.containerElement);

	this.staticElement = document.createElement('p');
	this.containerElement.appendChild(this.staticElement);
	this.staticElement.innerHTML = this.value;

	this.fieldElement = document.createElement('textarea');
	this.fieldElement.value = this.value;
	this.containerElement.appendChild(this.fieldElement);

	this.saveButton = document.createElement('input');
	this.saveButton.type = 'button';
	this.saveButton.value = 'Save';
	this.containerElement.appendChild(this.saveButton);

	this.cancelButton = document.createElement('input');
	this.cancelButton.type = 'button';
	this.cancelButton.value = 'Cancel';
	this.containerElement.appendChild(this.cancelButton);
	this.convertToText();
};
EditInPlaceArea.prototype.convertToEditable = function () {
	this.staticElement.style.display = 'none';
	this.fieldElement.style.display = 'block';
	this.saveButton.style.display = 'inline';
	this.cancelButton.style.display = 'inline';

	this.setValue(this.value);	
};
EditInPlaceArea.prototype.convertToText = function () {
	this.fieldElement.style.display = 'none';
	this.saveButton.style.display = 'none';
	this.cancelButton.style.display = 'none';
	this.staticElement.style.display = 'block';

	this.setValue(this.value);
}

4.7.2 原型式继承解决方案

var EditInPlaceField = {
	configure: function(id, parent, value) {
		this.id = id;
		this.value = value || 'default value';
		this.parentElement = parent;

		this.createElements(this.id);
		this.attachEvents();
	},
	createElements: function(id){
		this.containerElement = document.createElement('div');
		this.parentElement.appendChild(this.containerElement);

		this.staticElement = document.createElement('span');
		this.containerElement.appendChild(this.staticElement);
		this.staticElement.innerHTML = this.value;

		this.fieldElement = document.createElement('input');
		this.fieldElement.type = 'text';
		this.fieldElement.value = this.value;
		this.containerElement.appendChild(this.fieldElement);

		this.saveButton = document.createElement('input');
		this.saveButton.type = 'button';
		this.saveButton.value = 'Save';
		this.containerElement.appendChild(this.saveButton);

		this.cancelButton = document.createElement('input');
		this.cancelButton.type = 'button';
		this.cancelButton.value = 'Cancel';
		this.containerElement.appendChild(this.cancelButton);
		this.convertToText();
	},
	attachEvents: function() {
		var that = this;
		addEvent(this.staticElement, 'click', function() { that.convertToEditable();});
		addEvent(this.saveButton, 'click', function() { that.save();});
		addEvent(this.cancelButton, 'click', function() { that.cancel();});
	},
	convertToEditable: function() {
		this.staticElement.style.display = 'none';
		this.fieldElement.style.display = 'inline';
		this.saveButton.style.display = 'inline';
		this.cancelButton.style.display = 'inline';

		this.setValue(this.value);
	},
	save: function() {
		this.value = this.getValue();
		var that = this;
		var callback = {
			success: function() { that.convertToText(); },
			failure: function() { alert('Error saving value.');}
		};
		ajaxRequest('GET', 'save.php?id=' + this.id + '&value=' + this.value, callback);
	},
	cancel: function() {
		this.convertToText();
	},
	convertToText: function() {
		this.fieldElement.style.display = 'none';
		this.saveButton.style.display = 'none';
		this.cancelButton.style.display = 'none';
		this.staticElement.style.display = 'inline';

		this.setValue(this.value);
	},
	setValue: function(value) {
		this.fieldElement.value = value;
		this.staticElement.innerHTML = value;
	},
	getValue: function() {
		return this.fieldElement.value;
	}
}
// 创建一个就地编辑域
var titlePrototypal = clone(EditInPlaceField);
titlePrototypal .configure('titleClassical',$('doc'),'Title Here');
var currentTitleText = titlePrototypal.getValue();

上述代码中并没有创建类,而是创建了一个对象。原型式继承不使用构造函数。
这里不再使用new运算符,而是使用clone函数来创建一个对象副本,然后再对这个副本进行配置。

创建这个对象的子对象:

var EditInPlaceArea = clone(EditInPlaceField);
// Override certain methods.
EditInPlaceArea.createElements = function(id) {
	this.containerElement = document.createElement('div');
	this.parentElement.appendChild(this.containerElement);

	this.staticElement = document.createElement('p');
	this.containerElement.appendChild(this.staticElement);
	this.staticElement.innerHTML = this.value;

	this.fieldElement = document.createElement('textarea');
	this.fieldElement.value = this.value;
	this.containerElement.appendChild(this.fieldElement);

	this.saveButton = document.createElement('input');
	this.saveButton.type = 'button';
	this.saveButton.value = 'Save';
	this.containerElement.appendChild(this.saveButton);

	this.cancelButton = document.createElement('input');
	this.cancelButton.type = 'button';
	this.cancelButton.value = 'Cancel';
	this.containerElement.appendChild(this.cancelButton);
	this.convertToText();
};
EditInPlaceArea.convertToEditable = function () {
	this.staticElement.style.display = 'none';
	this.fieldElement.style.display = 'block';
	this.saveButton.style.display = 'inline';
	this.cancelButton.style.display = 'inline';

	this.setValue(this.value);	
};
EditInPlaceArea.convertToText = function () {
	this.fieldElement.style.display = 'none';
	this.saveButton.style.display = 'none';
	this.cancelButton.style.display = 'none';
	this.staticElement.style.display = 'block';

	this.setValue(this.value);
}

4.7.3 掺元类解决方案

首先创建一个包含了所有要共享的方法的掺元类,然后再创建一个新类,并使用augment函数来让这个新类共享到那些方法。

// 掺元类
var EditInPlaceMixin = function() {};
EditInPlaceMixin.prototype ={
	createElements: function(id){
		this.containerElement = document.createElement('div');
		this.parentElement.appendChild(this.containerElement);

		this.staticElement = document.createElement('span');
		this.containerElement.appendChild(this.staticElement);
		this.staticElement.innerHTML = this.value;

		this.fieldElement = document.createElement('input');
		this.fieldElement.type = 'text';
		this.fieldElement.value = this.value;
		this.containerElement.appendChild(this.fieldElement);

		this.saveButton = document.createElement('input');
		this.saveButton.type = 'button';
		this.saveButton.value = 'Save';
		this.containerElement.appendChild(this.saveButton);

		this.cancelButton = document.createElement('input');
		this.cancelButton.type = 'button';
		this.cancelButton.value = 'Cancel';
		this.containerElement.appendChild(this.cancelButton);
		
		this.convertToText();
	},
	attachEvents: function() {
		var that = this;
		addEvent(this.staticElement, 'click', function() { that.convertToEditable();});
		addEvent(this.saveButton, 'click', function() { that.save();});
		addEvent(this.cancelButton, 'click', function() { that.cancel();});
	},
	convertToEditable: function() {
		this.staticElement.style.display = 'none';
		this.fieldElement.style.display = 'inline';
		this.saveButton.style.display = 'inline';
		this.cancelButton.style.display = 'inline';

		this.setValue(this.value);
	},
	save: function() {
		this.value = this.getValue();
		var that = this;
		var callback = {
			success: function() { that.convertToText(); },
			failure: function() { alert('Error saving value.');}
		};
		ajaxRequest('GET', 'save.php?id=' + this.id + '&value=' + this.value, callback);
	},
	cancel: function() {
		this.convertToText();
	},
	convertToText: function() {
		this.fieldElement.style.display = 'none';
		this.saveButton.style.display = 'none';
		this.cancelButton.style.display = 'none';
		this.staticElement.style.display = 'inline';

		this.setValue(this.value);
	},
	setValue: function(value) {
		this.fieldElement.value = value;
		this.staticElement.innerHTML = value;
	},
	getValue: function() {
		return this.fieldElement.value;
	}
}
function EditInPlaceField(id, parent, value) {
	this.id=id;
	this.value = value || 'default value';
	this.parentElement = parent;
	this.createElements(this.id);
	this.attachEvents();
};
augment(EditInPlaceField,EditInPlaceMixin);
// 创建一个就地编辑域
var titleClassical = new EditInPlaceField('titleClassical',$('doc'),'Title Here');
var currentTitleText = titleClassical.getValue();

要创建使用多行文本框的类,你不是从EditInPlaceField派生子类,而是另行创建一个新类并用同样的掺元类扩充它。在扩充这个新类之前,先要为其定义一些方法。由于它们是在扩充之前定义的,所以不会被覆盖。

function EditInPlaceArea(id, parent, value) {
	this.id=id;
	this.value = value || 'default value';
	this.parentElement = parent;
	this.createElements(this.id);
	this.attachEvents();
};

// Add certain methods
EditInPlaceArea.prototype.createElements = function(id) {
	this.containerElement = document.createElement('div');
	this.parentElement.appendChild(this.containerElement);

	this.staticElement = document.createElement('p');
	this.containerElement.appendChild(this.staticElement);
	this.staticElement.innerHTML = this.value;

	this.fieldElement = document.createElement('textarea');
	this.fieldElement.value = this.value;
	this.containerElement.appendChild(this.fieldElement);

	this.saveButton = document.createElement('input');
	this.saveButton.type = 'button';
	this.saveButton.value = 'Save';
	this.containerElement.appendChild(this.saveButton);

	this.cancelButton = document.createElement('input');
	this.cancelButton.type = 'button';
	this.cancelButton.value = 'Cancel';
	this.containerElement.appendChild(this.cancelButton);
	this.convertToText();
};
EditInPlaceArea.prototype.convertToEditable = function () {
	this.staticElement.style.display = 'none';
	this.fieldElement.style.display = 'block';
	this.saveButton.style.display = 'inline';
	this.cancelButton.style.display = 'inline';

	this.setValue(this.value);	
};
EditInPlaceArea.prototype.convertToText = function () {
	this.fieldElement.style.display = 'none';
	this.saveButton.style.display = 'none';
	this.cancelButton.style.display = 'none';
	this.staticElement.style.display = 'block';

	this.setValue(this.value);
};

augment(this.value);

4.8 继承的适用场合

继承会使代码变得更加复杂、更难被JavaScript新手理解,所以只应该用在其带来的好处胜过缺点的那些场合。它的主要好处表现在代码的重用方面。通过建立类或对象之间的继承关系,有些方法我们只需要定义一次即可。同样,如果需要修改这些方法或排查其中的错误,那么由于其定义只出现在一个位置,所以非常有利于节省时间和精力。
各种继承范型都有自己的优缺点。在内存效率比较重要的场合原型式继承是最佳选择。如果与对象打交道的都是些只熟悉其他面向对象语言中的继承机制的程序员,那么最好使用类式继承。这两种方法都适合于类间差异较小的类层次体系。如果类之间的差异较大,那么用掺元类中的方法来扩充这些类往往是一种更合理的选择。

4.9 小结

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值