你真的懂JS原型链吗?

3,173 阅读8分钟

学习目标

  • 原型
  • 原型链
  • 原型指向改变后是如何添加方法和属性
  • 原型指向改变后的原型链
  • 实例对象的属性和原型对象的属性重名
  • 通过原型继承
  • 组合继承
  • 拷贝继承

一 原型

问题: 请看以下代码,如果我们要创建100个对象,应该怎么创建?

function Person(name, sex) {
    this.name = name;
    this.sex = sex;
    this.drink () {
        console.log('我想喝手磨咖啡!!')
    }
}
for (let i = 0; i < 100; i++) {
    var per = new Person('苏大强', '男');
    per.drink();
}

从上面的代码可以看出,如果我们要创建100个Person对象,这样要开一百个内存空间,每次都要调用drink()函数,由于drink()函数都是一样的,每个内存空间里都有它太过于浪费空间,那我们怎样才能避免这种情况,减少内存呢?我们接下来引入原型prototype

function Person(name, sex) {
    this.name = name;
    this.sex = sex;
}
//为原型添加方法
Person.prototype.drink = function () {
        console.log('我想喝手磨咖啡!!')
    }
    //实例化对象
let per = new Person('苏大强', '男');
per.drink();

我们运用了原型prototype,可以共享数据,减少内存空间。

二 原型链

我们既然清楚了原型,那我们再来看看原型链。首先我们打印一下构造函数Person和实例对象per。

console.dir(Person);//构造函数
console.dir(per);//实例对象

从图上看,构造函数中的prototype中的属行和实例对象per中的__proto__中的属性一模一样,那我们想想它们相等吗?我们可以验证一下。

console.log(per.__proto__ === Person.prototype);

由此我们可以判断出,构造函数Person中的prototype原型和实例对象per中的__proto__原型指向是相同的,我们一般是先有构造函数再有实例对象,实例对象由构造函数创建,所以说实例对象中的__proto__原型指向的是构造函数中的原型prototype

实例对象中proto是原型,浏览器使用的。构造函数中的 prototype 是原型,程序员使用的

那接下来我们看一幅图来看看原型链到底是什么?

我们来分析分析整张图

  • 首先,构造函数中的 prototype 属性指向自己的原型对象
  • 然后,原型对象中的构造器指向的是,原型对象所在的构造函数
  • 再然后,实例对象中的proto指向的是,它所在构造函数中 prototype 属性所指向的原型对象

所以从上图我们可以得到以下几点:

  • 实例对象的原型指向了构造函数中 prototype 属性所指向的原型对象,所以实例对象和原型对象之间有关系,它和构造函数是一个间接的关系。
  • 我们从代码中也可以得出,实例对象可以直接访问原型对象中的属性或方法。
  • 实例对象和原型对象之间有关系,它们的关系是通过原型proto来连接的。

最终我们可以得出,原型链:它是一种关系,实例对象和原型对象之间的关系,关系是通过原型proto来联系的

三 原型指向改变后是如何添加方法和属性

原型改变添加方法也无非就是两种:1.在原型改变前添加加方法。2.在原型改变以后添加方法。

首先,我们来看第一种:

function Person(name, sex) {
    this.name = name;
    this.sex = sex;
}
Person.prototype.drink = function () {
        console.log('我想喝水!!')
    }
function Student(name, sex) {
    this.name = name;
    this.sex = sex;
}
Student.prototype.eat = function () {
    console.log('我想吃东西!!')
}

//改变原型指向
Student.prototype = new Person('人', '男');
let stu = new Student('学生', '女');
stu.drink();
stu.eat();

我们来运行以下:

我们可以看到图中的信息,stu.eat()不是一个函数,刚才我们明明将eat()添加到了Student的原型上,怎么现在报错了? 原因是:由于 Student 的原型指向改变了,它指向了 new Person('人', '男'),并且 Person 的原型上并没有 eat(),所以报错,那么第一种情况在原型改变之前添加是错误的!

我们再来看第二种情况:在原型改变之后添加方法。

function Person(name, sex) {
    this.name = name;
    this.sex = sex;
}
Person.prototype.drink = function () {
        console.log('我想喝水!!')
    }
function Student(name, sex) {
    this.name = name;
    this.sex = sex;
}

//改变原型指向
Student.prototype = new Person('人', '男');
//为原型添加方法
Student.prototype.eat = function () {
    console.log('我想吃东西!!')
}
let stu = new Student('学生', '女');
stu.drink();
stu.eat();

我们来运行以下:

可以看出来,两个方法都被成功的调运了,所以说:如果原型指向改变了,那么就应该在原型改变指向之后添加原型方法。

四 原型指向改变后的原型链

那么,当原型指向改变之后,原型链会发生怎样的改变呢? 那我们来们分析以下:

  • 原型指向改变之前
  • 原型指向改变之后

我们先来分析原型指向改变之前:

//人的构造函数
function Person(name) {
    this.name = name;
}
//为原型添加方法
Person.prototype.drink = function () {
        console.log('我想喝水!!')
    }
//学生的构造函数
function Student(name) {
    this.name = name;
}
//为原型添加方法
Student.prototype.eat = function () {
    console.log('我想吃东西!!')
}
//实例对象
let per = new Person('老师');
let stu = new Student('学生');

console.dir(Person);//构造函数
console.dir(per);//实例对象
console.dir(Student);//构造函数
console.dir(stu);//实例对象

我们运行以下这段代码:

请看每个prototype和__proto__,我们可以得到它们的原型链图:

由此图,我们可以看出,原型没有改变之前,实例对象的proto都指向自己构造函数中 prototype 属性所指向的原型对象。

我们再来看看原型指向改变之后:

//人的构造函数
function Person(name) {
    this.name = name;
}
//为原型添加方法
Person.prototype.drink = function () {
        console.log('我想喝水!!')
    }
//学生的构造函数
function Student(name) {
    this.name = name;
}
//为原型添加方法
Student.prototype.eat = function () {
    console.log('我想吃东西!!')
}
//改变学生的原型指向
Student.prototype = new Person('老师');
//实例对象
let stu = new Student('学生');

console.dir(Person);//构造函数
console.dir(new Person('老师'))//实例对象
console.dir(Student.prototype)//Student的原型对象
console.dir(Student);//构造函数
console.dir(stu);//实例对象

我们来看看运行结果:

我们来分析分析:

我们代码和图结合来看,当 Student.prototype = new Person('老师');之后,① 学生构造函数的 prototype 属性会断开指向原型对象,② 原型对象中的构造器也会断开指向构造函数,③ 实例对象的proto会断开指向原型对象 这里的序号没有任何意义,相当于起的名字!!! 还没有完,我们再来看图:

当原型指向改变之后,学生的构造函数中的 prototype 属性指向了 new Person('老师');,随后,学生的实例化对象中的proto属性指向了学生构造函数中 prototype 属性所指向的 new Person('老师');

原型链改变完毕!

五 实例对象的属性和原型对象的属性重名

当实例对象中的属性和原型对象中的属性重名时应该先访问那个? 我们来看一看代码:

//人的构造函数
function Person(age, sex) {
      this.age = age;
      this.sex = sex;
    }
//为原型添加属性
Person.prototype.sex = "女";
//实例化对象
var per = new Person(10,"男");

console.log(per.sex);

看图:

从代码的运行结果来看,当实例对象中的属性和原型中的属性重名时,它会先访问实例对象中的属性。

如果在实例对象中找不到呢?我们来看代码:

function Person(age) {
      this.age = age;
    }
//为原型添加属性
Person.prototype.sex = "女";
//实例化对象
var per = new Person(10);

console.log(per.sex);

我们来看运行结果:

从这段代码,我们可以看出,当实例对象中访问不到属性时,它会向上查找原型对象上的属性

六 通过原型继承

    //js中通过原型来实现继承

    //人的构造函数
    function Person(name, age, sex) {
      this.name = name;
      this.sex = sex;
      this.age = age;
    }
    //为原型添加方法
    Person.prototype.eat = function () {
      console.log("人吃东西");
    };
    Person.prototype.sleep = function () {
      console.log("人在睡觉");
    };
    Person.prototype.play = function () {
      console.log("生活就是编代码!");
    };


    //学生的构造函数
    function Student(score) {
      this.score = score;
    }
    //改变学生的原型的指向即可==========>学生和人已经发生关系
    Student.prototype = new Person("小明", 10, "男");
    //为原型添加方法
    Student.prototype.study = function () {
      console.log("学习很累很累的哦.");
    };


    var stu = new Student(100);
    console.log(stu.name);
    console.log(stu.age);
    console.log(stu.sex);
    stu.eat();
    stu.play();
    stu.sleep();
    console.log("下面的是学生对象中自己有的");
    console.log(stu.score);
    stu.study();

看运行结果:

原型继承的精髓就是:使用原型,改变原型的指向进行继承。

七 组合继承


    //组合继承:原型继承+借用构造函数继承

    //人的构造函数
    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("金仔", 20, "男", "100分");
    console.log(stu.name, stu.age, stu.sex, stu.score);
    stu.sayHi();
    stu.eat();

    var stu2=new Student("含仔", 20, "女", "100分");
    console.log(stu2.name, stu2.age, stu2.sex, stu2.score);
    stu2.sayHi();
    stu2.eat();

看运行结果:

组合继承的精髓:首先在 Student 构造函数中使用 call()函数将属性传入 Person 构造函数中,并改变 this,然后改变 Student 的原型指向

八 拷贝继承

    function Person() {};
    Person.prototype.age = 10;
    Person.prototype.sex = "男";
    Person.prototype.height = 100;
    Person.prototype.play = function () {
      console.log("玩耍!");
    };
    var Student = {};
    //Person的构造中有原型prototype,prototype就是一个对象,那么里面,age,sex,height,play都是该对象中的属性或者方法

    for (let key in Person.prototype) {
      Student[key] = Person.prototype[key];
    }
    console.dir(Student);
    Student.play();

请看运行结果: