zoukankan      html  css  js  c++  java
  • 进击JavaScript核心 --- (3)面向对象

    JS中的对象定义为:无序属性的结合,其属性可以包含基本值、对象或者函数
     
    1、定义对象的方式
     
    (1)、Object构造函数
    var student = new Object();
    student.name = 'Jim Green';
    student.gender = 'Male';
    student.age = 8;
    student.say = function() {
      console.log(`My name is ${this.name}, I'm ${this.age} years old`);
    }
    
    student.say();      // My name is Jim Green, I'm 8 years old

    (2)、对象字面量

    var student = {
      name: 'Jim Green',
      gender: 'Male',
      age: 8,
      say: function() {
        console.log(`My name is ${this.name}`)
      }
    }
    
    student.say();    // My name is Jim Green

    2、属性类型

    Object.defineProperty()方法:用于修改对象属性的默认特性
    这个方法接收三个参数:属性所在的对象,属性的名字和一个描述符对象。其中,描述符对象必须是以下两种之一(不能同时是两者)
     
    2-1、数据属性
    数据属性包含一个数据值的位置,在这个位置可以读取和写入值
     
    configurable:默认值为true。表示能否通过delete删除该属性,能否修改属性的特性,或者能否把属性修改为访问器属性
    enumerable:默认值为true。表示能否通过 for-in 循环返回属性
    writable:默认值为true。表示能否修改属性值
    value:默认值为undefined。包含这个属性的属性值,读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置
    var person = {
      age: 8,
      gender: 'male'
    };
    Object.defineProperty(person, "name", {
      enumerable: false,
      value: "Roger"
    });
    for(var key in person) {
      console.log(`key --> ${key}, value --> ${person[key]}`);
    }
    
    // key --> age, value --> 8
    // key --> gender, value --> male
    如果将name属性的configurable值改为false,并尝试删除该属性,非严格模式下无效,delete操作被忽略;严格模式下报错
    // "use strict";
    var person = {};
    Object.defineProperty(person, "name", {
      configurable: false,
      writable: true,
      value: "Roger"
    });
    console.log(person.name);       // Roger
    delete person.name;
    console.log(person.name);       // Roger    (configurable属性为false时,返回undefined)
    
    // Uncaught TypeError: Cannot delete property 'name' of #<Object> (严格模式下报错)
     
    通过修改name属性的writable将其变为只读属性,非严格模式下修改该属性无效,赋值操作被忽略;严格模式下会报错
    // "use strict"
    var person = {};
    Object.defineProperty(person, "name", {
      writable: false,
      value: "Roger"
    })
    
    console.log(person.name);       // Roger
    person.name = "Frank";
    console.log(person.name);       // Roger 
    
    // console.log(person.name);       // Uncaught TypeError: Cannot assign to read only property 'name' of object '#<Object>'
    注意:
    (1)、调用Object.defineProperty()方法时,如果不指定,configurable、enumerable和writable特性的默认值就都是false
    var person = {};
    Object.defineProperty(person, "name", {
      configurable: true,
      value: "Roger"
    });
    person.name = "Frank";
    console.log(person.name);                // Roger
    
    Object.defineProperty(person, "name", {
      writable: true,
      value: "Roger"
    });
    
    person.name = "Kobe";                   
    console.log(person.name);                // Kobe
    (2)、一旦把属性定义为不可配置的(configurable为false),就不能再把它设为可配置了。此时,再修改除 writable之外的特性,都会导致报错
    var person = {};
    Object.defineProperty(person, "name", {
      configurable: false,
      value: "Roger"
    });
    person.name = "Frank";
    console.log(person.name);                // Roger
    
    Object.defineProperty(person, "name", {  // Uncaught TypeError: Cannot redefine property: name
      writable: true,
      value: "Roger"
    });
    
    person.name = "Kobe";                   
    console.log(person.name);

    2-2、访问器属性

    访问器属性不包含数据值,它们包含一对getter和setter函数(非必需)。共包含4个特性:

     
    configurable:默认值为true;表示能否通过delete删除属性,能否修改属性的特性,或者能否把属性修改为数据属性
    enumerable:默认值为true;表示能否通过for-in循环此属性
    get:默认值为undefined;在读取属性时调用函数
    set:默认值为undefined;在写入属性时调用函数
    var person = {
      name: 'Kobe',
      _number: 8
    }
    
    Object.defineProperty(person, 'number', {
      get: function() {
        return this._number;
      },
      set: function(newValue) {
        if(newValue > 8) {
          this._number = 24;
        }
      }
    })
    
    person.number = 9;
    console.log(person._number);          // 24
    Object.defineProperties() 方法:可以通过描述符一次定义多个属性,这个方法接收两个对象参数
     
    第一个对象是要添加和修改其属性的对象
    第二个对象与第一个对象中要添加或修改的属性一一对应
    var book = {};
    Object.defineProperties(book, {
      _year: {
        writable: true,
        value: 8
      },
      year: {
        get: function() {
          return this._year;
        },
        set: function(newValue) {
          if(newValue > 8) {
            alert('111')
            this._year = 24;
          }
        }
      }
    })
    
    console.log(book);    // {_year: 8}
    book.year = 9;
    console.log(book);    // {_year: 24}
    Object.getOwnPropertyDescriptor() 方法用于读取给定属性的描述符,这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称,返回值是一个对象。
     
    如果是访问器属性,这个对象包含 configurable, enumerable, get, set;
    如果是数据属性,这个对象包含 configurable,enumerable,writeable,value;
    var book = {};
    Object.defineProperties(book, {
      _year: {
        writable: true,
        value: 8
      },
      year: {
        get: function() {
          return this._year;
        },
        set: function(newValue) {
          if(newValue > 8) {
            alert('111')
            this._year = 24;
          }
        }
      }
    });
    
    var obj1 = Object.getOwnPropertyDescriptor(book, '_year');
    var obj2 = Object.getOwnPropertyDescriptor(book, 'year');
    
    console.log(obj1);   // {value: 8, writable: true, enumerable: false, configurable: false}
    console.log(obj2);   // {get: ƒ, set: ƒ, enumerable: false, configurable: false}
    3、创建对象
     
    面向对象的编程语言都有类的概念,通过类可以创建任意多个具有相同属性和方法的对象
    // 例如:Java中定义一个Student类
    public class Student {
    
      private String name; // 姓名
      private int age; // 年龄
    
      public Student(String name, int age) { //构造器
        super();
        this.name = name;
        this.age = age;
      }
      
      // 属性的getter和setter方法
      public String getName() {
        return name;
      }
    
      public void setName(String name) {
        this.name = name;
      }
    
      public int getAge() {
        return age;
      }
    
      public void setAge(int age) {
        this.age = age;
      }
      
      // 自定义方法
      public void say() {
        System.out.println("姓名:" + this.name + "年龄:" + this.age);
      }
    
    }
    
    // 通过Student类来创建实例对象
    Student xiaoMing = new Student("XiaoMing", 12);
    Student xiaoHong = new Student("XiaoHong", 9);
    
    xiaoMing.say();   // 姓名:XiaoMing年龄:12
    xiaoHong.say();   // 姓名:XiaoHong年龄:9
    如果是通过JS来创建两个学生对象,可以用对象字面量或者Object构造函数创建单个的对象:
    var xiaoMing = {
      name: "XiaoMing",
      age: 12
    };
    
    
    var XiaoHong = {
      name: "XiaoHong",
      age: 9
    }
    这么做有两个明显的弊端,一是重复的代码太多,如果一个班级有60名学生,就要重复60次;二是可读性差,只知道是个对象,到底是个什么对象,学生的集合,人的集合还是其它?
     
    3-1、工厂模式
     
    用函数来封装以特定接口创建对象的细节
    function createStudent(name, age) {
      var obj = new Object();
      obj.name = name;
      obj.age = age;
      obj.say = function() {
        console.log(`My name is ${this.name}, I'm ${this.age} years old.`);
      };
      return obj;
    }
    
    var xiaoMing = createStudent('XiaoMing', 12);
    var xiaoHong = createStudent('XiaoHong', 8);
    
    xiaoMing.say();     // My name is XiaoMing, I'm 12 years old.
    xiaoHong.say();     // My name is XiaoHong, I'm 8 years old.
    弊端:这样做虽然很好的解决了代码重复的问题,但还是不知道创建的到底是个什么类型的对象
     
    3-2、构造函数模式
     
    使用自定义构造函数来定义对象类型的属性和方法,通常为了区别于普通函数,会将构造函数的首字母大写
    function Student(name, age) {
      this.name = name;
      this.age = age;
      this.say = function() {
        console.log(`My name is ${this.name}, I'm ${this.age} years old.`);
      };
    }
    
    var xiaoMing = new Student('XiaoMing', 12);
    var xiaoHong = new Student('XiaoHong', 8);
    
    xiaoMing.say();     // My name is XiaoMing, I'm 12 years old.
    xiaoHong.say();     // My name is XiaoHong, I'm 8 years old.
    与工厂模式相比,有以下几个不同的地方:
    -、没有显示的创建对象
    -、直接将属性和方法赋给了this对象
    -、没有return语句

    使用new操作符创建自定义对象,主要经历了4个步骤:
    -、创建一个新对象
    -、将this指向新对象
    -、为新对象添加属性
    -、返回新对象

    对象都有一个constructor属性用于标识对象的类型,该属性指向创建对象的构造函数
    console.log(xiaoMing.constructor)
    
    // ƒ Student(name, age) {
    //   this.name = name;
    //   this.age = age;
    //   this.say = function() {
    //     console.log(`My name is ${this.name}, I'm ${this.age} years old.`);
    //   };
    // }
    
    console.log(xiaoMing.constructor == Student)                  // true
    console.log(xiaoMing.constructor == xiaoHong.constructor)     // true
    console.log(xiaoMing instanceof Student)                      // true
    console.log(xiaoMing instanceof Object)                       // true
    构造函数与普通函数的唯一区别,就在于调用方式。任何函数,只要使用new操作符来调用就可以作为构造函数,如果不通过new操作符来调用,那就是普通函数
    function Student(name, age) {
      this.name = name;
      this.age = age;
      this.say = function() {
        console.log(`My name is ${this.name}, I'm ${this.age} years old.`);
      };
    }
    
    
    // 将Student当作构造函数(this指向实例对象)
    var xiaoMing = new Student('XiaoMing', 12);   
    xiaoMing.say();     // My name is XiaoMing, I'm 12 years old.
    
    
    // 将Student当作普通函数(由于Student函数属于全局作用域,因此实际上是window.Student(),this指向window)
    Student('Bob', 9);
    say();              // My name is Bob, I'm 9 years old.
    
    
    // 在特定的作用域中调用函数(this指向obj)
    var obj = new Object();
    Student.call(obj, 'Ryan', 30);
    console.log(obj);       // {name: "Ryan", age: 30, say: ƒ}
    obj.say();              // My name is Ryan, I'm 30 years old.
     
    弊端:由于方法也是对象(如果对象的属性是一个函数就称之为方法),这就意味着每次实例化一个对象,都重新创建了一个对象
    // 上面的say方法等价于
    function Student(name, age) {
      this.name = name;
      this.age = age;
      this.say = new Function(`My name is ${this.name}, I'm ${this.age} years old.`);
    }
    为了解决这个问题,似乎可以把方法提出来
    function Student(name, age) {
      this.name = name;
      this.age = age;
      this.say = say;
    }
    function say() {
      console.log(`My name is ${this.name}, I'm ${this.age} years old.`);
    }
    
    var xiaoHong = new Student('XiaoHong', 8);
    xiaoHong.say();       // My name is XiaoHong, I'm 8 years old.

    这样做的话同样存在问题,首先是封装性不太好,对象的某些属性必须依赖于全局的属性;其次,我们期望全局作用域内的函数say只能用于构造函数Student,这样就跟js的理念相冲突了

    3-3、原型模式

    每个函数都有一个prototype属性,prototype属性是一个指针,指向函数的原型对象,该对象包含所有实例对象共享的属性和方法
    function Student() {
    
    }
    Student.prototype.name = 'Bob';
    Student.prototype.age = 12;
    Student.prototype.say = function() {
      console.log(`My name is ${this.name}, I'm ${this.age} years old.`);
    }
    
    var xiaoMing = new Student();
    var xiaoHong = new Student();
    
    xiaoMing.say();   // My name is Bob, I'm 12 years old.
    xiaoHong.say();   // My name is Bob, I'm 12 years old.
    与构造模式不同的时,所有实例对象访问的都是相同的属性和同一个say方法

    对比prototype和constructor
    prototype:每个函数都有一个 prototype属性,指向一个原型对象
    constructor:每个对象都有一个 constructor属性,指向创建该对象的构造函数
    通过构造函数创建的实例对象内部都包含一个指针(内部属性),指向构造函数的原型对象,这个指针就是 [[Prototype]]。换句话说,实例对象与构造函数没有直接关系
    但是,脚本中没有标准的方式访问[[Prototype]],一些现代浏览器中可以通过__proto__属性来访问
    console.log(xiaoMing.__proto__);      // {name: "Bob", age: 12, say: ƒ, constructor: ƒ}
    console.log(xiaoMing.__proto__ === Student.prototype);  // true

    __proto__属性是一个访问器属性,其中包含了getter(读取器)和 setter(设置器)
    getter:暴露了一个对象的内部[[Prototype]],这个值就是构造器函数的prototype属性,如:Array.prototype, Object.prototype
    setter:允许对象的[[Prototype]]被更改
    虽然无法直接访问 [[Prototype]] 属性,但可以通过 isPrototypeOf() 方法来测试一个对象是否存在于另一个对象的原型链上

    prototypeObj.isPrototypeOf(object)
    参数:在object对象的原型链上搜寻
    返回值:Boolean
    console.log(Student.prototype.isPrototypeOf(xiaoMing));   // true
    console.log(Student.prototype.isPrototypeOf(xiaoHong));   // true
    // 说明实例对象 xiaoMing和xiaoHong都存在于Student.prototype的原型链上
    尽管部分现代浏览器都实现了__proto__属性,但是该属性从未被包含在ECMA规范中,因此不推荐使用。

    从ES5开始,[[Prototype]] 可以通过 Object.getPrototypeOf() 访问器访问,用于返回指定对象的原型
    console.log(Object.getPrototypeOf(xiaoHong));  // {name: "Bob", age: 12, say: ƒ, constructor: ƒ}
    console.log(Object.getPrototypeOf(xiaoHong) === Student.prototype);   // true
    当为某个实例对象添加同名属性时,这个属性就会屏蔽掉原型对象中保存的同名属性,也就是说,如果实例对象自己有某个属性,就不会去它的原型对象上找
    xiaoMing.name = 'XiaoMing';
    
    console.log(xiaoMing.name);   // XiaoMing
    console.log(xiaoHong.name);   // Bob

    为了进一步对比,可以删除掉实例对象 xiaoMing 的属性name,然后再访问该属性

    delete xiaoMing.name;
    
    console.log(xiaoMing.name);   // Bob
    console.log(xiaoHong.name);   // Bob
    当前测试的是属性值是基本类型的情况,如果属性值是引用类型就不再屏蔽了
    function Student() {
      
    }
    Student.prototype = {
      name: 'Bob',
      age: 12,
      course: ['Chinese', 'Math']
    }
    
    var xiaoMing = new Student();
    var xiaoHong = new Student();
    
    xiaoMing.name = 'XiaoMing';
    console.log(xiaoMing.name);       // XiaoMing
    console.log(xiaoHong.name);       // Bob
    
    xiaoMing.course.push('English');
    console.log(xiaoMing.course);     // ["Chinese", "Math", "English"]
    console.log(xiaoHong.course);     // ["Chinese", "Math", "English"]
    如果需要判断一个属性是存在于实例对象中,还是原型对象中,可以使用 obj.hasOwnProperty(prop) 方法
    xiaoMing.name = 'XiaoMing';
    
    console.log(xiaoMing.hasOwnProperty('name'));   // true     --- 来自实例
    console.log(xiaoHong.hasOwnProperty('name'));   // false    --- 来自原型
    // 实例对象xiaoMing有自己的name属性,xiaoHong则没有 
    in操作符:只要通过对象能够访问到给定的属性(不管是对象自身的属性还是原型对象的属性),都返回true
    hasProperty():只有属性存在于对象时才返回true
    xiaoMing.name = 'XiaoMing';
    
    console.log('name' in xiaoMing);                // true
    console.log(xiaoMing.hasOwnProperty('name'));   // true
    
    console.log('name' in xiaoHong);                // true
    console.log(xiaoHong.hasOwnProperty('name'));   // false
    因此,要判断一个属性是原型中的属性,只需要同时满足in返回true,hasOwnProperty()返回false即可
    xiaoMing.name = 'XiaoMing';
    
    // 判断一个属性仅存在于对象的原型中
    function checkPropertyInPrototype(Object, prop) {
      return (prop in Object) && !Object.hasOwnProperty(prop)
    }
    
    console.log(checkPropertyInPrototype(xiaoMing, 'name'));    // false
    console.log(checkPropertyInPrototype(xiaoHong, 'name'));    // true
    for-in 循环不仅会遍历对象自身的属性,还会遍历对象原型的属性(这里说的属性必须是可枚举的)
    function Student() {
    
    }
    Student.prototype.name = 'Bob';
    Student.prototype.age = 12;
    Student.prototype.say = function() {
      console.log(`My name is ${this.name}, I'm ${this.age} years old.`);
    }
    
    var xiaoHong = new Student();
    xiaoHong.gender = 'female';
    
    for(var prop in xiaoHong) {
      console.log(prop +' --> '+ xiaoHong[prop]);
    }
    
    /*
    gender --> female
    name --> Bob
    age --> 12
    say --> function() {
      console.log(`My name is ${this.name}, I'm ${this.age} years old.`);
    }
    */
    弊端:所有实例只能共享属性,无法通过构造函数初始化参数
     
    3-4、组合构造函数模式和原型模式
     
    这也是创建自定义类型的最常见方式,构造函数模式用于定义实例属性,原型模式用于定义方法和共有的属性
    function Person(name, age) {
      this.name = name;
      this.age = age;
    }
    Person.prototype = {
      say: function() {
        console.log(`My name is ${this.name}, I'm ${this.age} years old.`);
      }
    }
    
    var xiaoMing = new Person('XiaoMing', 12);
    var xiaoHong = new Person('xiaoHong', 9);
    
    xiaoMing.say();     // My name is XiaoMing, I'm 12 years old.
    xiaoHong.say();     // My name is xiaoHong, I'm 9 years old.

    4、继承

    JS中的继承主要是依靠原型链来实现的

    4-1、原型链

    简单回顾下构造函数、原型和实例对象的关系

    (1)、每个构造函数都包含一个prototype属性指向原型对象

    (2)、每个原型对象都包含一个constructor指针指向构造函数

    (3)、每个实例都包含一个[[prototype]]指针指向构造函数的原型对象

    原型链的基本思想就是利用原型,让一个引用类型继承另一个引用类型的属性和方法

    具体说就是让原型对象等于另一个类型的实例,由于该实例包含指向其原型对象的指针,因此,此时的原型对象也包含了另一个原型对象的 指针。层层向上直到一个对象的原型对象是null。根据定义,null没有原型,并作为原型链中的最后一个环节

    function Person() {
        
      }
      Person.prototype = {
        leg: 4,
        ear: 2
      }
    
      function Student() {
        
      }
    
      // 让Student的原型对象等于Person的实例,因此Student的原型对象也包含了指向Person的原型对象的指针
      Student.prototype = new Person();
    
      // 给Student的原型对象添加方法
      Student.prototype.say = function() {
        console.log(`I have ${this.ear} ears and ${this.leg} legs.`);
      }
    
      var student = new Student();
      student.say();   // I have 2 ears and 4 legs. 
      console.log(student.toString());  // [object Object]
    
      console.log(Object.getPrototypeOf(student) === Student.prototype);  // true (student实例的原型对象就是Student.prototype)
      console.log(Person.prototype.isPrototypeOf(student));               // true (student实例存在于Person对象的原型链上)
      console.log(student instanceof Person);                             // true (student就是Person对象的实例)
      console.log(student.hello());                                       // Uncaught TypeError: student.hello is not a function

    图中所示,红色的粗线条代表的就是原型链,绿色细线条代表是构造函数与其原型对象之间的关联。

    执行 student.say(),student实例自己没有say方法,于是沿着原型链在它的原型对象是找到了,但是该原型对象中并没有ear和leg属性,由于该原型对象包含了指向Person原型对象的指针,因此,继续沿着原型链向上查找,在Person的原型对象上找到了ear和leg属性

    执行 student.toString(),student实例没有提供该方法,于是沿着原型链逐级向上查找,由于所有引用类型都继承自Object,最终在Object的原型对象上找到了

    执行 student.hello(),student实例没有提供该方法,沿着原型链逐级向上查找,都没有找到该方法,因此会报错

    注意:

    (1)、通过原型链实现继承时,不能使用对象字面量创建原型方法,否则会重写原型链

    function Person() {
        
    }
    Person.prototype = {
      say: function() {
        console.log('hello world');
      }
    }
    
    function Student() {
      
    }
    
    // 让Student的原型对象等于Person的实例,因此Student的原型对象也包含了指向Person的原型对象的指针
    Student.prototype = new Person();
    
    // 使用对象字面量给Student创建原型方法
    Student.prototype = {
      class: 2,
      grade: 1
    }
    
    var student = new Student();
    student.say();                // Uncaught TypeError: student.say is not a function

    此例中,先是把Person的实例赋值给Student的原型对象,构建出了一条图中红色粗线部分显示的原型链。然后,使用对象字面量的方法使得Student的原型对象指向了一个实例对象,切断了原来的原型链,重新构建出图中蓝色粗线部分的原型链,自然就找不到say方法了

    (2)、给原型添加方法的代码一定要放在替换原型的语句之后。如子类型重写父类型中的某个方法,或在子类型中添加一个父类型中不存在的方法

    function Person() {
        
    }
    Person.prototype = {
      say: function() {
        console.log('hello world');
      }
    }
    
    function Student() {
      
    }
    
    // 让Student继承Person
    Student.prototype = new Person();
    
    // 子类型重写父类型中的方法
    Student.prototype.say = function() {
      console.log(`I'm Iron man`)
    }
    
    // 子类型中添加一个父类型中不存在的方法
    Student.prototype.walk = function() {
      console.log('walk with legs')
    }
    
    var student = new Student();
    
    student.say();                // I'm Iron man
    student.walk();               // walk with legs

    首先确定了Student继承Person这一继承关系,Student原型对象可以读取到Person原型对象中的say方法,然后为Student原型对象添加的重写和新方法,会覆盖掉原来的say方法

    function Person() {
        
    }
    Person.prototype = {
      say: function() {
        console.log('hello world');
      }
    }
    
    function Student() {
      
    }
    
    // 子类型重写父类型中的方法
    Student.prototype.say = function() {
      console.log(`I'm Iron man`)
    }
    
    // 子类型中添加一个父类型中不存在的方法
    Student.prototype.walk = function() {
      console.log('walk with legs')
    }
    
    // 让Student继承Person
    Student.prototype = new Person();
    
    var student = new Student();
    
    
    console.log(Person.prototype.isPrototypeOf(student));  // true
    student.say();                // hello world
    student.walk();               // Uncaught TypeError: student.walk is not a function

    与前面唯一的区别就是继承关系是在Student原型对象添加方法之后确定的,尽管student实例和Person的原型对象依然在同一条原型链上,但是会用Person原型对象中的属性和方法覆盖掉Student原型对象中的属性和方法,导致输出和前面的不一样

    原型链弊端:
    (1)、针对属性值是引用类型的情况,当某一个实例对象改变该共享属性时,其它实例也会随之改变

    function Person() {
      this.course = ['chinese', 'math'];
    }
    function Student() {
    
    }
    Student.prototype = new Person();
    
    var student1 = new Student();
    
    console.log(student1.course);      // ["chinese", "math"]
    
    student1.course.push('english');
    var student2 = new Student();
    console.log(student2.course);      // ["chinese", "math", "english"]

    (2)、创建子类型的实例时,不能向父类型的构造函数中传递参数。实际上就是一旦给父类型的构造函数传递参数,就会影响所有的实例对象

    function Person(name, age) {
      this.name = name;
      this.age = age;
    }
    function Student() {
    
    }
    // 此处调用父类型的构造函数是需要传参的
    Student.prototype = new Person('Bob', 12); 
    
    var s1 = new Student();
    var s2 = new Student();
    
    console.log(s1.name);     // Bob
    console.log(s2.name);     // Bob

    4-2、借用构造函数

    用于解决原型中包含引用类型值所带来的问题

    基本思想是在子类型构造函数中调用父类型的构造 函数,通过call()或apply()方法在(将来)新创建的对象上执行构造函数

    function Person(name, age) {
      this.name = name;
      this.age = age;
      this.say = function() {
        console.log(`My name is ${this.name}, I'm ${this.age} years old`)
      }
    }
    
    function Student(name, age, grade) {
      Person.call(this, name, age);
      this.grade = grade;
    }
    function Teacher(name, age, height) {
      Person.apply(this, [name, age]);
      this.height = height;
    }
    
    var student = new Student("Jim", 9, 4);
    var teacher = new Teacher('Mr Lee', 33, 1.75);
    
    student.say();  // My name is Jim, I'm 9 years old
    teacher.say();  // My name is Mr Lee, I'm 33 years old

    通过这种方式,子类型不仅可以继承父类型,还可以向父类型构造函数传递参数

    如何解决原型中包含引用类型值带来的问题?

    function Person() {
      this.course = ['chinese', 'math'];
    }
    function Student() {
      Person.call(this);
    }
    
    var s1 = new Student();
    
    console.log(s1.course);       // ["chinese", "math"]
    s1.course.push('english');
    console.log(s1.course);       // ["chinese", "math", "english"]
    
    var s2 = new Student();
    console.log(s2.course);       // ["chinese", "math"]

    利用call方法将this绑定到了实例对象,所以即便修改了引用类型的值,也只是在实例对象的作用域范围内,不影响其他实例

    弊端:方法都在构造函数中定义,如果在父类型的原型对象中定义方法,对子类型还是不可见的

    function Person(name, age) {
      this.name = name;
      this.age = age;
    }
    Person.prototype.say = function() {
      console.log(`My name is ${this.name}`);
    }
    
    function Student(name, age) {
      Person.call(this, name, age);
    }
    
    var s = new Student("Jim", 2);
    s.say();     // Uncaught TypeError: s.say is not a function

    4-3、组合继承

    就是将原型链和借用构造函数两种方法组合在一起实现继承,这也是JS中最常用的继承模式

    思路:使用原型链实现对原型属性和方法的继承,通过借用构造函数实现对实例属性的继承

    function Person(name) {
      this.name = name;
      this.course = ['chinese', 'math'];
    }
    Person.prototype.say = function() {
      console.log(`My name is ${this.name}`);
    }
    
    function Student(name, age) {
      Person.call(this, name);     // 继承实例属性
      this.age = age;
    }
    
    // 继承原型属性和方法
    Student.prototype = new Person();
    
    // 添加子类方法
    Student.prototype.sayAge = function() {
      console.log(`I'm ${this.age} years old`);
    }
    
    // 重写父类方法
    Student.prototype.say = function() {
      console.log(`My name is ${this.name}, I'm ${this.age} years old`);
    }
    
    var s1 = new Student('Bob', 8);
    
    // 修改引用类型值的属性
    s1.course.push('english');
    
    s1.sayAge();                  // I'm 8 years old
    s1.say();                     // My name is Bob, I'm 8 years old
    console.log(s1.course);       // ["chinese", "math", "english"]
    
    var s2 = new Student('Jim', 11);
    console.log(s2.course);       // ["chinese", "math"]
  • 相关阅读:
    php读取excel文件的实例代码
    PHP连接局域网MYSQL数据库的实例
    一个经典实用的iptables shell脚本
    PHP中strtotime函数使用方法分享
    php strtotime 函数UNIX时间戳
    解析php时间戳与日期的转换
    有关Mysql连接问题
    PHP获取时间日期的多种方法
    PHP引用符&的用法详细解析
    PHP获取与操作php.ini文件的几个函数示例
  • 原文地址:https://www.cnblogs.com/rogerwu/p/10970848.html
Copyright © 2011-2022 走看看