本系列内容由ZouStrong整理收录
整理自《JavaScript权威指南(第六版)》,《JavaScript高级程序设计(第三版)》
每个JavaScript对象都是属性的集合,相互之间没有任何联系
在JavaScript中可以定义对象的类,让一类对象都共享某些属性
在JavaScript中,类的实现是基于原型继承机制的;如果两个实例都从同一个原型对象继承属性,我们称他们为同一个类的实例,并且它们往往都是由同一个构造函数创建的
尽管写法类似,并且可以模拟出很多经典的"类特性"(封装、继承、多态),但是JavaScript中的类和基于原型的继承与传统的java的类和基于类的继承有很大不同,因此我们更喜欢称JavaScript中的类为——类型
JavaScript中类的一个重要特征就是“动态可继承”
在JavaScript中,类的所有实例都从同一个原型对象上继承属性,因此,原型对象是类的核心
之前定义过一个inherit()函数,返回一个新函数,新函数继承自一个指定的原型对象
function inherit(prototype){
if(prototype==null){
throw TypeError();
}
if(Object.create){
return Object.create(prototype);
}
var type = typeof prototype;
if(type!=="object" && type!=="function"){
throw TypeError();
}
function F(){};
F.prototype=prototype;
return new F();
}
如果定义了一个原型对象,然后通过inherit()函数创建一个继承自它的对象,这样就定义了一个JavaScript类,通常,类的实例还需要进一步的初始化,通常是通过定义一个函数来创建并初始化这个新对象
下面实现了一个简单的JavaScript类,给一个表示“值的范围”的类定义了原型对象,还定义了一个“工厂函数”用以创建并初始化类的实例
//这个工厂方法返回一个新的“范围对象”
function range(from,to){
//使用inherit()函数创建对象,并继承自在下面定义的原型对象
//原型对象作为函数的一个属性存储,并定义所有范围对象所共享的方法
var r = inherit(range.methods);
//存储新的“范围对象”的起始位置和结束位置
//这两个属性是不可继承的,每个对象都拥有唯一的属性
r.from = from;
r.to = to;
//返回新创建的对象
return r;
}
//原型对象定义方法,这些方法被每个“范围对象”所继承
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 obj = range(1,10);
obj.includes(6); //true
obj.foreach(console.log); //1,2,3,4,5,6,7,8,9,10
console.log(r); //"(1-10)"
上面的代码有问题???????????????????
这段代码定义了一个工厂方法range(),用来创建新的范围对象
我们给range()函数定义了一个属性range.methods,用以快捷的存放定义类的原型对象,from和to属性不是共享的,也是不可继承的
range.methods中的可共享、可继承的方法都用到了from和to属性,而且使用了this关键字来指代调用这个方法的对象
,任何类的方法都可以通过this来读取对象的属性
上面定义类的方式不常用,因为没有定义构造函数,构造函数是用来初始化新创建的对象的
使用构造函数代替工厂函数
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);
},
toString: function() {
return "(" + this.from + "..." + this.to + ")";
}
};
var r = new Range(1,3);
r.includes(2); //true
r.foreach(console.log); // 输出 1 2 3
console.log(r); // 输出 (1...3)
从某种意义上讲,定义构造函数就是定义类,因此构造函数的首字母要大写,并且首字母大写,也可以传递这样一种信息:这是构造函数,请使用new调用
使用构造函数时,没有显示创建新对象,因为在调用构造函数之前就已经创建了新对象,通过this关键字可以获取这个新对象,构造函数只不过是通过this初始化新对象而已
构造函数甚至不必返回这个新对象,构造函数会自动创建新对象,然后将构造函数作为这个对象的方法调用一次,然后返回新对象
第一段代码里,我们随便命名了一个原型对象,然后让新对象继承,第二段代码使用了protorype属性,这是强制的;调用构造函数,会自动将构造函数的prototype属性作为新对象的原型
原型对象是类的唯一标识,当且仅当两个对象继承自同一个原型时,它们才是同一个类的实例
初始化对象状态的构造函数则不能作为类的标识,只有两个构造函数的prototype属性指向同一个原型时,它们创建的实际才属于同一个类
虽然构造函数不像原型那么基础,但是构造函数是类的公有标识,构造函数的名字通常就是类名
a instanceof A
但是,instanceof运算符并不会检查a是否由A构造函数初始化的,而回检查a是否继承自A.prototype,
function A(){}
function B(){}
var obj = {};
A.prototype = obj;
B.prototype = obj;
var a = new A();
var b =new B();
a instanceof A; //true
b instanceof A; //true
第二个例子完全将Range.prototype定义为一个新的对象,其实没有必要
因为每个函数都可以用作构造函数,并且调用构造函数是需要用prototype属性的,因此每个函数(bind()方法返回的函数除外)都自动有一个prototype属性
这个属性是一个对象,因此可以直接为该对象添加属性,而不用重写该对象
Range.prototype.includes = function(){};
原型对象包含唯一一个不可枚举属性constructor,该属性返回函数对象
var F = function(){};
var p = F.prototype;
var c = p.constructor; //c===F
构造函数的原型中存在预先定义好的constructor属性,所以对象继承的constructor属性均指向它们的构造函数,由于构造函数是类的“公共标识”,因此这个constructor属性为对象提供了类
var o = new F();
o.constructor = F;
第二个例子完全将Range.prototype定义为一个新的对象,重写了原型对象,导致这个对象不包含constructor属性
补救措施1:显式设置构造函数
Range.prototype = {
constructor:Range,
fun1:fun1,
.....
};
补救措施2:不要重写原型对象,而是扩展预定义的原型对象
Range.prototype.fun1 = fun1;
Range.prototype.fun2 = fun2;
....
在Java中,属性和方法分为以下几种
JavaScript和Java的一个不同之处在于,JavaScript中的函数也是一种值,因此属性和方法之间没有什么本质的区别,当一个属性是函数的时候,它就是方法,反之,它就是普通的属性
但是我们仍然可以模拟出Java中的四种类成员类型,JavaScript中的类牵扯三种不同的对象,他们和下面三种类成员非常相似
在JavaScript中定义类的步骤可以分为三步
封装起来
function defineClass(constructor,methods,statics){
if(methods){
constructor.prototype.methods = methods;
}
if(statics){
constructor.statics = statics;
}
return constructor;
}
JavaScript基于原型的继承是动态的
对象从原型继承属性,如果创建对象之后,原型的属性发生了变化,都会影响到继承这个原型的所有实例对象,这意味着我们可以通过给原型对象添加新方法来扩充JavaScript类
所以可以给任意内置对象的原型添加方法,从而使相应的实例对象可以调用这些方法,但是在ECMAScript5之前,这些新增的方法都是可枚举的,无法将他们设为不可枚举
...