谈谈JS中的原型链继承

前言

关于原型链继承一直是JS中比较重要的知识点,最近也浏览了一些资料与总结,现在整理一下,做个学习笔记

一、基本概念
new运算符

我们经常使用new运算符和自定义构造函数来实例化对象:

 function Person(name, age, movement){
     this.name = name;
     this.age = age;
     this.movement = movement;
 }
 var person1 = new Person('王五', 20, function(){
     console.log('studying~');
 });

实际上new运算符在调用构造函数时,函数内部的代码发生以下变化:

  function Person(name, age, movement){
      var this = new Object();
      this.name = name;
      this.age = age;
      this.movement = movement;
      return this;
  }

此时,构造函数内的this就指向实例化的对象,保证各个实例对象的独立性

实例属性和实例方法
  • 概括:都是绑定在使用构造函数所创建的对象上,并且通过该实例对象来访问
    • 上述的nameagemovement就是实例属性和实例方法
静态属性和静态方法
  • 首先要明确,在JS里,函数也是对象,所以函数也可以动态地添加属性和方法
  • 静态属性和方法就是绑定在函数自身上的属性和方法(非实例对象上的)
   function Person(name, age, movement){
       this.name = name;
       this.age = age;
       this.movement = movement;
       
       // Person.personCount就是一个静态属性,用来记录创建了几次实例对象
       if(!Person.personCount){
           Person.personCount = 0;
       }
       Person.personCount++;
   }
   
   // Person.printPersonCount就是静态方法,可以直接调用
   Person.printPersonCount = function(){
       console.log('共创建了' + Person.personCount + '个实例对象');
   }
   
   var person1 = new Person('王五', 20, function(){
       console.log('studying~');
   });
   
   Person.printPersonCount();  // 1
内置对象类型的判断

当我们想知道一个引用类型对象的内置对象类型时,使用typeof打印结果是不准确的,因为很可能返回的都是object,此时我们可以使用以下两种方法:

  • (对象).constructor.name
  • Object.prototype.toString.call(对象)
    var obj = {name: '王五'};
    console.log(typeof obj);   // object
    console.log(obj.toString());  // [object Object]
    console.log(obj.constructor.name);  // Object
    console.log(Object.prototype.toString.call(obj)); // [object Object]
    
    var arr = [1, 2, 3];
    console.log(typeof arr);   // object
    console.log(arr.toString());  // 1, 2, 3
    console.log(arr.constructor.name);  // Array
    console.log(Object.prototype.toString.call(arr)); // [object Array]
    
    var date = new Date();
    console.log(typeof date);   // object
    console.log(date.toString());  // Sun Nov 17 2019 11:48:23 GMT+0800 (China Standard Time)
    console.log(date.constructor.name);  // Date
    console.log(Object.prototype.toString.call(date)); // [object Date]
自定义对象类型的判断

上面提到在判断内置对象类型时,可以使用:Object.prototype.toString.call(对象)(对象).constructor.name方法,可是在判断自定义构造函数所创建出的对象时,这两种方法都有效吗?

    function Dog(name, age){
        this.name = name;
        this.age = age;
    }
    function Cat(name, age){
        this.name = name;
        this.age = age;
    }
    
    var dog = new Dog('小狗', 1);
    var cat = new Cat('小猫', 2);
    
    console.log(dog.constructor.name);  // Dog
    console.log(cat.constructor.name);  // Cat
    console.log(Object.prototype.toString.call(dog));  // [object Object]
    console.log(Object.prototype.toString.call(cat));  // [object Object]

可以发现此时Object.prototype.toString.call(对象)方法“失效”了。原因在于,别忘了上面提到的new的作用

    function Dog(name, age){
        var this = new Object();
        this.name = name;
        this.age = age;
        return this;
    }

所以Object.prototype.toString.call(对象)方法是查找实际创建出对象的对象类型

instanceof操作符

用来判断实例与原型的继承关系,只要是原型链上的构造函数,都会返回true

isPrototypeOf操作符

用来判断某个对象是不是某个实例的原型对象

访问函数原型对象的方式
  • 函数名.prototype
  • 通过对象中的__proto__属性访问
    • 注意:__proto__是一个非标准属性,它只是某些浏览器为方便开发和调试而提供的属性,所以应该避免在正式代码中出现
hasOwnPropertyin属性操作

两者都是用来判断某一对象上是否拥有某个属性,但不一样的在于:

  • hasOwnProperty: 只在对象自身上查找是否有某个属性
  • in:当对象自身不具有该属性时,会继续到原型链上的原型对象中查找
二、原型链继承
画出Date的原型链

单纯的原型链继承
    function Person(){
        this.hobbies = ['阅读','看电影'];
    }
    Person.prototype.run = function(){
        console.log("跑步");
    }
    
    function Student(){
        this.school = 'xx小学';
    }
    Student.prototype = Person.prototype;   // 修改原型对象的指向
    var stu = new Student():
    stu.run();  // 跑步

可以看到通过修改原型对象的指向,确实可以调用Person原型对象上的run方法,但是只能访问到Person原型对象上的属性和方法,而Person构造函数中的实例属性和方法访问不到

借助构造函数(组合继承)
    function Person(){
        this.hobbies = ['阅读','看电影'];
    }
    Person.prototype.run = function(){
        console.log("跑步");
    }
    
    function Student(){
        this.school = 'xx小学';
    }
    
    // 1. 构造父类实例
    var person = new Person();
    // 2. 修改Student原型对象为父类实例
    Student.prototype = person;
    
    var stu = new Student():
    stu.run();  // 跑步
    console.log(stu.hobbies[0]);  // 阅读

这下,Student构造函数创建的实例就可以访问到Person类的原型属性、方法和实例属性、方法,但是新的问题出现了:打印stu.constructor.name,结果为Person,所以还要解决这个问题:

    // 1. 构造父类实例
    var person = new Person();
    // 2. 修改Student原型对象为父类实例
    Student.prototype = person;
    // 3. 修复constructor指向
    Student.prototype.constructor = Student;
    
    var stu = new Student():
    stu.run();  // 跑步
    console.log(stu.hobbies[0]);  // 阅读
    console.log(stu.constructor.name);  // Student
借助构造函数继承的问题

上面提到借用构造函数继承,但是有一个问题:继承过来的实例属性如果是引用类型,会被多个子类的实例共享

    function Person(){
        this.hobbies = ['阅读','看电影'];
    }
    Person.prototype.run = function(){
        console.log("跑步");
    }
    
    function Student(){
        this.school = 'xx小学';
    }
    
    // 1. 构造父类实例
    var person = new Person();
    // 2. 修改Student原型对象为父类实例
    Student.prototype = person;
    // 3. 修复constructor指向
    Student.prototype.constructor = Student;
    
    var stu1 = new Student():
    stu1.hobbies.push('睡觉');
    var stu2 = new Student();
    console.log(stu2.hobbies);  // ['阅读','看电影', '综艺']

这是因为在子类的实例对象上找不到hobbies数组,就会到原型链上查找,所以子类的实例对象对hobbies数组的修改实际上操作的是同一块内存空间,可以使用callapply来替代

callapply继承

可以使用callapply继承父类的实例属性和方法

    function Person(){
        this.name = '王五';
        this.age = 20;
        this.hobbies = ['阅读','看电影'];
    }
    
    function Student(){
        Person.call(this);
        // Person.apply(this);
        // 使用call apply相当于把父类的实例属性和方法复制到了子类,
        // 保证了其独一性,不会发生父类引用类型的实例属性被多个子类实例共享的情况
    } 
寄生组合式继承

以上的继承模式可以让子类同时访问到父类的实例属性、方法和原型对象上的属性、方法,但还有一个弊端:必须要调用两次父类构造函数,导致父类属性重复
所以最终可以使用寄生式组合继承的方式完美继承

    function Person(name, age){
        this.name = name;
        this.age = age;
        this.hobbies = ['阅读','看电影'];
    }
    Person.prototype.run = function(){
        console.log("跑步");
    }
    
    function Student(name, age){
        // 借调  访问父类构造函数中的实例属性和方法
        Person.call(this, name, age);
        this.school = 'xx小学';
    }
    
    // 寄生式继承  访问父类原型对象上的属性和方法
    function Temp(){}
    Temp.prototype = Person.prototype;
    var studentProto = new Temp();
    Student.prototype = studentProto;
    studentProto.constructor = Student;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值