美文网首页
面向对象

面向对象

作者: 泡杯感冒灵 | 来源:发表于2020-07-04 22:06 被阅读0次

    面对对象的考察方式无非是两种

    第一种:类与实例

    • 类的声明
            // 类的声明两种方式
    
            // 方式一:传统的构造函数类的方式
            function Animal() {
                this.name = 'name'
            }
    
            // 方式二:ES6中的class的声明
            // class后跟的是类名,constructor是构造函数,构造函数里跟ES5中的写法是一样的
            class Animal2 {
                constructor(){
                    this.name = 'name2'
                }
            }
    
    • 生成实例
            // 类的实例化,无论是哪种方式声明的类,实例化都是通过new来实现的
            // 实例化的时候,如果构造函数没有参数,那么new的时候,后边的括号是可以不要的
            console.log(new Animal(),new Animal2())  //Animal {name: "name"} Animal2 {name: "name2"}
            console.log(new Animal, new Animal2)  // Animal {name: "name"} Animal2 {name: "name2"}
    

    第一种:类与继承

    • 记重点:继承的本质是原型链
    • 继承的方式一
            // 方式一:借助构造函数实现继承,步骤如下:
    
            // 先声明一个父类的构造函数
            function Parent1(){
                this.name = 'parent1'
            }
            Parent1.prototype.say = function(){}  
            // 再声明一个子类的构造函数
            function Child1(){
                // 既然是要继承,所以要关联父类,要在子类的构造函数里执行父类构造函数里的代码
                // call和apply方法,都是用来改变函数运行时的上下文(context),换句话说,也就是为了改变函数体内部this的指向
                // Parent1.call(this) 会执行Parent1里的代码,并把Parent1里this指向了现在的Child1
                Parent1.call(this)
    
                // 再给子类增加一个自己的属性type
                this.type = 'child1'
            }
    
            console.log(new Child1)  // Child1 {name: "parent1", type: "child1"}
            console.log(new Child1().say())  //(intermediate value).say is not a function
    

    通过构造函数实现继承的缺点:因为是通过改变父级构造函数内部的this的指向实现的,所以子类能继承父类的所有的属性和方法,但是也仅限于父类内部的属性和方法,如果是父类的原型链上的东西并没有被继承

    • 继承的方式二:为了弥补构造函数实现继承的不足,出现了借助原型链来实现继承的方式
            // 借助原型链实现继承
            function Parent2(){
                this.name = 'parent2'
            }
    
            function Child2(){
                this.type = 'child2'
            }
    
            Child2.prototype = new Parent2()
            console.log(new Child2)  // Child2 {type: "child2"}
            console.log(new Child2().__proto__ === Child2.prototype)  // true
            console.log(new Child2().__proto__.name)  // parent2
    

    在学原型链的时候我们知道,任何一个函数,都有一个prototype属性,这个属性的作用就是为了让这个构造函数的实例,能访问到它的原型对象上,这是原型链的基本原理。
    正常情况下,当我们实例化了Child2后,这个实例化对象上会有一个proto属性。
    这个属性指向了 构造函数Child2的原型对象,也就是Child2.prototype。
    现在我们把Parent2的实例化对象,赋值给了Child2.prototype。
    也就造成了 Child2的实例化对象的原型对象变成了Parent2的实例化对象。
    当我们去Child2的实例化对象里去找name属性的时候 是找不到的,我们要到Child2.prototype指向的原型对象里去找。
    Child2.prototype现在指向了Parent2的实例化对象,它里边是有name属性的,这就是原型链继承

    通过原型链实现继承也是有缺点的,我们来看一下代码
            function Parent2(){
                this.name = 'parent2';
                this.friends = [1,2,3];
            }
    
            function Child2(){
                this.type = 'child2'
            }
            Child2.prototype = new Parent2()
            console.log(new Child2)  // Child2 {type: "child2"}
    
            var s1 = new Child2();
            var s2 = new Child2();
            console.log(s1.friends,s2.friends)  //  [1, 2, 3] [1, 2, 3]
            s1.friends.push(4)
            console.log(s1.friends, s2.friends) //[1, 2, 3, 4] [1, 2, 3, 4]
            console.log(s1.__proto__ === s2.__proto__)  // true
    

    我们实例化了两个Child2对象,这两个对象也都继承了Parent2的friends属性,所以s1和s2属性都可以访问到friends属性。
    但是,当我们通过s1去修改friends属性的时候,我们发现s2这个对象的friends属性也受到了影响。
    这是因为friends属性是s1和s2原型链上的属性,这他们俩的原型对象是引用的同一个对象(Parent2的实例对象),当s1修改friends属性的时候,实际上就是修改了Parent2的实例对象的属性,所以通过s2去访问friends属性的时候,才发现也跟着发生了变化

    • 继承的方式三:组合式(继承比较常用的方式)
      为了继承上边两种方式的优点,弥补它们的不足,出现了组合式的继承方法,看代码:
                function Parent3() {
                    this.name = 'parent3';
                    this.friends = [1, 2, 3];
                }
    
                function Child3() {
                    Parent3.call(this);
                    this.type = 'child3';
                }
                Child3.prototype = new Parent3()
    
                var s3 = new Child3();
                var s4 = new Child3();
                console.log(s3.friends,s4.friends); // [1, 2, 3] [1, 2, 3]
                s3.friends.push(4);
                console.log(s3.friends, s4.friends); // [1, 2, 3, 4] [1, 2, 3]
    

    这种组合式的继承也是有缺点的,看代码我们可以知道,当我们new一个Child3实例对象的时候,会执行一次Child3构造函数,而Child3构造函数体内部,通过call的调用会执行一次Parent3构造函数。然后当我们把Parent3的实例赋值给Child3的原型对象的时候,又执行了一次Parent3构造函数,也就是说,这种组合方式会执行两次父类的构造函数。这是没有必要的。
    我们要怎么优化呢?

    • 组合继承的优化一
      之前为了拿到父类原型对象上的属性和方法,我们把父类的实例对象赋值给了子类的原型对象。但是这造成了父类构造函数多执行了一次。
      现在我们直接把父类的原型对象的引用,直接赋值给子类的原型对象,也可以实现拿到父类的原型对象上的属性和方法的目的,而且父类构造函数并没有再执行一次。
        function Parent4() {
            this.name = 'parent3';
            this.friends = [1, 2, 3];
        }
    
        function Child4() {
            Parent3.call(this);
            this.type = 'child3';
        }
        Child4.prototype =Parent4.prototype
    
        var s5 = new Child4();
        var s6 = new Child4();
        console.log(s5.friends, s6.friends); // [1, 2, 3] [1, 2, 3]
        s5.friends.push(4);
        console.log(s5.friends, s6.friends); // [1, 2, 3, 4] [1, 2, 3]
    

    这种组合优化的方式也并非没有缺点,缺点就是,无法区分实例是由子类直接直接创建的还是由父类直接创建的。因为此时子类的原型对象和父类的原型对象是
    之前通过学习原型链,我们得知要判断一个对象是否是一个类的实例,可以通过instanceof

    // s5即是Child4类的实例,又是Parent4类的实例
    console.log(s5 instanceof Child4,s5 instanceof Parent4) // true true
    

    但是通过instanceof我们无法区分,这个实例化对象s5是由子类Child4直接实例化的对象,还是由父类Parent4直接实例化的对象呢?这个时候我们需要借助另外一个办法那就是 constructor属性,
    这个属性也可以判断一个对象是否是一个类的实例

    console.log(s5.constructor) //Parent4
    

    这个时候,我们发现,s5竟然是通过Parent4直接实例化的,但是我们明明是通过new Child4得到的s5啊,显示这不是我们想要的结果。

    分析原因,我们可以得知,子类Child4的原型对象,已经被赋值为了父类Parent4的原型对象,而父类Parent4的原型对象里是有constructor属性的,而这个属性就指向了构造函数Parent4本身,所以s5实例的原型对象的constructor属性当然就是Parent4啦
    所以,如果我们有了组合继承的优化二

    • 组合继承的优化二
            // 组合式继承优化2
                function Parent5() {
                    this.name = 'parent3';
                    this.friends = [1, 2, 3];
                }
    
                function Child5() {
                    Parent3.call(this);
                    this.type = 'child3';
                }
          
                Child5.prototype = Object.create(Parent5.prototype)
                Child5.prototype.constructor = Child5
                var s7 = new Child5();
                console.log(s7 instanceof Child5, s7 instanceof Parent5) // true true
                console.log(s7.constructor)  // Child5
    
    

    我们知道 通过Object.create创建的对象的原型对象,就是Object.create的参数。
    通过上边代码可知,我们通过Object.create创建了一个新对象,然后把这个新对象赋值给了子类Child5的原型对象。
    通过创建中间对象的方式,就把父类和子类的的原型对象区分开了,但是因为我们是通过Object.create创建的的这个新对象,所以我们又把 子类和父类在原型链上又连接起来了。
    但是这个时候,我们并没有解决问题,s7的直接构造函数,还是Parent5,因为s7的原型对象是Object.create创建的新对象,而这个新对象是没有自己的constructor属性的,不过因为新对象的原型对象是Parent5的原型对象,而Parent5的原型对象是有constructor属性的,而且这个constructor属性指向的就是Parent5。
    所以,我们还需要再加一步,那就是给Child5的原型对象的constructor属性重新赋值,也就是修改Child5的原型对象的constructor属性的指向,让它重新指向Child5,这个时候,我们就能正常区分父类和子类的实例的构造函数了

    这里可能会有人有疑问,既然就是改一下constructor属性的指向,那直接在组合继承优化1里加不就行了吗?也就是和像下边这样

        Child4.prototype =Parent4.prototype
        Child4.prototype.constructor = Child4
    

    其实是不行的,因为这个时候Child4的原型对象和Parent4的原型对象就是一个对象,修改了子类的原型对象的constructor属性,就是修改了父类的原型对象的constructor属性,当我们把Child4.prototype.constructor = Child4的时候,我们就不能区分父类的实例的构造函数了

    所以,这就是组合继承方式的完美写法

    第二种问法 ,一个对象继承了某个类,问它的原型链。

    • 在一个对象上找不到某个属性,会通过原型链一级一级往上找,这个就完全可以把它理解为是一种继承关系。

    相关文章

      网友评论

          本文标题:面向对象

          本文链接:https://www.haomeiwen.com/subject/mfuuqktx.html