首先,javascript是一种弱类型的语言,也就是javascript中的变量都不需要像C或者java那样明确定义它的类型。相比于java等的语言来说,javascript在使用上更加灵活,没有那么多限制。
为了方便后续的讲解,先来说一说在函数中存在的三种类型的变量,结合代码来看:
function Fun () {
varOne = 10;
var varTwo = 20;
this.varThree = 30;
console.log(window);
}
var fun1 = new Fun;
注:javascript中的函数,既是类,也是对象,函数本身是“以函数为类”的构造函数,在new对象时被自动执行
如上所示:
(1)varOne = 10:
没有任何前缀定义的变量varOne 实际上默认有一个隐藏的前缀window,就相当于是window.varOne = 10,如果直接写变量名,不加任何前缀的话,就相当于是在window中增加了一个以该变量名为键的键值对(在javascript中的所有变量都是以键值对的形式存在的);
(2)var varTwo = 20:
以var为“命令”定义的变量varTwo 是函数Fun()的一个局部变量,当该函数运行时,给局部变量申请一段系统堆栈空间,当函数运行结束时,局部变量占用的系统堆栈空间随着释放,该局部变量也就不存在了;
(3)this.varThree = 30:
以this为前缀的变量,在该函数被当成构造函数执行时,this就表示该类实例化产生的对象,而这个变量就相当于该类的成员,这类成员是public修饰的,或者说是模拟public的,调用的方法是对象.成员;
下面思考一个问题:能否利用javascript模拟类和对象的思想,产生一个“静态”的成员,起始准确来说应该是“模拟静态”的成员?这就涉及到了原型的概念,来看:
先来简单举例,看一下什么是原型(原型对象)和原型链:
定义一个函数:
function Fun (real,vir) {
real = real || 0.0;
vir = vir || 0.0;
this.real = real;
this.vir = vir;
}
有关上面提到的逻辑或||可以查看javascript的逻辑与和逻辑或
实例化两个上述类的对象
var fun1 = new Fun(1.2,3.4);
var fun2 = new Fun(6.7);
输出Fun()函数以及两个对象来看看:
可以看到js的类(也是函数)有一个prototype(原型,或者原型对象)和(原型链),而js的对象不存在prototype(原型),只存在(原型链);前面说到,类是对象,也是函数,
那么要区分一个对象既是对象,也是类,还是仅仅只是对象,可以看是否存在prototype成员,如果存在的话,它既是类,也是对象,否则就只是对象。
还有一点注意:本人使用的是火狐浏览器,上面对原型链的表示是这种形式,但其实早一点的时候,原型链表示为__proto__,现在被更高级的火狐系统内部更改成了,可以输出看看:
console.log("fun1.__proto__ : ",fun1.__proto__ );
而我们知道,fun1和fun2对象是从属于Fun类的,即fun1 instanceof Fun的输出结果为true,那么这个从属关系是从何得来的?
任何一个对象都有原型链,指向它所属的类的原型对象
即判断一个对象是否属于某个类,可以看该对象的原型链是否指向这个类的原型对象。
原型链除了可以表示一个对象的从属类之外,还可以更改对象的从属类,这在java中是觉得不可能实现的,
function ABC () {
}
fun2.__proto__ = ABC.prototype;
console.log("fun2 instanceof Fun : " , fun2 instanceof Fun);
console.log("fun2 instanceof ABC : " , fun2 instanceof ABC);
fun2.proto = ABC.prototype这句代码的意思是,把fun2对象的原型链指向了类ABC的原型对象,这样做就将fun2的从属类变成了ABC,输出结果是:
上面提到的利用js模拟类和对象的思想,产生一个“模拟静态”的成员,做法是给类Fun的原型对象中增加一个成员count,那么这个成员就可以被Fun类的两个对象都可以使用,如下:
Fun.prototype.count = 1;
console.log("fun1.count:", fun1.count);
console.log("fun2.count:", fun2.count);
结果都是1,
这个count就相当于是java中的静态成员了,因为不管是fun1对象还是fun2对象,调用的count成员都是Fun类中的,而它的表示方法也和java中类似,为类.prototype.成员。
接着来看,如果执行了fun1.count = "ac";这条语句的话
,就相当于给fun1新增加了一个count为键,字符串ac为值的键值对,并不是对Fun.prototype.count = 1产生的count值做更改,
来看结果:
注:这里涉及到了“左值”和“非左值”的概念;
左值,是指等号=左侧的变量,其它场合的变量都是非左值
js对于左值的处理原则:
1、查看该左值是否存在;
2、若不存在,则,创建;
3、完成赋值操作。
对于fun1.count = "ac";
这条语句,fun1.count在=号的左侧,属于左值,处理方法是在fun1中查看是否存在count,结果不存在,所以在fun1中创建一个键count;而像类似console.log("fun1.count:", fun1.count); console.log("fun2.count:", fun2.count);
的这种语句中的count是非左值,在fun1中找到了count,就直接输出,而在fun2中没有找到count 的存在,就继续通过原型链查找,结果在类的原型对象中找到了,就输出原型对象中count的值。
模拟继承
下面来看看在javascript中是怎么实现继承关系的;
前面说,任何一个对象的原型链都指向它所从属的类的原型对象,
那么要把某个类设置为另一个类的的派生类,就把这个目标子类的原型对象的原型链指向目标父类的原型对象,这样就实现了继承的关系
具体代码是:
var Parent = function() {
}
var Child = function() {
}
Child.prototype.__proto__ = Parent.prototype;
上述代码可以实现Child类和Parent类之间的继承关系,但是需要特别注意的是:
在几乎所有的程序语言中,用下划线开头的变量(例如__proto__),几乎都是被这个语言隐藏的变量,也即是说,在该语言中,及其不建议编程者直接操作该变量。
所以我们不建议用Child.prototype.__proto__ = Parent.prototype;
的方法完成模拟继承,下面给出另外的实现方法:
function Extends (superClass,childClass) {
var superObject = new superClass;
superObject.constructor = childClass;
childClass.prototype = superObject;
}
该函数可以作为模拟继承的一个工具来用,首先给父类实例化出一个对象,将该父类对象的构造设置为子类,然后将父类的这个对象当成子类的原型对象,结合图示来看:
我觉得大概意思就是把父类产生的对象当成了子类的原型对象,但里面的更改constructor的这一步,我其实觉得好像没有必要,因为constructor实际上只算是个构造的标识,并没有什么实质性的作用,由于constructor是可以更改的,就像前面说过的js可以通过更改constructor从而更改对象的从属类,所以来说实例化类的对象也不是用这个constructor构造来产生的,它好像只是用来得到(或更改)某个对象的从属类的。另外,我还了解到constructor属性不影响任何js的内部属性,属于js的历史遗留物,虽然没有什么实质性的作用,但是为了维持编程惯例,就将对象的constructor执行其构造函数。
可以来看一下模拟继承的结果:
var Parent = function() {
}
var Child = function() {
}
Extends(Parent,Child);
var obj = new Child;
console.log("obj1 instanceof Parent : ",obj1 instanceof Parent);
console.log("obj1 instanceof Child : ",obj1 instanceof Child) ;
可以看出Child类实例化出的对象既是Child的对象,也是Parent类的对象,即Child类成了Parent类的派生类。
还有一点,子类中怎么出现父类中的成员,两个类之间满足了继承关系之后,需要进行一步操作,子类中才能包含父类的成员,
var Parent = function() {
this.parentMember = 1;
}
var Child = function() {
Parent.call(this);
this.childMember = 2;
}
注意,如果直接在Child函数中调用Parent()
函数,由于没有任何前缀,默认以window作为前缀,但是现在我们需要的是this应该是Parent类new出的对象,所以使用call()函数执行Parent()函数,call()函数可以更改所执行的函数的this,在这将this由window改成了Parent类产生的对象,具体操作就是Parent.call(this);
,这句代码相当于是java中的super()方法,但是super()必须要放在子类构造的第一行代码的位置,而Parent.call(this)不需要遵循一样的原则,可以放在子类构造的任意位置。
看结果:
var obj1 = new Parent;
var obj2 = new Child;
console.log("obj1 : ",obj1);
console.log("obj2 : ",obj2);
最后再说明一个问题,即prototype和__proto__的相关指向:
我们知道函数既是类也是对象,它包含prototype(原型对象)和__proto__(原型链)成员,那么一个类的原型链指向哪里?答案是指向Function的原型对象,几乎都有的类的原型链都指向Function的原型对象,可以把Function当成是所有类的基类(或者是基函数,但它不用来表示类与类之间的继承关系),需要注意Function类产生的是函数,而Function类的原型链也指向它自己的原型对象,类似下图显示,: function()。
还有,子类的原型对象的原型链指向的是父类的原型对象,那么父类的原型对象的原型链只想的是哪里?可以这样说,只要没有明确说明一个类的父类,都默认是Object类的派生类,意思是所有类的原型对象的原型链最终都指向其“父类”的原型对象,知道最顶层的原型对象的原型链指向Object类的原型对象,而Object类的原型对象的原型链指向的是null,作为最后的终结,结合图示来看: