面向对象

2020-07-04  本文已影响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的时候,我们就不能区分父类的实例的构造函数了

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

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

上一篇下一篇

猜你喜欢

热点阅读