• 【JavaScript】-- 继承


    注意:在JS里面是没有子类和父类的概念的,在这里只是为了方便才简称为子类与父类。我们应该完整的将其描述为子类构造函数与父类构造函数。

    原型链

    构造函数、原型和实例的关系:每个构造函数都有一个原型对象(prototype),原型有一个属性(constructor)指回构造函数,而实例有一个内部指针(__proto__)指向原型。

    如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。

    实现继承是 ECMAScript 唯一支持的继承方式,而这主要是通过原型链实现的。

     原型链实现代码如下:

    1. // 创建Animal
    2. function Animal(){
    3. this.name = 'Animal';
    4. }
    5. // prototype
    6. Animal.prototype.getAnimalName = function(){
    7. console.log(this.name + 'getAnimalName');
    8. }
    9. // 创建Dog
    10. function Dog(){
    11. this.name = "dog";
    12. }
    13. // 这一句将Animal继承给Dog
    14. // 将Animal的实例赋值给Dog的prototype对象,相对于将Animal的实例中的__proto__赋值给Dog的prototype对象
    15. // 如此一来,就能通过Animal的[[prototype]](__proto__)来访问到Dog的原型对象中的属性与方法
    16. Dog.prototype = new Animal()
    17. // 不建议使用Animal.__proto__ === Dog.prototype
    18. // 在使用原型链继承时,要先继承再在自己的原型对象里定义自己的属性和方法
    19. Dog.prototype.getDogName = function(){
    20. console.log(this.name + 'getDogName');
    21. }
    22. var dog1 = new Dog();
    23. dog1.getAnimalName();
    24. dog1.getDogName();

    注意:getAnimalName() 是一个方法,在Animal.protptype对象上,而name是一个属性在Dog.prototype上,因为getAnimalName是一个原型方法,name是一个实例属性

    在这里我们是定义了两个类型Animal与Dog

    并将Animal的实例赋值给Dog,于是Dog.prototype实现了对Animal的继承

    这样赋值重写了Dog最初的原型对象,使其变成了Animal的实例

    这样写使得Animal可以访问的属性和方法都存在于Dog.prototype

    在这样实现了继承之后我们又向 Dog.prototype 里面添加了一个 getDogName 的方法

    最后创建Dog的实例 dog1 并调用其继承的 getAnimalName 方法

    dog1通过内部的[[prototype]]属性指向Dog.prototype,

    Dog又通过内部的[[prototype]]属性指向Animal.prototype

    由于将Dog.prototype的constructor属性指向了Animal,因此dog1的constructor也指向了Animal

    可以通过修改 Dog.prototype.constructor 来改变指向,使其重新指回Dog

    当我们想要读取实例上的属性时,会首先在实例上进行搜索,

    如果没有找到,就往上搜索实例的原型

    如果还没有找到就继续往上搜索原型的原型(前提是完成了原型链的继承)

    dog1.getAnimalName()总共进行了三步搜索:dog1--Dog.prototype--Animal.prototype

    对属性和方法的搜索会一直持续到原型链的末端

    默认原型

    在原型链中还有一环,就是在默认情况下所有的引用类型都继承自Object

    任何函数的默认原型都是Object的一个实例,意味着实例内部还有一个内部指针指向Object.prototype

    这也就是为什么自定义类型能够继承包括 toString()、valueOf()在内的所有默认方法的原因。

    原型与继承关系

    在上面的案例中,我们可以用instanceof运算符用于检测构造函数的protptype属性是否在某个实例的原型链上:

    1. // instanceof运算符用于检测构造函数的protptype属性是否在某个实例的原型链上
    2. console.log(dog1 instanceof Object); // true
    3. console.log(dog1 instanceof Animal); // true
    4. console.log(dog1 instanceof Dog); // true
    5. // isPrototypeOf() 原型链中每个原型都可以调用这个方法,若原型链中包含这个原型,返回true
    6. console.log(Object.prototype.isPrototypeOf(dog1));
    7. console.log(Animal.prototype.isPrototypeOf(dog1));
    8. console.log(Dog.prototype.isPrototypeOf(dog1));

    让子类上的方法覆盖父类上的方法,或者在子类里添加新的方法: 

    1. // 若要让子类上的方法覆盖父类上的方法,或者在子类添加新方法,则需要先给在原型赋值之后再添加到原型上
    2. function Animal(){
    3. this.name = "animal";
    4. }
    5. Animal.prototype.getAnimalName = function(){
    6. console.log(this.name + "getAnimalName");
    7. }
    8. var test = new Animal();
    9. test.getAnimalName(); // animalgetAnimalName
    10. function Dog(){
    11. this.name = "dog";
    12. }
    13. Dog.prototype = new Animal();
    14. Dog.prototype.getDogName = function(){
    15. console.log(this.name + "getDogName");
    16. }
    17. Dog.prototype.getAnimalName = function(){
    18. console.log(this.name + "我被覆盖了");
    19. }
    20. var dog2 = new Dog();
    21. dog2.getAnimalName(); // dog我被覆盖了
    22. dog2.getDogName(); // doggetDogName

    getDogName()方法是 Dog 的新方法

    而后一个 getAnimalName()是原型链上已经存在但在这里被遮蔽的方法

    后面在Dog实例上用的就是后面这个被改写过的方法

    而第一次Animal的实例执行的仍然是最初的原型里的方法

    重点在于上述两个方法都是在把原型赋值为 Animal 的实例之后定义的。

     原型链的破坏:

    以对象字面量形式创建原型方法就可以破坏原型链:

    1. // 只要以对象字面量的形式创建原型方法就可以破坏原型链,因为这相当于重写了原型链
    2. function Animal() {
    3. this.name = "animal";
    4. }
    5. Animal.prototype.getAnimalName = function () {
    6. console.log(this.name + "getAnimalName");
    7. };
    8. function Dog(){
    9. this.name = "dog";
    10. }
    11. // 继承
    12. Dog.prototype = new Animal();
    13. // 以对象字面量形式创建原型方法
    14. Dog.prototype = {
    15. getDogName(){
    16. console.log(this.name + "getDogName");
    17. },
    18. someOtherMethod() {
    19. return false;
    20. }
    21. };
    22. var dog = new Dog();
    23. dog.getDogName(); // doggetDogName
    24. dog.getAnimalName(); // 报错!

    在这里,Dog被赋值为一个Animal的实例后,又被一个对象字面量覆盖了

    覆盖后的原型是一个Object的·实例,而不是Animal的实例

    因此原先的原型链就断了,Dog和Animal也就没了联系

    原型链的问题:

    •  类似于原型模式的问题, 原型中包含的引用值会在所有实例间共享
    1. function Animal(){
    2. this.category = ['cat', 'rabbit']
    3. }
    4. function Dog(){};
    5. // 继承
    6. Dog.prototype = new Animal();
    7. var dog1 = new Dog();
    8. dog1.category.push('piggy');
    9. console.log(dog1.category); // [ 'cat', 'rabbit', 'piggy' ]
    10. var dog2 = new Dog();
    11. console.log(dog2.category); // [ 'cat', 'rabbit', 'piggy' ]

    在这里我们在Animal构造函数里面定义了一个category属性,并传入一个数组(引用值)

    因此每个Animal的实例里都会有一个category属性,包括其中的数组

    当我们在后面完成了Dog原型对Animal的继承时,Dog.prototype也成为了一个Animal的实例,获得了category属性

    也就相当于创建了一个Dog.prototype.category属性

    结果Dog的每个实例都会共享这个category属性,因此我们在修改dog1.category时也会修改dog2.category

     原型链的第二个问题是:

    • 无法在不影响所有对象实例的情况下将参数传入父类的构造函数里

    盗用构造函数/对象伪装/经典继承

    这是为了解决原型的包含引用值的问题。

    思路:在子类构造函数中调用父类构造函数。

    因为函数就是在特定上下文执行代码的简单对象,因此我们可以使用call或者apply方法以新创建的对象为上下文执行代码

    1. function Animal(){
    2. this.category = ['cat', 'rabbit']
    3. }
    4. function Dog(){
    5. // 继承
    6. Animal.call(this)
    7. };
    8. var dog1 = new Dog();
    9. dog1.category.push('piggy');
    10. console.log(dog1.category); // [ 'cat', 'rabbit', 'piggy' ]
    11. var dog2 = new Dog();
    12. console.log(dog2.category); // [ 'cat', 'rabbit' ]

    var dog1 = new Dog();是dog1调用Dog构造函数,因此其内部的this指向dog1,

    Animal.call(this)相当于是Animal.call(dog1),即dog1.Animal()

    当dog1调用Animal里的方法时,Animal内部的this就指向了dog1

    因此Animal上所有的属性和方法都被拷贝到了dog1上面

    所以每个实例都会有自己的category属性副本,互不影响

      经典继承还可以向父类构造函数内传递参数:

    1. // 经典继承还可以向父类构造函数内传递参数
    2. function Animal(name){
    3. this.name = name;
    4. }
    5. function Dog(){
    6. Animal.call(this,'wangwang');
    7. this.age = 3;
    8. }
    9. var dog = new Dog();
    10. console.log(dog.name); // wangwang
    11. console.log(dog.age); // 3

    在这里我们向Animal里传递了一个参数name,然后将它赋值给一个属性

    在Dog构造函数调用Animal构造函数时同时将这个参数传入,实际上也会在Dog构造函数里定义一个name属性

    为了不让父类构造函数将子类构造函数上的属性覆盖,我们可以先调用父类构造函数再给子类构造函数添加别的属性

    经典继承的问题

    经典继承的问题也就是构造函数模式的问题,即必须在构造函数中定义方法,函数不能复用,

    子类也不能访问父类原型上定义的方法,所以经典继承也不能单独使用。

    总结:

    1. 创建的实例只是子类的实例,不是父类的实例
    2. 没有拼接原型链,不能使用instanceof(),因为子类只继承了父类里的属性和方法,没有继承父类原型对象里的属性和方法
    3. 每个子类实例都保存了父类的实例方法副本,浪费内存影响性能,而且不能实现父类实例方法的复用

    组合继承(伪经典继承)

    结合了原型链和经典继承

    思路:使用原型链继承原型上的属性和方法,使用经典继承函数继承实例属性。

    这样做既可以将方法定义在原型上以实现复用,又可以使每个实例拥有自己的属性

     组合继承代码如下:

    1. function Animal(name){
    2. this.name = name;
    3. this.category = ['cat', 'rabbit']
    4. }
    5. Animal.prototype.sayName = function(){
    6. console.log(this.name);
    7. }
    8. function Dog(name, age){
    9. // 继承属性
    10. Animal.call(this,name);
    11. this.age = age;
    12. }
    13. Dog.prototype = new Animal();
    14. Dog.prototype.sayAge = function(){
    15. console.log(this.age);
    16. }
    17. // 继承方法
    18. var dog = new Dog("love", 11);
    19. dog.category.push('pig');
    20. console.log(dog.category);
    21. dog.sayName();
    22. dog.sayAge();
    23. var doggy = new Dog("sick", 9);
    24. console.log(doggy.category);
    25. doggy.sayName();
    26. doggy.sayAge();

    在这个例子里,我们在Animal的构造函数里定义了两个属性name和category

    在Animal的原型对象里定义了方法sayName()

    Animal.call(this,name);使Dog构造函数调用了Animal构造函数,并传入name属性

    然后又额外定义了一个age属性

    然后,Dog.prototype 也被赋值成为Animal的实例,原型实例被赋值后又添加了一个叫sayAge()的方法

    这样,我们就可以创建两个Dog实例,让这两个实例都有自己的属性,同时共享相同的方法

    组合继承弥补了原型链和经典继承函数的不足,是 JavaScript 中使用最多的继承模式。

    而且组合继承也保留了 instanceof 操作符和 isPrototypeOf()方法识别合成对象的能力。

  • 相关阅读:
    tensorflow-serving实战
    为提高 SDLC 安全,GitHub 发布新功能|GitHub Universe 2022
    Linux命令详解(12)-crontab命令
    实战派 | Java项目中玩转Redis6.0客户端缓存!
    docker registry 镜像同步
    微服务项目:尚融宝(6)(上手复习mybatisplus)
    Python基础学习(11)常用模块
    POST请求
    “好物”推荐+Xshell连接实例+使用Conda创建独立的Python环境
    Android HIDL 介绍学习之客户端调用
  • 原文地址:https://blog.csdn.net/qq_48802092/article/details/126463309