关于原型继承
和编译型语言C++以及编译解释型语言JAVA的类继承不同,Javascript采用了基于原型的继承。在javascript语言中,实质没有类,除了基本类型,其他均为对象,包括函数,一个对象继承自另一个对象而不是类。而构造器函数既可以产生对象,也可以当做函数或者对象来对待。OOP概念在javascript上似乎体现的更加明显:万物皆对象!但javascript的目的不是OOP的,函数才是Javascipt的核心。
1.原型继承基本原理
Javascript继承通过原型链来实现,在每个对象内,都有一个保存着该对象所继承的对象引用的属性__proto__,利用该属性,可以找到该元素所继承的对象。但javascript中函数也为对象,而且还可以当做构造器,因此肯定会与普通的对象会有所区别,当我们使用typeof 来检测一个函数时,返回的不是object而是function。事实上,函数至少比不同对象多出两个属性constructor和prototype。既然有区别,那么函数的__proto__是否存在,prototype又是干什么用的呢?普通对象的__proto__保存的到底是什么?来看下面的例子:
var obj0 ={}; //构造空对象
console.log(obj0.__proto__); //查看__proto__属性
console.log(obj0.prototype); //查看prototype属性
console.log(typeof obj0); //查看类型
var obj1 = {name:'likai'}; //构造另一个对象
console.log(obj1.__proto__); //查看__proto__属性
var obj2 = {age:22}; //好吧,再构造另一个对象
console.log(obj2.name); //查看对象本身不存在的属性
obj2.__proto__ = obj1; //嗯哼,注意,继承开始了
console.log(obj2.name); //再次查看对象本身不存在的属性
function Obj(){ //搞个构造函数玩玩
this.name='kai';
}
console.log(typeof Obj); //查看类型
console.log(Obj.prototype); //查看prototype属性
console.log(Obj.__proto__); //查看__proto__属性
Obj.prototype.age = 22; //来,再搞个继承看看,这个会成功么
Obj.__proto__.ages = 23; //来,继续搞个继承看看,这个会成功么
console.log(Obj.prototype); //查看prototype属性
console.log(Obj.__proto__); //查看__proto__属性
var obj4 = new Obj(); //用构造函数来构造
console.log(obj4.age); //看看有没有继承到
console.log(obj4.ages); //看看有没有继承到
console.log(obj4.__proto__); //瞧瞧__proto__属性
function Objs(){ //另一个构造函数
this.ages =24;
}
Obj.__proto__ = Objs; //在来继承试试
console.log(Objs.prototype); //查看prototype属性
console.log(Objs.__proto__); //查看__proto__属性
var obj5 = new Obj(); //用构造函数来构造
console.log(obj5.age); //看看有没有继承到age
console.log(obj5.ages); //看看有没有继承到ages
console.log(obj5.__proto__);
//最后,来点大逆不道的事
obj2.__proto__.addr="湖北孝感"; //在继承属性上加个地址属性
console.log(obj2.addr); //有没有继承到
console.log(obj1.addr); //父对象有没有改变呢
//千呼万唤的ages,来打个招呼吧
console.log(Obj.ages);
------结果如下-------
{}
undefined
object
{}
undefined
likai
function
{}
[Function: Empty]
{ age: 22 }
{ [Function: Empty] ages: 23 }
22
undefined
{ age: 22 }
{}
{ [Function: Empty] ages: 23 }
22
undefined
{ age: 22 }
湖北孝感
湖北孝感
23
从上面的例子,我们可以清楚的明白javascript是如何来实现继承的,对象在生成时就会有个__proto__属性包括构造函数,但普通的对象__proto__属性保存着对象,而函数保存着函数对象(暂且这样称呼吧)。噢,对了,再看了最后3行的结果之后,我们应该把保存着对象改为保存着对象的引用才对。直接生成的对象(此处应该也包括函数)应该是javascript内置的默认构造器行为,而函数构造器在构造对象的时候,会将属性prototype保存的内容复制到新产生对象的__proto__属性中,而构造函数自身的__proto__属性只标示自己作为对象时的父对象,并不会复制给新产生的对象。因此函数的prototype属性值和其产生的对象的__proto__属性值是一样的,至少在你手动改变之前是一样的。对象的__proto__属性以及函数对象的prototype属性构成了一条从下至上的对象引用链即为原型链。
2.原型继承追本溯源
对象和函数对象产生的区别从1中的例子也看以看出一二,函数的__proto__显示[Function: Empty]
。
直接构造的对象的原型属性为空对象,即使构造的对象是空对象,在此我觉得称为根对象更好点,因为当我们在Object
函数的原型属性上挂上值后,他就不为空了。不信么,看下面的例子吧:
var a = {};
var b={name: 'likai'};
console.log(a.__proto__);
Object.prototype.age = 2;
console.log(a.__proto__);
console.log(b.__proto__);
------结果如下-------
{}
{ age: 2 }
{ age: 2 }
显然可以认为,直接对象应该也会默认调用Object()
来构造。我们说了,构造函数就是把自己prototype属性的值复制给创建的新对象,而函数也是一种对象,那他的默认prototype属性和__proto__属性值是什么呢?试试就知道了:
var a = {};
console.log(a.__proto__);
Object.prototype.age = 2;
console.log(a.__proto__);
function b(){};
console.log(b.prototype);
console.log(b.__proto__);
------结果如下-------
{}
{ age: 2 }
{}
[Function: Empty]
我们注意到prototype属性的值和普通对象的__proto__属性值一样,而函数对象的__proto__属性值却为 [Function: Empty]
,这让我们想起了和Object
类似的东西–Function
,他本身是个函数,可以用来创建函数对象,那么他的功能是否和Object类似呢,试试就知道了:
function a(){};
console.log(a.prototype);
console.log(a.__proto__);
Function.prototype.age = 2;
Function.__proto__.ages = 22;
console.log(a.prototype);
console.log(a.__proto__);
------结果如下-------
{}
[Function: Empty]
{}
{ [Function: Empty] age: 2, ages: 22 }
显然同样可以认为函数对象应该也会默认调用Function()
来构造,但从结果看Function
就比Object
多了个[Function: Empty]
,Object
也是个函数,既然函数默认由Function()
来构造,而函数也属于一种对象,那么我们似乎可以用instanceof
看看他们,来试试:
var a ={};
function b(){};
console.log(a instanceof Object);
console.log(b instanceof Object);
console.log(a instanceof Function);
console.log(b instanceof Function);
console.log(Function instanceof Object);
console.log(Object instanceof Function);
------结果如下-------
true
true
false
true
true
true
哇,这结果…Object
和Function
果然验证了函数也是对象这句话,但这你中有我,我中有你却让人摸不着头脑。就这样看肯定是不行的,这两个东东毕竟是Javascript本身提供的东西,看来得换一种思路了,问不了他们和他们儿子,去问他父亲去!来看看结果:
console.log(Function.prototype);
console.log(Function.__proto__);
console.log(Object.prototype);
console.log(Object.__proto__);
------结果如下-------
[Function: Empty]
[Function: Empty]
{}
[Function: Empty]
结果看着有三个都差不多的,继续来试试:
console.log(Function.prototype === Function.__proto__);
console.log(Function.prototype === Object.__proto__);
console.log(Object.prototype === Object.__proto__);
------结果如下-------
true
true
false
啊哈,从上面可以看到,Object
和Function
有着相同的__proto__属性值,而且Function
的prototype和__proto__属性值相同,根据上文所说的对象构建规则推测,那么Object
和Function
指向同一个父对象,结果显示为[Function: Empty]
,暂且喊他根函数对象吧。而我们的原型链,似乎到这里就到头了。两个内置的函数,一个负责产生普通对象,一个负责产生函数对象。前面说了一个根对象,现在又来一个根函数对象,那到底谁是根啊,不行,既然前面说了都可以看做是对象,那看来还得往上挖:
console.log(Function.__proto__.__proto__);
console.log(Function.__proto__.__proto__ === Object.prototype);
console.log(Function.__proto__.__proto__.__proto__);
console.log(Object.prototype.__proto__);
------结果如下-------
{}
true
null
null
看,空对象(根对象)出现了,从上面可以总结下:原型链的尽头就是根对象,根对象的__proto__值为null,虽然似乎Object
可以看做是用Funciton
构建的,但从前面的测试来看,绝不简单是这样,因为这是内置的东西,我们就不需要以Javascript的规则来看待,不然容易钻进牛角尖了。我们只需要记住Object的prototype代表这根对象就行。而想通过函数找到根对象,就需要使用__proto__属性向上找。因此我们可以做如下测试:
var a = {};
function b(){};
var c = new Function();
Object.prototype.age = 22;
console.log(Function.__proto__.__proto__);
console.log(a.__proto__);
console.log(b.__proto__ === Function.prototype);
console.log(c.__proto__ === Function.prototype);
console.log(b.__proto__.__proto__);
console.log(b.prototype === Object.prototype);
结果是显而易见的,就不多写了,需要注意的是普通函数的prototype属性值”{}
“为空对象,而不是根对象。
3.原型继承学习总结
原型继承是javascript吸收自self语言的一个特色,通过稍微深入的挖掘,对其原理,特别是原型链有一个较为清晰的理解,而且原型链上都使用的是引用,也就是说在子对象中改变原型引用的值,也会对父对象造成影响,在这里我没有研究继承对子对象的影响,因为对大多数语言来说,继承对子类的影响都差不多的,特别是涉及到相同名字的属性都会造成覆盖效果,因此无需做过多的研究。
关于this
绑定
在大多数oop语言中,
this
关键字用来代表当前对象(可以说这个词即容易理解又难以理解)。Jacascript中,在定义构造函数时,基本上都会用到this关键字,一般情况下定义属性还好,但当构造函内部继续定义函数的时候,很多时候对于构造函数内部的this值所代表的对象会产生混淆,在其内部函数想使用this关键字来引用当前对象的属性,当对此不了解的时候,就会发生难以理解的错误,原因就在于构造函数的内部函数使用的this关键字指向的并不是上文所说的当前对象,而是全局的宿主对象(node.js的root(global),浏览器的window)。
1.一句话搞定this绑定
谁(将)调用该方法,this就绑定到谁身上,没人就找全局宿主对象!来看:
root.age = 33;
function Constru(){
this.age = 22;
console.log(this.age);
function innerFunc(){
console.log(this.age);
}
innerFunc();
this.printf = function(){
function inMethodFunc(){
console.log(this.age);
}
inMethodFunc();
console.log(this.age);
}
}
var obj = new Constru();
obj.printf();
------结果如下-------
22
33
33
22
上面的例子中,按照谁调用,this
就绑定到谁身上,第一个打印的this.age
直接在构造函数下,一定是绑定到产生的对象上,obj.printf()
内的this很显然绑定到obj对象上,而innerFunc
和inMethodFunc
是直接调用的,所以就找到了全局宿主对象。
2.this绑定全局的解决方案
要解决上述问题,只需使用一个代替值保存正确的this
引用,然后再使用代替值去表示当前对象:
root.age = 33;
function Constru(){
var me = this; //用变量me来保存this绑定的对象
this.age = 22;
console.log(this.age);
function innerFunc(){
console.log(me.age); //me.age
}
innerFunc();
this.printf = function(){
function inMethodFunc(){
console.log(me.age);
}
inMethodFunc();
console.log(this.age);
}
}
var obj = new Constru();
obj.printf();
------结果如下-------
22
22
22
22
3.this绑定总结
该错误在熟悉javascript之后就很少会犯,但有时候使用时却还是会疏忽而产生意外结果或错误,没什么难点,需要自己多注意。