
原型链继承的实现
在js中继承就是通过原型链来实现的,那么到底如何实现呢?
我们废话不多说,直接看个案例!
代码
//猫类
function Cat(){
this.username='小猫';
}
//狗类
function Dog(){
this.username='小狗';
}
//老虎类
function tiGer(){
this.username='老虎';
}
//猫类的原型对象中有一个方法
Cat.prototype.behavior=function (){
console.log('【'+this.username+'】 这种动物真的会要咬人!!....');
console.log(this);//谁调用this归谁!
}
//实例化猫类
var cat=new Cat();
//把狗类的原型对象指向猫
Dog.prototype=cat;
//实例化狗类
var dog=new Dog();
//把老虎的原型对象指向狗
tiGer.prototype=dog;
//实例化老虎类
var tiger=new tiGer();
//调用方法
tiger.behavior();
dog.behavior();
这里我们修改了原型对象的指向, 也就是修改了构造函数的prototype属性值,对吧!
那么这样一来会造就什么样的情况呢?
简单一点说, 我们就会顺着一个: C实例−>C原型(B实例)−>B原型(A实例)−>A原型 这样一个过程来进行查找!
也就是实例tiger-->Tiger原型对象(dog实例)--->Dog原型对象(cat实例)--->Cat原型对象 进行查找
这里也很明显,tiGer的实例和原型对象都没有一个叫behavior的方法, 那么就会顺着一条线路,一直往上寻找
如图

当然你也可以通过修改__proto__来实现!
代码如下
//猫类
function Cat() {
this.username = '小猫';
}
//狗类
function Dog() {
this.username = '小狗';
}
//老虎类
function tiGer() {
this.username = '老虎';
}
//猫类的原型对象中有一个方法
Cat.prototype.behavior = function () {
console.log('【' + this.username + '】 这种动物真的会要咬人!!....');
console.log(this);//谁调用this归谁!
}
//实例化猫类
var cat = new Cat();
//实例化狗类
var dog = new Dog();
//实例化老虎类
var tiger = new tiGer();
//修改原型链指针
tiger.__proto__= dog;
dog.__proto__= cat;
//console.log(tiger);
tiger.behavior();
原理分析
首先,定义了三个构造函数:Cat、Dog和Tiger,每个构造函数都有一个属性username, 分别赋值为小猫"、"小狗"和"老虎
接下来,在Cat类的原型对象中定义了一个方法behavior,该方法用于打印出动物的名字以及调用该方法的对象信息。
然后,通过实例化Cat类创建了一个名为cat的实例对象,并将Dog类的原型对象指向了cat。
这样就建立了一个继承关系,即Dog类会继承Cat类的属性和方法。
接着,通过实例化Dog类创建了一个名为dog的实例对象,并将Tiger类的原型对象指向了dog。同样地,这也建立了一个继承关系,即Tiger类会继承Dog类的属性和方法。
最后,通过实例化Tiger类创建了一个名为tiger的对象,并调用了它的behavior方法。
由于原型链上的继承关系,调用这个dog.behavior()和tiger.behavior()都会查找到最终的原型对象也就是Cat.prototype中的behavior方法进行调用!
当然如果这里再调用Object.prototype.__proto__往上就没有了,就会返回null
这样就形成了一个父子级别的关系,因为我们通过修改prototype或者__proto__形成了一个链条
毕竟原型对象,其实也是一个Object的实例,所以它也有一个__proto__属性,本身它在一个普通原型对象下的指向为Object.Prototype原型对象,也就是说所有函数的默认原型对象都是Object的实例, 但是这里我们把它修改了!
通过__proto__相连接, 每个继承父函数的实例对象都包含一个__proto__指针
最后会指向我们指定父函数的prototype原型对象
这样一直可以以此类推,进行迭代父函数的原型对象, 利用__proto__属性一直可以再往上一层继承。
在这个程中就形成了原型链
我们也可以使用Chrome并且打印一下实例对象来进行查看这个链条的走向!
console.log(tiger);
如图

这里如果眼尖的朋友可能已经注意到了一个问题,那就是constructor这个属性显示不见了, 构造函数的指向也不对了、原型的显示也不对了, 全部都指向了Cat构造函数, 当然从继承的效果上是不影响的!
我们可以用以下代码测试一下:
console.log(tiGer.prototype.constructor);
console.log(Dog.prototype.constructor);
如图

原因:简单点说因为修改原型对象的时候,指向了另一个新的实例对象,所以把 constructor给丢失了!
如果你想看上去比较合理一点,加入以下代码
解决方案
Dog.prototype.constructor=Dog;
tiGer.prototype.constructor=tiGer;
修改之后如图

这就是原型链查找的关系,一层一层的链接关系就是:原型链
有些实例对象能够直接调用Object.prototype中的方法也是因为存在原型链的机制!
所以说JavaScript 中原型链用于实现继承就是这样实现的!
给大家专门准备了一张通用默认原型链原理图,拿去背吧!!
如图

基于原型链的继承
看了以上的案例和图例之后,我们应该就对javascript中的继承有个深入的理解了!
JavaScript的对象其实都会有一个指向一个原型对象的链条, 当我们试图访问一个对象的属性时,它不仅仅在该对象本身上去进行搜寻,还会搜寻该对象的原型,以及原型的原型依次层层向上搜索,直到找到一个名字匹配的属性为止, 或到原型链的末尾!
对象属性的继承
但是有一点我觉得值得注意,就是修改原型链 也就是使用{ __proto__: ... } 和obj.__proto__ 有点不同,前者是标准且未被弃用的一种方式!
举个栗子
var obj={
a: 值1,
b: 值2,
__proto__: c
}
比如在像这样的对象字面量中,c的值必须为 null 或者指向另一个对象用来当做字面量所表示的对象的 原型链,而其他的如a 和 b将变成对象的自有属性, 这种语法读起来非常自然,并且兼容性也比较不错!
我们来看个实际的小案例
代码如下
const obj = {
a: '张三',
b: '李四',
__proto__: {
b: "王五",
c: "绿巨人",
},
};
console.log(obj);
console.log(obj.a);
delete obj.b;
console.log(obj.b);
console.log(obj.c);
分析
当前obj的原型链中具有属性 b和c两个属性
如果obj.__proto__.__proto__ 依照之前的图例肯定是访问到Object.prototype
最后obj.__proto__.__proto__.__proto__则是 null, 这里就是原型链的末尾,值为null
完整的原型链看起来像这样:{ a: 张三, b: 李四 } ---> { b:王五, c: 绿巨人 } ---> Object.prototype ---> null
那么要说继承关系的话,那就是__proto__设置了原型链,也就是说它在这里的原型链被指定为另一个对象字面量!
即便是这里我使用了delete obj.b删除了属性b,也会从__proto__这个链条找到原型链中所继承来的属性b
如图

但是注意了如果这里我没有使用delete obj.b来删除了属性b 那么,当我们调用obj.b返回的则是李四而不是王五,这里其实叫做属性遮蔽(Property Shadowing),意思是虽然没有访问到王五,但这只是被遮住了而已,并不是被覆盖和删除的意思!
当然我们也可以根据这个原理来创建更长的原型链,并在原型链上查找一个属性
代码如下
const obj = {
a: 1,
b: 2,
// __proto__ 设置了原型链。它在这里被指定为另一个对象字面量。
__proto__: {
b: 3,
c: 4,
// __proto__ 设置了原型链。它在这里被指定为另一个对象字面量。
__proto__: {
d: 5,
},
},
};
console.log(obj.d); // 输出5
那么它的原型链就是如下这样:
{ a: 1, b: 2 } ---> { b: 3, c: 4 } ---> { d: 5 } ---> Object.prototype ---> null
其实就是这样就可以嵌套很多层出来,让对象看起来更加有层次结构,也方便管理一些特殊的数据!
对象方法的继承
方法或者函数的继承在js中其实和属性的继承也没有差别!
特别要说明的其实也就是this的指向,当继承的方法被调用时,this值指向的是当前继承的对象,而不是拥有该函数属性的对象
代码说明
const parent = {
username: '张三',
age:35,
method() {
return '我的年龄是'+(this.age + 1)+'岁';
}
}
console.log(parent.method()); //输出36
//然后我们通过child继承了parent的对象
const child = {
__proto__: parent,
}
console.log(child.method());//输出36
child.age = 5;
console.log(child.method());
代码分析
当调用 parent.method 时,this指向了parent所以按照正常逻辑执行 所以输出36
然后我们通过child继承了parent的对象
现在调用 child.method 时,this虽然指向了child,但是又因为 child 继承的是 parent 的方法,
首先在 child 上寻找有没有method方法, 但由于child本身没有名为method方法,则会根据原型链__proto__找上去,最后找到即 parent.method方法来执行!
然后我们在child添加一个age属性赋值为5, 这会就会遮蔽parent上的age属性
child对象现在的看起来是如下这样的:{ age: 5, __proto__: { username: '张三', age: 35, method: [Function] } }
最后输出:6, 是因为child现在拥有age属性,就不会去找parent对象中的age属性了, 但是方法还是会去找parent中的method方法, 而方法中的this.age现在表示 child.age然后再这个基础上+1结果就是这样了!
更多参考案例
function Test() {
this.username = '张三';
this.age = 33;
this.job='软件开发';
}
Test.prototype.say=function (){
return '我的名字叫:'+this.username+',我的年龄是:'+this.age+'我的职业是:'+this.job;
}
var test=new Test();
//新建一个对象,并且修改原型链
var obj = {
username:'李四',
age:'35',
__proto__:test
}
console.log(obj);
console.log(obj.username);
console.log(obj.age);
console.log(obj.say());