面向对象-构造函数使用

245 阅读8分钟

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

前言

上一篇我们基本认识到了什么是面向对象(传送门),今天来看看面向对象的三大特性,构造函数使用的一些注意事项。以及对构造函数进行一些优化

一、面向对象-三大特性

1. 封装

简要的理解就是:把一些比较散的, 单一的值, 有结构的组装成为一个整体,把一些值隐藏在内部, 不暴漏给外界。

比如我们人有姓名,年龄,性别等等,有没有使用面向对象是有区别的。

直接定义变量形式

var name1 = '张三';
var name2 = '李四';
var age1 = 18;
var age2 = 28;
var address1 = '上海';
var address2 = '北京';

console.log(name1);
console.log(name2);

这时候我们要取张三的信息是这样的

var msg = `${name1}今年${age1}岁,家住${address1}`;
console.log(msg); // 张三今年18岁,家住上海

这种方式的弊端主要有以下几点

  1. 过于分散
  2. 所有变量暴漏外界, 不安全
  3. 易被修改或产生冲突
  4. 表述含义的变量名无法统一

使用对象封装优化

var p1 = {
    name: '张三',
    age: 18,
    address: '上海'
};

// 取信息
var msg = `${p1.name}今年${p1.age}岁,家住${p1.address}`;
console.log(msg); // 张三今年18岁,家住上海

相对这个来说,一个人的信息都存在了同一个对象身上,对于后期的维护会方便很多。

2. 继承

获取已经存在的对象已有属性方法的一种方式

var p1 = {
    name: '张三',
    age: 18,
    address: '上海'
};
var p2 = {};

// 继承: 继承资源
for(var key in p1){
    p2[key] = p1[key];
}

console.log(p2); // {name: "张三", age: 18, address: "上海"}

3. 多态

多态表现为:同一操作,作用于不同的对象,会产生不同的解释和行为。

例如: toString() --- 不同的对象, 调用相同的方法, 产生不同的结果

var obj = {name: '张三'};
console.log(obj.toString()); // [object Object]

var arr = [1, 2, 3];
console.log(arr.toString()); // 1,2,3

多用于强类型语言中,JavaScript具备与生俱来的多态特性。

  • 弱类型: 不同类型之间运算, 会存在隐式转换
  • 强类型: 不同类型之间运算, 需要显示的转换为同一个类型进行运算
// swift
// var i = 1; // Int
// var f = 1.1; // float
// i+f

var i = 1; // Int
var f = 1.1; // float
console.log(i + f); // 2.1 float

二、面向对象-构造函数使用-注意事项1

1. 构造函数设置属性和方法

1.1 实例属性/实例方法

都是绑定在使用构造函数创建出来的对象p上; 最终使用的时候也是使用对象p来进行访问;

function Person(name, age, doFunc) {
    this.name = name;
    this.age = age;
    this.doFunc = doFunc;
}

1.2 静态属性/静态方法

概念: 绑定在函数身上的属性和方法

注意:

函数本质也是一个对象, 既然是个对象, 那么就可以动态的添加属性和方法

只要函数存在, 那么绑定在它身上的属性和方法, 也会一直存在

这里啊,我们做个场景模拟记录总共创建了多少个人对象,分别有3种方式,我们做个比对。

(1) 全局变量

    // 1. 设置一个全局变量
    var personCount = 0;

    function Person(name, age, doFunc) {
        this.name = name;
        this.age = age;
        this.doFunc = doFunc;
        personCount++;
    }

    var p1 = new Person('张三', 18, function () {
        console.log('张三在上课!');
    });

    var p2 = new Person('王小二', 18, function () {
        console.log('王小二在放羊!');
    });

    console.log('总共创建了' + personCount + '个人'); // 总共创建了2个人

(2) 实例属性 ❌

注意: 这个方法是不可取的,因为不管你创建多少个,最终都是输出1。

    function Person(name, age, doFunc) {
        this.name = name;
        this.age = age;
        this.doFunc = doFunc;
        
        if(!this.personCount){
            this.personCount = 0;
        }
        this.personCount ++;
    }

    var p1 = new Person('张三', 18, function () {
        console.log('张三在上课!');
    });

    var p2 = new Person('王小二', 18, function () {
        console.log('王小二在放羊!');
    });

    console.log('总共创建了' + p1.personCount + '个人'); // 总共创建了1个人
    console.log('总共创建了' + p2.personCount + '个人'); // 总共创建了1个人

(2) 静态属性/静态方法

    function Person(name, age, doFunc) {
        this.name = name;
        this.age = age;
        this.doFunc = doFunc;

        // 创建静态属性
        if(!Person.personCount){
            Person.personCount = 0;
        }
        Person.personCount++;
    }

    // 创建静态的方法
    Person.printPersonCount = function () {
        console.log('总共创建了' + Person.personCount + '个人');
    };

    var p1 = new Person('张三', 18, function () {
        console.log('张三在上课!');
    });

    var p2 = new Person('王小二', 18, function () {
        console.log('王小二在放羊!');
    });

    var p3 = new Person('王小二xxx', 18, function () {
        console.log('王小二在放羊xxx!');
    });

    var p4 = new Person('xxx王小二xxx', 18, function () {
        console.log('xxxx王小二在放羊xxx!');
    });


    // 输出
    Person.printPersonCount();  // 总共创建了4个人

1.3 概念补充

  • 实例化 ---- 通过构造函数, 构造出对象这个过程
  • 实例 ---- 被构造出来的对象

2. 关于创建出来的对象类型获取

2.1 获得内置对象的类型

  • {}
  • [1,2,3]
var obj = {'name': '张三'};
// console.log(typeof obj);
// console.log(obj.toString());
// console.log(obj.constructor.name); // Object

var arr = [1, 2, 3];
// console.log(arr);
/*console.log(typeof arr);
console.log(arr.toString());
console.log(arr.constructor.name); // Array*/
console.log(Object.prototype.toString.call(arr));

var date = new Date();
// console.log(date);
/*console.log(typeof date);
console.log(date.constructor.name);*/
// Object.prototype.toString.call(date);

2.1 获取根据自己声明的构造函数创建的对象

constructor: 类似于一个产品上,关于厂家的标识

constructor的主要作用是:告诉我们当前对象是由哪个构造函数产生的。

function Person(){ 

}

var p = new Person(); 
console.log(p);

3.png

打印出来的实例对象上有个 __proto__属性,里面就有一个constructor,这里我们可以使用: p.constructor.name,获取到当前对象p的构造函数的名字。在实践开发中,我们可以使用这种方法得知当前对象是由哪个构造函数产生的。

p.constructor.name // Person

3. 关于创建出来的对象类型验证

这里使用 instanceof 进行一个判断

instanceof的原理是: 判断构造函数的原型是否在这个对象的原型链

例子:

[1,2,3] instanceof Object  // true

为什么是true呢?

首先,[1,2,3] 是等价于new Aarray(1, 2, 3),也就是说两者都是Array类型实例,所以[1,2,3]的原型对象为Array.prototype,而Array.prototype这个对象中的一个内部指针(proto),即它的原型指针指向Object.prototype,因此Object.prototype就在[1,2,3]的原型链上,因此结果为true.

4.png

5.png

三、面向对象-构造函数使用-注意事项2

1. 构造函数的调用

function Person (name, age) {
    this.name = name;
    this.age = age;
    
    this.run = function () {
      console.log(this.name + '跑步');
    }

    console.log('this ==> ', this);
}

// 标准调用有参数
var p1 = new Person('小明', 18);
// 标准调用无参数
var p2 = new Person

console.log('p1 == > ', p1); // Person
console.log('p2 == > ', p2); // Person

console.log('----------分割线------------');

// 错误调用
var p3 = Person(); // this 指向 window
console.log('p3 == > ', p3); // undefined

6.png

2. 实例方式的调用

正确调用

// 正确调用有参数
var p1 = new Person('小明', 18);
p1.run();

7.png 错误调用

// 正确调用有参数
var p1 = new Person('小明', 18);
var tmp = p1.run;
tmp();

8.png

四、面向对象-构造函数-优化-方案1

我们先总结一下之前的写法的问题所在吧

  1. 每个实例的方法一般都是相同的;
  2. 方法本质也是一个对象
  3. 针对于每个对象, 都会产生一个新的方法对象,造成资源的浪费

对此我们的第一种优化方案

  • 抽取函数对象到全局
  • 构造函数内部直接赋值 这个可以算是一个传参的优化吧!
<script>
    /**
     * 构造函数
     * @param {object}option
     * @constructor
     */
    function Dog(option) {
        // 属性
        this.name = option.name;
        this.age = option.age;
        this.dogFriends = option.dogFriends;

        // 行为
        this.eat = function (someThing) {
            console.log(this.name + '吃' + someThing);
        };

        this.run = function (someWhere) {
            console.log(this.name + '跑' + someWhere);
        }
    }

    // 产生对象
    var smallDog = new Dog({name: '小花', age: 1, dogFriends: ['球球', '嘎嘎嘎']});
    console.log(smallDog.name, smallDog.age); // 小花 1

</script>

五、面向对象-构造函数-优化-方案2

老样子,总结一下方案1的问题先哈。

1. 方案1的问题

  • ① 全局变量增多,造成污染
  • ② 代码结构混乱,不易维护
  • ③ 函数可以不通过对象直接调用,封装性不好

2. 优化方案-原型对象方法扩展

2.1 什么是原型对象?

  • 每当我们声明了一个函数(本质对象); 那么这个函数上就会被附加很多属性来描述这个函数
  • 其中有个属性 叫 prototype , 是一个引用类型, 指向着的对象, 就被成为原型对象
  • 原型对象中, 有一个属性 叫 constructor , 指向着关联的函数

2.2 原型对象有什么作用?

每次我们通过一个构造函数创建一个对象的时候, 被创建的对象, 就会自动添加一个属性 proto 来指向构造函数的原型对象

那这个指向有什么用?

  • 当我们调用一个对象的属性,或者方法时, 会先到对象内部查找, 如果查找到了, 就直接调用
  • 如果查找不到, 则,根据这条线, 到原型对象上面去查找
  • 原型对象中如果存在该属性或方法,那么就直接使用,如果不存在指定的属性则返回undefined,如果不存在指定的方法则报错 原型对象, 可以说是各个对象公共的区域

2.3 具体步骤

Person.prototype.run = function () {
    console.log(this.name, '跑吧, 熊孩子');
}
  • 给原型对象增加一个方法 ---- 所有的对象, 都可以根据关联的线查找到这个方法
  • 内部的this, 是调用者 修改后的代码如下:
    /**
     * 构造函数
     * @param {object}option
     * @constructor
     */
    function Dog(option) {
        // 属性
        this.name = option.name;
        this.age = option.age;
        this.dogFriends = option.dogFriends;

        // 行为
      /*  this.eat = function (someThing) {
            console.log(this.name + '吃' + someThing);
        };

        this.run = function (someWhere) {
            console.log(this.name + '跑' + someWhere);
        }*/
    }

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

    Dog.prototype.run = function (someWhere) {
        console.log(this.name + '跑' + someWhere);
    };

    // 产生对象
    var smallDog = new Dog({name: '小花', age: 1, dogFriends: ['球球', '嘎嘎嘎']});
    var bigDog = new Dog({name: '大花', age: 11, dogFriends: ['球球', '嘎嘎嘎', 'hh']});

    console.log(smallDog.eat === bigDog.eat);  // true

2.4. 注意事项

2.4.1 如果原型对象和构造对象的属性和方法,冲突,会有什么效果?

答:就近原则

2.4.2 通常在创建对象"之前"设置构造函数的原型对象(提供共享的属性|方法)

2.4.3 访问原型对象的正确方法是 构造函数.prototype 而不是 对象.prototype

2.4.4 设置原型对象的属性和方法

  • 通过原型对象来修改
  • 不要通过对象来修改 ❌

2.5. 概念区分

  • 实例属性/方法 ------ 构造函数内部, 绑定的属性和方法
  • 静态属性/方法 ------ 构造函数自身, 绑定的属性和方法
  • 原型属性/方法 ------ 原型对象, 绑定的属性和方法

六、结语

码字不易,如果觉得对你有帮助,觉得还不错的话,欢迎点赞收藏~

当然由于是个人整理,难免会出现纰漏,欢迎留言反馈。