JavaScript网页编程之面向对象2

209 阅读10分钟

体验面向过程和面向对象

  • 案例:处理学生的成绩表,打印输出学生成绩
//  案例:处理学生的成绩表,打印输出学生成绩 
    // 面向过程
    // 学生:年龄、成绩 -> 对象
    var std1 = {name:"王五",score:79};
    var std2 = {name:"李四",score:88};
    // 打印输出成绩
    // 重复代码,封装函数
    function printScore(student){
        console.log(student.name + ":" + student.score);
    }
    // 调用函数,输出
    printScore(std1);
    printScore(std2);

    /* 由上可知:面向过程是亲力亲为,所有的事情都要自己做
       而面向对象不是这样的
       面向对象首先考虑的不是整个流程,而是将一个学生当一个对象,
       对象有两个属性存储姓名和成绩,
       并且对打印成绩的功能,将所有跟学生有关的属性和方都封装在对象身上。
    */ 
    /* 在这个过程中,我们已知会有多个类似的对象,
       可以利用构造函数的方法先进行封装,
       然后创建单独的实例对象
    */
    // 抽象所有的数据行为成一个模板
    // 注意:构造函数首字母要大写
    function Student(name,score){
        this.name = name;
        this.score = score;
        this.printScore = function (){
            console.log(this.name + ":" + this.score;
        }
    }
    // 根据模板创建具体的实例对象(Instance)
    var std1 = new Student("李华",23);
    var std2 = new Student("美丽",18);
    // 调用实例对象自己的方法
    std1.printScore();
    std2.printScore();

面向对象的设计思想

  • 抽象出class(构造函数)
  • 根据 Class(构造函数) 创建 Instance(实例)
  • 指挥 Instance 得结果

创建对象的四种方式

  • new Object() 构造函数
// 1. new Object() 构造函数
        var std1 = new Object(); // new 一个空对象
        // 给空对象绑定属性和方法
        std1.name = "梨花";
        std1.score = 99;
        std1.printScore = function (){
            console.log(std1.name + ":" + std1.score);
        }
        // 打印成绩
        std1.printScore();
  • 对象字面量 {}
// 2. 对象字面量 {}
        var std1 = {
            name:"梨花",
            age:18,
            sayName:function(){
                console.log(this.name);
            }
        };
        std1.sayName();
  • 工厂函数
// 3. 工厂函数:相当于一个生产对象的工厂
    // 不难发现,工厂函数就是将:Object创建方法封装    了
    function createStudent(name,score){
        var std = new Object(); // new 一个空对象
        // 给空对象绑定属性和方法
        std.name = name;
        std.score = score;
        std.printScore = function (){
            console.log(std.name + ":" + std.score);
        }
        // 切记:不要忘了返回值
        return std;
    }
    // 创建实例
    // 切记:这不是构造函数,不能使用new 来调用
    var std1 = createStudent("老四",99);
    std1.printScore();
    /* 虽然工厂方法对object()方法进行了封装,
       我们在使用的时候,不用关心内部是如何实现的。
       但是仍然存在问题:object方式创建的实例对象,
       显得有点太泛了,我们也想像数组类型那样,具体点。
       因此:我们想到了自定义构造函数
    */ 
    var arr = [1,2];
    console.log(arr instanceof Array); // true
    console.log(std1 instanceof createStudent);     false
    console.log(std1 instanceof Object); // true
  • 自定义构造函数
// 4. 自定义构造函数
    // 第一步:抽象模板
    function Student(name,score){
        this.name = name;
        this.score = score;
        this.printScore = function (){
            console.log(this.name + ":" + this.score;
        }
    }
    // 第二步:生成实例
    var std1 = new Student("周五",78);
    // 第三步:调用实例对象得到结果
    std1.printScore();
    // 再次验证
    console.log(std1 instanceof Student); // true

构造函数和实例对象的关系

  • 构造函数是根据具体的事物抽象出来的抽象模板
  • 实例对象是根据抽象的构造函数模板得到的具体实例对象
  • 每一个实例对象都通过一个 constructor 属性,指向创建该实例的构造函数。
    • 注意:constructor 是实例的属性的说法不严谨,具体后面的原型会提到。
    console.log(std1.constructor === Student);  // true
    
  • 可以通过 constructor 属性判断实例和构造函数之间的关系。
    • 注意:这种方式不严谨,推荐使用 instanceof 操作符
    console.log(std1 instanceof Student); // true
    

静态成员和实例成员

  • 使用构造函数方法创建对象时,可以给构造函数和创建的实例对象添加属性和方法,这些属性和方法都叫做成员。
  • 实例成员:在构造函数内部添加给 this 的成员,属于实例对象的成员,在创建实例对象后必须由对象调用
  • 静态成员:添加给构造函数自身的成员,只能使用构造函数(函数本身也是一个对象)调用,不能使用生成的实例对象调用。
    function Student(name,score){
        // 实例成员:通过this赋值
        this.name = name;
        this.score = score;
        this.printScore = function (){
            console.log(this.name + ":" + thiscore);
        }
    }
    var std1 = new Student("周五",78);
    // 实例成员:通过对象调用
    std1.printScore();
    // 静态成员
    Student.num = 1000;
    // 只能通过构造函数调用
    console.log(Student.num);
  • 拓展:Math不是一个构造器。Math的所有属性和方法都是静态的。

构造函数的问题

  • 浪费内存
function Student(name,score){
       this.name = name;
       this.score = score;
       /* 这里的方法,对于每个对象来说代码相同,
          生成多个就会造成内存的浪费
       */
       this.printScore = function (){
           console.log(this.name + ":" + thi   score);
       }
   }
   var std1 = new Student("周五",78);
   var std2 = new Student("周日",99);
   console.log(std1.printScore === std   printScore); // false
  • 解决方法1:引用唯一的函数地址避免内存浪费,但是这样好像不太符合面向对象的思想->所有的属性和方法都封装在一个对象中执行
function Student(name,score){
       this.name = name;
       this.score = score;
       // 引用唯一的函数地址,可避免内存浪费
       this.printScore = printScore;
   }
   // 将函数从构造函数中提取出来,单独封装
   function printScore(){
           console.log(this.name + ":" + thi   score);
   }
   var std1 = new Student("周五",78);
   var std2 = new Student("周日",99);
   console.log(std1.printScore === std   printScore); // true
  • 解决方法2:将多个公共的函数封装到一个对象字面量中
// 将多个公共的函数封装到一个对象字面量中
    var fns = {
        printScore : function (){
            console.log(this.name + ":" + this.score);
        }
    }
    function Student(name,score){
        // 实例成员:通过this赋值
        this.name = name;
        this.score = score;
        // 引用唯一的函数地址,可避免内存浪费
        this.printScore = fns.printScore;
    }
    
    var std1 = new Student("周五",78);
    var std2 = new Student("周日",99);
    console.log(std1.printScore === std2.printScore); //true

原型

  • 使用原型对象可以更好的解决构造函数的内存浪费问题。

prototype原型对象

  • 任何函数都具有一个prototype属性,该属性是一个对象。
  • 可以在原型对象上添加属性和方法。
  • 构造函数的prototype对象默认都有一个constructor属性,指向prototype对象所在的函数。
  • 通过构造函数得到的实例对象内部会包含一个指向构造函数的prototype对象的指针__proto__。
  • 实例对象可以直接访问原型对象的成员。
//  自定义构造函数
        function Person(name,age){
            this.name = name;
            this.age = age;
        }

        /* 可以在原型对象上添加属性和方法,
           供所有所有实例对象共享的属性和方法,
           解决了:内存浪费问题
        */
        Person.prototype.sayName = function (){
                console.log(this.name);
        }

        // 构造函数创建实例对象
        var person1 = new Person("梨花",33);
        // 每个构造函数都有一个prototype的属性,获得其原型对象
        console.dir(Person.prototype);

        
        // 通过实例对象可以直接访问原型对象的成员
        person1.__proto__.sayName(); // undefined
        /* 因为:我们此时调用对象不是对象实例而是原型对象,
           所以this没法找到name
        */
        person1.sayName(); // 梨花

        /* 构造函数的原型对象都有一个constructor属性,
           指向原型对象所在的函数
        */
        console.dir(Person.prototype.constructor);
        /* 所有对象都有一个__proto__的属性,
           是一个指针,
           指向的就是生成实例对象的构造函数的原型对象
        */
        // 实例对象的__proto__属性,指向其构造函数的原型对象
        console.dir(person1.__proto__);

        /*  每个构造函数的prototype对象都有一个constructor属性
           下面两个语句输出结果相同
           因为:person1实例对象 找到 原型对象里面的constructor,
                执行调用,但是其本身是不属于实例对象的属性
                因此不推荐使用的原因:1.不是自己的属性 2.可以修改值
                    推荐使用:instanceof
        */
       console.log(Person.prototype.constructor);
        console.log(person1.constructor);

构造函数、实例、原型对象三者之间的关系

  • 见下图

续接上次:构造函数浪费内存问题 之 更好的解决方法

  • JavaScript 规定,每一个构造函数都有一个prototype 属性,指向构造函数的原型对象。
  • 这个原型对象的所有属性和方法,都会被构造函数的实例对象所拥有。
  • 因此,我们可以把所有对象实例需要共享的属性和方法直接定义在 prototype 对象上。
//  自定义构造函数
        function Person(name,age){
            this.name = name;
            this.age = age;
        }

        /* 可以在原型对象上添加属性和方法,
           供所有所有实例对象共享的属性和方法,
           解决了:内存浪费问题
        */
        Person.prototype.sayName = function (){
        // 方法调用时,哪个对象调用,this指向的就是谁
                console.log(this.name);
        }

        // 构造函数创建实例对象
        var person1 = new Person("梨花",33);
        var person2 = new Person("梅花",13);
        // 此时方法调用,this指向的就是person1
        person1.sayName(); // 梨花
        console.log(person1.sayName === person2.sayName); //true

原型链

  • 思考:为什么实例对象可以调用构造函数的 prototype 原型对象 的属性和方法?
  • 如下图

原型链查找机制

  • 每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。
    • 1.搜索首先从对象实例本身开始。
    • 2.如果在实例中找到了具有给定名字的属性,则返回该属性的值。
    • 3.如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。
    • 4.如果在原型对象中找到了这个属性,则返回属性的值。

实例对象读写原型对象成员

读取

  • 先在自己身上找,找到即返回
  • 自己身上找不到,则沿着原型链向上查找,找到即返回
  • 如果一直到原型链的末端还没有找到,则返回undefined
  • 展示代码
  // 实例对象访问不存在属性和方法
      console.log(person1.city); // undefined
      console.log(person1.sayAge()); // 报错 undefined执行函数

值类型成员写入(实例对象.值类型成员 = xx)

  • 当实例期望重写原型对象中的某个普通数据成员时,实际上会把该成员添加到自己身上。
  • 也就是说该行为实际上会屏蔽掉对原型对象成员的访问

引用类型成员写入(实例对象.引用类型成员 = xx)

  • 同值类型成员写入(屏蔽对原型对象的访问)

复杂类型成员修改(实例对象.成员.xx = xx)

  • 同样会先在自己身上找该成员,如果自己身上找到则直接修改
  • 如果找不到,则沿着原型链继续查找,如果找到则修改
  • 如果一直到原型链的末端还没有找到该成员,则报错(实例对象.undefined.xx = xx)

更简单的原型语法

  • 前面在原型对象每添加一个属性和方法就要书写一遍 Person.prototype。
  • 为减少不必要的输入,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象,将Person.prototype重置到一个新的对象。
  • 注意:原型对象会丢失 constructor 成员,所以需要手动将 constructor 指向正确的构造函数。

原型对象使用建议

  • 在定义构造函数时,可以根据成员的功能不同,分别进行设置:
    • 私有成员(一般就是非函数成员)放到构造函数中
    • 共享成员(一般就是函数)放到原型对象中
    • 如果重置了prototype记得修正constructor的指向

原生构造函数的原型对象

JS原生构造函数的原型对象

  • 所有函数都有 prototype 属性对象。
  • JavaScript中的内置构造函数也有 prototype 原型对象属性:
    • Object.prototype
    • Function.prototype
    • Array.prototype
    • String.prototype
    • Number.prototype

练习

  • 为数组对象扩展原型方法

随机方块案例