每个js对象都是一个属性的集合,相互之间没有任何联系。在js中也可以定义对象的类,让每个对象都共享某些属性,这种‘共享’的特性是非常有用的。类的成员或实例包含一些属性,这种‘共享’的特性是非常有用的。类的成员或实例都包含一些属性,用以存放或定义它们的状态,其中有些属性定义了它们的行为(通常成为方法)。这些行为通常由类定义的,而且为所有实例所共享。假如,假设有一个名为Complex的类用来表示复数,同时还定义了一些复数运算。一个Complex实例应当包含复数的实部和虚部(状态),同样Complex类还会定义复数的加法和乘法操作(行为)。
在js中,类的实现是基于其原型继承机制的。如果两个实例都从同一个原型对象上继承属性,我们说它们是同一个类的实例。
如果两个对象继承属性来自同一个原型,往往意味着它们是由同一个构造函数并初始化的。
如果你对诸如Java和C++这种强类型的面向对象编程比较熟悉,你会发现js中的类和Java和C++中的类有很大不同。尽管在写法上类似,而且js中也能‘模拟’出很多经典类的特性,但是最好的理解js的类和基于原型的继承机制,以及和传统的Java的类和基于类的继承机制的不同之处。
1 类和模型
在js中,类的所有实例对象都从同一个原型对象上继承属性。因此,原型对象是类的核心。比如定义了一个原型对象,然后通过inherit()函数,这个函数返回一个新创建的对象,后者继承某个原型对象。如果定义一个原型对象,然后通过inherit()函数创建一个继承自它的对象,这样就定义了一个js类。通常,类的实例还需要进一步的初始化,通常是通过定义一个函数来创建并初始化这个新对象 eg
下面的这段代码定义一个方法是range(),我们给range函数定义了一个range.methods,用以快捷地存放定义类的原型对象,把原型对象挂在函数上没什么大不了的,但也不是惯用做法。再者,注意range()函数给每个范围对象都定义了from和to属性,用以定义范围的起始位置和结束位置,这两个属性是非共享的,当饭也是不可继承的。最后,注意在rang.methods中定义的那些可共享,可继承的方法都用到了from和to属性,而且使用了this关键字,为了指代它们,二者使用this关键字来指代调用这个方法的对象。任何类的方法都可以通过this的这种基本用法来读取对象属性
eg:9-1
function range(from,to){
var r=Object.create(range.methods);
r.from=from;
r.to=to;
return r;
}
range.methods={
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);
}
}
}
var r= range(1,4);
r.includes(2);
r.foreach(function(item){
console.log(item)
})//1,2,3,4
2 类和构造函数
例(eg:9-1)展示了js中定义类的其中一种方法。但这种方法并不常用,毕竟它没有定义构造函数,构造函数是用来初始化新创建的对象的,使用关键字new来调用构造函数。使用new调用构造函数来自动创建一个新对象,因此构造函数本身只需初始化这个新对象的状态即可。调用构造函数的一个重要特征是,构造函数的prototype属性被用做新对象的原型。这意味着通过同一个构造函数创建的对象都继承自一个相同的对象,因此它们都是同一个类的成员。 eg
从某种意义上来讲定义构造函数就是定义类,并且类名首字母要大写,而普通函数和方法首字母都是小写。构造函数需要通过new关键字来调用。如果将构造函数用做普通函数的话,往往不会正常工作。Range.prototype这是一个强制命名,对Range()构造函数的调用会自动的适应Rang.prototype作为新Range对象的原型
eg:9-2
function Range(from,to){
this.from=from;
this.to=to;
}
Range.prototype={
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);
}
}
}
var r=new Range(1,4);
r.includes(2);
r.foreach(function(item){
console.log(item)
})//1,2,3,4
2.1 构造函数和类的标识
上文提到,原型对象是类的唯一标识:当且仅当两个对象继承来自同一个原型对象时,它们才属于同一个类的实例。而初始化对象的状态的构造函数则不能作为类的标识,两个构造函数的prototype属性可能指向同一个原型对象。那么这两个构造函数创建的实例属于同一个类。
尽管构造函数不想原型那样基础,但构造函数时类的‘外在表现’。很明显的,构造函数的名字通常用做类名。比如,我们说Range()构造函数创建Range对象。然而,更根本地讲,当使用instanceof运算符来检测对象是否属于某个类时会用到构造函数。假设这里有一个对象r,我们想知道r是否是Range对象
r instanceof Range //如果r继承自Range.prototype,则返回true
实际上instanceof运算符并不会检测r是不是由Range()构造函数初始化来的,而会检查r是否继承自Range.prototype.不过,instanceof的语法强调了‘构造函数是类的公共标识’的概念。
2.2 constructor属性
在(eg:9-2)中,将Range.prototype定义为一个新对象,这个对象包含类所需的方法,其实没必要新创建一个对象,用单个对象直接量的属性就可以方便地定义原型上的方法。任何js函数都可以用做构造函数,并且调用构造函数是需要用到一个prototype属性的。因此,每个js函数(除了Function.bind()方法返回的函数除外)都自动拥有一个prototype属性。这个属性的值是一个对象,这个对象包含唯一一个不可枚举属性constructor。constructor属性的值是一个函数对象。
let p=F.prototype;//这是F相关联的原型对象
let c=p.construtor;//这是与原型相关联的函数
c === F ;//true
F.prototype.construtor=true; //true
可以看到构造函数的原型中都存在预先定义好的constructor属性,这意味这对象通常继承的constructor均指代它们的构造函数。由于构造函数是类的‘公共标识’,因此这个constructor属性为对象提供了类。
var o=new F();//创建类F的一个对象
o.constructor===F;//true,constructor 这是与原型相关联的函数
构造函数 → 原型 ← 实例(继承原型属性)
Range() includes new Range()
注:图9-1Range()构造函数为例,实际上Range.prototype={},是使用它自身的一个新对象重新写预定义的Range.prototype对象。这个新属性不含有constructor属性。我们可以通过补救措施显示给原型添加一个构造函数
Range.prototype={
constructor:Range
}
另一种解法的方法,是给预定义的原型对象依次添加方法
Range.prototype.includes=function(x){
}
9.3 js中java式的类继承
如果你有java或其他类似强类型面对对象语言的开发经历,类成员的模样可能会是这样。
实例字段
它们是基于实例的属性或变量,用于保存独立对象的状态。
实例方法
它们是类的所有实例所共享的方法,由每个独立的实例调用。
类字段
这些属性或变量属于类,而不是属于类的某个实例
类方法
这些方法是属于类的,而不是属于类的某个实例
js和Java的不同之处在于,js中的函数都是以值的形式出现的,方法和字段直接并没有太大的区别。如果属性值是函数,那么这个属性就定义一个方法,否则它就是一个普通的属性或‘字段’。尽管存在诸多差异,我们还是可以用js模拟出Java中的这四类成员类型。js中的类牵扯三种不同的对象,三种对象的属性的行为和下面三种类成员非常相似
构造函数对象
之前提到,构造函数(对象)为js的类定义了名字。任何添加到这个构造函数对象中的属性都是类字段和方法(如果属性值是函数的话就是类方法)。
原型对象
原型对象的属性被类的所有实例所继承,如果原型的属性值是函数的话,这个函数就作为类的实例方法来调用。
实例对象
类的每个实例都是一个独立的对象,直接给这个实例定义的属性是不会为所有实例对象所共享的。定义在实例上的非函数属性,实际上是实例的字段。
在js中定义类的步骤可以缩减为一个三步的算法。第一步,先定义一个构造函数,并设置初始化新对象的实例属性。第二步,给构造函数的prototype对象定义实例的方法。第三步,给构造函数定义类的字段和类属性。我们可以将这三个步骤封装到一个简单的defineClass()函数中。
//一个用以定义简单类的函数
function defineClass(constructor,methods,static){
if(methods){
Object.assign(constructor.prototype,methods);
}else if(statics){
Object.assign(constructor,statics);
}
return constructor;
}
var F=function(f,t){
this.f=f;
this.t=t;
}
var methods={
incluedes:function(x){
console.log(' incluedes方法')
},
foreach:function(){
console.log('foreach 方法')
}
}
defineClass(F,methods);
let o=new F(1,2);
o.foreach();//foreach 方法
console.log(o.t)//1
写一个表示复数的类
/*
*这构造函数为他创建的每个实例对象定义了实例字段r和i
*这两个字段分别保存复数的实部和虚部
*它们是对象的状态
*/
function Complex(real,imaginary){
if(isNaN(real) || isNaN(imaginary)){
throw new TypeError();
}
this.r=real;
this.i=imaginary;
}
/*
*类的实例方法定义为原型对象的函数值属性
*这里定义的方法可以被所有实例继承,并为它们提供共享行为
*注,js的实例方法必须使用关键字this,来存取实例的字段
*/
//当前复数对象加上另外一个复数,并返回一个新的计算和值之后的复数对象
Complex.prototype.add=function(that){
return new Complex(this.r+that.r,that.i+this.i);
}
//当前复数乘以另外一个复数,并返回一个新的计算乘积之后的复数对象
Complex.prototype.mul=function(that){
return new Complex(this.r*that.r-this.i*that*i,this.r*that.i+this.i*that.r);
}
//计算复数的模,复数的模定义为圆点(0,0)到复平面的距离
Complex.prototype.mag=function(){
return Math.sqrt(this.r*this.r+this.i*this*i)
}
//复数的求负运算
Complex.prototype.neg=function(){
return new Complex(-this.r,-this.i);
}
//将复数的对象转换为一个字符串
Complex.prototype.toString=function(){
return `{ r:${this.r},i:${this.i}}`
}
//检测当前复数对象是否和另外一个复数值相等
Complex.prototype.equals=function(that){
return that !== null;
that.constructor == Complex && this.r === that.r && this.i === that.i;
}
/*
*类字段(比如常量)和类方法直接定义构造函数的属性
*需要注意的是,类的方法通常不使用关键字this
*它们只对其参数进行操作
*/
//这里预定义了一些对复数运算有所帮助的类字段
//它们的命名全都是大写,用以表明它们是常量
//(在es5中,还能设置这些类字段的属性为只读)
Complex.ZERO = new Complex(0,0);
Complex.ONE=new Complex(1,0);
Complex.I=new Complex(0,1);
//这个类方法将由实例对象的toString方法返回的字符串格式解析为一个Complex对象,或抛出一个类型错误异常
Complex.parse=function(s){
try{
var m=Complex._format.exec(x);
return new Complex(parseFloat(m[1],parseFloat(m[2])));
}catch(x){
throw new TypeError(`Can not parse ${s} as a complex number`)
}
}
//下划线前缀表明它是类内部使用的,而不属于类的公有API的部分
Complex._format=/^\{([^,]+),([^}]+)\ }$/
var c=new Complex(2,3);
var d=new Complex(c.i,c.r);
console.log(c.add(d).toString());
尽管js可以模拟出Java式的类成员,但Java中有很多重要的特性是无法在js类中模拟的。首先,对于Java类的实例方法来说,实例字段是可以做局部变量,而不需要使用关键字this来引用它们。js无法模拟这个特性,但可以使用with语句来近似的实现这个功能(但是这种做法不推荐)
Complex.prototype.toString=function(){
with(this){
return `{${this,r} , ${this.i}}`
}
}
4.类的扩充
js中基于原型的继承机制是动态的:对象从其原型继承属性,如果创建对象之后的原型的属性发生改变,也会影响到继承这个原型的所有实例对象。这意味着我们可以通过给原型对象添加新的方法来扩充js类。eg
我们可以给类Complex添加方法
Complex.prototype.conj=function(){
return new Complex(this.r,-this.i)
}
js 内置类的原型对象也是一样如此‘开放’,也就是说可以给数字,字符串,数组,函数等数据类型添加方法
eg
let str=' kkksv '
String.prototype.trim= String.prototype.trim || function(){
if(!this) return this;
return this.replace(/^\s+|\s+$/g,'')
}
console.log(str.trim().length)//5
可以给Object.prototype添加方法,从而使用所有的对象都可以调用这些方法。但这种方法是不推荐的,可以使用Object.defineProperty()方法安全的扩充Object.prototype
5.类和类型
js定义了少量的数据类型:null,undefined ,布尔值,数字,字符串,函数和对象。typeof运算符可以看的出值的类型。然而,我更希望将类作为类型来对待,这样就可以根据对象所属的类来区分它们。js语言核心中的内置对象(通常是指客户端js的宿主对象)可以根据它们的class属性来区分彼此,实例对象的class属性都是'Object'
我们可以用instanceof运算符,constructor属性,以及构造函数名字来检测任意对象的类的技术。但是每种都不是很完美
5.1 instanceof运算符
●instanceof 运算符。左操作数是待检测其类的对象,右操作数是定义类的构造函数。如果o继承自c.prototype,则表达式 o instanceof c 值为true,这里的继承可以不是直接继承,如果o所继承的对象继承自另一个对象,后一个对象继承自c.prototype,这个表达式运算结果也是true.
构造函数是类的公共标识,但是原型是唯一的标识。尽管instanceof 运算符的右操作数是构造函数,但计算过程实际上是检测了对象的继承关系,而不是检测创建对象的构造函数。
eg
var c=new Complex(2,3);
console.log(c instanceof Complex)// true
●isPrototypeOf 检测对象是否存在某个特定的原型对象
eg
let o={x:1};
let o1=Object.create(o);
console.log(o,o1);
console.log(o.isPrototypeOf(o1));//true
注:两者的缺点是,我们无法通过对象来获取类名,只能检测对象是否属于指定的类名。
5.2 constructor属性
识别对象是否存属于某个类的方法是使用constructor属性,
var c=new Complex(2,3);
console.log(c.__proto__.constructor==Complex);//true
并非所有的对象都包含constructor属性,在每个新创建的函数原型上默认会有constructor属性。
5.3 构造函数的名称
使用instanceof运算符和constructor 属性来检测对象所属的类有一个主要问题,在多个执行上下文中存在构造函数的多个副本的时候,这两种方法的检测结果会出错。多个执行上下文中国的函数看起来是一模一样的,但它们是相互独立的对象,因此彼此也不相等。
一种可能解决的方案是使用构造函数的名字而不是构造函数本身作为类标识符。一个窗口的Array构造函数和另一个窗口的Array构造函数时不相等的,但是它们的名字是一样的。在js的实现中为函数对象非标准的属性name,用来表示函数名称。对于那些name属性的js来说,可以将函数转为字符串,然后从中提取函数名
下面的案例是以字符串的形式返回对象的类型。
//获取函数的名字
Function.prototype.getName=function(){
if(name in this){
return this.name
} else{
return this.name=this.toString().match(/function\s*([^(]*)\(/)[1];
}
}
function classof(o){
return Object.prototype.toString.call(o).slice(8,-1);
}
function type(o){
if(typeof o === null){
return 'null';
}else if(o !== o){
return 'NaN'
}else if(o=='undefined'){
return 'undefined'
}else if(typeof o !== 'object'){
return typeof o;
}else if(classof(o)!=='Object'){
return classof(o);
}else if(o.constructor && typeof o.constructor === 'function' && o.constructor.getName()){
return o.constructor.getName()
}else{
return 'Object'
}
}
function Add(a,b){
}
Add.prototype.sum=function(){
}
let obj=new Add();
console.log(type(Add));//Add
console.log(obj.__proto__==obj.constructor.prototype)//true
上面函数有个问题并不是所有函数都有名字,如果使用不带名字的函数定义表达式定义一个构造函数。getName()方法则会返回空字符串
如:var Complex=function(x,y){
this.r=x;
rhis.i=y;
}
var Range=function(f,t){
this.from=f;
this.to=t;
}
5.4鸭式辩型
上文所描述的检测对象的类的各种技术多少都会有些问题,至少在客户端js中如此。解决方法就是规避掉这些问题:不要关注‘对象的类时什么’,而是关注‘对象能做什么’。这种思考问题的方式称为‘鸭式辩型’。
像鸭子一样走路,游泳挨并且嘎嘎叫的鸟就是鸭子。
对于js程序员来说,这句话可以理解为‘如果一个对象可以像鸭子一样走路,游泳并且嘎嘎叫,就认为这个对象是鸭子,哪怕它并不是从鸭子类的原型对象继承而来的’。
在很多场景下,我们并不知道一个对象是都是真正的Array的实例,当然是可以通过判断是否包含非负的length属性来得知是否是Array的实例,我们说‘包含一个值是非负整数的length’是数组的一个特征-----‘会走路’,任何具有‘会走路’这个特征的对象都可以当做数组来对待。然而必须了解的是,真正数组的length属性有一些独有行为:当添加新的元素时,数组的长度会自动更新,并且当给length属性设置一个更小的整数时,数组会自动截断。我们说这些特征是‘会游泳’和‘嘎嘎叫’。如果所实现的代码需要‘会游泳’且‘嘎嘎叫’,则不能使用只‘会走路’的类似数组的对象。
鸭式辩型的实现方法让人感觉太‘放任自流’:仅仅是假设输入对象实现了必要的方法,根本没有执行进一步的检查。如果输入对象没有遵循‘假设’,那么当代吗试图调用那些不存在的方法时就会报错。另一种实现方法时对输入对象进行检查。但不是检查它们的类,而是适当的名字来检查它们的实现的方法。这样可以将非法输入尽可能早地拦截在外,并可给出带有更多提示信息的报错。
6 js中的面向对象技术
到目前为止,我们讨论了js类的基础知识:原型对象的重要性,它和构造函数之间的联系,instanceof运算符如何工作等。本节将目光转向一些实际的例子(尽管这不是基础知识),包括如何利用js中的类进行编程。我们从两个重要的例子开始
6.1 一个例子:集合类
集合(set)是一种数据结构,用以表示非重复值的无序集合。集合的基础方法包括添加值,检测值是否在集合中,这种集合需要一种通用的实现,以证操作效率。js的对象属性名以及与之对应的值的基本集合。因此将对象只用做字符串的集合是大材小用。用js实现一个更加通用的set类,它实现了从js值到唯一字符串的映射,然后将字符串用做属性名。对象和函数都不具备如此简明可靠的唯一字符串表示。因此集合类必须给集合中的每一个对象或函数定义一个唯一的属性标识
function Set() {
this.values = {};
this.n = 0;
this.add.apply(this, arguments);
}
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);
}
}
function objectId(o) {
let val = "|**objectid**|";
if (!o.hasOwnProperty(o)) {
o[prop] = Set._v2s.next++;
return o[prop];
}
}
}
Set._v2s.next = 100;
//把每个参数参加添加到集合中
Set.prototype.add = function () {
for (var i = 0; i < arguments.length; i++) {
let val = arguments[i];
let 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(val){
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]);
}
}
}
*/
let s=new Set('1','1','true','3333');
console.log(s)
6.2 一个例子:枚举类型
枚举类型是一种类型,它是值的有限集合,如果值定义为这个类型则该值是可列出(或‘可枚举’)的。下面的例子如何定义一个返回枚举类型的数据
//这个函数创新的枚举类型,实参对象表示类的每个实例的名字和值
//返回值是一个构造函数,它标识和这个新类
//注意,这个构造函数也会抛出异常:不能使用它来创建该类型的新实例
//返回的构造函数包含名/值对的映射表
//包括由值组成的数组,以及一个forEach()迭代器函数
function enumeration(nameToValues) {
//这个虚拟函数返回值
var enumeration = function () {
throw 'Can not Instantiate Enumerations'
}
var proto = enumeration.prototype = {
constructor: enumeration,
toString: function () {
return this.name
},
valueOf: function () {
return this.value;
},
toJSON: function () {
return this.name
}
}
enumeration.values = [];//用以存放枚举对象的数组
//现在创建新类型的实例
for (name in nameToValues) {
let e = Object.create(proto);
e.name = name;
e.value = nameToValues[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;
}
//定义一个表示‘玩牌的类’
function Card(suit, rank) {
this.suit = suit;
this.rank = rank;
}
//使用枚举类型定义花色和点数
Card.suit = enumeration({ Clubs: 1, Diamonds: 2, Hearts: 3, Spades: 4 });
Card.rank = enumeration({ Two: 2, Three: 3, Four: 4, Five: 5, Six: 6, Senven: 7, Eight: 8, Nine: 9, Ten: 10, Jack: 11, Queen: 12, King: 13, Ace: 14 });
//定义用以描述牌面的文本
Card.prototype.toString = function () {
return this.rank.toString() + 'of' + this.suit.toString();
}
Card.prototype.compareTo = function (that) {
if (this.rank < that.rank) {
return -1;
}
if (this.rank > that.rank) {
return 1;
}
return 0;
}
//以扑克牌的玩法规则对牌进行排序的函数
Card.orderByBank = function (a, b) {
return a.compareTo(b)
}
//以桥牌的玩法规则对扑牌进行排序的函数
Card.orderBysuit = function (a, b) {
if (a.suit < b.suit) {
return -1;
}
if (a.suit > b.suit) {
return 1;
}
if (a.rank < b.rank) {
return -1;
}
if (a.rank < b.rank) {
return 1;
}
}
//定义一个用以表示一副标准扑克牌的类
function Deck() {
var cards=this.cards =[];
Card.Suit.foreach(function(s){
Card.Rank.foreach(function(r){
cards.push(new Card(s,r));
})
})
}
//洗牌的方法:重新洗牌并返回洗好的牌
Deck.prototype.shuffle=function(){
//遍历数组中的每个元素,随机找出牌面最小的元素,并与之(当前遍历的元素)交换
var deck=thia.cards,len=deck.length;
for(var i=len-1;i>0;i--){
var r=Math.floor(Math.random()*(i+1)),temp;
temp=deck[i],deck[i]=deck[r],deck[r]=temp;
}
return this;
}
//发牌的方法:返回牌的数组
Deck.prototype.deal=function(n){
if(this.cards.length<n){
throw 'Out of cards';
}
return this.cards.splice(this.cards.length - n ,n);
}
//创建一副新扑克牌,洗牌并发牌
var deck=(new Deck()).shuffle();
var hand = deck.deal(13).sort(Card.orderBysuit)
6.3 标准转换方法
对象类型转换所用到的重要方法,有一些方法是在需要做类型转换的时由js解释器自动调用的。不需要为定义的每个类都实现这些方法,但这些方法的确非常重要,如果没有为定义的类实现这些方法,也应当是有意为之,而不应当因为疏忽而漏掉它们。
最重要的方法首当toString()。这个方法的作用返回一个可以表示这个对象的字符串。在希望使用字符串的地方用到对象的话(比如将对象用做属性名或使用‘+’运算符来进行字符串连接运算),js会自动调用这个方法。如果没有实现这个方法,类会默认从Object.prototype中继承toString()方法,这个方法返回一个可读的字符串,这样最终用户才能将这个输出值利用起来,然而有时候并不一定非要如此,不管怎样,可以返回可读字符串的toString()方法也会让程序调试变得更加轻松。
toLocaleString()和toString()极为相似:toLocaleString()是以本地敏感性的方式来将对象转换为字符串。默认情况下,对象所继承的toLocaleString()方法只是简单的调用toString()方法。有一些内置类型包含有用的toLocaleString()方法用以实际上返回本地化的字符串。
第三个方法是valueOf(),它用来将对象转换为原始值。比如,当数学运算符(除了‘+’运算符)和关系运算符作用于数字文本表示的对象时,会自动调用valueOf()方法。大多数对象都没有合适的原始值来表示它们,也没有定义这个方法。
第四个方法是toJSON(),这个方法是由JSON.stringify()自动调用的。JSON格式用于序列化良好的数据结构,而且可以处理js原始值,数组和纯对象。它和类无关,当对一个对象执行序列化操作时,它会忽略对象的原型和构造函数。比如将Range对象或Complex对象作为参数传入JSON.stringify(),将会返回诸如{'from':1,'to':3} 这种字符串。如果将这些字符串传入JSON.parse(),则返回一个和Range和Complex对象具有相同属性的纯对象,但这个对象不包含继承来的方法。
这种序列化操作非常适用于诸如Range和Complex这种类,但对于其他一些类则必须自定义toJSON()方法来定制个性化的序列化格式化。如果一个对象有toJSON()来执行序列化操作(序列化的值可能是原始值也可能是对象)。比如,Date对象的toJSON()返回一个表示日期的字符串。
6.4 比较方法
js的相等运算符比较对象时,比较的是引用而不是值。也就是说,给定两个对象引用,如果要看它们是否指向同一个对象,不是检测这两个对象是否具有相同的属性名和相同的属性值,而是直接比较这两个单独的对象是否相等,或者比较它们的顺序(就行‘<’和‘>’运算符进行比较一样)。如果定义一个类,并且希望比较类的实例,应该定义合适的方法来执行比较操作。
java编程语言有很多用于比家谱的方法,将Java中的这些方法借用到js中是一个不错的主意。为了能让自定义的类实例具备比较的功能,定义一个名叫equals()实例方法。这个方法只能接收一个实参,如果这个实参和调用此方法的对象相等的话则返回true。当然,这里所说的‘相等’的含义是根据类的上下文决定的。对于简单的类,可以通过简单地比较它们的constructor 属性来确保两个对象是相同类型,然后比较两个对象的实例属性以保证它们的值相等。
eg
//Range类重写它的constructor属性,现在将它添加进去
Range.prototype.constructor=Range;
Range.prototype.equals=function(that){
if(that==null){
return false;
}
if(that.constructor!==Range){
return false;
}
return this.form == that.form && this.to == that.to;
}
按照我们需要的方式比较对象是否对象相等常常是很有用的。对于某些类来说,往往需要比较一个实例‘大于’或者‘小于’另外一个实例。比如,你可能会基于Range对象的下边界来定义实例的大小关系。枚举类型可以根据名字的字母表顺序来定义实例的大小,也可以根据它包含的数值来定义大小。另一方面,set对象其实是无法排序。
如果将对象用于js的关系比较运算符,比如‘<’和‘<=’,js会首先调用对象的valueOf()方法,如果这个方法返回一个原始值,则直接比较原始值。但是大多数类并没有valueOf()方法,为了按照显示定义的规则来比较这些类型的对象,可以定义一个名叫compareTo()的方法
eg
Range.prototype.compareTo=function(that){
return this.from - that.from ;
}
待替换 替换为
a<b a.compareTo(b)<0
a<=b a.compareTo(b)<=0
a>b a.compareTo(b)>0
a>=b a.compareTo(b)>=0
a==b a.compareTo(b)==0
a!=b a.compareTo(b)!=0
注意,这个方法中的减法操作根据两个Range对象的关系正确地返回小于0,等于0和大于0的值。但这个方法compareTo()没有参数做任何类型检查,因此如果给compareTo()方法传入错误类型的参数,往往会抛出异常。修改下对参数类型做判断
Range.prototype.compareTo=function(that){
if(!(that instanceof Range)){
throw new Error('Can not compare a Range with'+that);
}
return this.from - that.from ;
}
6.5 方法借用
js中的方法没什么特别:无非是一些简单的函数,赋值给对象的属性,可以通过对象来调用它。一个函数可以赋值给两个属性,然后作为方法来调用它。
多个类中的方法可以共用一个单独的函数。比如,Array类通常定义了一些内置方法,如果定义了一个类,它的实例就是类数组对象,则可以从Array.prototype将函数复制至所定义的类的原始对象中。如果已经典的面向对象语言的视角来看js的话,把一个类的方法用到其他的类中的做法也称作‘多重继承’。也可以成为‘方法借用’。如
Range.prototype.equals=generic.equals;
//借用的泛型实现
var generic={
toString:function(){
var s='[';
if(this.constructor && this.comstrutor.name){
s += this.constructor.name +':';
}
var n=0;
for(var name in this){
if(!this.hasOwnProperty(name)){
continue;
}
if(typeof value === 'function'){
continue;
}
if(n++) s+= ',';
s+=name+'='+value
}
return s+']'
},
equals:function(that){
if(that==null){
return false;
}
if(this.constructor !== that.constructor){
return false;
}
for(name in this){
if(name === '|**objectid**|'){
continue;//跳过特殊属性
}
if(!this.hasOwnProperty(name)){
continue;//跳过继承来的属性
}
if(this[name]!==that[name]){
return false;//比较是否相等
}
}
return true;
}
}
6.6私有状态
在经典的面向对象编程中,经常需要将对象的某个状态封装或隐藏在对象内,只有通过对象的方法才能访问这些状态,对外只暴露一些重要的状态变量可以直接读写。为了实现这个目的,类似Java的编程语言允许声明类的‘私有’实例字段,这些私有实例字段只能被类的实例方法访问,且在类的外部是不可见的。
我们可以通过变量(或参数)闭包在一个构造函数内来模拟实现私有字段,调用构造函数会创建一个实例。为了做到这一点,需要在构造函数内部定义一个函数(因此这个函数可以访问构造函数内部的参数和变量),并将这个函数赋值给新创建对象的属性。 eg:
//对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对象,端点数据是不可修改的。
var r=new Range(1,5); //一个不可修改的范围
r.from =function(){
return 0;
}
注,这种封装技术造成了更多系统开销。使用闭包状态来封装类的状态的类一定会比不使用封装的状态变量的等价运行速度更慢,并占有更多内存
6.7 构造函数的重载和工厂方法
有时候,我们希望对象的初始化有多种方式。比如,我们想通过半径和角度(极坐标)来初始化一个Complex对象,而不是通过实部和虚部来初始化它。
有一个方法可以实现,通过重载这个构造函数让它根据传入参数的不同来执行不同的初始化方法。eg
function Set(){
this.values={};
this.n=0;
if(arguments.length == 1 && isArrayLike(arguments[0])){
this.add.apply(this,arguments[0]);
}else{
this.add.apply(this,arguments);
}
}
7、子类
在面对对象编程中,类B可以继承自另外一个类A。我们将A称为父类,将B称为子类。B的实例从A继承了所有的实例方法。有些方法可以重载类A中的同名方法,如果B的方法重载了A中的方法,B中的重载方法可能会调用A的重载方法,这种做法被称为‘方法链’,同样,子类的构造函数B有时需要调用父类的构造函数A,这种做法被称为‘构造函数链’。子类还可以有子类,当涉及类的层次结构时,往往需要定义抽象类。抽象类定义的方法没有实现。抽象类中的抽象方法实在抽象类的具体子类中实现的。
原型链
B.prototype=A.prototype
7.1 定义子类
js的对象可以从类的原型对象中继承属性(通常继承是方法)。如果O是类B的实例,B是A的子类,那么O也一定从A中继承属性。为此,首先要确保B的原型对象继承自A的原型对象。
B.prototype=Object.create(A.prototype);
B.prototype.constructor=B;
这两行代码是在js中创建子类的关键,如果不这样做,原型对象仅仅是一个普通对象,
function A(){
}
A.prototype.add=function(){
console.log(this,'添加方法')
}
let o1=new A();
o1.add();
//A {} "添加方法"
function B(){
}
B.prototype=Object.create(A.prototype);
B.prototype.constructor=B;
let o=new B();
//B {} "添加方法"
o.add();
定义一个子类
function A(from,to){
this.from=function(){
return from;
};
this.to=function(){
return to;
};
}
A.prototype.add=function(){
console.log(this,'添加方法',this.from(),this.to())
};
A.prototype.one='1111'
let o1=new A(1,12);
o1.add();
//A {} "添加方法"
function B(from,to){
this.from=function(){
return from;
};
this.to=function(){
return to;
};
}
let methods={
constructor:B,
add(){
console.log(111,this)
}
}
//B.prototype=Object.create(A.prototype);
//B.prototype.constructor=B;
B.prototype=methods;
B.prototype.one='一一一'
function defineSubclass(superclass,constructor,methods){
constructor.prototype=Object.create(superclass.prototype);
constructor.prototype.constructor=constructor;
if(methods) Object.assign(constructor.prototype,methods);
return constructor;
}
Function.prototype.extend=function(superclass,constructor,methods){
return defineSubclass(this,constructor,methods)
}
defineSubclass(A,B,methods);
let o=new B(33,44);
console.log(B.one)
//B {} "添加方法"
o.add();//111 函数B
SingletonSet 一个简单的子类
//定义一个构造函数
function SingletonSet(member){
this.member=member
}
SingletonSet.prototype=Object.create(Set.prototype);
//给原型添加属性,如果有同名的属性就覆盖掉Set.prototype属性
Object.assign(SingletonSet.prototype,{
constructor:SingletonSet,
add:function(){
}
})
7.2构造函数和方法链
在定义子类时,我们希望对父类的行为进行修改和扩充,而不是完全替换掉它们。
//NonNullSet设置为Set子类
function NonNullSet(){
Set.apply(this,arguments);
}
//将NonNullset设置为Set子类
NonNullSet.prototype.add=Object.create(Set.prototype);
NonNullSet.prototype.constructor=NonNullSet;
//为了将null和undefined排除在外,只须重写add()方法
NonNullSet.prototype.add=function(){
for(var i=0;i<arguments.length;i++){
if(arguments[i]===null){
throw new Error('Can not add null or undefined to a NonNullSet');
}
return Set.prototype.add.apply(this,arguments)
}
}
这个函数发返回具体Set类的子类
并重写该类的add方法用以对添加元素做特殊处理
function filteredSetSubclass(superclass,filter){
var constructor=function(){
superclass.apply(this,arguments);
}
var proto=constructor.prototype=Object.create(superclass.prototype);
proto.constructor=constructor;
proto.add=function(){
for(var i=0;i<arguments.length;i++){
var v=arguments[i];
if(!filter(v)){
throw error('value'+v+'rejected by filter');
}
}
superclass.prototype.add.apply(this,arguments);
return constructor;
}
}
7.3组合vs子类
然而还有更好的方法来写成这种需求,即OOP中一条广为人知的设计原则:“组合优于继承”可以利用组合的原理定义一个新的集合实现,它“包装”了另外一个集合对象,在将受限制的成员过滤掉之后会用到这个集合对象。
//使用组合代替继承的集合的实现
7.4 类的层次结构和抽象类
在上一节中给出了‘组合由于继承’的原则,但为了将这条原则阐述清楚,创建了Set的子类。这样做的原因是最终得到的类时Set的实例。它会从Set继承有用的辅助方法,比如toString()和equals()。尽管这是一个实际的原因,但不用创建类似Set类这种具体类的子类也可以很好地用组合实现范围