初学JavaScript,一直很好奇这个没有类定义的语言如何实现了面向对象的程序设计模式,以及如何实现类属性和方法的继承。经过我两天的冥思苦想,终于有了自己的一些感悟。
首先我们明确几个重要的概念:
1 什么是原型?
根据JavaScript权威指南上的解释:每一个JavaScript对象(null除外)都和另外一个对象关联。“另一个”对象就是我们熟知的原型,每一个对象都从原型继承属性。没有原型的对象不多,Object.prototype就是其中之一。它不继承任何属性。其他原型对象都是普通对象,普通对象都具有原型。所有内置构造函数(以及大部分自定义的构造函数)都具有一个继承自Object.prototype的原型。
2 JavaScript 中Function(函数)和Object(对象)的关系?
JavaScript最开始定义一个对象叫做Object.prototype,本质上就是一组无序key-value存储({}),之后再此基础上,研发出可以保存一段“指令”并“生产产品”的原型,叫作函数,起名Function.prototype,本质上就是[Function:Empty](空函数)。
为了规模化产生对象,JS在函数的基础上构造出了两个构造器:生产函数对象的构造器叫做Function,生产key-value存储对象的构造器叫做Object。
JavaScript在每个对象上打了个标签__proto__,以标明这个对象是根据哪个原型生产的。为原型打了个标签constructor,标明哪个构造器可以依照这个原型生产对象。为构造器打了标签prototype,标明这个构造器可以从哪个原型生产对象。
3 什么是原型链?
原型链是一种机制,指的是JavaScript的每一个对象包括原型对象都有一个[[proto]](通过__proto__访问)属性指向创建它的函数对象的原型对象,即函数的prototype属性。原型对象又可以通过[[proto]]属性访问它的上层原型对象,一层一层往上继承属性和方法,这就是原型链。原型链的作用就是用来实现对象的继承。
原型链示意图:
默认JavaScript中的构造函数当做类,new出来的对象称为实例,原型对象简称原型,具体实现上代码:
首先我们构造父类SuperClass,并创建它的原型:
function SuperClass() {
if(!(this instanceof SuperClass))
return new SuperClass();
else{
this.id1 = '1';
this.color1 = ['red'];
}
}
SuperClass.prototype={
id2 : '2',
color2 : ['yellow']
};
然后构造一个空的子类:
function SubClass() {
if(!(this instanceof SubClass))
return new SubClass();
else{}
}
定义一个打印对象所有属性和方法的函数以便调试:
function ConsoleDetail(obj) {
for(var i in obj){
console.log(i+':'+obj[i]);
}
}
这里我对构造函数本身的属性方法和创建出来对象的属性方法有点迷惑,于是测试:
console.log('-----------------------------------');
SuperClass.name = 'wenjun';
ConsoleDetail(SuperClass);
console.log('-----------------------------------');
var obj = new SuperClass();
ConsoleDetail(obj);
Chrome控制台输出:
可以看到实例化的对象构造属性方法和原型属性方法都符合预期。但是SubClass对象上明明定义了name属性,却什么也没打印出来,出了什么错?
原来Function.name是一个自带属性,它的值就是我们定义Function时取得名字(字符串类型),对于匿名函数,它的值为“anonymous”。详情可以阅读文档。
既然name不行,换个age试试:
console.log('-----------------------------------');
SuperClass.age = '20';
ConsoleDetail(SuperClass);
console.log('-----------------------------------');
var obj = new SuperClass();
ConsoleDetail(obj);
结果:
构造函数本身也是一个对象,它自身可以挂载属性和方法,然而这和使用它构造出来的实例的属性和方法(通过关键字
不纠结这个了,正式开始继承:
console.log('-----------------------------------');
SubClass.prototype = new SuperClass();
ConsoleDetail(SubClass);
console.log('-----------------------------------');
var obj = SubClass();
ConsoleDetail(obj);
结果:
子类原型赋值为父类实例,对子类本身没有任何影响,子类上依旧没有任何属性,而子类构造的实例却可以继承子类构造函数和父类构造函数的属性和方法(通过关键字
为了对比实例调用父类继承属性和方法的效果,我多构建一个obj2同样继承SubClass:
var obj2 = SubClass();
先更改SuperClass构造函数中的id1和color1:
obj.id1 = '11';
obj.color1.push('black');
ConsoleDetail(obj2);
结果:
发现id1没变,而color1变了。
继续更改SuperClass原型上的id2和color2:
obj.id2 = '22';
obj.color2.push('white');
ConsoleDetail(obj2);
结果:
发现id2没变,而color2变了。
子类实例继承父类构造函数和原型对象的属性及方法,只要是值类型的都是赋值(深拷贝),对象类型都是引用(浅拷贝)!
接着放弃之前的修改,单纯尝试重新定义属性或方法:
obj.color1 = function () {
console.log("redefine color1.");
};
ConsoleDetail(obj);
console.log('-----------------------------------');
ConsoleDetail(obj2);
结果:
这里的color1是Function对象,如果按照之前的逻辑,对象类型都是引用,为什么这里obj2没有任何变化?
仔细思考了很久,我能给出的最合理解释是:obj.color1 = function(){...}实际上是在实例上挂载了与通过原型链继承的属性或方法同名的属性或方法,导致新定义的属性或方法覆盖了obj1原型链上的同名属性或方法,只是查询(从实例本身->实例原型->原型的原型->...,浏览器似乎也是按照这个顺序打印结果的)该属性会忽略使用原型上的同名属性或方法,但实际上该操作并没有改变原型对于该属性或方法的定义!所以obj2继承过来的colo1没有任何改变,但是浏览器打印obj的顺序却有了变化,这又刚好印证了我的解释。
这就是我对于原型链在类式继承模式下的理解,初学JavaScript,水平有限,里面一些解释还只是自圆其说,没有找到资料确认,但是我会好好加油的!也希望大家指出我的不足,给我提供一些学习JavaScript的意见和建议,之后我还会分享其他工厂模式下的继承链分析,敬请期待!