【JS】原型链、es5继承和es6继承

原型链及es5继承可参考链接
es6继承及es5的对比。可参考链接

原型

原型分为显式原型和隐式原型,之前写过一篇小总结链接

构造函数对象有prototype显式原型属性
实例对象有__proto__隐式原型属性

首先明确声明一个函数有三种形式,一般只用前两种(暂不考虑箭头函数,后面会做说明)

  • 可以写成 function Xxx( ){ };
  • 可以写成 var Xxx = function( ){ };
  • 也可以写成var Xxx = new Function( ) ,参数是字符串形式的函数体

new Function()的基本用法:最后一个参数是函数的 body(函数体),类型为 string;

    function F1(){
        console.log('我是F1');
    }

    var F2 = function(){
        console.log('我是F2');
    }


    var F3 = new Function("console.log('我是F3')")

    F1()
    F2()
    F3()

在这里插入图片描述 在这里插入图片描述

可以看到三种方式都可以生效,并且前两种跟最后一种只是多了name属性,第三个是匿名函数,为了方便理解,以第三种来讲解。

在JS构造函数中有一个属性prototype,叫做原型(显式原型)。实例对象中有一个属性__proto__,它是隐式原型,这是供浏览器使用的,它不是标准的属性,

不是标准的属性是什么意思呢,引用一句话:一般情况下,实例对象.__proto__才能访问到构造函数中的prototype的属性或者方法,即 per. __ proto __ .eat()。 因为__proto__不是标准的属性,所以直接写成 per.eat()即可,原型是一个属性,而这个属性也是一个对象。,所以个人的理解就是因为这个原因,才会有原型链的概念,在实例本身上没有找到,就会顺着__proto__往上去找,而且可以不用写 实例. __proto __.属性 ,可以只写 实例.属性。

注意

但是注意,这种简写方式只适用于读取属性,或者方法,不适用于修改该属性,如果要修改原型上的属性,却简写,那么js会认为是给实例对象本身添加一个属性,而不是顺着原型链找到那个同名属性并修改
比如:

        function F() { }
        F.prototype.arr = [1]
        F.prototype.b = 1
        
        let obj1 = new F()
        
        obj1.arr.push(2)
        obj1.b = 2

        let obj2 = new F()
        console.log(obj2.arr, obj2.b)

对于上面代码中的 obj1.arr.push(2),虽然是简写模式,但是arr是有引用类型的数据结构,所以就算是简写模式,读取了属性,即读取到引用地址,也可以进行修改堆里面那个数据的具体内容;
但是obj1.b = 2这种,就是直接简写,所以会判断为给obj1 添加一个b属性为2
最后输出[ 1, 2 ] 和 1
在这里插入图片描述

原型的作用:数据共享,节省内存空间
多个对象,有的属性可以用一样的,这种情况利用原型就可以省很多空间;

通过原型为内置对象添加原型的属性或者方法

首先要知道,在构造函数中,this指向实例对象,比如:

    function F1(){
        this.fun1 = 'fun1'
        console.log(this);
        console.log('我是F1');
    }

    F1()
    
    var f1 = new F1()

在这里插入图片描述

F1()的时候,F1不算是构造函数,就是一个普通函数,所以这个时候的 console.log(this); this会指向调用该函数的Obj,这个时候是外部的window,所以会输出window,
执行 var f1 = new F1()的时候,同样也会调用一遍F1(),但这个时候就是构造函数了,所以这个时候的this指向的是实例对象f1,
下图可以看到,确实是把fun1属性加到了f1上,而不是F1构造函数上,这里也可以看得到F1的prototype和f1的__proto__其实是一样的,但是要明确先后顺序,现有prototype,然后实例对象的__proto__属性,再指向prototype,所以他们是一样的,俩都指向一个对象,
在这里插入图片描述 在这里插入图片描述

上面知道了prototype == proto,那么可以给构造函数的原型上加属性,实例对象就可以通过原型来使用了,而且上面讲过了,实例对象调用的时候,可以不写__proto__,简便的写就行,如下:

    function F1(){
        this.fun1 = 'fun1'
        console.log(this);
        console.log('我是F1');
    }
    F1.prototype.protofun1 = 'protofun1'
    
    F1()
    
    var f1 = new F1()

在这里插入图片描述 在这里插入图片描述
可以通过截图看到构造函数F1的prototype有了该属性,而且实例对象f1的__proto__也有了该属性,

那么我们可以在实例对象上通过 f1.proto.protofun1 = ‘protofun1+++’ 来修改原型上的该属性;
也可以通过构造函数的 F1.prototype.protofun1 = ‘protofun1+++’ 来修改;
但是不能通过f1.protofun1 = ‘protofun1+++’来修改,这样只会给实例对象本身上加protofun1属性

 f1.__proto__.protofun1 = 'protofun1+++' 
 或者
 F1.prototype.protofun1 = 'protofun1+++' 

都是下面的结果
在这里插入图片描述 在这里插入图片描述

f1.protofun1 = ‘protofun1+++

在这里插入图片描述 在这里插入图片描述
可以看到,没有修改原型链,只是给实例对象加了个属性,这个在上面有讲到(那个注意小标题里),要区分清楚。

在这里插入图片描述

原型链

原型链是实例对象和原型对象之间通过原型(proto)来联系的一种关系,上面已经讲到了,据图也可知,JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。当实例对象与原型链上都有一个同名对象的时候,以实例对象上为准,当然也有可能是多层的原型链,就以最先找到的为准
例子代码如下:

    function Person(){
        this.p1 = 'p1'
        this.p3 = 'p3'
    }
    Person.prototype.p2 = 'p2'
    Person.prototype.p3 = 'p3_plus'

    
    var p = new Person()


    console.log(p.p1);
    console.log(p.p2);
    console.log(p.p3);

在这里插入图片描述
在这里插入图片描述
刚刚提到,原型链可以是多层次的,比如,上面的Person.prototype也是一个对象,那么它也会有一个__proto__,依次往上到Object构造函数,那么原型链的终点是什么,放一张经典图,原型链最顶部是Object.prototype,再往上的__proto__就会指向null,即终点

在这里插入图片描述
文章再开始提到函数可以是var Xxx = new Function( )生成,那么任何函数都可以看做是Function构造函数的一个实例对象,那么必然有Xxx.__proto__的属性指向Function.prototype,Function.prototype里面有constructor,指向Function构造函数,形成了闭环。

箭头函数不能作为构造函数

箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。参考:链接

  • 没有单独的this
  • 不绑定arguments
  • 箭头函数不能用作构造器,和 new一起用会抛出错误
  • 箭头函数没有prototype属性
        let Fun = function (a) {
            this.x = a
        }

        let farray = (a) => {
            this.x = a
            console.log('我是箭头函数');
        }

在这里插入图片描述
在这里插入图片描述
可以看到farray没有prototype的属性,所以可以算是原型链底端,要说原型链顶端是object再往上的null,那么箭头函数应该可以算是一个底端了

继承

先说es5的继承,
继承是一种类(class)与类之间的关系,JS中没有类,但是可以通过构造函数模拟类,然后通过原型来实现继承,继承是为了实现数据共享,js中的继承当然也是为了实现数据共享。

es5中通过原型来实现继承,具体参考链接

继承实例
动物有名字,体重等属性,还有吃东西的行为。
狗有名字,体重,毛色等属性, 还有吃东西和咬人的行为。
哈士奇有名字,体重,毛色,年龄等属性, 还有吃东西,咬人, 逗人玩等行为。
它们之间的继承关系代码实现如下:

// 动物的构造函数
function Animal(name, weight) {
	this.name = name;
	this.weight = weight;
}
//动物的原型的方法
Animal.prototype.eat = function() {
	console.log("弟兄们冲啊,赶快吃吃吃!!!");
};
 
//狗的构造函数
function Dog(color) {
	this.color = color;
}
Dog.prototype = new Animal("小三", "30kg");
Dog.prototype.bitePerson = function() {
	console.log("~汪汪汪~,快让开,我要咬人了!!!");
};
 
//哈士奇构造函数
function Husky(age) {
	this.age = age;
}
Husky.prototype = new Dog("黑白色");
Husky.prototype.playYou = function() {
	console.log("咬坏充电器,咬坏耳机,拆家...哈哈,好玩不!!!");
};
var husky = new Husky(3);
console.log(husky.name, husky.weight, husky.color);
husky.eat();
husky.bitePerson();
husky.playYou();

通过把子构造函数的prototype,挂在父构造函数实例对象上,以后子构造函数的实例的__proto__就会去父实例对象找,找不到,再顺着父实例对象的__proto__往上找,
在这里插入图片描述
但是这样有一个问题是,实例化父构造函数的时候,已经提前初始化了一些属性,想要改变,还得重新去挨个写,比较麻烦,
所以另一种继承方式–借用构造函数继承,如下:
继承的时候,不改变原型的指向,直接调用父级的构造函数来为属性赋值,即把要继承的父级的构造函数拿过来,借用一下为属性赋值,这叫做借用构造函数。借用构造函数需要使用call ()这个方法,


function Person(name, age, sex, weight) {
	this.name = name;
	this.age = age;
	this.sex = sex;
	this.weight = weight;
}
Person.prototype.sayHi = function() {
	console.log("你好帅呀!!!");
};
 
function Student(name, age, sex, weight, score) {
	//借用构造函数
	Person.call(this, name, age, sex, weight);
	this.score = score;
}
var stu1 = new Student("小三", 16, "男", "50kg", "110");
console.log(stu1.name, stu1.age, stu1.sex, stu1.weight, stu1.score);
 
var stu2 = new Student("小红", 22, "女", "45kg", "88");
console.log(stu2.name, stu2.age, stu2.sex, stu2.weight, stu2.score);
 
var stu3 = new Student("小舞", 16, "女", "40kg", "100");

借用构造函数继承,解决了继承的时候属性重复的问题。但是这又导致一个问题即父类中的原型方法不能被继承
原型继承的时候,子函数的原型挂的是父构造函数实例对象,共享的是原型上的方法,构造函数继承时,没有进行挂原型,只是在构造函数内部进行操作,修改实例属性,
所以要想达到目的,就把两种方法结合起来使用,即组合继承
即在继承过程中,既可以保证每个实例都有它自己的属性,又能做到对一些属性和方法的复用。这时组合继承应运而生,组合继承=原型继承+借用构造函数继承。


function Person(name, age, sex) {
	this.name = name;
	this.age = age;
	this.sex = sex;
}
Person.prototype.sayHi = function() {
	console.log("你好帅呀!!!");
};
 
function Student(name, age, sex, score) {
	//借用构造函数:解决属性值重复的问题
	Person.call(this, name, age, sex);
	this.score = score;
}
//改变原型指向---原型继承解决原型方法不能被继承问题
Student.prototype = new Person(); //不传值
Student.prototype.eat = function() {
	console.log("吃吃吃!!!");
};
var stu = new Student("小三", 16, "男", "111分");
console.log(stu.name, stu.age, stu.sex, stu.score);
stu.sayHi();
stu.eat();
var stu2 = new Student("小舞", 15, "女", "1111分");
console.log(stu2.name, stu2.age, stu2.sex, stu2.score);
stu2.sayHi();
stu2.eat();

ES6的继承

以上两种继承方式都是通过原型去解决的,在 ES6 中,我们可以使用 class 去实现继承,并且实现起来很简单
其实在 JS中并不存在类,class 只是语法糖,本质还是函数,instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype,所以可以用来判断继承关系。

class Person {}
Person instanceof Function // true
  • instanceof运算符介绍? instanceof运算符返回一个布尔值,表示对象是否为某个构造函数的实例。关键字左边是实例,右边是构造函数。检测原理是:检查右边构造函数的prototype属性,是否在左边对象的原型链上

所有函数都是Function构造函数的实例对象,所以上述class Person构造函数 instanceof Function 应该返回true

class Parent {
  constructor(value) {
    this.val = value
  }
  getValue() {
    console.log(this.val)
  }
}
class Child extends Parent {
  constructor(value) {
    super(value)
    this.val = value
  }
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true

class 实现继承的核心在于使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super,因为这段代码可以看成 Parent.call(this, value)。

ES5 和 ES6 继承的区别:

  • ES6 继承的子类需要调用 super() 才能拿到子类,ES5 的话是通过 apply 这种绑定的方式
  • 类声明不会提升,和 let 这些一致

在ES6中的class,如果想给构造函数自身加属性,可以在内部用static关键字参考,但是es5里面只能在外部Xxx.属性添加参考
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值