《原型链重置版》一万多字让你读懂JavaScript原型对象与原型链的继承,探秘属性的查找机制! (4)

简介: 利用JavaScript原型链实现继承

logo.png

原型链继承的实现

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的方法, 那么就会顺着一条线路,一直往上寻找

如图

9.jpg

当然你也可以通过修改__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();

原理分析

首先,定义了三个构造函数:CatDogTiger,每个构造函数都有一个属性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);

如图

10.png

这里如果眼尖的朋友可能已经注意到了一个问题,那就是constructor这个属性显示不见了, 构造函数的指向也不对了、原型的显示也不对了, 全部都指向了Cat构造函数, 当然从继承的效果上是不影响的!

我们可以用以下代码测试一下:

console.log(tiGer.prototype.constructor);
console.log(Dog.prototype.constructor);

如图

10-1.png

原因:简单点说因为修改原型对象的时候,指向了另一个新的实例对象,所以把 constructor给丢失了!

如果你想看上去比较合理一点,加入以下代码

解决方案

Dog.prototype.constructor=Dog;
tiGer.prototype.constructor=tiGer;

修改之后如图

10-2.png

这就是原型链查找的关系,一层一层的链接关系就是:原型链

有些实例对象能够直接调用Object.prototype中的方法也是因为存在原型链的机制!

所以说JavaScript原型链用于实现继承就是这样实现的!

给大家专门准备了一张通用默认原型链原理图,拿去背吧!!

如图

11.jpg

基于原型链的继承

看了以上的案例和图例之后,我们应该就对javascript中的继承有个深入的理解了!

JavaScript对象其实都会有一个指向一个原型对象链条, 当我们试图访问一个对象的属性时,它不仅仅在该对象本身上去进行搜寻,还会搜寻该对象的原型,以及原型的原型依次层层向上搜索,直到找到一个名字匹配的属性为止, 或到原型链的末尾!

对象属性的继承

但是有一点我觉得值得注意,就是修改原型链 也就是使用{ __proto__: ... }obj.__proto__ 有点不同,前者是标准且未被弃用的一种方式!

举个栗子

var obj={
   
    
    a:1, 
    b:2,
    __proto__: c 
}

比如在像这样的对象字面量中,c的值必须为 null 或者指向另一个对象用来当做字面量所表示的对象原型链,而其他的如ab将变成对象自有属性, 这种语法读起来非常自然,并且兼容性也比较不错!

我们来看个实际的小案例

代码如下

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

如图

11-0-1.png

但是注意了如果这里我没有使用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());
相关文章
|
7天前
|
JavaScript 前端开发
JS将两个数组和合并成数组包对象格式的方法
JS将两个数组和合并成数组包对象格式的方法
8 0
|
14天前
|
JavaScript 前端开发
js遍历对象的方法
js遍历对象的方法
22 1
|
3天前
|
JavaScript 前端开发
javascript的对象
javascript的对象
|
4天前
|
移动开发 JavaScript 前端开发
编程笔记 html5&css&js 028 HTML输入属性(2/2)
编程笔记 html5&css&js 028 HTML输入属性(2/2)
|
4天前
|
JavaScript 前端开发
编程笔记 html5&css&js 027 HTML输入属性(1/2)
编程笔记 html5&css&js 027 HTML输入属性(1/2)
|
4天前
编程笔记 html5&css&js 023 HTML表单属性
编程笔记 html5&css&js 023 HTML表单属性
|
4天前
|
存储 移动开发 前端开发
编程笔记 html5&css&js 010 HTML全局属性
编程笔记 html5&css&js 010 HTML全局属性
|
5天前
|
前端开发 JavaScript 算法
在 JavaScript 中,有哪些方式可以达到继承的效果?
在 JavaScript 中,有哪些方式可以达到继承的效果?
21 0
|
7天前
|
JavaScript 前端开发
JavaScript常用的属性
JavaScript常用的属性
|
8天前
|
消息中间件 存储 前端开发
理解JavaScript事件循环机制
理解JavaScript事件循环机制
10 0

相关产品

  • 云迁移中心