原型链继承的实现
在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());