面向对象编程
什么是对象
JS中我们可以理解成对象是一个容器,封装了属性和方法
- 属性:对象的状态
- 方法:对象的行为
在实际开发中,对象是一个抽象的概念,可简单理解为数据(属性)和一些功能(方法)的集合
ES262中,对象定义为无序属性的集合,其属性可以是基本值、对象或者函数等
面向对象编程
将真实世界中各种复杂的关系,抽象为一个个对象,然后由对象之间的分工和合作,完成对真实世界的模拟
面向对象和面向过程
- 面向过程就是亲力亲为,事无巨细我们都要考虑清楚,一旦中间某个环节没有考虑到都可能会发生错误,需要我们面面俱到,步步紧跟,有条不紊
- 面向对象即使找一个对象,通过对象指挥得到结果
- 面向对象是将执行者转变为指挥者
- 面向对象不是面向过程的替代 ,而是面向过程的封装
特性
封装性:把所有面向过程的编程封装到对象中
继承性:
多态性 (抽象):传入不同的参数可能会输出不同的结果
总结
在面向对象编程思想中,每个对象都是功能中心 ,具有明确的分工,可以完成接收信息、处理数据、发出信息等任务
因此,面向对象编程具有灵活性、代码可复用性、容易维护和开发,比起以一系列函数或者指令组成的传统面向过程编程,更适合多人合作的大型软件项目
面向对象和面向过程
//面向过程编程
// var std1 = {
// name: '张三',
// chengji: 98
// },
// std2 = {
// name: '李四',
// chengji: 70
// };
// function go(student) {
// console.log('姓名:' + student.name + ' 成绩:' + student.chengji);
// }
// go(std1);
// go(std2);
//面向对象编程
function Student(name, chengji) {
this.name = name;
this.chengji = chengji;
this.go = function () {
console.log('姓名:' + this.name + ' 成绩:' + this.chengji);
}
}
var std1 = new Student('张三', 98),
std2 = new Student('李四', 70);
std1.go();
std2.go();
面向对象编程设计思想
- 抽象一个Class构造函数
- 根据Class构造函数创建实例(instance)
- 只会实例(instance)得到结果
创建对象的方式
- new Object()构造函数: 很复杂,我们需要分别定义它的各种属性和方法,而且需要多个对象需要书写重复代码
- 对象字面量 = {}:也比较复杂,我们如果需要创建多个对象,需要书写很多重复性代码
- 工厂函数:简单,我们可以很方便地创造对象,但创造出来的对象不是很具象,不能有效分类,因为这些对象都是属于Obiect创造的,我们不能知道生成的这个对象是通过那个构造函数构造的,而且总是需要return出来
- 自定义构造函数:简单,当对象生成后可以得知是由哪个构造函数生成的,我们可以使用
this.constructor
查看是由哪个构造函数生成的
判断对象具体的构造函数类型还是需要使用instanceof
来判断,因为this.constructor是可以更改的,可能会有所改变
构造函数和实例之间的关系
- 构造函数是一个抽象模版,是根据具体事物抽象出来的
- 实例对象就是根据这个模版(构造函数)创建出来的具体对象(根据传入参数不同,可能会形成不同的对象)
- 每一个实力对象都可以通过
constructor
属性指向创建该对象的构造函数(constructor
是实例属性的说法不是很严谨,具体的后面说) - 可以通过
constructor
属性判断实例和某个构造函数之间的关系(这种方法不是很严谨,还是推荐使用instanceof
方法进行判断,后面会解释)
静态成员和实例成员
使用构造函数创造对象时,可以给构造函数和创建的实例添加属性和方法,这些属性和方法叫做成员
实例成员:在构造函数内部,通过this.xxx
创建的成员,在创建对象之后只能通过实例才能调用,不能直接通过构造函数调用
静态成员:添加给函数自身的成员,排除全部通过this创造的成员,只能由构造函数调用,不能通过实例对象调用
function Perope(name, age) {
//实例成员:只有实例对象可以调用
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
}
}
//静态成员,只有构造函数自身可以调用
Perope.sayHello = function () {
console.log('hello');
}
//创建实例
var p1 = new Perope('张三', 19);
// 尝试调用实例成员
console.log(p1.name); //张三
console.log(p1.age); //19
//构造函数无法调用
console.log(Perope.name); //Perope(这里实际不能调用,但是构造函数自带一个name属性,所以会输出Perope)
console.log(Perope.age); //undefined()不能调用
//尝试调用静态成员
Perope.sayHello(); //hello
//实力对象无法调用
p1.sayHello(); //报错
总结:我们可以给构造函数创建成员,只有在构造函数内部使用this.xxx
创建的成员有且仅能由实例调用,而使用构造函数名.xxx
创建的成员实例是无法使用的,只能由构造函数调用
构造函数的问题
浪费内存
如果构造函数内部有多个相同的方法或者属性,假如我们要创建多个对象,那么每个对象都会有这些相同的属性和对象,我们会发现这些方法和属性都是独立群在的(在内存中),所以说如果有大量的实例对象,那么就会非常消耗内存
// 构造函数
function Perope(name, age) {
//实例成员:只有实例对象可以调用
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
}
}
//创建实例
var p1 = new Perope('a', 19);
var p2 = new Perope('b', 20);
//验证p1和p2的sayname方法是否相同
//结果显示flase,那么这两个对象的方法会在内存中占用两块内存,如果实例比较多,同样的方法会在内存中占用很多内存,导致内存浪费
console.log(p1.sayName === p2.sayName); //flase
解决方案
- 方案1:把方法写在全局的函数中,然后在构造函数内部把方法指向这些函数。
- 缺点:如果我们的方法很多,我们就需要在全局创建大量函数,可能会出现一些问题
- 方案2:把函数封装在一个对象内部,在构造函数内部调用这个对象的方法
- 缺点:我们还是想让这些属性和方法归于我们实例对象自己,这样比较简洁,也便于管理
原型对象
使用原型对象可以很好的避免前面构造函数造成的内存浪费的问题
prototype原型对象
在任何函数内部都有一个pertotype属性,这个属性的属性值是一个对象
-
可以在原型对象上添加方法或者属性
-
构造函数的prototype默认都有一个cinstructor属性,指向原型对象所在的函数(构造函数)
-
同构构造函数创建的实例内部会包含一个指针(
_proto_
),这个指针指向生成市里的构造函数内部的prototype -
实例对象可以直接访问原型对象的成员(属性和方法)
在工作中,我们不会使用_proto_
调用方法或者属性,我们会省略_proto_
直接使用实例.方法/属性调用,因为_proto_
不是一个标准的属性,是浏览器根据语法自动生成的
构造函数浪费内存解决方案-原型对象
- js规定,每个构造函数都有一个prototype属性,指向构造函数的原型对象
- 实例对象可以直接或者间接的继承原型对象上面的方法或者属性
因此,我们可以把所有实例对象需要的共享属性和方法定义在原型上
// 构造函数
function Perope(name, age) {
this.name = name;
this.age = age;
}
//把所有实例共享的方法或者属性添加给原型对象
//给原型添加sayName方法
Perope.prototype.sayName = function () {
//这里面的this谁调用指向谁
console.log(this.name);
}
//给原型添加sayAge方法
Perope.prototype.sayAge = function () {
//这里面的this谁调用指向谁
console.log(this.age);
}
//创建两个实例对象
var p1 = new Perope('a', 12),
p2 = new Perope('b', 20);
//执行实例对象的方法,这里省略_proto_
p1.sayAge();
p2.sayName();
//判断一下p1和p2的sayName是不是同一个引用
console.log(p1.sayName === p2.sayName); //true
在工作中,我们可以把所有实例对象私有的属性或者方法放到构造函数上,把公共的方法或者属性添加到原型上
原型链
为什么实例对象可以调用原型上的属性和方法?
当我们调用实例对象的某个方法或者属性时,会优先在实例对象内部寻找,如果对象实例中没有这个方法或者属性,会向上寻找构造函数的原型上的方法或者属性,如果还没有再向上寻找Object构造函数原型上的方法或者属性。
// 构造函数
function Perope(name, age) {
this.name = name;
this.age = age;
//在构造函数上创建一个属性和一个方法
this.num = 2;
this.sayName = function () {
console.log('hello');
}
}
//把所有实例共享的方法或者属性添加给原型对象
//给原型添加sayName方法
Perope.prototype.sayName = function () {
//这里面的this谁调用指向谁
console.log(this.name);
}
//给原型添加sayAge方法
Perope.prototype.sayAge = function () {
//这里面的this谁调用指向谁
console.log(this.age);
}
//在原型上添加一个构造函数内部就有的属性
Perope.prototype.num = 1;
//创建实例对象
var p1 = new Perope('a', 12);
//调用sayName方法,发现执行结果为hello,说明执行的是实例对象本身的sayName方法
p1.sayName();
//同上,也是调用了实例对象本身
console.log(p1.num);
//我们调用实例上没有的方法sayAge,可以发现会可以执行,说明调用了原型上的方法
p1.sayAge();
//我们可以看出来
//当实例对象上和原型对象上有同样的方法的时候,会优先调用实例的方法,不会调用原型的方法
//当我们调用实例上没有的方法的时候,会向上寻找调用原型上的方法
查找机制
每当代码读取某个对象的属性时,会执行一次查找
- 首先搜索实例对象本身,如果在实例中查找到了相应的属性,会返回属性值
- 如果搜索不到,则继续搜索指针指向的原型对象,在原型对象中查找,如果找到了,返回属性值
实例对象读写原型对象成员
读取:
- 自身查找,找到返回
- 找不到找原型,找到返回
- 如果找到原型链末端还没找到,返回undefined
写入:
- 值类型成员写入(
实例.值类型成员 = xxx
):- 会屏蔽掉原型对象,也就是说无法修改原型对象上的属性或者方法,只能添加或者修改自身
- 引用类型成员写入(
实例.引用类型成员 = xxx
):同上 - 复杂类型成员修改(
实例.成员.xx = xxx
):- 现在自身寻找成员,找到修改自身,找不到找原型链,找到则修改原型链上的值,找不到报错
// 构造函数
function Perope(name, age) {
this.name = name;
this.age = age;
}
//给原型添加sayName方法
Perope.prototype.sayName = function () {
//这里面的this谁调用指向谁
console.log(this.name);
}
//给原型添加sayAge方法
Perope.prototype.sayAge = function () {
//这里面的this谁调用指向谁
console.log(this.age);
}
//原型上添加一个对象
Perope.prototype.me = {
num: 1
};
//原型上添加一个值属性
Perope.prototype.sex = 'nan';
//创建一个实例
var p1 = new Perope('zhangsan', 19);
//试图修改原型上的方法
//修改引用
p1.sayAge = function () {
console.log('1111');
}
//修改值
p1.sex = 'nv';
//修改复杂类型成员
p1.me.num = 2;
//打印实例
//我们发现,p1上面新增了sayAge和sex属性,并且属性值和我们试图修改的一样,说明我们修改值或者引用无法修改原型上的
//同时我们发现,原型上p1.me.num的值变成了2,那么证明我们修改复杂类型成员却是可以成功的,并且也不会在实例上新建属性
console.log(p1);
更简单的原型语法
使用对象字面量{} 重新赋值的方式书写,即构造函数.prototype = {}
但要注意一个问题,这样直接修改会导致constructor丢失,我们打印会发现指向了Object,所以我们需要手动将constuctor指向正确的构造函数
// 构造函数
function Perope(name, age) {
this.name = name;
this.age = age;
}
// //给原型添加sayName方法
// Perope.prototype.sayName = function () {
// //这里面的this谁调用指向谁
// console.log(this.name);
// }
// //原型上添加一个值属性
// Perope.prototype.sex = 'nan';
//上面添加原型方法的优化写法
Perope.prototype = {
//手动添加constructor并指向对应构造函数
constructor: Perope,
//直接在对象内部添加属性和方法
sayName: function () {
console.log(this.name);
},
sex: 'nan'
}
var p1 = new Perope('zhangsan', 1);
console.log(p1);
原型对象使用建议
在定义构造函数时,可以根据成员的功能不同,分别进行设置:
- 私有成员:一般是非函数成员,放在构造函数内部
- 共享成员:一般是函数,放在原型中
如果重置了prototype记得修正constructor的指向
内置构造函数的原型对象
所有函数都有prototype属性对象,js中内置的构造函数也有prototype原型对象属性:
- Object.prototype
- Function.prototype
- Array.prototype
- String.prototype
- Number.prototype
- .........
在内置构造函数对象原型上添加方法
- 不允许使用对象字面量的方式直接更改原型对象,因为js内置会被保护起来,不允许直接更改
- 可以使用打点滴调用的方法添加方法,但是我们工作中一般是不允许在内置对象上添加方法的
案例:随机方块
需求:在一个舞台内,让10个小方块随机改变位置,刷新后小方块颜色可辨,不刷新小方块颜色不变
- 这里我们采用面向对象编程,把每个小方块都想象成一个对象,那我们就需要自定义构造函数,使用构造函数构造小方块,小方块内部应该有可以把自己渲染到html结构的方法,还有可以修改自身位置的方法
- 另外,我们还需要一个工具函数集合,可以写在一个对象内部,对象哪需要有能够生成随机数和随机颜色的的方法;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- 引入css样式 -->
<link rel="stylesheet" href="./css/index.css">
</head>
<body>
<!-- 创建容纳小方块的舞台 -->
<div id="box" class="box"></div>
<!-- 引入js代码 -->
<script src="./js/tools.js"></script>
<script src="./js/block.js"></script>
<script src="./js/index.js"></script>
</body>
</html>
/* 清空样式 */
* {
margin: 0;
padding: 0;
}
/* 设置舞台定位,宽高、背景色 */
.box {
position: relative;
width: 800px;
height: 800px;
background: #ccc;
}
/* 设置舞台内部小方块绝对定位 */
.box span {
position: absolute;
}
// 获取元素
var box = document.getElementById('box');
//创建一个数组,容纳创建的实例对象
var blocks = [];
// 循环10次,创建小方块对象
for (let i = 0; i < 10; i++) {
//创建实例对象
var me = new Block(box, {
// 传入随机颜色参数
backgroundColor: Tools.getColor()
});
// 执行创建的实例对象的渲染方法
me.toHtml();
//把创建的实例对象添加进数组
blocks.push(me);
}
//遍历数组里面的所有实例对象
for (let i = 0; i < blocks.length; i++) {
//创建定时器,每1秒一次,执行实例对象中的随机位置方法
setInterval(function () {
blocks[i].chagePosition();
}, 1000)
}
// 创建构造函数,构建小方块
//参数1:小方块的舞台元素
//参数2:小方块的样式属性对象
function Block(father, obj) {
//避免用户没有传入boj,用来做备案
var obj = obj || {};
//创建所有小方块需要的属性
this.width = obj.width || 20;
this.height = obj.heigth || 20;
this.backgroundColor = obj.backgroundColor || 'red';
this.top = obj.top || 0;
this.left = obj.left || 0;
//创建舞台属性,获取舞台(这里必须获取,否则会显示未定义)
this.father = father;
}
//原型对象修改
Block.prototype = {
//手动指向Block构造函数
constructor: Block,
//添加原型方法:渲染
toHtml: function () {
//创建元素节点,并把节点传入构造函数,以供其他方法使用
this.me = document.createElement('span');
//修改元素style行内样式
this.me.style.width = this.width + 'px';
this.me.style.height = this.height + 'px';
this.me.style.backgroundColor = this.backgroundColor;
this.me.style.top = this.top + 'px';
this.me.style.left = this.left + 'px';
//将小方块插入到父元素底部
this.father.appendChild(this.me);
},
//原型方法:随机位置
chagePosition: function () {
//修改自己的left属性(使用Tools随机数(1-舞台宽度/小方块宽度 - 1)*小方块宽度),为了避免小方块重合
this.left = Tools.getNumber(0, this.father.clientWidth / this.width - 1) * this.width;
//同上,修改top值
this.top = Tools.getNumber(0, this.father.clientHeight / this.height - 1) * this.height;
//修改小方块定位属性
this.me.style.top = this.top + 'px';
this.me.style.left = this.left + 'px';
}
}
//创建工具对象,防止随机数方法
var Tools = {
//对象方法:获取指定两个数之间的随机数,包括这两个数
getNumber: function (min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1) + min);
},
//返回随机颜色值
getColor: function () {
return 'rgb(' + this.getNumber(0, 255) + ',' + this.getNumber(0, 255) + ',' + this.getNumber(0, 255) + ')';
}
}
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!