JS进阶(一)-JS中的面向对象

ES6中的类和对象

  1. 类: 在ES6中,新增加了类的概念,可以使用class关键字声明一个类,之后用这个类实例化对象

类、对象、属性、方法

  1. 创建类

     class name {
         // class body
     }
    
  2. 通过类创建对象

     // 类必须用new实例化对象
     var obj = new name();
    
  3. 类constructor构造函数
    1. constructor() 方法是类的构造函数(默认方法),用于传递参数,返回实例对象,通过 new 命令生成对象实例时 ,自动调用该方法。如果没有显示定义, 类内部会自动给我们创建一个constructor()
    2. 语法:

       class Person { 
           constructor(name,age) {// constructor 构造方法或者构造函数
               this.name = name;
               this.age = age; 
           }
       }
      
    3. 创建实例:

       var ldh = new Person('二哈', 18); 
       console.log(ldh.name)
      
  4. 类添加方法
    1. 语法:

       class Person { 
           constructor(name,age) {// constructor 构造器或者构造函数
               this.name = name;
               this.age = age; 
           }
           //成员方法
           say() {
               console.log(this.name + '你好');
           } 
       }
      
    2. 创建实例:

       var ldh = new Person('刘德华', 18); 
       ldh.say();
      
    3. 注意: 方法之间不能加逗号分隔,同时方法不需要添加 function 关键字。

类的继承

  1. 继承
    1. 语法:

       class Father{ // 父类
       }
       class Son extends Father { // 子类继承父类 
       }
      
    2. 举例:

       class Father {
           constructor(surname) {
               this.surname= surname; 
           }
           say() {
               console.log('你的姓是' + this.surname);
           } 
       }
       class Son extends Father{ // 这样子类就继承了父类的属性和方法
       }
       var damao= new Son('刘');
       damao.say();
      
  2. super 关键字
    1. super 关键字用于访问和调用对象父类上的函数。可以调用父类的构造函数,也可以调用父类的普通函数
    2. 调用父类的构造函数:

       class Person { // 父类 
           constructor(surname){ 
               this.surname = surname;
           } 
       } 
       class Student extends Person { // 子类继承父类
           constructor(surname,firstname){ 
               super(surname); // 调用父类的constructor(surname)    
               this.firstname = firstname; // 定义子类独有的属性
           } 
       }
      
    3. 注意:子类在构造函数中使用super, 必须放到 this 前面 (必须先调用父类的构造方法,在使用子类构造方法)
    4. 案例:

       class Father { 
           constructor(surname) { 
               this.surname = surname; 
           } 
           saySurname() { 
               console.log('我的姓是' + this.surname);
           } 
       } 
       class Son extends Father { // 这样子类就继承了父类的属性和方法 
           constructor(surname, fristname) { 
               super(surname); // 调用父类的constructor(surname) 
               this.fristname = fristname; 
           }
           sayFristname() { 
               console.log("我的名字是:" + this.fristname);
           }
       }
       var damao = new Son('刘', "德华"); 
       damao.saySurname(); 
       damao.sayFristname();
      
    5. 调用父类的普通函数。

       class Father {
           say() { 
               return '我是爸爸';
           }
       }
       class Son extends Father { // 这样子类就继承了父类的属性和方法
           say() { 
               // super.say(); super 调用父类的方法 
               return super.say() + '的儿子';     
           } 
       } 
       var damao = new Son(); 
       console.log(damao.say());
      

ES6 中的类和对象的三个注意点:

  1. 在 ES6 中类没有变量提升,所以必须先定义类,才能通过类实例化对象.
  2. 类里面的共有属性和方法一定要加this使用.
  3. 类里面的this指向问题.
    1. constructor 里面的this:指向实例对象;
    2. 方法里面的this:指向这个方法的调用者
  4. 代码举例:

     <button>点击</button>
     <script>
         var that;
         class Star {
             constructor (name, age) {
                 // 记录this到that,便于成员函数内部获取,必须放在前面
                 that = this;
        
                 // constructor这里面的this指向的是向实例对象
                 this.name = name;
                 this.age = age;
                 // 要用this
                 this.sing();
                    
                 this.btn = document.querySelector("button");
                 this.btn.onclick = this.sing;
             }
             // 方法里面的this,指向该方法的调用者
             sing(){
                 // 要用this
                 // 当点击按钮时,这个方法的直接调用者是button,而不是当前实例对象
                 console.log(this.name + "唱歌");
                 // 点击按钮时,如何拿到当前实例的this? 搞一个全局变量记录
                 console.log(that.name + "唱歌");
        
             }
         }
         let ldh = new Star("刘德华");
     </script>
    

经典案例

  1. github上web练习项目JsStudyDemo中“02-JS高级”->tab选项卡demo

构造函数和原型(ES6之前的面向对象)

  1. 在JS面向对象中的类class是ES6之后才开始新增的,在ES6之前是通过构造函数和原型来模拟类的实现机制
  2. 在典型的OOP的语言中(如 Java),都存在类的概念,类就是对象的模板,对象就是类的实例,但在ES6之前,JS中并没用引入类的概念。
  3. ES6,全称 ECMAScript 6.0 ,2015.06 发版。但是目前浏览器的JavaScript是ES5版本,大多数高版本的浏 览器也支持 ES6,不过只实现了 ES6 的部分特性和功能。
  4. 在 ES6之前 ,对象不是基于类创建的,而是用一种称为构造函数的特殊函数来定义对象和它们的特征。

构造函数

  1. 构造函数是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,它总与 new 一起使用。我们可以把对象中一些公共的属性和方法抽取出来,然后封装到这个函数里面。
  2. 在JS中,使用构造函数时要注意以下两点:
    1. 构造函数用于创建某一类对象,其首字母要大写
    2. 构造函数要和 new 一起使用才有意义
  3. 成员
    1. JavaScript 的构造函数中可以添加一些成员,可以在构造函数本身上添加,也可以在构造函数内部的 this 上添 加。通过这两种方式添加的成员,就分别称为静态成员实例成员
    2. 静态成员:在构造函数本身上添加的成员称为静态成员,只能由构造函数本身来访问
    3. 实例成员:在构造函数内部创建的对象成员称为实例成员,只能由实例化的对象来访问
      1. 就是构造函数内部通过this添加的成员
      2. 实例成员只能通过实例化的对象来访问
      3. 不可以通过构造函数来访问
    4. 代码举例

       // 创建构造函数
       function Star(name,age){
           // 1. 实例成员:构造函数内部通过this添加的成员
           // 成员属性
           this.name = name;
           this.age = age;
           // 成员函数
           this.sing = function(){
               console.log("我会唱歌");
           }
       }
       //利用构造函数创建实例对象
       var ldh = new Star('刘德华',18);
       console.log(ldh);
              
       //实例对象调用实例成员
       ldh.sing();
       // 不能通过构造函数直接访问实例成员
       // Star.sing();
       // console.log(Star.name);
      
       // 2. 静态成员:在构造函数本身上添加
       Star.sex = '男';
       // 静态成员只能通过构造函数来调用
       console.log(Star.sex);
       // 静态成员不能用实例对象来访问
       // console.log(ldh.sex);
      
  4. 构造函数的问题
    1. 构造函数方法很好用,但是存在浪费内存的问题。 pic

      1. 可以发现两个对象分别分配了一个内存用于存放sing这个成员函数,因此造成内存浪费
    2. 我们希望所有的对象使用同一个函数,这样就比较节省内存,那么我们要怎样做呢?

构造函数原型prototype

  1. 构造函数通过原型分配的函数是所有对象所共享的
  2. JavaScript 规定,每一个构造函数都有一个 prototype 属性(对象),指向另一个对象。注意这个 prototype 就是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有
    1. 注意:这里是构造函数有一个prototype属性,那么这个构造函数的所有实例都会共享这个prototype
    2. 这个prototype对象的所有属性被构造函数所拥有,那么构造函数创建的实例对象可以直接访问prototype对象的所有属性
  3. 我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。
  4. 总结:
    1. 一般情况下,公共属性定义到构造函数中
    2. 公共方法定义到构造函数的原型对象prototype中
    3. 原型对象prototype作用就是实现实例对象方法共享
  5. 举例

     function Star(name,age){
         this.name = name;
         this.age = age;
         // 成员函数,
         // this.sing = function(){
         //     console.log("我会唱歌");
         // }
     }
     // 成员函数直接赋值给prototype属性
     Star.prototype.sing = function(){
         console.log("我会唱歌");
     }   
    
     var ldh = new Star('刘德华',18);
     var zxy = new Star('刘德华',18);
     // 构造函数的实例对象直接访问prototype对象的成员
     ldh.sing();
     zxy.sing();
     // 返回true
     console.log(ldh.sing === zxy.sing);
     // 可以查看每个构造函数都有一个prototype属性
     console.dir(Star)
    
  6. 疑问:
    1. 成员方法是放在构造函数的的原型对象prototype中,那么为什么构造函数的实例对象可以直接访问原型对象prototype的成员方法呢?
    2. 因为实例对象原型__proto__

实例对象原型 __proto__

  1. 对象都会有一个属性 __proto__ 指向构造函数的 prototype 原型对象,之所以我们对象可以直接使用构造函数 prototype 原型对象的属性和方法,就是因为对象有 __proto__ 原型的存在。
  2. __proto__对象原型和原型对象 prototype 是等价的
  3. __proto__对象原型的意义就在于为对象的查找机制提供一个方向,或者说一条路线,但是它是一个非标准属性, 因此实际开发中,不可以使用这个属性,它只是内部指向原型对象 prototype
  4. 举例:

     // 返回true,构造函数的原型对象与实例对象的原型对象指向同一个对象
     console.log(ldh.__proto__ === Star.prototype);
    
  5. 公共方法的查找规则 ldh.sing
    1. 首先看ldh实例对象是否有sing方法,如果有就执行
    2. 如果没有,就到当前实例对象的原型__proto__对象的成员方法中去查找sing这个方法
  6. 注意:
    1. 为了以后方便区分,我们把构造函数的原型对象prototype称作原型对象
    2. 把实例对象的原型对象__proto__称作对象原型

constructor 构造函数

  1. 对象原型( __proto__)和构造函数(prototype)原型对象里面都有一个属性 constructor 属性 ,constructor 我们称为构造函数,因为它指回构造函数本身
  2. constructor 作用:
    1. 记录查看该对象引用于哪个构造函数
    2. 它可以让原型对象重新指向原来的构造函数。
  3. 一般情况下,对象的方法都在构造函数的原型对象中设置。如果有多个对象的方法,我们可以给原型对象采取对象形式赋值,但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了。 此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。

     function Star(name,age){
         this.name = name;
         this.age = age;
     }
     // 成员函数直接赋值给prototype属性
     // Star.prototype.sing = function(){
     //         console.log("我会唱歌");
     // }   
     // Star.prototype.movie = function() {
     //     console.log("我会演电影");
     // }
     // 有多个对象的方法,我们可以给原型对象采取对象形式赋值
     // 如果我们修改了原来的原型对象,给原型对象赋值的是一个新对象,那么必须手动的利用constructor指回原来的构造函数
     // 替换掉了Star.prototype默认指向的对象,那么它的constructor的指向肯定也会变化
     // 创建一个字面常量对象赋值给prototype
     Star.prototype = {
         //作用2:手动的利用constructor这个属性指回原来的构造函数
         constructor:Star,
         sing: function() {
             console.log("我会唱歌");
         },
         movie: function() {
             console.log("我会唱歌");
         }
     }
     var ldh = new Star('刘德华',18);
     var zxy = new Star('张学友',18);
     // 查看原型对象、对象原型的constructor构造函数
     console.log(ldh.__proto__);
     console.log(Star.prototype);
     // 作用1:查看当前对象到底指向的是哪个构造(Star)函数,原型对象、对象原型的constructor构造函数都指向构造函数本身Star
     console.log(ldh.__proto__.constructor);
     console.log(Star.prototype.constructor);
    

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

pic

原型链

pic

  1. 原型对象prototype、对象原型__proto__也是一个对象,那么这个对象也有一个__proto__属性,那么它指向什么呢?
  2. 它指向系统默认的Object构造函数的原型对象Object.prototype
    1. Object是系统默认最基本的对象构造函数
  3. 那么Object.prototype也是一个对象,那么它的__proto__属性指向谁呢?指向null

     console.log(ldh.__proto__);
     console.log(Star.prototype);
     console.log(Star.prototype.__proto__);
     console.log(Star.prototype.__proto__ === Object.prototype)
     console.log(Object.prototype.__proto__);
    

JavaScript 的成员查找机制(规则)

  1. 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
  2. 如果没有就查找它的原型(也就是 __proto__指向的 prototype 原型对象)。
  3. 如果还没有就查找原型对象的原型(Object的原型对象)。
  4. 依此类推一直找到 Object 为止(null)。
  5. __proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。

     function Star(name,age){
         this.name = name;
         this.age = age;
     } 
     //4. Object的prototype也没有
     // 返回undefined
     // 3. Object的prototype有
     // Object.prototype.sex = '男';
     // 2. 原型对象有
     // Star.prototype.sex = '男';
     var ldh = new Star('刘德华',18);
     // 1. 本身有成员
     // ldh.sex = '男';
     console.log(ldh.sex);
    

原型对象this指向

  1. 构造函数中的this 指向我们实例对象.
  2. 原型对象里面放的是方法, 这个方法里面的this 指向的是 这个方法的调用者, 也就是这个实例对象.

     function Star(name,age){
         this.name = name;
         this.age = age;
     }
     var that;
     Star.prototype.sing = function(){
         that = this;
         console.log("我会唱歌");
         // this指向方法的调用者ldh
         console.log(this);
     }   
     var ldh = new Star('刘德华',18);
     ldh.sing();
     // true
     console.log(that === ldh);
    

扩展内置对象

  1. 可以通过原型对象,对原来的内置对象进行扩展自定义的方法。比如给数组增加自定义求偶数和的功能
  2. 注意:数组和字符串内置对象不能给原型对象覆盖操作 Array.prototype = {}(会覆盖掉系统默认的方法) ,只能是 Array.prototype.xxx = function(){} 的方式。
// 扩展内置对象
// 为Array扩展一个sum方法
Array.prototype.sum = function(){
    var sum = 0;
    for(var i = 0;i<this.length;i++){
        sum+= this[i];
    }
    return sum;
}
var arr = [1,2,3];
console.log(arr.sum());
// 会覆盖系统自带的方法
// Array.prototype = {
//      sum: function(){
//         var sum = 0;
//         for(var i = 0;i<this.length;i++){
//             sum+= this[i];
//         }
//         return sum;
//     }
// }

继承

  1. ES6之前并没有给我们提供 extends 继承。我们可以通过构造函数+原型对象模拟实现继承,被称为组合继承
  2. call()
    1. 调用这个函数, 并且修改函数运行时的 this 指向

       fun.call(thisArg, arg1, arg2, ...)
      
      1. thisArg :当前调用函数 this 的指向对象
      2. arg1,arg2:传递的其他参数
    2. 举例:

       function fn(){
           console.log('hello world !');
           // 默认this指向window
           console.log(this);
       }
       var o = {};
       // 函数调用
       fn();
       // 函数调用
       fn.call();
       // 改变this指向,指向o对象
       fn.call(o);
      
  3. 借用构造函数继承父类型属性
    1. 核心原理: 通过 call() 把父类型的 this 指向子类型的 this ,这样就可以实现子类型继承父类型的属性。
    2. 代码举例:

       // 父构造函数
       function Person (name, age, sex){
           this.name = name;
           this.age = age;
           this.sex = sex;
       }
       // 子构造函数
       function Student (name, age, sex, score){
           // 此时父类的 this 指向子类的 this,同时调用这个函数
           Person.call(this,name,age,sex);
           this.score = score;
       }
       var s1 = new Student('zs', 18, '男', 100);
       console.dir(s1);
      
  4. 借用构造函数继承父类型方法
    1. 一般情况下,对象的方法都在构造函数的原型对象中设置,通过构造函数无法继承父类方法。
    2. 实现步骤:
      1. 将子类所共享的方法提取出来,让子类的 prototype 原型对象 = new 父类()
        1. 本质:子类原型对象等于实例化父类,因为父类实例化之后另外开辟空间,就不会影响原来父类原型对象
      2. 将子类的 constructor 从新指向子类的构造函数
    3. 注意:不能让子原型对象直接等于父原型对象

       Student.prototype = Person.prototype;
      
      1. 这样的话,父子指向同一个对象,子类新增一个独有方法,则父类也会有
    4. 代码举例:

       // 父构造函数
       function Person (name, age, sex){
           this.name = name;
           this.age = age;
           this.sex = sex;
       }
       // 方法
       Person.prototype.eat = function(){
           console.log('吃饭');
       }
       // 子构造函数
       function Student (name, age, sex, score){
           // 此时父类的 this 指向子类的 this,同时调用这个函数
           Person.call(this,name,age,sex);
           this.score = score;
       }
       // 修改问题这么一修改,子构造函数的原型对象跟父构造函数的原型对象指向同一个对象
       // 那么如果子构造函数原型对象添加了一个方法,父构造函数原型对象也会有子的方法
       // Student.prototype = Person.prototype;
      
       // 1. 让子类的 prototype 原型对象 = new 父类()
       // 子构造函数原型对象指向父类构造函数实例对象
       // 如果子类构造函数新增了一个独有方法,那这个方法是添加在了父类构造函数的实例对象中,而不会影响父类的原型对象
       Student.prototype = new Person();
      
       // 2. 将子类的 constructor 从新指向子类的构造函数
       // 利用对象的形式修改了原型对象,别忘了利用constructor指回原来的原型对象
       Student.prototype.constructor = Student;
      
       // 子构造函数独有方法
       Student.prototype.study = function(){
           console.log('学习');
       }
              
       var s1 = new Student('zs', 18, '男', 100);
       console.log(s1);
       console.log(Person.prototype);
      
Table of Contents