javascript不像java是基于类(class)继承的,子类可以继承父类的属性,而是基于原型来实现继承的。要理解原型继承的原理,关键在于理解原型链,本帖将先介绍与之相关的对象、原型对象(prototype)和原型指针(__proto__
),有了这些基础之后进而阐述原型链的构成,最后以一个应用实例来启发读者怎么在实际当中使用它。
1.什么是对象?
按照官方的解释是:Object in Javascript is a “name-value pair” which is a name that maps to an unique value。意思是javascript中的对象是“键值对”的集合,每一个键(key)都对应一个唯一的值(这个值也可以是一个函数),它的样子如下
//定义一只猫对象,它有名字,颜色,抓老鼠等属性
var cat = {
name: "kitty", //key为name,value为“kitty”
color: "white",
skill: function(){ //key为skill,value为一个函数对象
console.log("我会抓老鼠!");
}
};
而定义对象有两种方法:一种是通过大括号直接创建;另一种是使用new来创建,JavaScript 提供了多个内建对象(就是javascript已经定义好了,可以直接使用的对象,比如 String、Date、Array、Object 等等),栗子如下
//1.通过大括号定义对象
var a = {
name:"mike",
age:20
};
//2.通过new操作符创建
//通过new内建对象来创建
var b = new Object(); //创建一个空对象b
b.name = "mike"; //给对象b添加name属性
b.age = 20; //给对象b添加age属性
//或者先自定义一个构造函数,然后再通过new来创建
var C = function(name, age){ //创建一个构造函数C,需两个参数name,age
this.name = name;
this.age = age;
}
var c1 = new C("mike", 20); //通过new C并传入两个参数来创建对象c1
2.prototype 和 __proto__
2.1 原型指针__proto__
每个对象都有一个__proto__
属性 ,它是一个指针,指向(等于)它父对象的prototype 。如果我们通过new操作符来创建一个对象,则该对象的__proto__
指向创建它的函数的prototype。可以举个栗子来看一下
var Programmer = function(){} //定义一个“程序员”构造函数
var FProgrammer = new Programmer(); //创建一个“女程序媛”对象
FProgrammer.__proto__ === Programmer.prototype; //->true
其中FProgrammer (女程序员)对象是由自定义的构造函数Programmer (程序员)通过new操作符创建的,所以女程序媛的__proto__
指向她的父对象(程序员)的prototype。
2.2 原型对象 prototype
prototype是每一个函数类型自带的属性,而其它非函数类型并没有。那我们就定义一个函数a,然后看一下它的prototype长什么样,如下图所示
如上图红框部分所示,prototype本身是Object类型,所以我们称prototype为原型对象,既然prototype是个对象,那自然就包含了__proto__
属性,除此之外,它还包含了一个constructor构造函数,该函数指向函数a本身,验证如下
var Programmer = function(){}
Programmer.prototype.constructor === Programmer; //-> true
那么为什么原型对象里会有constructor,它有什么用呢?
答案是在通过new操作符创建对象时,就是通过这个constructor来实例化对象的。例如以 var o = new Person(“aty”,25);
形式创建对象的过程实际上可以分为下面几步,如下图所示
所以这个constructor是new一个对象的大功臣,是必不可少的。
2.3 prototype 和 __proto__
的关系
由var FProgrammer = new Programmer();
这句代码,我们可以得到__proto__
与prototype的关系如下图所示
3.原型链
弄清楚了原型对象(prototype)与原型指针(__proto__
)之后,我们再来理解原型链。当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么他就会去__proto__
指向的“父类”prototype去找这个属性,由于prototype也是一个对象,所以它又有自己的__proto__
,于是就这样一直找下去,这就形成了我们平时所说的原型链的概念。
还是以上面“程序员”的代码为例,FProgrammer的__proto__
指向了Programmer 的prototype,而Programmer 的prototype也是一个对象啊,那它的__proto__
又指向谁呢?我们来看下下面的代码
FProgrammer.__proto__.__proto__ === Object.prototype; //->true
// 即 Programmer.prototype.__proto__ === Object.prototype;
由此可见,Programmer.prototype的__proto__
指向的是Object的prototype,说明函数是特殊的对象,是继承自Object的,那么Object原型的__proto__
指向的是谁呢?
FProgrammer.__proto__.__proto__.__proto__ === null; //->true
//Object.prototype.__proto__ === null;
所以原型链的尽头是一个null对象,将这条原型链画出来会更加清楚,如下图所示
4.原型链的应用
从上面可以看出原型链可以达到继承的效果,也就是说我们可以通过原型链来实现原型继承,下面来看一下原型继承在实际中是怎么应用的,比如说我们要定义一个男程序员对象,他有写代码(code)和以打游戏为乐(fun)这两个属性,还有一个女程序员对象,她有写代码(code)和看电视剧娱乐(fun)这两个属性,一般的方法的写法如下
//男程序员
function MProgrammer(){
this.code = function(){
console.log("写代码!!");
},
this.fun = function(){
console.log("我以打游戏为乐,哈哈!!");
}
}
//女程序员
function FProgrammer(){
this.code = function(){
console.log("写代码!!");
},
this.fun = function(){
console.log("我就是喜欢追剧,吼吼!!");
}
}
这样的写法明显有重复代码,既然他们有共同的工作(写代码),何不把这个方法提出来放到一个公共对象里作为“父类”供他们继承呢,这里就可以用到原型链了,优化后的代码如下
//“父类”
var Programmer = function(){}
Programmer.prototype.code = function(){
console.log("写代码!!");
}
//以“父类”通过new操作符创建的男程序员对象
var MProgrammer = new Programmer();
MProgrammer.fun = function(){
console.log("我以打游戏为乐,哈哈!!");
}
//以“父类”通过new操作符创建的女程序员对象
var FProgrammer = new Programmer();
FProgrammer.fun = function(){
console.log("我就是喜欢追剧,吼吼!!");
}
MProgrammer.code();
MProgrammer.fun();
FProgrammer.code();
FProgrammer.fun();
运行结果如下图所示
这种以原型继承的方式来写是不是看着舒服多啦,既没有重复代码,又简洁,那如果男程序员和女程序员的共同方法不止那一个敲代码code方法呢,比如还有上下班打卡和开会等,那样在“父类”的prototype上分别加上每个属性会比较繁琐,用以下方式写会更简洁一点
//“父类”
var Programmer = function(){}
Programmer.prototype = {
code:function(){
console.log("写代码!!");
},
clock:function(){
console.log("上下班打卡!");
},
meeting:function(){
console.log("每天早上9点开早会!");
}
// ...
}
如果公司职员有一个共同的日常:每天都要写日报,那写日报这个属性是属于全体员工的,不能加在程序员的属性里,而应该加在程序员的“父类”(职员)里,那代码应该怎么写呢,如下所示
//程序员的“父类”——职员
var Staff= function(){}
Staff.prototype.daily = function(){
console.log("公司职员每天都要写日报!");
}
//女程序员的“父类”——程序员
var Programmer = function(){}
//将程序员的原型对象实例化为职员就可以拥有职员的所有属性了
Programmer.prototype = new Staff();
//另外给程序员的原型对象附加写代码的属性
Programmer.prototype.code = function(){
console.log("写代码!!");
}
//以“父类”(程序员)通过new操作符创建的男程序员对象
var MProgrammer = new Programmer();
MProgrammer.fun = function(){
console.log("我以打游戏为乐,哈哈!!");
}
//以“父类”通过new操作符创建的女程序员对象
var FProgrammer = new Programmer();
FProgrammer.fun = function(){
console.log("我就是喜欢追剧,吼吼!!");
}
MProgrammer.code();
MProgrammer.fun();
MProgrammer.daily();
FProgrammer.code();
FProgrammer.fun();
FProgrammer.daily();
结果如下图所示
以上代码中为了继承职员Staff的属性不能写成var Programmer = new Staff()
;因为这样写的话Programmer就只是一个普通的对象了,并不是函数,下句Programmer.prototype.code = function(){}
这里就会报错,因为普通对象并没有prototype属性,只有函数才有,此时Programmer只有一个__proto__
属性了,如下图所示
总结
既然javasript是一门面向对象语言,那我们在写js代码时就应该有面向对象的思想,发扬它的原型继承特性,多使用原型链,而不应该以面向过程的方式去编写代码。