一二面_面向对象

Posted 煜成'Studio

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一二面_面向对象相关的知识,希望对你有一定的参考价值。

一二面_面向对象

考察主要有两种方式:

第一种直接问面向对象相关的事件

类与实例?

类的声明(看代码)

生成实例(如果通过类来实例化生成一个对象)(看代码)

类与继承?

如何实现继承?

实现继承的基本原理就是原型链

继承的几种方式?

JS的继承有几种形式,每种形式的优缺点(最终考察的是对原型链的掌握程度)

第二种问法:比如说一个对象继承了某个类,问你它的原型链,这就延伸到原型链的内容了

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>面向对象</title>
</head>
<body>
    <script>
        // 类的声明有两种方式:第一种就是传统的用构造函数来模拟一个类的方式;另一种就是ES6中对class的声明
        // 第一种方式
        function Animal () {
            // 通过this来表明这是一个构造函数
            this.name = 'name';
        }
        // 第二种方式
        class Animal2 {
            // constructor就是构造函数,它的里面的内容和ES5中的写法相同
            constructor () {
                this.name = 'name';
            }
        }

        // 实例类的对象
        // 第一种方式:通过new可以实例化一个类,虽然类的声明方式不一样,但实例化的方式是一样的
        // 注:如果构造函数没有参数的话,实例化时的括号可以不写,例如下面的句子可以写为 new Animal, new Animal2
        console.log(new Animal (), new Animal2 ());


        // 类的继承
        // 实现继承的基本原理就是原型链
        // 第一种方式:借助构造函数实现继承(以下除了表明,都是说的ES5)
        // 父类
        function Parent1 () {
            this.name = 'parent1';
            this.pll = [1, 2, 3];
        }
        Parent1.prototype.say = function () {};
        // 子类
        function Child1 () {
            // 子类的构造函数体里面执行父类的构造函数
            Parent1.call(this);//apply也行,他俩改变的是函数运行的上下文,
            // 这也是利用构造函数实现继承的原理:将父级构造函数的this指向子构造函数的实例上去,导致父级构造函数的所有属性在子类中也有
            // 再增加自己的属性
            this.type = 'child1';
        }

        // 这下面注释的代码是测试第二种借助原型链继承的方式的缺点在这种(第一种)方式里是否存在,实践证明,不存在
        // aa = new Child1 ();
        // aaa = new Child1 ();
        // aa.pll.push(5);
        // console.log(aa.pll, aaa.pll);
        // console.log('----------------');
        console.log(new Child1);
        // 缺点:父类原型对象上的方法没有继承,像Parent1.prototype.say方法就没有实现继承,所以并没有真正实现继承,只是实现部分意义的继承,
        // 如果父类上的属性都在构造函数里面那没有问题,完全可以实现继承,但是如果父类的原型对象上还有方法的话,那么子类是拿不到这些方法的

        // 第二种:借助原型链实现继承
        // 弥补构造函数实现继承的不足,来实现一个借助原型链实现继承
        function Parent2 () {
            this.name = 'parent2';
            this.play = [1, 2, 3];
        }
        function Child2 () {
            this.type = 'child2';
        }
        Child2.prototype = new Parent2 ();

        console.log(new Child2 ());
        console.log(new Child2 ().__proto__ === Child2.prototype); //true
        console.log(new Child2 ().__proto__.name); //parent2
        // 这种继承方式的原理:
        // new Child2 ()是一个实例对象,new Child2 ().__proto__ === Child2.prototype,Child2.prototype又被赋值了父类的一个实例,
        // 所以new Child2 ().__proto__这个属性引用的是父类Parent2的一个实例对象
        var s1 = new Child2 ();
        var s2 = new Child2 ();
        console.log(s1.play, s2.play);
        s1.play.push(4);
        console.log(s1.play, s2.play);
        console.log(s1.__proto__ === s2.__proto__); //true
        // 缺点:在一个类上实例了两个对象,发现改第一个对象的属性,第二个对象也跟着改变了,这不是我们所想要的,这两个对象应该是隔离的,
        // 这就是这种继承方式的缺点,缺点造成的原因是原型链中的原型对象他俩是共用的,s1.__proto__ === s2.__proto__是严格相等的,因为他俩引用的是同一个对象(父类的实例对象),
        // 而那个play是在它的实例对象上,而且是引用类型,所以当改变s1的原型链上的对象时其实也就改了s2原型链上的对象的值,所以他俩会同时跟着变,同理改变s2,s1也会跟着变
        // 注:第二种借助原型链继承的方式的缺点在第一种借助构造函数实现继承的方式中不存在

        // 第三种方式:组合继承
        function Parent3 () {
            this.name = 'parent3';
            this.play = [1, 2, 3];
        }
        function Child3 () {
            Parent3.call(this);
            this.type = 'child3';
        }
        Child3.prototype = new Parent3 (); //在子类的原型链上父类的构造函数又执行了一次
        var s3 = new Child3 (); //这里子类实例创建的时候(new的时候),父类的构造函数执行第一次(Parent3.call(this);)
        var s4 = new Child3 ();
        s3.play.push(5);
        console.log(s3.play, s4.play);
        // 这种方式把上面两种方式的缺点都弥补了,是真正地写面向对象继承时的通用方式
        // 这种方式的缺点:实例化子类的时候,父类的构造函数执行了两次(上面有标注), 
        // 然而没有必要执行两次(因为第一种构造函数实现继承原理的时候,父类的构造函数体会自动执行,所以没有必要再把它的内容放到子类的原型对象上去,子类的构造函数里面已经有了父类这份内容)
        console.log(s3.constructor); //Parent3 (),这个问题接下来解决***

        // 第三种方式组合继承的优化方式
        function Parent4 () {
            this.name = 'parent4';
            this.play = [1, 2, 3];
        }
        function Child4 () {
            Parent4.call(this);
            this.type = 'child4';
        }
        Child4.prototype = Parent4.prototype; //能拿到所有的父类构造函数的属性和方法,既然是想继承父类的原型对象,所以直接附给子类原型对象父类的原型对象就行了,
        // 这样既可以把父类构造体内的属性和方法拿到,还可以把父类原型对象也拿到,就已经完全实现继承了,此时父类也就只执行了一次,父类中的play是个引用类型,不会再执行父级的构造函数
        var s5 = new Child4 (); 
        var s6 = new Child4 ();
        s5.play.push(6);
        console.log(s5.play, s6.play);

        console.log(s5 instanceof Child4); //true
        console.log(s5 instanceof Parent4); //true
        // 怎么区分一个实例对象是直接由子类实例化的(没有父类参与)还是由它的父类实例化的
        // 根据原型链那部分的知识,实例对象的constructor属性可以判断,于是用下面的代码测试一下发现不对呀,
        console.log(s5.constructor); //Parent4 (),很显然指向了父类,不是我们想要的,为什么是Parent4 (),
        // 因为Child4.prototype = Parent4.prototype;之后,子类的原型对象和父类的原型对象是一个对象,Child4.prototype没有自己的constructor,
        // Child4.prototype的constructor就是Parent4.prototype的constructor,Parent4.prototype的constructor当然指的是Parent4自己了,这就是问题的根源所在
        // 其实在第二种借助原型链继承的时候就已经出现了这个问题(***处),指向了父类,接下来解决这个问题

        // 组合继承优化二(最优方案)
        function Parent5 () {
            this.name = 'parent5';
            this.play = [1, 2, 3];
        }
        function Child5 () {
            Parent5.call(this);
            this.type = 'child5';
        }
        // 通过Object.create来创建一个中间对象,把两个原型对象区分开,这个中间对象还具备的一个特性就是它的原型对象是父类的原型对象,这样的话在原型链上就连起来了
        // 再解释一下Object.create,之前最开始介绍Object.create已经解释过了,就是Object.create创建的原型对象就是参数(在这里也就是父类的原型对象)
        // 通过这样即实现了继承还达到了父类和子类原型对象的隔离
        Child5.prototype = Object.create(Parent5.prototype); 
        // var s7 = new Child5 (); 
        // console.log(s7.constructor); //Parent5
        // 但是此时虽然父类和子类原型对象的隔离开了,但是Child5.protorype的constructor还是没有自己的constructor,
        // 它要找的话也是向它的原型对象上找,向上找还是会找到Parent5,此时console.log(s7.constructor); //Parent5,
        // 所以Child5.protorype的constructor必须覆盖
        Child5.prototype.constructor = Child5;
        // 倘若只把这句代码写在上一个优化方式中,放在Child4.prototype = Parent4.prototype;后面也不能解决,因为上一个优化方式中Child4和Parent4引用的同一个原型对象,
        // 直接加在后面无非是把父类的原型对象和子类的原型对象构改为子类了,就没法区分实例的父类的原型对象了,所以这个做法不可取
        var s8 = new Child5 (); 
        console.log(s8 instanceof Child5); //true
        console.log(s8 instanceof Parent5); //true
        console.log(s8.constructor); //Child5 
        
        // 注意:面试技巧:如果面试官让你去写继承,不要直接把最完美的答案写出来,要把这几种继承方式都写下来,
        // 因为每个面试的环节基本的时间是控制在一个小时,你要在这一个小时之内让面试官对你产生极好的印象,
        // 不要让面试官问你太多题目,如果说他一个小时问你了十五道题,这十五道题你回答上来五道题或者是十道题,和他问了八道题你回答上了八道题是不一样的效果的,他对你的印象是完全不同的,
        // 在这道题上不是故意的拖沓也不是故意的转移方向,而是利用这个技巧,彰显自己在原型对象、原型链、面向对象继承方面掌握的深度足够扎实,才能给面试官留下一个完美的印象,
        // 写完以后可以问他要不要讲一下这几种继承方式的区别,引导着他问这个问题,然后把上面的一一讲解,控制在十五到二十分钟是最好的,
        // 注意,不拖沓,简要,还能把所有的知识点、难度都讲出来,切记不要挤药膏(让面试官一点一点追问你),而是引导面试官来问你已经胸有成竹的问题



    </script>
</body>
</html>

以上是关于一二面_面向对象的主要内容,如果未能解决你的问题,请参考以下文章

一二面_页面布局

一二面_安全类

一二面_算法类

一二面_通讯类

一二面_CSS盒模型

一二面_DOM事件类