彻底搞懂~JS原型、原型链与继承

263 阅读4分钟

原型与原型链

为什么会出现原型与原型链?

我们都知道面向过程编程、面向对象编程,JS原型出现的原因就是要实现面向对象。说到这里肯定还不明白,下面就来详细说明一下这张图:

首先把握两个点:对象一定有__proto__,函数一定有prototype.

image.png

这张图描述了以下几个意思:

1.JS通过构造函数(一般用首字母大写的函数)实现类,通过new一个构造函数来创建对象。

2.构造函数的显式原型prototype等于他所创建出来对象的隐式原型__proto__

3.Object构造函数也是Function创建的,因为它也是个函数吧,不然怎么创建对象,是函数那就肯定是Function new出来的。Function.prototype是一个对象吧,它的__proto__就是Object.prototype,注意:不是说对象的__proto__就一定是Object.prototype,String new 出来的对象__proto__是String.prototype

4.Function是一个构造函数,他可以new出来很多实例其中包括Array,String,以String为例:

(1)String这些具有双重身份,作为对象,它的__proto__指向Function.prototype。

(2)作为构造函数,可以new出示例,比如[1,2,3],因为万物皆对象 String.prototype是Object创建出来的对象,所以它的__proto__指向Object.prototype。

5.Object.prototype.proto==null,总得有个尽头吧,这就是原型链的尽头。

面向对象和原型链

根据上面的描述,原型链其实就定义了一套对象函数变量之间的规则,用于实现类与对象,实现面向对象。

手写new

function myNew() {
        //拿到第一个值,构造函数
        let content = Array.prototype.shift.call(arguments);
        //1.shift会改变原数组,2.call可以借用方法
        let args = Array.prototype.slice.call(arguments);
        //typeof类型判断
        if (typeof content !== "function") throw new Error("error");
        //创建对象
        let obj = Object.create(content.prototype); //这个里面的this已经维护好了
        //调用函数,并维护创建出来的对象的this,在构造函数内部,this指向创建出来的实例
        let res = content.apply(obj, args);
        let flag =
          res && (typeof res === "function" || typeof res === "object");
        return flag ? res : obj;
      }
      //测试用例1,有返回值,new出来的对象只有返回的对象上的属性c
      // let obj = { c: 1 };
      // function foo(a, b) {
      //   this.a = a;
      //   this.b = b;
      //   return obj;
      // }
      // let o = myNew(foo, 1, 2);
      // console.log(o.c);
      //测试用例2,没有返回值,new出来的对象包含foo构造函数的属性a,b
      let obj = { c: 1 };
      function foo(a, b) {
        this.a = a;
        this.b = b;
      }
      let o = myNew(foo, 1, 2);
      console.log(o.a);

继承

为什么需要继承?

继承是将不同类(Student,Teacher)之间的共有的部分提取出来,写到一个父类中(Person) 减少代码冗余

继承的方法

1.原型链继承:

直接将子类的prototype指向父类new出来的示例。

      function Father() {
        this.name = "zhangsan";
        this.age = 12;
      }
      function Son() {
        this.school = "123";
      }
      Son.prototype = new Father();
      Son.prototype.sing = function () {
        console.log("singing");
      };

存在的问题

1.Object.keys拿不出来,因为这个方法拿的是自身可枚举属性

2.要给子类加方法需要在继承语句的后面

2.借用构造函数继承

          function Father(name, age) {
            this.name = name;
            this.age = 12;
          }
          Father.prototype.sing = function () {
            console.log("singing");
          };
          function Son(name, age) {
            Father.call(this, name, age);
            this.school = "123"; //子类独有的属性
          }
          Son.prototype = new Father();
          let son3 = new Son("zs", 18); //这个对象拥有它的类继承来的name和age
          console.log(son3.name);
          console.log(son3.school);
          

解释

1.Father.call是把name,age传给父类的构造函数,让父类构造函数的逻辑在子类里面跑一下,然后使用call改变this的指向为子类,相当于给子类添加了两个属性name,age,实现继承。

2.为了继承方法,使用Son.prototype=new Father()

存在的问题

父类构造函数调用了两次,一次是call,一次是new。

3.寄生组合式继承

需要改变子类prototype的constructor指向

        function Person(name, age) {
            this.name = name;
            this.age = age;
          }
          Person.prototype.sing = function () {
            console.log("singing");
          };
          Person.prototype.run = function () {
            console.log("running");
          };
          function Student(name, age, score, course) {
            Person.call(this, name, age);
            this.score = score;
            this.course = course;
          }
          inherit(Student, Person); //使用继承函数继承,改变construcotor 使用Object.create
          let student = new Student("zs", 18, 98, ["Math", "English"]);
          console.log(student.constructor); //这里的constructor指向person,需要修改

          function inherit(subType, superType) {
            subType.prototype = Object.create(superType.prototype);
            Object.defineProperty(subType.prototype, "constructor", {
              enumerable: false,
              writable: true,
              configurable: true,
              value: subType,
            });
          }

4.ES6的继承

              class Father {
                    constructor(age, name) {
                      this.age = age;
                      this.name = name;
                    }
                    singing() {
                      console.log("sing");
                    }
                    dance() {
                      console.log("dance");
                    }
                    method() {
                      console.log(1);
                      console.log(2);
                      console.log(3);
                    }
                  }
                  class Son extends Father {
                    constructor(age, name, score) {
                      super(age, name);
                      this.score = 100;
                    }
                    study() {
                      console.log("study");
                    }
                    method() {
                      super.method();
                      console.log(4);
                      console.log(5);
                      console.log(6);
                    }
                  }
                  let son = new Son(18, "zs,");
                  console.log(son.method());
              
              

1.使用extend+super关键字实现继承,可以继承父类的属性和方法。

super的两种用法 1.直接在constructor里面调用传入子类的参数,借用父类的逻辑实现继承。 2.在子类的某个方法里调用父类中方法的一些逻辑,实现逻辑的复用和方法的重写。

总结

继承的方法有:原型链继承,借用构造函数继承,寄生组合式继承,ES6继承(extends+super)