JavaScript 进阶教程(3)---让你彻底搞懂原型链和继承

目录

1 原型链

1.1 原型

1.1.1 什么是原型?

1.1.2 原型的作用:数据共享,节省内存空间

1.1.3 原型的写法:

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

1.2 原型链

1.3 原型的指向

1.3.1 原型的指向是可以改变的

1.3.2 原型的最终指向

1.3.3 在原型改变指向之后添加原型方法

1.4 实例对象属性和原型对象属性重名

1.5 一条神奇的原型链

2 继承

2.1 什么是继承

2.2 通过原型实现继承

2.3 继承实例

2.4 借用构造函数实现继承

2.5 组合继承

2.6 另一种继承方式:拷贝继承(for-in)

3 总结


1 原型链

1.1 原型

关于原型在JS面向对象编程这篇文章已经讲过了,今天简单来复习一下。

1.1.1 什么是原型?

在JS构造函数中有一个属性prototype,叫做原型,这是给程序员使用的。在JS实例对象中有一个属性__proto__,它也是原型,这是供浏览器使用的,它不是标准的属性。 实例对象中的__proto__指向的是该实例对象中的构造函数中的prototype,构造函数中的prototype里面的属性或者方法,可以直接通过实例对象调用。

一般情况下,实例对象.__proto__才能访问到构造函数中的prototype的属性或者方法,即 per._proto_.eat()。 因为__proto__不是标准的属性,所以直接写成 per.eat()即可,原型是一个属性,而这个属性也是一个对象。

1.1.2 原型的作用:数据共享,节省内存空间

在构造函数中定义的属性和方法,当实例化对象的时候,实例对象中的属性和方法都是在自己的空间中存在的,如果是多个对象。这些属性和方法都会在单独的空间中存在,浪费内存空间,所以,为了数据共享,把想要节省空间的属性或者方法写在原型对象中,达到了数据共享,节省了内存空间。

1.1.3 原型的写法:

构造函数.prototype.属性=值
构造函数.prototype.方法=值---->函数.prototype,函数也是对象,所以,里面也有__proto__
实例对象.prototype-------->实例对象中没有这个属性,只有__proto__
原型的简单写法
缺陷:--->原型直接指向{}---->就是一个对象,没有构造器

  1.  
    构造函数.prototype={
  2.  
    切记:如果这这种写法,要把构造器加上
  3.  
     
  4.  
    };
  5.  
     

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

系统的内置对象的属性和方法可能不满足现在需求,所以可以通过原型的方式加入属性或者方法。为内置对象的原型添加属性和方法,这个内置对象的实例对象可以直接使用添加的属性或方法。

  1.  
    //为内置对象添加原型方法
  2.  
    //我们在系统的对象的原型中添加方法,相当于在改变源码
  3.  
    //我希望字符串中有一个倒序字符串的方法
  4.  
    String.prototype.myReverse = function() {
  5.  
    for (var i = this.length - 1; i >= 0; i--) {
  6.  
    console.log(this[i]);
  7.  
    }
  8.  
    };
  9.  
    var str = "abcdefg";
  10.  
    str.myReverse();
  11.  
     
  12.  
     
  13.  
    //为Array内置对象的原型对象中添加方法
  14.  
    Array.prototype.mySort = function() {
  15.  
    for (var i = 0; i < this.length - 1; i++) {
  16.  
    for (var j = 0; j < this.length - 1 - i; j++) {
  17.  
    if (this[j] < this[j + 1]) {
  18.  
    var temp = this[j];
  19.  
    this[j] = this[j + 1];
  20.  
    this[j + 1] = temp;
  21.  
    } //end if
  22.  
    } // end for
  23.  
    } //end for
  24.  
    };
  25.  
     
  26.  
    var arr = [100, 3, 56, 78, 23, 10];
  27.  
    arr.mySort();
  28.  
    console.log(arr);
  29.  
     
  30.  
     
  31.  
    String.prototype.sayHi = function() {
  32.  
    console.log(this + "哈哈,我又变帅了");
  33.  
    };
  34.  
     
  35.  
    //字符串就有了打招呼的方法
  36.  
    var str2 = "小杨";
  37.  
    str2.sayHi();

1.2 原型链

原型链是一种关系,是实例对象和原型对象之间的关系,这种关系是通过原型(proto)来联系的。JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
请看下边的代码:

  1.  
    // 人的构造函数
  2.  
    function Person(name,age) {
  3.  
    // 属性
  4.  
    this.name=name;
  5.  
    this.age=age;
  6.  
    // 在构造函数中的方法
  7.  
    this.eat=function () {
  8.  
    console.log("吃吃吃");
  9.  
    };
  10.  
    }
  11.  
    // 添加共享的属性
  12.  
    Person.prototype.sex="男";
  13.  
    // 添加共享的方法
  14.  
    Person.prototype.sayHi=function () {
  15.  
    console.log("哈哈哈");
  16.  
    };
  17.  
    // 实例化对象,并初始化
  18.  
    var per=new Person("张三",18);
  19.  
    per.sayHi();

上边的代码中实例对象的原型__proto__和构造函数的原型prototype指向是相同的,实例对象中的__proto__原型指向的是构造函数中的原型prototype。

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

它们的关系如下:

上图中红线链接部分即Person实例对象和Person原型对象之间的关系,可以看成一个原型链(不完整,目前可以先这样理解)。

1.3 原型的指向

1.3.1 原型的指向是可以改变的

请看下面的代码:

  1.  
    //人的构造函数
  2.  
    function Person(age) {
  3.  
    this.age=10;
  4.  
    }
  5.  
    //人的原型对象方法
  6.  
    Person.prototype.eat=function () {
  7.  
    console.log("吃吃吃!!!");
  8.  
    };
  9.  
    //学生的构造函数
  10.  
    function Student() {
  11.  
     
  12.  
    }
  13.  
    Student.prototype.sayHi=function () {
  14.  
    console.log("学学学,为中华崛起而读书!!!");
  15.  
    };
  16.  
    //学生的原型,指向了一个人的实例对象
  17.  
    Student.prototype=new Person(18);
  18.  
    var stu=new Student();
  19.  
    console.dir(stu);
  20.  
    stu.eat(); // 吃吃吃!!!
  21.  
    stu.sayHi();//错误stu.sayHi is not a function,原型指向发生改变,找不到sayHi()方法

输出结果:

上边的代码中,实例对象stu调用原型方法sayHi()的时候发生错误,这是因为Student.prototype=new Person(18);使原型指向发生了改变,stu对象中已经找不到sayHi()方法。原型指向改变分析: 图中的红线构成原型链。

1.3.2 原型的最终指向

通过上边的学习我们可以知道,实例对象中有__proto__原型,构造函数中有prototype原型,prototype也是是一个对象,那么,prototype这个对象中应该也有__proto__,那么它又指向了哪里呢?
即然实例对象中的__proto__指向的是构造函数的prototype,所以prototype这个对象中__proto__指向的应该是某个构造函数的原型prototype。

  1.  
    function Person() {
  2.  
     
  3.  
    }
  4.  
    Person.prototype.eat=function () {
  5.  
    console.log("吃吃吃");
  6.  
    };
  7.  
     
  8.  
    var per=new Person();
  9.  
    console.dir(per);
  10.  
    console.dir(Person);


通过分析我们发现Person的prototype中的__proto__的指向为Object;Object.prototype的__proto__是null。

console.log(Object.prototype.__proto__);// null

所以per实例对象的原型的指向为:

per实例对象的__proto__------->Person.prototype,Person.prototype的__proto__---->Object.prototype,Object.prototype的__proto__是null。这构成了一个完整的原型链。

上图中的红线链接构成了一个完整的原型链。

1.3.3 在原型改变指向之后添加原型方法

在1.3.1的代码中,因为原型指向发生改变,找不到sayHi()方法而发生错误,我们可以在原型改变指向之后添加原型方法,改变代码如下:

  1.  
    //人的构造函数
  2.  
    function Person(age) {
  3.  
    this.age=10;
  4.  
    }
  5.  
    //人的原型对象方法
  6.  
    Person.prototype.eat=function () {
  7.  
    console.log("吃吃吃!!!");
  8.  
    };
  9.  
    //学生的构造函数
  10.  
    function Student() {
  11.  
     
  12.  
    }
  13.  
    //学生的原型,指向了一个人的实例对象
  14.  
    Student.prototype=new Person(18);
  15.  
    Student.prototype.sayHi=function () {
  16.  
    console.log("学学学,为中华崛起而读书!!!");
  17.  
    };
  18.  
    var stu=new Student();
  19.  
    console.dir(stu);
  20.  
    stu.eat(); // 吃吃吃!!!
  21.  
    stu.sayHi();// 学学学,为中华崛起而读书!!!


注意:简单的原型写法,会将 Person.prototype 重置到了一个新的对象,即改变原型的指向。我们也应该在原型改变指向之后添加原型方法。

  1.  
    function Person(age) {
  2.  
    this.age = age;
  3.  
    }
  4.  
     
  5.  
    //指向改变了
  6.  
    Person.prototype = {
  7.  
    eat: function () {
  8.  
    console.log("吃");
  9.  
    }
  10.  
    };
  11.  
    //添加原型方法
  12.  
    Person.prototype.sayHi = function () {
  13.  
    console.log("你好帅呀!!!");
  14.  
    };
  15.  
    var per = new Person(10);
  16.  
    per.sayHi();// 你好帅呀!!!

1.4 实例对象属性和原型对象属性重名

实例对象访问某个属性,应该先从实例对象中找,找到了就直接用,找不到就去指向的原型对象中找,找到了就使用,找不到返回undefined。

  1.  
    function Person(age,sex) {
  2.  
    this.age=age;
  3.  
    this.sex=sex;
  4.  
    }
  5.  
    Person.prototype.sex="女";
  6.  
    var per=new Person(18,"男");
  7.  
    console.log(per.sex); // 男
  8.  
    // 因为JS是一门动态类型的语言,如果对象没有这个属性,只要对象.属性名字,对象就有了这个属性了
  9.  
    // 但是该属性没有赋值, 所以per.albert结果是:undefined
  10.  
    console.log(per.albert); // undefined
  11.  
    console.log(albert); // 报错

通过实例对象不能改变原型对象中的属性值,要想改变原型对象中的属性值,应该直接通过原型对象.属性=值;进行改变。

  1.  
    Person.prototype.sex="我的天呐!!!我为什么这么帅?";
  2.  
    per.sex="不知道";
  3.  
    console.log(per.sex);
  4.  
     
  5.  
    console.dir(per);

1.5 一条神奇的原型链

  1.  
    <!DOCTYPE html>
  2.  
    <html lang="en">
  3.  
    <head>
  4.  
    <meta charset="UTF-8">
  5.  
    <title>微信公众号:AlbertYang</title>
  6.  
    </head>
  7.  
    <body>
  8.  
    <div id="dv"></div>
  9.  
    <script>
  10.  
    var divObj = document.getElementById("dv");
  11.  
    console.dir(divObj);
  12.  
    </script>
  13.  
    </body>
  14.  
    </html>
  15.  
     

通过在控制台分析divObj,得出divObj原型链指向为:

divObj.proto---->HTMLDivElement.prototype的__proto__--->HTMLElement.prototype的__proto__---->Element.prototype的__proto__---->Node.prototype的__proto__---->EventTarget.prototype的__proto__---->Object.prototype没有__proto__,所以,Object.prototype中的__proto__是null

2 继承

面向对象编程思想是根据需求分析对象,找到对象有什么特征和行为,然后通过代码的方式来实现需求。要想实现这个需求,就要创建对象,要想创建对象,就应该有构造函数,然后通过构造函数来创建对象,通过对象调用属性和方法来实现相应的功能及需求。
因为面向对象的思想适合于人的想法,编程起来会更加的方便,后期维护的时候也要会更加容易,所以我们才要学习面向对象编程。但JS不是一门面向对象的语言,而是一门基于对象的语言。JS不像JAVA,C#等面向对象的编程语言中有类(class)的概念(也是一种特殊的数据类型),JS中没有类(class),但是JS可以模拟面向对象的思想编程,在JS可以通过构造函数来模拟类的概念(class)。在ES6中,class (类)作为对象的模板被引入,可以通过 class 关键字定义类,但是它的的本质还是 function。

2.1 什么是继承

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

继承是子类继承父类的特征和行为,使得子类对象(实例)具有父类的属性和方法,或子类从父类继承方法,使得子类具有父类相同的行为。继承可以使得子类具有父类的各种属性和方法,而不需要再次编写相同的代码。

例如:人有 姓名, 性别, 年龄 ,吃饭, 睡觉等属性和行为。
学生有: 姓名, 性别, 年龄 ,吃饭, 睡觉 学习等属性和行为。
老师有: 姓名, 性别, 年龄 ,吃饭, 睡觉 ,教学等属性和行为。
先定义一个人类,人有姓名, 性别, 年龄等属性,有,吃饭, 睡觉等行为。由人这个类派生出学生和老师两个类,为学生添加学习行为,为老师添加教学行为。

2.2 通过原型实现继承

  1.  
    // 人
  2.  
    function Person(name, age, sex) {
  3.  
    this.name = name;
  4.  
    this.sex = sex;
  5.  
    this.age = age;
  6.  
    }
  7.  
    Person.prototype.eat = function() {
  8.  
    console.log("吃吃吃!!!");
  9.  
    };
  10.  
    Person.prototype.sleep = function() {
  11.  
    console.log("睡睡睡!!!");
  12.  
    };
  13.  
     
  14.  
     
  15.  
    //学生
  16.  
    function Student(score) {
  17.  
    this.score = score;
  18.  
    }
  19.  
    //改变学生的原型的指向即可让学生继承人
  20.  
    Student.prototype = new Person("张三", 18, "男");
  21.  
    Student.prototype.study = function() {
  22.  
    console.log("学习真的太累了!!!");
  23.  
    };
  24.  
     
  25.  
    var stu = new Student(99);
  26.  
    console.log("学生从人中继承的属性和行为:"); // >学生从人中继承的属性和行为:
  27.  
    console.log(stu.name); // >张三
  28.  
    console.log(stu.age); // >18
  29.  
    console.log(stu.sex); // >男
  30.  
    stu.eat(); // >吃吃吃!!!
  31.  
    stu.sleep(); // >睡睡睡!!!
  32.  
    console.log("学生中自己有的属性和行为:"); // >学生中自己有的属性和行为:
  33.  
    console.log(stu.score); // >99
  34.  
    stu.study(); // >学习真的太累了!!!

2.3 继承实例

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

  1.  
    // 动物的构造函数
  2.  
    function Animal(name, weight) {
  3.  
    this.name = name;
  4.  
    this.weight = weight;
  5.  
    }
  6.  
    //动物的原型的方法
  7.  
    Animal.prototype.eat = function() {
  8.  
    console.log("弟兄们冲啊,赶快吃吃吃!!!");
  9.  
    };
  10.  
     
  11.  
    //狗的构造函数
  12.  
    function Dog(color) {
  13.  
    this.color = color;
  14.  
    }
  15.  
    Dog.prototype = new Animal("小三", "30kg");
  16.  
    Dog.prototype.bitePerson = function() {
  17.  
    console.log("~汪汪汪~,快让开,我要咬人了!!!");
  18.  
    };
  19.  
     
  20.  
    //哈士奇构造函数
  21.  
    function Husky(age) {
  22.  
    this.age = age;
  23.  
    }
  24.  
    Husky.prototype = new Dog("黑白色");
  25.  
    Husky.prototype.playYou = function() {
  26.  
    console.log("咬坏充电器,咬坏耳机,拆家...哈哈,好玩不!!!");
  27.  
    };
  28.  
    var husky = new Husky(3);
  29.  
    console.log(husky.name, husky.weight, husky.color);
  30.  
    husky.eat();
  31.  
    husky.bitePerson();
  32.  
    husky.playYou();

它们的原型链关系为:

2.4 借用构造函数实现继承

在上边的讲解中,我们为了数据共享,改变了原型指向,做到了继承,即通过改变原型指向实现了继承。这导致了一个问题,因为我们改变原型指向的同时,直接初始化了属性,这样继承过来的属性的值都是一样的了。这是个问题,如果我们想要改变继承过来的值,只能重新调用对象的属性进行重新赋值,这又导致我们上边的初始化失去了意义。

  1.  
    function Person(name, age, sex, weight) {
  2.  
    this.name = name;
  3.  
    this.age = age;
  4.  
    this.sex = sex;
  5.  
    this.weight = weight;
  6.  
    }
  7.  
    Person.prototype.sayHi = function() {
  8.  
    console.log("你好帅呀!!!");
  9.  
    };
  10.  
     
  11.  
    function Student(score) {
  12.  
    this.score = score;
  13.  
    }
  14.  
    //希望人的类别中的数据可以共享给学生---继承
  15.  
    Student.prototype = new Person("小三", 18, "男", "58kg");
  16.  
     
  17.  
    var stu1 = new Student("99");
  18.  
    console.log(stu1.name, stu1.age, stu1.sex, stu1.weight, stu1.score);
  19.  
    stu1.sayHi();
  20.  
     
  21.  
    var stu2 = new Student("89");
  22.  
    console.log(stu2.name, stu2.age, stu2.sex, stu2.weight, stu2.score);
  23.  
    stu2.sayHi();
  24.  
     
  25.  
    var stu3 = new Student("66");
  26.  
    console.log(stu3.name, stu3.age, stu3.sex, stu3.weight, stu3.score);
  27.  
    stu3.sayHi();

重新调用对象的属性进行重新赋值,非常的麻烦,而且使我们上边的new Person("小三", 18, "男", "58kg");初始化失去了意义。

  1.  
    var stu3 = new Student("66");
  2.  
    stu3.name = "小红";
  3.  
    stu3.age = 16;
  4.  
    stu3.sex = "女";
  5.  
    stu3.weight = "45kg";
  6.  
    console.log(stu3.name, stu3.age, stu3.sex, stu3.weight, stu3.score);
  7.  
    stu3.sayHi();

如何解决上边的问题呢?答案是借用构造函数实现继承。
继承的时候,不改变原型的指向,直接调用父级的构造函数来为属性赋值,即把要继承的父级的构造函数拿过来,借用一下为属性赋值,这叫做借用构造函数。借用构造函数需要使用call ()这个方法,我会在后边的文章中进行讲解,大家在这里先记住用法就行了。

  1.  
    function Person(name, age, sex, weight) {
  2.  
    this.name = name;
  3.  
    this.age = age;
  4.  
    this.sex = sex;
  5.  
    this.weight = weight;
  6.  
    }
  7.  
    Person.prototype.sayHi = function() {
  8.  
    console.log("你好帅呀!!!");
  9.  
    };
  10.  
     
  11.  
    function Student(name, age, sex, weight, score) {
  12.  
    //借用构造函数
  13.  
    Person.call(this, name, age, sex, weight);
  14.  
    this.score = score;
  15.  
    }
  16.  
    var stu1 = new Student("小三", 16, "男", "50kg", "110");
  17.  
    console.log(stu1.name, stu1.age, stu1.sex, stu1.weight, stu1.score);
  18.  
     
  19.  
    var stu2 = new Student("小红", 22, "女", "45kg", "88");
  20.  
    console.log(stu2.name, stu2.age, stu2.sex, stu2.weight, stu2.score);
  21.  
     
  22.  
    var stu3 = new Student("小舞", 16, "女", "40kg", "100");
  23.  
    console.log(stu3.name, stu3.age, stu3.sex, stu3.weight, stu3.score);


借用构造函数继承,解决了继承的时候属性重复的问题。但是这又导致一个问题即父类中的原型方法不能被继承。

  1.  
    function Person(name, age, sex, weight) {
  2.  
    this.name = name;
  3.  
    this.age = age;
  4.  
    this.sex = sex;
  5.  
    this.weight = weight;
  6.  
    this.eat = function() {
  7.  
    console.log('吃吃吃!!!');
  8.  
    }
  9.  
    }
  10.  
    Person.prototype.sayHi = function() {
  11.  
    console.log("你好帅呀!!!");
  12.  
    };
  13.  
     
  14.  
    function Student(name, age, sex, weight, score) {
  15.  
    //借用构造函数
  16.  
    Person.call(this, name, age, sex, weight);
  17.  
    this.score = score;
  18.  
    }
  19.  
    var stu = new Student("小舞", 16, "女", "40kg", "100");
  20.  
    console.log(stu.name, stu.age, stu.sex, stu.weight, stu.score);
  21.  
    stu.eat();
  22.  
    stu.sayHi(); // >报错 stu.sayHi is not a function

无论是单独使用原型链继承,还是单独使用借用构造函数继承,都有很大的缺点,最好的办法是,将两者结合一起使用,发挥各自的优势,这就是我们下面要讲的组合继承。

2.5 组合继承

原型继承和借用构造函数继承都存在各自的缺点,我们可以将这二者结合到一起,从而发挥二者之长。即在继承过程中,既可以保证每个实例都有它自己的属性,又能做到对一些属性和方法的复用。这时组合继承应运而生,组合继承=原型继承+借用构造函数继承。

  1.  
    function Person(name, age, sex) {
  2.  
    this.name = name;
  3.  
    this.age = age;
  4.  
    this.sex = sex;
  5.  
    }
  6.  
    Person.prototype.sayHi = function() {
  7.  
    console.log("你好帅呀!!!");
  8.  
    };
  9.  
     
  10.  
    function Student(name, age, sex, score) {
  11.  
    //借用构造函数:解决属性值重复的问题
  12.  
    Person.call(this, name, age, sex);
  13.  
    this.score = score;
  14.  
    }
  15.  
    //改变原型指向---原型继承解决原型方法不能被继承问题
  16.  
    Student.prototype = new Person(); //不传值
  17.  
    Student.prototype.eat = function() {
  18.  
    console.log("吃吃吃!!!");
  19.  
    };
  20.  
    var stu = new Student("小三", 16, "男", "111分");
  21.  
    console.log(stu.name, stu.age, stu.sex, stu.score);
  22.  
    stu.sayHi();
  23.  
    stu.eat();
  24.  
    var stu2 = new Student("小舞", 15, "女", "1111分");
  25.  
    console.log(stu2.name, stu2.age, stu2.sex, stu2.score);
  26.  
    stu2.sayHi();
  27.  
    stu2.eat();

2.6 另一种继承方式:拷贝继承(for-in)

拷贝继承:把一个对象中的属性或者方法直接复制到另一个对象中。

  1.  
    function Person() {}
  2.  
    Person.prototype.name = "小三";
  3.  
    Person.prototype.age = 18;
  4.  
    Person.prototype.sex = "男";
  5.  
    Person.prototype.height = 100;
  6.  
    Person.prototype.play = function() {
  7.  
    console.log("玩的好开心呀!!!😃");
  8.  
    };
  9.  
    var obj = {};
  10.  
    // Person中有原型prototype,prototype就是一个对象,那么里面,name,age,sex,height,play都是该对象中的属性或者方法
  11.  
    // 新对象obj通过拷贝Person中原型prototype对象中的属性和方法继承Person中原型prototype对象的属性和方法
  12.  
    for (var key in Person.prototype) {
  13.  
    obj[key] = Person.prototype[key];
  14.  
    }
  15.  
    console.dir(obj);
  16.  
    obj.play();

缩略图链接

posted @ 2020-09-10 01:09  zhang黑科技时间到  阅读(133)  评论(0编辑  收藏  举报