【深入JavaScript日记十二】剖析继承的多种方式

news/2024/7/19 13:17:07 标签: js, javascript, 前端

目录

    • 前言
    • 正文内容
      • 原型链继承
      • 构造函数继承
      • 组合继承[常用]
      • 原型式继承
      • 寄生式继承
      • 寄生组合式继承[最高效]
    • 总结

前言

在之前的学习中总能看到继承这个词,今天就拿出来单独学习一下

正文内容

原型链继承

javascript">    //父类
    function Parent() {
        this.name = '小A同学';
        this.age = 18;
    }

    //子类
    function Child() {

    }

    //原型链继承
    Child.prototype = new Parent();

    var child1 = new Child();

    console.log(child1.name)		//小A同学
    console.log(child1.age)			//18

这种方式比较好理解,但是有一个弊端,对于创建每一个 child 实例时都不能传参,如果想要传参就必须每一次重新在原型链继承里修改(Child.prototype = new Parent(这里添加参数)),所以需要频繁传参时效率很低。
而且这样继承会让引用类型的属性被所有实例共享

javascript">    //父类
    function Parent() {
        this.name = ['小A同学','小B同学'];	//将姓名改为数组,方便增加
    }

    //子类
    function Child() {

    }

    //原型链继承
    Child.prototype = new Parent();

    var child1 = new Child();
    child1.name.push('新转来的小C同学')		//改变了 child1 实例的属性 
    console.log(child1.name)		

    var child2 = new Child();
    console.log(child2.name)	

结果
emm可以看到我们仅仅在代码里改了 child1 的姓名,然后 child2 也变了…(形象化理解就是你爹生了五个孩子,你穿了一件衣服,其他兄弟姐妹身上也莫名其妙多出来一件衣服)所以除了某些特定场景下比较好用,一般的操作并不好用。

至于具体原因,我们通过简单的实验来看看

javascript">    //父类
    function Parent() {
        this.name = ['小A同学','小B同学'];	
    }

    //子类
    function Child() {
        console.log(this)		//打印每一次创建实例的时候的东西
    }

    //原型链继承
    Child.prototype = new Parent();

    var child1 = new Child();
    child1.name.push('新转来的小C同学')		
    console.log("-----分割线-----")
    var child2 = new Child();

结果
显然没有东西,所以在进行console.log(child1.name)console.log(child2.name)时在 Child 里找不到属性 name,于是顺着原型链往上找,所以两个打印出来都是一样的。
在这里插入图片描述


构造函数继承

javascript">    //父类
    function Parent() {
        this.name = ['小A同学', '小B同学'];
    }

    //子类
    function Child() {
        Parent.call(this);      //将 Parent 的指针指向 Child
    }

    var child1 = new Child();
    child1.name.push('新转来的小C同学');   //新增一个
    console.log(child1.name);

    var child2 = new Child();
    console.log(child2.name);

结果
首先这个方式避免了所有的属性共享(夸!!)
这里也详细展开看看为什么可以避免属性共享

javascript">    //父类
    function Parent() {
        this.name = ['小A同学', '小B同学'];
    }

    //子类
    function Child() {
        Parent.call(this);      //将 Parent 的指针指向 Child
        console.log(this)
    }

    var child1 = new Child();
    child1.name.push('新转来的小C同学');   //新增一个


    console.log("------分割线------")
    var child2 = new Child();

结果
我们可以看到,在使用Parent.call(this)改变了指针指向之后,每一次的Child实例都有自己的 name属性。
【----恍然大悟----】

我们继续往下看,这种继承方法除了不共享外,而且传参起来更加快捷,就像这样。

javascript">    //父类
    function Parent(name) {
        this.name = name;
    }

    //子类
    function Child(name) {
        Parent.call(this,name);      //将 Parent 的指针指向 Child
    }

    var child1 = new Child("传入小A同学");
    console.log(child1.name);

    var child2 = new Child("传入小B同学");
    console.log(child2.name);

结果
但是这样的写法有一个弊端,就是如果构造函数稍微复杂一点,里面包含了一些方法,那么在创建实例时不管用没用到这些方法,JavaScript都会创建这些方法,会造成内存空间浪费,比如这个例子

javascript">    //父类
    function Parent(name) {
        this.name = name;
		
		//这里有一个方法 fun
        this.fun = function () {

        }
    }

    //子类
    function Child(name) {
        Parent.call(this, name);      
        console.log(this)           //打印一下当前this包含的内容

    }

    var child1 = new Child("小A同学");
    console.log(child1.name);

    var child2 = new Child("小B同学");
    console.log(child2.name);

结果
我们可以很清楚的看到,我们创建的实例并没有使用 fun 方法,但是 this 指针里已经包含了。


组合继承[常用]

这种继承方式就是为了解决以上两个方法的缺点而产生,结合了原型链继承和构造函数继承。
设计思路猜测(使用面向对象思想):一个类分为属性和方法,因为属性需要更高的自由度(例如传参以及每一个实例不同的值),所以不能采用原型链继承,而类里的方法属于每一个实例,所以可以直接绑定到原型链最顶端,不用的时候不由去创建,用的时候再顺着原型链去找。

javascript">    //父类
    function Parent (name) {
        this.name = name;
        this.colors = ['red', 'blue', 'green'];
    }

    Parent.prototype.getName = function () {
        console.log(this.name)
    }

    //子类
    function Child (name) {
        Parent.call(this, name);    //构造继承
    }
    
    //原型链继承
    Child.prototype = new Parent();

    
    var child1 = new Child('小A同学');

    child1.colors.push('black');    

    console.log(child1.name);
    console.log(child1.colors);

    var child2 = new Child('小B同学');

    console.log(child2.name);
    console.log(child2.colors);

结果
通过结果,我们看到,首先支持传参,其次不会出现数据共享
我们再来看看是否会有类方法的赘余

javascript">    //父类
    function Parent (name) {
        this.name = name;
        this.colors = ['red', 'blue', 'green'];
    }

    Parent.prototype.getName = function () {
        console.log(this.name)
    }

    //子类
    function Child (name) {
        Parent.call(this, name);    //构造继承
        console.log(this)
    }

    //原型链继承
    Child.prototype = new Parent();


    var child1 = new Child('小A同学', '18');

    console.log("-------分割线--------")
    var child2 = new Child('小B同学', '20');

结果
我们看到在创建实例时并没有我们在代码里写的getName方法,那他究竟跑哪去了呢,我们展开原型链看看。
结果
在通过 child1.getName()调用时,会先在 Child里找这个方法,但是没有,那就去父类里找(图中红框),还是找不到那就只能再去父类的父类里找(蓝框里),最后终于找到了!比起之前每次创建实例都要创建一个方法,这样的效率显然更高!!!是 JavaScript 中最常用的继承模式。

但还是有一个缺点,就是无论如何都会出现重复调用的情况
结果


原型式继承

javascript">    function objcet(obj) {
        function F() {
        }
        F.prototype = obj;
        return new F();
    }

首先区别一下,这不是原型链继承!!!!
这个东西实质上就是 Object.create()方法,但是奈何 ES6 之前没有这种用法,所以就有了上面那段代码。
先来看看Object.create()方法

javascript">    const cat = {
        colors: ['white', 'black']
    }

    var cat1 = Object.create(cat)
    var cat2 = Object.create(cat)
    
    cat1.colors.push('blue')

    console.log(cat1)
    console.log(cat2)

    console.log(cat1.colors)
    console.log(cat2.colors)

结果
通过Object.create(cat)创建了一个原型为 cat 的空对象,并且属性会共享
然后我们看看ES6 之前的写法,也就是刚开始那段
先回忆一下Object.create()得流程

  • 接受一个对象A
  • 返回一个新对象B
  • 把 A 设置为 B 的原型

然后我们试验一下

javascript">    const cat = {
        colors: ['white', 'black']
    }
    
    function objcet(obj) {
        function F() {
        }
        F.prototype = obj;
        return new F();
    }

    var cat1 = objcet(cat)
    var cat2 = objcet(cat)

    cat1.colors.push('blue')

    console.log(cat1)
    console.log(cat2)

    console.log(cat1.colors)
    console.log(cat2.colors)

结果
和前面的结果一模一样

寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。

javascript">    function createObj(o) {
        var clone = Object.create(o);		//通过调用函数创建一个新对象
        clone.sayName = function () {		//增加这个对象的方法函数
            console.log('hi');
        }
        return clone;						//返回这个对象
    }

    var person = {
        name: "小A同学",
        color: ["red", "blue", "green"]
    };
    var anotherPerson = createObj(person);

    anotherPerson.sayName();
    console.log(anotherPerson.name)
    console.log(anotherPerson.color)

缺点也很清楚,每一次创建实例都会重复创建方法。

javascript">    function createObj(o) {
        var clone = Object.create(o);
        clone.sayName = function () {
            console.log('hi');
        }

        console.log(clone)      //打印一下看看

        return clone;
    }

    var person = {
        name: "小A同学",
        color: ["red", "blue", "green"]
    };
    var Person1 = createObj(person);
    console.log("----------分割线----------")
    var Person2 = createObj(person);

结果


寄生组合式继承[最高效]

寄生组合式继承顾名思义,就是寄生继承+组合继承的结合体,前面也说了,组合继承会有重复调用的问题,寄生继承会重复创建方法,所以我们要攻克这两个问题。
现在就要组合继承中的Parent.call(this, name);Child.prototype = new Parent();哪一句可以找到别的方法替换掉,从而避免二次调用。
Parent.call(this, name);是去不掉的,如果去掉了实例的属性就共享了
那就只能想办法替换 Child.prototype = new Parent();,既然直接调用不行,我们找点间接访问的步骤

javascript">//父类
 	function Parent (name) {
        this.name = name;
        this.colors = ['red', 'blue', 'green'];
    }

    Parent.prototype.getName = function () {
        console.log(this.name)
    }
//子类
    function Child (name, age) {
        Parent.call(this, name);
        this.age = age;
    }

    // 关键的三步
    var F = function () {};
    F.prototype = Parent.prototype;
    Child.prototype = new F();


    var child1 = new Child('小A同学', '18');
    console.log(child1);

这里的思路很清晰了,就是通过复制父类原型的办法避免直接调用父类构造函数,减少一次调用。那我们把这个思路结合到寄生式继承里面看看,寄生式继承的核心在于用来封装的那个函数,所以我们要在那里面动动手脚。

于是看看寄生组合式继承的思路:通过借用构造函数来继承属性(call),通过原型链的混成形式来继承方法(Object.create)
这里直接把《JavaScript高级程序设计》中的代码改一改拿来解释,因为学到这里还是不会写呜呜

javascript">// 实现继承的核心函数
    function inheritPrototype(Child,Parent) {
        function F() {}

        //F()的原型指向的是Parent
        F.prototype = Parent.prototype;

        //Child的原型指向的是F()
        Child.prototype = new F();

        // 重新将构造函数指向自己,修正构造函数
        Child.prototype.constructor = Child;
    }

    // 设置父类
    function Parent(name) {
        this.name = name;
        this.colors = ["red", "blue", "green"];
        Parent.prototype.sayName = function () {
            console.log(this.name)
        }
    }

    // 设置子类
    function Child(name, age) {
        //构造函数式继承--子类构造函数中执行父类构造函数
        Parent.call(this, name);
        this.age = age;
    }

    // 核心:因为是对父类原型的复制,所以不包含父类的构造函数,也就不会调用两次父类的构造函数造成浪费
    inheritPrototype(Child, Parent)
    // 添加子类私有方法
    Child.prototype.sayAge = function () {
        console.log(this.age);
    }

    var child1= new Child("小A同学",18)

结果
可以看到,该有的属性都有,而且比起组合继承只调用一次构造函数更加高效。

梳理一下流程
在这里插入图片描述

总结

到这里终于学习完了,通过深入学习这些,得到的知识更多的是一种编程的思想和规范。

这一阶段的学习到这里也就告一段落,这十二篇看起来不是很多,但是真正自己钻研起来还是有很多内容。JavaScript中东西有很多,也许之后会更一个新的系列来继续学习,这个系列先到这里啦。


http://www.niftyadmin.cn/n/644316.html

相关文章

ca31a_demo_31_C++_C风格字符串c_strcpy_strcmp_strcat

/* ca31a_demo_31_C_C_txwtech风格字符串c_strcpy_strcmp_strcat C风格字符串的使用 const char *cp"some value"; 字符常量数组 C风格字符串的标准函数 永远不要忘记字符串结束符null--\0 使用strn函数处理C风格字符串 尽可能使用标准库类型string cout << &…

【深入JavaScript_笔记一_防抖与节流】

目录前言正文内容防抖非立即执行方式立即执行方式节流时间戳节流定时器节流防抖和节流的区别总结前言 在前段日子&#xff0c;在内心的逼迫下跟着众多大佬的脚步&#xff0c;磨磨蹭蹭慢慢悠悠的学完了一些原生JavaScript底层的知识。产生了很多感触&#xff0c;编程技术太多了…

最近的学习---一句话收获(备查用)(2)

1.在内核中分配大内存问题。 首先要明白内核没有义务为你分配连续的物理内存&#xff0c;因此你需要的内核中连续的物理内存就是苛刻的要求&#xff0c;既然在计算机系统抽象给用户一个连续的美丽的一维的虚拟内存&#xff0c;那么任何时候计算机都没有义务让你看到物理内存&am…

【懒狗福音】平板 to 扩展屏 有效提高生产力

目录前言正文下载软件运行软件实际操作总结前言 为了下学期自己可以提高一些效率&#xff0c;最进入了个华为的平板&#xff0c;matepad10.8&#xff0c;打算进入一波无纸化学习。&#xff08;主要是懒&#xff0c;不想整天背着专业书和电脑跑这跑那&#xff09;&#xff0c;东…

C++动态数组的作用

假如要创建一个数组&#xff0c;数组长度由用户输入 那就可以这样写&#xff1a; int bufferSize; cin>>bufferSize; int *pnew int[bufferSize]; 但是&#xff0c;如果用静态数组实现这个功能请问怎么实现&#xff1f; int bufferSize; cin>>bufferSize; int arr[…

【闲暇时间整活】给 Windows 命令行窗口 “改头换面”

目录前言正文内容安装软件自定义样式自定义设置主题设置背景图片总结前言 也快要到开学的日子了&#xff0c;是时候整理一下东西准备迎接新学期了&#xff0c;整理归类进行时。 看看桌面&#xff0c;嗯不错&#xff0c;简单干净&#xff08;利用双击隐藏程序&#xff0c;中间狗…

ca32a_demo_c++创建动态数组

/* ca32a_demo_c创建动态数组20200127 静态是在堆栈上创建的&#xff0c;动态实在堆&#xff08;heap&#xff09;上创建的 堆&#xff08;heap&#xff09;-自由存储区&#xff08;free store&#xff09;-内存的一个区域 c语言&#xff1a;malloc创建和free释放 c语言&#x…

Nginx配置实战

一、基于域名虚拟主机配置 1、修改nginx.conf配置文件 [rootlinux-node2 ~]# cat /etc/nginx/nginx.conf worker_processes auto; error_log /var/log/nginx/error.log; events {worker_connections 1024; } http {include mime.types;default_type application/octet…