这是我参与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岁,家住上海
这种方式的弊端主要有以下几点
- 过于分散
- 所有变量暴漏外界, 不安全
- 易被修改或产生冲突
- 表述含义的变量名无法统一
使用对象封装优化
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);
打印出来的实例对象上有个 __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.
三、面向对象-构造函数使用-注意事项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
2. 实例方式的调用
正确调用
// 正确调用有参数
var p1 = new Person('小明', 18);
p1.run();
错误调用
// 正确调用有参数
var p1 = new Person('小明', 18);
var tmp = p1.run;
tmp();
四、面向对象-构造函数-优化-方案1
我们先总结一下之前的写法的问题所在吧
- 每个实例的方法一般都是相同的;
- 方法本质也是一个对象
- 针对于每个对象, 都会产生一个新的方法对象,造成资源的浪费
对此我们的第一种优化方案:
- 抽取函数对象到全局
- 构造函数内部直接赋值 这个可以算是一个传参的优化吧!
<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. 概念区分
- 实例属性/方法 ------ 构造函数内部, 绑定的属性和方法
- 静态属性/方法 ------ 构造函数自身, 绑定的属性和方法
- 原型属性/方法 ------ 原型对象, 绑定的属性和方法
六、结语
码字不易,如果觉得对你有帮助,觉得还不错的话,欢迎点赞收藏~
当然由于是个人整理,难免会出现纰漏,欢迎留言反馈。