美文网首页
面向对象之构造函数及原型模式

面向对象之构造函数及原型模式

作者: 阿兰十一 | 来源:发表于2020-01-15 22:30 被阅读0次

    鉴于总是资料丢失,借这个平台对知识进行梳理。

    认识对象

    在学习JS的时候对象是个比较抽象的东西,其实对象是单个事物的抽象。比如一间房,一辆车,一台电脑,一支笔都可以是一个对象。
    对象是一个容器,封装了属性和方法。比如:一辆车。它的颜色,大小,重量等是它的属性,而启动,加速,减速,刹车等是它的方法。
    这些特征,或者说是属性定义了一个物体由什么构成的。需要注意的是:那些相似的物体可以拥有相同的属性,但是这些属性可能会有不同的值。举个例子:所有的汽车都有轮子,但并不是所有汽车的轮子个数都是一样的。

    面向对象编程和面向对象过程

    请看如下代码:
    需求:给这三个p设置边框.

    <style>
        p {
          width: 100px;
          height: 100px;
          margin-top: 20px;
        }
      </style>
    </head>
    <body>
      <p></p>
      <p></p>
      <p></p>
    </body>
    </html
    

    面向过程 注重过程===造轮子

      1.以前的做法
       let ps = document.getElementsByTagName('p');
       // //遍历出每一个p
       for (let i = 0; i < ps.length; i++) {
          ps[i].style.border = '1px solid green';
        }
       //2.上面的代码可以封装成函数
        function getEles(tagName) {
          return document.getElementsByTagName(tagName);
        }
       function setStyle(eles, value) {
         for (let i = 0; i < eles.length; i++) {
           eles[i].style.border = value;
         }
        }
       // //调用我们自己封装的函数来完成需求
        let ps = getEles('p');
        setStyle(ps, '1px solid red');
    

    面向对象编程 注重结果===用现成的

       //3.把上面这个代码封装到一个对象中  面向对象思想. 
       let obj = {
         getEles: function (tagName) {
           return document.getElementsByTagName(tagName);
         },
         setStyle: function (eles, value) {
           for (var i = 0; i < eles.length; i++) {
             eles[i].style.border = value;
           }
         }
       }
       //调用我们自己封装的对象,里面专业的方法来完成我们的需求. 
       let ps = obj.getEles('p');
       obj.setStyle(ps,'1px solid blue');
     </script>
    

    从以上代码中可以看出面向对象其实是面向过程的一种封装.

    创建对象

    首先先复习下对象的创建方式

    1.通过对象字面量来创建。
    var Person = {
      name: 'alanshiyi',
      age: 18,
      gender : 'male',
      sayHi: function () {
        console.log("hi,my name is "+this.name);
      }
    }; 
    
    2. 通过 new Object() 创建对象。
    var Person= new Object();
      student.name = 'alanshiyi',
      student.age = 18,
      student.gender = 'male',
      student.sayHi = function () {
        console.log("hi,my name is "+this.name);
      }
    

    上面两种都是简单的创建对象的方式,但是如果有n多个实例对象呢?显然如果这样做的法太过繁杂,是不可取的。所以做了简单的改进,就引入了:工厂函数。

    3. 通过工厂函数来创建对象。
    function createPerson(name, age, gender) {
      var person = new Object();
      person.name = name;
      person.age = age;
      person.gender = gender;
      person.sayHi = function(){
        console.log("hi,my name is "+this.name);
      }
      return student;
    }
    var s1 =  createPerson('lisi', 19, 'male');
    var s2 =createPerson( 'alanshiyi', 18, 'male');
    

    这样封装代码确实解决了代码冗余的问题,但是每次调用函数 createPerson() 都会创建新函数 sayHi(),也就是说每个对象都有自己的 sayHi() 版本,而事实上,每个对象都共享一个函数。为了解决这个问题,就引入面向对象编程里的一个重要概念:构造函数。

    4. 通过构造函数来创建对象。
    function Person(name,age,gender){
      this.name = name;
      this.age = age;
      this.gender = gender;
      this.sayHi = function(){
           console.log("hi,my name is "+this.name);
      }
    }
    var s1 = new Person( 'alanshiyi', 18, 'male');
    
    对比一下构造函数与工厂函数的区别:
    • 首先在构造函数内没有创建对象,而是使用 this 关键字,将属性和方法赋给了 this 对象。
    • 构造函数内没有 return 语句,this 属性默认下是构造函数的返回值。
    • 函数名使用的是大写的 Student。
    • 用 new 运算符和类名 Student 创建对象。
    构造函数虽然科学,但仍然存在一些问题
      function Student(name,age){
                this.name = name;
                this.age = age;
                this.sayHi = function(){
                    console.log('我的名字是'+this.name);
                }
            }
            //实例化学生对象
            let s1 = new Student('达达',18);
            s1.sayHi();
            let s2 = new Student('云旺',19);
            s2.sayHi();
            //判断一下下面的这句话的结果是什么?
            console.log(s1.sayHi === s2.sayHi);//false
    
    
    image.png

    由于每个对象都是由 new Student 创建出来的,因此每创建一个对象,函数 sayHi 都会被重新创建一次,这个时候,每个对象都拥有一个独立的,但是功能完全相同的方法,这样势必会造成内存浪费。有的人可能会想,既然是一样的那我们就单独把它提出来,写一个函数,每次调用不就可以了吗?比如:

            function test1(){
                console.log('我的名字是'+this.name);
            }
            function Student(name,age){
                this.name = name;
                this.age = age;
                this.sayHi = test1;
            }
            //实例化学生对象
            let s1 = new Student('王晓',20);
            s1.sayHi();
            let s2 = new Student('郭荣',21);
            s2.sayHi();
            //判断一下下面的这句话的结果是什么?
            console.log(s1.sayHi === s2.sayHi);//true
    
    image.png

    解决空间浪费的问题: 把函数体提炼到构造函数外面
    缺点:
    但是这样做会导致全局变量增多,可能会引起命名冲突,代码结果混乱,有全局变量污染的危险,维护困难。
    既然是全局变量的问题那可以把提炼出来的函数放在对象中.

      var obj = {
                test1: function () {
                    console.log('我的名字是' + this.name);
                }
            }
            function Student(name, age) {
                this.name = name;
                this.age = age;
                this.sayHi = obj.test1;
            }
            //实例化学生对象
            let s1 = new Student('万山2', 30);
            s1.sayHi();
            let s2 = new Student('富婆娟2', 32);
            s2.sayHi();
            //判断一下下面的这句话的结果是什么?
            console.log(s1.sayHi === s2.sayHi); //true
    

    以上方法似乎已经完美的解决了各种问题
    但好的地方是: 每写一个构造函数,都要写一个和这个构造函数配套的对象. 所以就引出了原型的概念,因为在javascript不管哪个构造函数被创建,系统都会帮我们自动的创建一个与之对应的对象,这个对象就是原型. 所以每写一个构造函数,用原型对象匹配即可,无需再创建与构造函数配套的对象。

    原型:prototype

    在 JavaScript 中,每一个函数都有一个 prototype 属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。

    我们来看看前面例子原型的写法:

     Student.prototype.test1= function () {
                    console.log('我的名字是' + this.name);
                }
            
            function Student(name, age) {
                this.name = name;
                this.age = age;
                this.sayHi = test1;
            }
            //实例化学生对象
            let s1 = new Student('万山2', 30);
            s1.sayHi();
            let s2 = new Student('富婆娟2', 32);
            s2.sayHi();
            //判断一下下面的这句话的结果是什么?
            console.log(s1.sayHi === s2.sayHi); //true
    

    原型对象访问的方式:构造函数名.prototype
    原型对象添加/调用(属性/方法) 的方式:

       Student.prototype.sb = '随便';
            Student.prototype.dsb = function(){
                console.log('你是一个大随便...');
            }
            //调用原型对象中的属性和方法
            console.log(Student.prototype.sb);
           Student.prototype.dsb();
    
    • 使用原型需要注意的地方
    • 对象访问成员的访问规则:
      如果这个成员对象自己有,那就访问自己的; 如果对象自己没有,那就访问原型的,往原型链一层层往上找...(原型链在之后的文章再细谈)
    • 访问原型(给原型添加内容/修改原型中的内容/删除原型中的内容),一定要使用 构造函数名.prototype这种格式 ,原型可以被覆盖.

    之前提到过:每一个函数都有一个 prototype 属性,指向另一个对象。

    <script type="text/javascript">
        function F() {}
        console.log(F.prototype);//Object
    </script>
    

    上述代码在浏览器中打印结果为 Object,验证了我们所说的 prototype 属性,指向另一个对象。

    构造函数的 prototype 对象默认都有一个 constructor 属性,指向 prototype 对象所在函数。在控制台中运行下面的代码:

    function F() {}
    console.log(F.prototype.constructor === F);//结果为ture
    

    更简单的原型语法

    在前面的例子中,我们是使用 xxx.prototype. 然后加上属性名或者方法名来写原型,但是每添加一个属性或者方法就写一次显得有点麻烦,因此我们可以用一个包含所有属性和方法的对象字面量来重写整个原型对象:

    function Student(name, age, gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }
    Student.prototype = {
        hobby:"study",
        sayHi:function(){
        console.log("hi");
        }
    }
    var s1 = new Student("wangwu",18,"male");
    console.log(Student.prototype.constructor === Student);//结果为 false
    

    但是这样写也有一个问题,那就是原型对象丢失了 constructor 成员。所以为了保持 constructor 成员的指向正确,建议的写法是:

    function Student(name, age, gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }
    Student.prototype = {
        constructor: Student, //手动将 constructor 指向正确的构造函数
        hobby:"study",
        sayHi:function(){
        console.log("hi");
        }
    }
    var s1 = new Student("wangwu",18,"male");
    console.log(Student.prototype.constructor === Student);//结果为 true
    

    通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype 对象的指针protoproto属性最早是火狐浏览器引入的,用以通过实例对象来访问原型,这个属性在早期是非标准的属性。在控制台中运行下面的代码:

    function F() {}
    var a = new F();
    console.log(a.__proto__ === F.prototype); //结果为true
    

    实例对象可以直接访问原型对象成员。所有实例都直接或间接继承了原型对象的成员。

    image.png

    总结:每个构造函数都有一个原型对象,原型对象包含一个指向构造函数的指针 constructor,而实例都包含一个指向原型对象的内部指针proto

    以上只是均来源于上课笔记与MDN及牛客网实验楼<<JS高级程序设计>>学习资料总结

    相关文章

      网友评论

          本文标题:面向对象之构造函数及原型模式

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