面向对象-原型链继承(下)

224 阅读5分钟

这是我参与8月更文挑战的第20天,活动详情查看:8月更文挑战

前言

昨天我们主要把继承的知识先告了一段落,今天我们主要搞搞练习,直接开搞。

面向对象-继承(上)

面向对象-继承(中)

一、原型链继承-练习

这版块的练习其实也比较简单,之前的文章都有提到。简单说明一下。

我们这里书写3个构造函数:Animal(动物类)Person(人)Student(学生类)

其中:

  • Animal的实例属性上有name,age;原型方法上有eat,run
  • Person的实例属性上有job,原型方法上有jump
  • Student的实例属性上有className,原型方法上有study

也有同学觉得文字好像不太懂啥意思,图我也准备好了

image.png

动手前咋们先捋一捋,第一点构造函数长啥样?什么是实例属性?什么是原型方法? 不管前2个可能需要想想,但是原型我们应该立马脑子要浮现一个叫prototype的东西。对吧,这样一来代码就好写了。

那就开始写代码了喔,注释都写在代码里了哈。

// ------------- Animal ------------ //

function Animal(name, age) {
    // 实例属性
    this.name = name;
    this.age = age;
}

// 原型方法
Animal.prototype.eat = function () {
    console.log('吃');
};

Animal.prototype.run = function () {
    console.log('跑');
}

// ------------- Person ------------ //

function Person(job) {
    // 实例属性
    this.job = job;
}

// 原型方法
Person.prototype.jump = function () {
    console.log('跳');
};


// ------------- Student ------------ //

function Student(className) {
    // 实例属性
    this.className = className;
}

// 原型方法
Student.prototype.study = function () {
    console.log('学习');
}

继承继承,没继承怎么完整呢?接下来我们就来实现继承

    <!--Animal-->
    <script>
        function Animal(name, age) {
            this.name = name;
            this.age = age;
        }

        Animal.prototype.eat = function () {
            console.log(this.name + '会吃');
        };

        Animal.prototype.run = function () {
            console.log(this.name + '会跑');
        }
    </script>
    <!--Person-->
    <script>
        function Person(job, name, age) {
            // 1. 借助构造函数继承 改变this
            Animal.call(this, name, age);
            this.job = job;
        }

        // 2. 原型链继承 和 寄生式继承
        function Temp() {}
        Temp.prototype = Animal.prototype;

        // Temp实例
        var personPrototype = new Temp();
        // 修改constructor指向
        personPrototype.constructor = Person;
        Person.prototype = personPrototype;

        // 注意注意注意....  这里是Person的原型方法
        Person.prototype.jump = function () {
            console.log(this.name + '会跳');
        };
    </script>
    <script>
        var p = new Person('厨师', '小明', 18);
        console.log(p);
        p.run();
        p.eat();
        p.jump();
    </script>

    <!--Student-->
    <script>
        function Student(className) {
            this.className = className;
        }

        Student.prototype.study = function () {
            console.log('学习');
        }
    </script>

我们来看看结果是怎么样的

image.png

这里我们可以看到 __proto__指向了Animal,即使是Person实例,也可以调用Animalrun方法和eat方法。

二、继承-应用练习(扩展内置对象)

方式1

直接给对象动态添加属性和方法

var arr = [1, 2, 3];
arr.run = function () {
    console.log('跑');
};
arr.run();

var arr2 = [2];
arr2.run();

弊端:

  1. 如果操作很多个对象, 则无法共享
  2. 代码比较冗余

方式2

直接给Array原型对象添加方法

Array.prototype.push = function () {
    console.log('跑');
};

var arr = [1, 2, 3];
arr.push('111');  // 跑
console.log(arr); // [1, 2, 3]

var arr2 = [2];
arr2.push('2222'); // 跑
console.log(arr2); // [2]

弊端: 可能会产生覆盖的情况

为什么push之后输出而不是4呢?arr输出不是 [1, 2, 3, "111"]。这就是直接给Array原型对象添加的弊端了呀,就产生了一个覆盖的操作

image.png

方式3

提供一个新的构造函数

修改构造函数的原型指向为数组的原型对象,为了能够获取数组里面的属性和方法。

问题: 依然会修改数组原型对象内容

优化:

原型对象就是一个对象

可以直接根据Array创建一个对象, 给新创建的函数原型对象进行赋值

function MyArray() {}
MyArray.prototype = new Array();
MyArray.prototype.run = function () {
    console.log('跑');
};

var arr2 = new MyArray();
console.log(arr2); // MyArray()
arr2.run(); // 跑

三、知识点补充

1. 回顾

  • 函数的prototype属性
    • 每个函数都有一个prototype属性, 它默认指向一个Object空对象(即称为: 原型对象)
    • 原型对象中有一个属性constructor, 它指向函数对象
  • 原型对象添加属性(一般都是方法)
    • 函数的所有实例对象自动拥有原型中的属性(方法)

2. call和apply

两个方法是Function的原型对象上面的方法

两个函数的作用是一样的,都可以用来借用方法实现

参数:

  • 第一个参数是调用该方法的对象(函数内部的this绑定的对象)

  • 第二个参数

    • call:参数列表
    • apply:数组 实例:
  • 对象1.方法.call(调用方法的真正的对象,参数1,参数2,参数3);

  • 对象1.方法.apply(调用方法的真正的对象,[参数1,参数2,参数3...])

3.this总结

this -- > 总是指向一个对象

函数的调用方式

  • 001 作为对象的方法来调用 this--->当前的对象

  • 002 作为普通的函数调用 this--->window

  • 003 作为构造函数和new使用 this--->构造函数内部新创建的对象

  • 004 被call或者是apply调用(函数上下文调用) this--->第一个参数

四、继承-拷贝属性

1. 手动拷贝

遍历一个对象的键值对,分别动态的添加到另外一个对象上

var obj = {
    name: 'Jack',
    age: 19
};
var copyObj = {};

for (var key in obj) {
    copyObj[key] = obj[key];
}

obj.name = 'Tom';
console.log(obj); // {name: "Tom", age: 19}
console.log(copyObj); // {name: "Tom", age: 19}

2. Object.assign

语法: Object.assign(参数1, 参数2, 参数3..)

第一个参数是目标对象,后面参数是要拷贝属性的对象

var obj = {
    name: 'Jack',
    age: 19
};
var copyObj = {};

Object.assign(copyObj, obj, {
    address: '北京'
});

obj.name = 'Tom';

console.log(obj); // {name: "Tom", age: 19}
console.log(copyObj); // {name: "Jack", age: 19, address: "北京"}

五、继承-拷贝属性-深拷贝

弊端

如果属性的值是引用类型的,那么子对象和父对象共享一块数据,修改了某个对象对另外一个对象有影响

解决方案

深拷贝/浅拷贝的区别在于对引用值的处理

  • 浅拷贝, 直接拷贝一份地址

  • 深拷贝-拷贝地址对应的具体内容

深拷贝实现思路

  • 01 提供一个函数两个参数(元对象,要拷贝属性的对象)

  • 02 在函数中先检查第一个参数是否有值,如果没有值那么就初始化一个空的对象

  • 03 for..in循环来变量参数2

    • 001 检查当前的属性值是什么类型

    • 002 如果是值类型,那么就直接拷贝赋值

    • 003 如果是引用类型,那么就再调用一次这个方法,去内部拷贝这个对象的所有属性

代码如下:

// 1. 要拷贝的对象
var obj = {
    name: 'Jack',
    age: 18,
    friends: ['小明', '小花'],
    goodFri: {
        name: 'Rose',
        age: 16,
        adress: '北京',
        hobby: [{
            name: '羽毛球'
        }, {
            name: '乒乓球'
        }]
    },
};


function deepCopyObj2NewObj(fromObj, toObj) {
    //  在函数中先检查第一个参数是否有值,
    //  如果没有值那么就初始化一个空的对象
    for (var key in fromObj) {
        var fromValue = fromObj[key];
        if (!isObj(fromValue)) {
            toObj[key] = fromValue;
        } else {
            // 如果是引用类型,那么就再调用一次这个方法,去内部拷贝这个对象的所有属性
            var temObj = new fromValue.constructor;
            deepCopyObj2NewObj(fromValue, temObj);
            toObj[key] = temObj
        }
    }
}

function isObj(obj) {
    return obj instanceof Object;
}

function isArr(obj) {
    return Array.isArray(obj);
    // return Object.prototype.toString.call(obj) === '[object Array]'
}

var newObj = {};
deepCopyObj2NewObj(obj, newObj);

obj.goodFri.hobby[0]['category'] = 'ball';  // 不会被拷贝

console.log('obj ==> ', obj);
console.log('newObj ==> ', newObj);

看看控制台输出结果吧

image.png

到这里练习也搞了,面向对象的继承就算是结束啦!不知道你们有没有收获呢?