继承

104 阅读9分钟

面对对象语言的三大特征之一:继承

知识点:

  • <span style="color:red;">prototype原型对象</span>

  • <span style="color:red;">静态方法与实例方法 </span>

  • <span style="color:red;">constructor构造器</span>

  • <span style="color:red;">原型链 </span>

  • <span style="color:red;">包装对象</span>

  • <span style="color:red;">常见的几种继承方式</span>

    prototype原型对象

    在每一个构造函数中都拥有一个属性叫做:prototype(原型对象)

    通过prototype来添加新的属性和方法为该构造函数的所有对象所共有。

    function Student(name,age){
            this.name = name;
            this.age = age;
        }
    Student.prototype.study = function(){
            console.log(我是${this.name},我正在学习!);
        };
    let stu1 = new Student('tom',20);
    let stu2 = new Student('jack',22);
    let stu3 = new Student('chris',25);
    
    stu1.study();// 我是tom,我正在学习
    stu2.study();// 我是jack,我正在学习
    stu3.study();// 我是chris,我正在学习
    
    stu1.study === stu2.study === stu3.study;// true

    由该构造函数创建的对象会默认链接到该属性上。

    prototype是一个对象属性,其属性值为对象,称为原型对象。

    prototype的作用:

    • 对象间共享数据。

    • 为“类”增加新的属性、方法,并且新增内容对于页面中已经创建的对象也有效。

    为系统类添加属性和方法

    通过prototype可以为系统类添加一些属性和方法,以达到加强系统类的目的。

    // 为Array数组类添加属性,输出类型
    Array.prototype.type = 'Array';
    
    // 为Array数组类添加方法,输出自身的长度
    Array.prototype.size = function(){
    	return this.length;
    }
    
    let arr = [1,2,3,4,5];
    arr.type;// "Array"
    arr.size();// 5

    为Array数组类添加一个max()方法,用于获取当前数组中所有数字的最大值。

    Array.prototype.max = function(){
       if(!this.length) return NaN; //数组长度为0时
       let max = this[0];
       for( let i=1;i<this.length;i++ ){
          if(typeof this[i] !== 'number') return NaN;  //当数组元素不是数字时
          if(max<this[i]){
              max = this[i];
           }
        }
          return max;
     }

    prototype与proto

    prototype对于一个对象来说是一个隐藏属性

    浏览器为每一个对象都提供了一个属性:__ proto __

    function Animal(){}
    let cat = new Animal();
    
    cat.__proto__;// 直接访问该对象的原型

    proto是站在对象的角度讨论其原型对象。

    prototype 是站在构造函数的角度讨论原型属性,是构造函数创建的对象的原型对象。

    <span style="color:red;">注意:其实构造函数的prototype和对象的proto是一个东西.</span>

    cat.__proto__ === Animal.prototype;// true
     console.log(cat.__proto__)  //{constructor: ƒ}

    由于proto属性是浏览器提供的,在开发中是使用Object类的静态方法:Object.getPrototypeOf();

     Object.getPrototypeOf(cat) === Animal.prototype;// true

    静态方法与实例方法

    在所有的类中都可以用:

    • 静态方法:直接绑定在类上的方法,由类直接调用。

    • 实例方法:绑定在类的prototype上的方法,由该类下的对象调用

    静态方法

    常见的静态方法

    // 判断参数是否为一个数组
        Array.isArray();
    
     // 获取一个对象所有的key
     Object.keys();
    
     // 获取对象的原型
     Object.getPrototypeOf();
     ...   

    静态方法的直接调用者不是对象,而是"类"。 定义一个自己的类和静态方法。

    // 类
    function Str(){
    }
    
    // str类的静态方法
    Str.getSize = function(args){
    return args.length;
    }
    
    Str.getSize('hello');// 5 (由类直接调用)

    实例方法

    实例方法在开发中使用最多。

      // 获取字符串中指定字符出现的次数
     String.prototype.getCharCount=function(char){
     	let reg = new RegExp(char,'g');
     	let count = 0;
     	while(Array.isArray(reg.exec(this))==true){
     		count++;
     	}
     	return count;
     }
        'hello'.getCharCount('l');// 2

    constructor构造器

    每个对象都有一个constructor属性,该属性描述的就是其构造函数。

    function Student(){}  //创建构造函数
    
    let stu = new Student();  //声明一个Student类的对象
    
    stu.prototype.constructor; // function Student(){}

    constructor属性是由对象的原型对象提供,所以对象可以直接访问该属性。

    我们可以通过函数的"name"属性来获取当前对象的"类"名。

    stu.constructor.name;// Student

    原型链

    JS中所有的对象都有自己的原型对象(prototype),而原型对象也拥有自己的原型对象。如果这样一层层上溯,所有对象的原型最终都可以上溯到Object.prototype,也就是Object构造函数的prototype属性。也就是说,所有的对象都继承了Object.prototype的属性。(这也说明了为什么所有的对象都有valueOf和toString方法的原因)。

    Object.prototype对象的原型,最终为null。由于null没有任何属性,所以原型链到此为止。

     function Student(){}
     let stu = new Student();
     
     console.log(Student.prototype);
     console.log(Object.getPrototypeOf(Student.prototype));
     console.log(Object.getPrototypeOf(Student.prototype).constructor);// Object
     console.log(Object.getPrototypeOf(Object.getPrototypeOf(Student.prototype)));// null

    我们可以利用原型链的继承性完成一些子类获得父类能力的操作(结合继承)。

    包装对象

    在讲解包装对象之前我们需要清楚一点,在JS语言中只有对象才能调用方法。

    // 创建一个数组对象(引用数据类型都可以看作对象)
    let arr = [];
    // 数组对象调用方法
    arr.push('hello');
    arr.push('JS');
    arr;// ['hello','JS']

    然而基本数据类型的值并不是一个对象,但是它们也能够调用方法,例如:

    let str = 'hello JS';
    str.charAt(4);// 'o'
    
    let num = 10;
    num.toString();// '10'

    这里就要说到:包装对象了。

    JS中拥有三个包装对象:Number、String、Boolean,可将原始数据类型变成对象

    我们直接使用一个字符串调用方法的时候,其实JS在内部悄悄的把字符串转换成了字符串对象。当字符串方法调用结束时,JS又会悄悄的把字符串对象转换成原始的值类型的String。

    具体步骤如下

    'hello world'.indexOf('o');
    // JS会在内部执行以下操作
    
    // 1,将字符串值转换成字符串对象
    let str = new String('hello world');
    str;// String{"hello world"}
    
    // 2,对象调用方法
    str.indexOf('o');
    
    //3,将字符串对象再转换成基本数据类型的值
    let str = new String('hello world').valueOf();
    str;// 'hello world'

    常见的几种继承方式

    对于JS的继承,是我们工作中提高工作效率的非常实用且高效的一种手段。

    常见的继承方式有:

    • <span style="color:red;">原型式继承</span>

    • <span style="color:red;">构造函数式继承</span>

    • <span style="color:red;">拷贝式继承</span>

    • <span style="color:red;">hasOwnProperty():检测属性</span>

    原型式继承

    原型式继承指的是:<span style="color:red;">将父类的实例作为子类的原型。</span>

    // 父类
    function Person(name){
    	this.name = name||'tt';
    	this.sayHello = function(){
    	alert('hello');
    	}
    }
    Person.prototype.sayGoodbye = function(){
    	alert('goodbye');
    }
    
    // 子类
    function Student(){}
    // 改变子类的原型指向
    Student.prototype = new Person(); //将父类的实例作为子类的原型
    let stu = new Student();
    stu.name;// tt
    Student.prototype.name = 'jack';
    stu.name;// 'jack'
    stu.sayHello();
    stu.sayGoodbye();

    构造函数式继承

    构造函数式继承主要利用了call()和apply()两个方法.

    JavaScript中的每一个Function对象都有一个apply()方法和一个call()方法。

    • apply():调用一个对象的一个方法,用另一个对象替换当前对象。例如:B.apply(A, arguments);即A对象应用B对象的方法。

    • call():调用一个对象的一个方法,用另一个对象替换当前对象。例如:B.call(A, args1,args2);即A对象调用B对象的方法。

    基本用法:

    // 求和
    function getSum(num1,num2){
    	return num1+num2;
    }
    // 求差
    function getDiff(num1,num2){
    	return num1-num2;
    }
    
    // getDiff调用getSum, 特点:里面调用外面(只有两个参数,参数2为数组或类数组对象)
    getSum.apply(getDiff,[10,5]);// 15
    // getSum调用getDiff
    getDiff.apply(getSum,[10,5]);// 5
    
    // getDiff调用getSum, 特点:里面调用外面(可以有多个参数)
    getSum.call(getDiff,10,5);// 15
    // getSum调用getDiff
    getDiff.call(getSum,10,5);// 5

    由以上的代码我们可以发现,这俩函数的特点都是里面调用外面,真正执行的都是call()或apply()方法的调用者。弄明白这俩函数的使用方式后我们就可以实现我们需要的构造函数式继承了。

    // 父类
    function Person(name){
    	this.name = name||'tt';
    	this.sayHello = function(){
    	alert('hello');
    	}
    }
    Person.prototype.sayGoodbye = function(){
    	alert('goodbye');
    }
    
    // 子类
    // 使用父类的构造函数来增强子类,等于赋值父类的实例属性给子类(不用原型)
    function Student(name){
    	Person.call(this); // 使用call()方法改变了this指向
    	this.name = name;
    }
    let stu = new Student('jack');
    stu.name; //'jack'
    stu.sayHello();
    stu.sayGoodbye();

    关于apply方法我们还可以衍生出一些小技巧。

    // 利用数学对象的max方法迅速得到数组中的最大数
    let arr = [10,2,3,8,17,22,8,21,14];
    // 第一个参数null表示不需要改变任何对象的this
    console.log(Math.max.apply(null,arr));

    call()方法和apply()方法的区别

    这两个方法都是用另一个对象替换当前的对象。这两个方法的不同点主要表现在参数上。

    • call():可以有无限制个参数

      • 参数1:新的this对象

      • 参数2,3,4...:其他参数

    • apply():只能有两个参数

      • 参数1:新的this对象

      • 参数2:数组或类数组对象

    拷贝式继承

    拷贝式继承也是我们常用的一种手段,但是拷贝也有区分:

    • 浅拷贝:直接赋值拷贝。

    • 深拷贝:将A对象的属性全部复制到B对象上。

    浅拷贝

    let obj1 = {
    	name:'tom',
    	age:20
    };
    // 浅拷贝,直接赋值拷贝(只是将obj1的地址值传给了obj2)
    let obj2 = obj1;
    obj2.height = '180cm';
    
    obj1;// {name:'tom',age:20,height:'180cm'}
    obj2;// {name:'tom',age:20,height:'180cm'}

    深拷贝

    let obj1 = {
    	name:'tom',
    	age:20
    };
    // 深拷贝,将A对象的属性全部复制到B对象上。
    let obj2 = {};
    for( let key in obj1 ){
    	obj2[key] = obj1[key]
    }
    obj2.height = '180cm'
    console.log(obj1);// {name: "tom", age: 20}
    console.log(obj2);// {name: "tom", age: 20, height: "180cm"}

    深拷贝的好处在于对obj2的操作不会影响到obj1对象,但是上面的代码其实是有bug的。

    let obj1 = {
    	name:'tom',
    	age:20,
    	hobby:['game','eat']
    };
    // 深拷贝,将A对象的属性全部复制到B对象上。
    let obj2 = {};
    for( let key in obj1 ){
    	obj2[key] = obj1[key]
    }
    obj2.hobby.push('basketball');
    console.log(obj1.hobby);// ['game','eat','basketball']
    console.log(obj1.hobby);// ['game','eat','basketball']

    当对象obj1的属性的值不再是基本数据类型时,我们就不能直接简单的复制了,因为简单的复制还是直接搬运别人的地址值。 所以我们需要更加严谨一些,我们可以使用递归。

    let obj1 = {
            name:'tom',
            age:20,
            hobby:['game','eat']
        };
        // 定义递归函数
        function deepClone(obj){
            let objClone = Array.isArray(obj)?[]:{};
            if( obj && typeof obj==='object' ){
                for( let key in obj ){
                    // 如果是引用数据类型,就使用递归
                    if(obj[key] && typeof obj[key]==='object'){
                        objClone[key] = deepClone(obj[key]);
                    }else{
                        // 如果是基本数据类型就直接复制
                        objClone[key] = obj[key]
                    }
                }
            }
            return objClone;
        }
        let obj2 = deepClone(obj1);
        obj2.hobby.push('haha');
        console.log(obj1);
        console.log(obj2);

    更简单的方法

    let obj1 = {
            name:'tom',
            age:20,
            hobby:['game','eat']
        };
    let obj4 = JSON.parse( JSON.stringify(obj1) )
            obj4.hobby.push("haha")
            console.log(obj1);
            console.log(obj4);