105:继承

142 阅读10分钟

继承

面向对象语言的三大特征之一:继承。 继承在JS中是一个非常重要的概念,也是看一个程序员水平的重要标杆之一。 本章知识点:

  • prototype原型对象
  • 静态方法与实例方法
  • constructor构造函数
  • 原型链
  • 包装对象
  • 常见的几种继承方式(面试)

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

当然上面的代码仅仅只是为了演示prototype的功能,下面我们正式为Array数组类添加一个max() 方法,用于获取当前数组中所有数字的最大值。

    Array.prototype.max = function(){
        if(!this.length) return NaN;
        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 是站在构造函数的角度讨论原型属性,是构造函数创建的对象的原型对象。 注意:其实构造函数的prototype和对象的proto是一个东西。

    cat.__proto__ === Animal.prototype;// true

注意:由于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(char){
        let reg = new RegExp(char,'g');
        let count = 0;
        while(Array.isArray(reg.exec(this))==true){
            count++;
        }
        return count;
    }
    'hello'.getCharCount('l');// 2
 练习:
 为数组添加一个max方法,取出数组中的最大值
 
 为数组添加一个min方法,取出数组中的最小值

constructor构造器

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

    function Student(){}

    let stu = new Student();

    stu.prototype.constructor; // function Student(){}

因为constructor属性是由对象的原型对象提供的,所以对象可以直接访问该属性。 我们借此可以通过函数的"name"属性来获取当前对象的"类"名。

    stu.constructor.name;// Student

判断一个变量是否是某种类型,之前我们说了 typeof 、Object.prototype.toString.call()

学了原型之后,我们还可以用那些呢

Array.isArray()

arr.constructor.name === 'type'

instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

原型链

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'

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

所谓“包装对象”,就是将三种值类型相对应的Number,Boolean,String三个原生对象。这三个原生对象可以把原始类型的值变成对象。

我们直接使用一个字符串调用方法的时候其实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非常勤劳? 没错,JS语言经常会帮助我们完成一些费心劳神的工作,这种机制是一把双刃剑,我们需要牢记于心。明白这种机制我们会省心省力,否则就会被"坑"。

常见的几种继承方式

对于JS的继承,是我们工作中提高工作效率的非常实用且高效的一种手段。也是面试中必问的一个知识模块。 下面就针对学习和开发中常见的几种继承进行学习与探讨。

  • 原型式继承
  • 构造函数式继承
  • 拷贝式继承
  • hasOwnProperty():检测属性

原型式继承

原型式继承指的是:将父类的实例作为子类的原型。

    // 父类
    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对象的方法。 或者说 执行B 但是将B的this指向绑定到A上

            var name = "小李",age = 23;
            var obj = {
                name: "小陈",
                age: '11',
                myFun: function(){
                    console.log(this);
                    console.log(obj.name+"的年龄是"+this.objAge);
                    console.log(obj.name+"的年龄是"+this.age);
                }                                            
            }
            console.log(obj.objAge);
            
            
            var db = {
    		        name: '啊啊啊',
    		        age: 99
            }
            
            
            obj.myFun.call(db);    // 德玛年龄 99
            obj.myFun.apply(db);    // 德玛年龄 99
            obj.myFun.bind(db)();   // 德玛年龄 99
    
  • call():调用一个对象的一个方法,用另一个对象替换当前对象。例如:B.call(A, args1,args2);即A对象调用B对象的方法。

    /*apply()方法*/
    function.apply(thisObj[, argArray])

    /*call()方法*/
    function.call(thisObj[, arg1[, arg2[, [,...argN]]]]);

当然,仅仅靠上面的基本解释与代码还是不能理解它们是做什么的。 下面我们看下它们的基本用法。

    // 求和
    function getSum(num1,num2){
        return num1+num2;
    }
    // 求差
    function getDiff(num1,num2){
        return num1-num2;
    }

    // getDiff调用getSum, 特点:里面调用外面
    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){
        // 使用call()方法改变了this指向
        Person.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
    };
    // 浅拷贝,直接赋值拷贝
    let obj2 = obj1;
    obj2.height = '180cm';

    obj1;// {name:'tom',age:20,height:'180cm'}
    obj2;// {name:'tom',age:20,height:'180cm'}

没错,所谓的浅拷贝跟我们之前学过的数组直接赋值是一回事,实际上就是把obj1的地址值传给了obj2。

深拷贝

    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);

hasOwnProperty():检测属性