目录
2.2需求二:需求变更:其中某一个实现选项卡点击切换下一页功能
主要内容和目标
主要内容
- 工厂模式
- new运算符
- 构造函数
- 原型prototype
- 面相对象和面相过程编程
- 类和对象
主要目标:
- - 理解面相对象思想
- - 会使用工厂模式
- - 会使用new运算符
- - 会使用构造函数
- - 理解原型
- - 理解类和对象
1.面向对象编程,类和对象:
一、面相过程:注重解决问题的步骤,分析问题需要的每一步,实现函数依次调用;
二、面相对象:面向对象编程是用抽象方式创建基于现实世界模型的一种编程模式;
三、面相对象特性: 抽象、 继承、封装、多态
对象和类:
一、对象:具体的某个事物;(如:小明、叮当猫)
二、类:一类事物的抽象;(如:人类、猫类)
2.面向过程编程
2.1需求一:实现多个选项卡的
- 问题一:如何写?按照以前方式写
- 问题二:如何提高复用性?(函数封装)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.cont div {
display: none;
}
.active {
background: red;
}
.cont .show {
display: block;
}
</style>
</head>
<body>
<div id="wrap1">
<div class="btns">
<button class="active">选卡一</button>
<button>选卡二</button>
<button>选卡三</button>
</div>
<div class="cont">
<div class="show">内容一</div>
<div>内容二</div>
<div>内容三</div>
</div>
</div>
<div id="wrap2">
<div class="btns">
<button class="active">选卡一</button>
<button>选卡二</button>
<button>选卡三</button>
</div>
<div class="cont">
<div class="show">内容一</div>
<div>内容二</div>
<div>内容三</div>
</div>
</div>
<script>
{
//需求:实现多个选项卡
let wrap1 = document.querySelector("#wrap1");
function tab(el){
let btns = el.querySelectorAll(".btns button");
let cont = el.querySelectorAll(".cont div");
btns.forEach((item,index)=>{
item.onclick = function(){
btns.forEach((item,index)=>{
item.classList.remove("active");
cont[index].classList.remove("show");
});
item.classList.add("active");
cont[index].classList.add("show");
};
});
}
tab(wrap1);
let wrap2 = document.querySelector("#wrap2");
tab(wrap2);
}
</script>
</body>
</html>
2.2需求二:需求变更:其中某一个实现选项卡点击切换下一页功能
- 通过传统的传参数来解决 ;逻辑和判断越来越复杂;
- 实现:返还函数解决问题
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.cont div {
display: none;
}
.active {
background: red;
}
.cont .show {
display: block;
}
.next {
margin: 20px 0 50px 0;
}
</style>
</head>
<body>
<div id="wrap1">
<div class="btns">
<button class="active">选卡一</button>
<button>选卡二</button>
<button>选卡三</button>
</div>
<div class="cont">
<div class="show">内容一</div>
<div>内容二</div>
<div>内容三</div>
</div>
<button class="next">第一个选项卡实现下一页</button>
</div>
<div id="wrap2">
<div class="btns">
<button class="active">选卡一</button>
<button>选卡二</button>
<button>选卡三</button>
</div>
<div class="cont">
<div class="show">内容一</div>
<div>内容二</div>
<div>内容三</div>
</div>
<button class="play">开始轮播</button>
</div>
<script>
{
/*
方法参数封装:tab(el,pager=false,player=false)的方式,发现会有很多私有代码比如第一个选卡的下一页功能,
和第二个选卡的自动播放功能,都分别属于第一个和第二个选卡的私有功能,不适合放在整个选项卡切换的tab方法里.
因此需要将tab方法中的私有功能提取出来
*/
//需求:改造选项卡
let wrap1 = document.querySelector("#wrap1");
//第二个参数用于实现第一个选项卡的下一页功能,pager=false不传第二个参数时第二个参数默认为false
function Tab(el){
let btns = el.querySelectorAll(".btns button");
let cont = el.querySelectorAll(".cont div");
btns.forEach((item,index)=>{
item.onclick = function(){
changeTab(index);
};
});
function changeTab(num){
btns.forEach((item,index)=>{
item.classList.remove("active");
cont[index].classList.remove("show");
});
btns[num].classList.add("active");
cont[num].classList.add("show");
}
return changeTab;
}
// Tab(wrap1);
//第一个选项卡私有方法:下一页
let next = wrap1.querySelector(".next");
let btns = wrap1.querySelectorAll(".btns button");
let tab1 = Tab(wrap1);//这里的tab返回的是整个changeTab函数
let num1 = 0;
next.onclick = function(){
num1++;
if(num1>btns.length-1){
num1=0;
}
tab1(num1);
};
let wrap2 = document.querySelector("#wrap2");
let btns2 = wrap2.querySelectorAll(".btns button");
let tab2 = Tab(wrap2);
let num2 = 0;
let playBtn = wrap2.querySelector(".play");
playBtn.onclick = function(){
let timer = 0;
timer = setInterval(()=>{
num2++;
if(num2>btns2.length-1){
num2=0;
}
tab2(num2);
},1000);
};
}
</script>
</body>
</html>
2.3需求三:需求变更:另一个选项卡实现轮播图功能
- 如何灵活的自动播放?—>需要返还函数还需要返还属性:可以通过返还对象来解决;
见需求四示例
2.4需求四:需求变更:多个选项卡分别更改数量
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.cont div {
display: none;
}
.active {
background: red;
}
.cont .show {
display: block;
}
.next {
margin: 20px 0 50px 0;
}
</style>
</head>
<body>
<div id="wrap1">
<div class="btns">
<button class="active">选卡一</button>
<button>选卡二</button>
<button>选卡三</button>
</div>
<div class="cont">
<div class="show">内容一</div>
<div>内容二</div>
<div>内容三</div>
</div>
<button class="next">第一个选项卡实现下一页</button>
</div>
<div id="wrap2">
<div class="btns">
<button class="active">选卡一</button>
<button>选卡二</button>
<button>选卡三</button>
<button>选卡四</button>
<button>选卡五</button>
</div>
<div class="cont">
<div class="show">内容一</div>
<div>内容二</div>
<div>内容三</div>
<div>内容四</div>
<div>内容五</div>
</div>
<button class="play">开始轮播</button>
</div>
<script>
{
/*
工厂模式:生成一个对象;返回对象
*/
//需求:选项卡数量不定——使用工厂模式实现
let wrap1 = document.querySelector("#wrap1");
//将函数和选项卡的个数通过工厂模式返回
function Tab(el){
let obj = {};
let btns = el.querySelectorAll(".btns button");
let cont = el.querySelectorAll(".cont div");
btns.forEach((item,index)=>{
item.onclick = function(){
obj.changeTab(index);
};
});
obj.changeTab = function(num){
btns.forEach((item,index)=>{
item.classList.remove("active");
cont[index].classList.remove("show");
});
btns[num].classList.add("active");
cont[num].classList.add("show");
}
obj.tabNum = btns.length;
return obj;
}
// Tab(wrap1);
//第一个选项卡私有方法:下一页
let next = wrap1.querySelector(".next");
let btns = wrap1.querySelectorAll(".btns button");
let tabObj1 = Tab(wrap1);
let num1 = 0;
next.onclick = function(){
num1++;
if(num1>tabObj1.tabNum-1){
num1=0;
}
tabObj1.changeTab(num1);
};
let wrap2 = document.querySelector("#wrap2");
let btns2 = wrap2.querySelectorAll(".btns button");
let tabObj2 = Tab(wrap2);
let num2 = 0;
let playBtn = wrap2.querySelector(".play");
let timer = 0;
playBtn.onclick = function(){
clearInterval(timer);
timer = setInterval(()=>{
num2++;
if(num2>tabObj2.tabNum-1){
num2=0;
}
tabObj2.changeTab(num2);
},1000);
};
}
</script>
</body>
</html>
3.工厂模式
工厂模式解决了代码复用的问题,
1.但是却没有解决对象识别的问题。即创建的所有实例都是对象即Object类型。(不清楚是哪个对象的实例)
2.没有原型,占用内存。
3.工厂类型必须要有return返回
工厂模式:需要初始化obj对象;给对象中赋值;返回对象;使用时调用工厂模式中的属性或方法
//工厂模式:需要初始化obj对象;给对象中赋值;返回对象;调用工厂模式中的属性或方法
function Factory(){
//初始化原料
let obj = {};
//加工原料
obj.num = 1000;
obj.product = function(){
console.log("生产产品");
}
//出厂
return obj;
}
let obj1 = Factory();
console.log(obj1.num);//1000
obj1.product();//生产产品
- 有没有更好的方式?见下一节构造函数的使用
4.new运算符
new的特点及功能:
- new可以执行函数
- 自动创建空对象;
- 把空对象指向另外一个对象
- this绑定到空对象
- 隐式返还this;
new执行函数:可以写括号,也可以不写,建议写括号
function Tab(){
console.log("Tab...");
}
//new运算符可以执行函数
// Tab();//Tab...
new Tab();//Tab...
// new Tab;//Tab...
如下:
- 把空对象指向另外一个对象:即this可以指向实例化后返回的tab1和tab2。
- new的过程,即new实例化后,this指的是实例化后的对象tab1和tab2;
- 自动创建空对象,把this绑定到创建好的空对象上,隐式返还this:
- 隐式返还this:如果手动return,就会返回手动return的内容,如果没有就会默认返回this
示例:通过new来改造工厂模式
//new运算符可以自动创建空对象,将创建好的空对象和this进行绑定,并隐式返还this
function Tab(){
//这里创建的空对象就是this绑定的对象
// let obj = {};//已经不需要手动创建了
this.num = 1000;
this.product = function(){
console.log("生产产品");
}
// return this;//因为new运算符会隐式返还this,所以不需要手动返回
}
let tab1 = new Tab();
console.log(tab1);//把空对象指向另外一个对象,即new实例化后,this可以指向实例化后的对象tab1。Tab {num: 1000, product: ƒ}
tab1.product();//生产产品
let tab2 = new Tab();
tab2.product();//生产产品
new的原理分析:自定义一个new实例化的过程
- 创建一个空对象;
- 将创建的空对象和构造函数通过原型链进行链接;
- 将构造函数的this绑定到新的空对象上;
- 根据构造函数返回的类型判断:如果是值类型则返回对象,如果是引用类型则返回这个引用类型
//自定义一个new的实例化过程
function MyNew(constructor,...args){
//1.定义一个空对象
let obj = {};
//2.将创建好的空对象和实例化的对象关联起来
const result = constructor.call(obj,...args); //const result = constructor.apply(obj, args)
//3.将构造函数的this指向新的对象的原型链上
obj.__proto__ = constructor.prototype;
//4.根据构造函数返回的类型判断,如果是值类型就返回对象,如果是引用类型,就返回这个引用类型
return result instanceof Object? result : obj;
}
function Tab(){
this.tabNum = 4;
this.product = function(){
console.log("product...")
}
}
//此处实例化MyNew后,this就执行了mynew1。
//tab传入MyNew后,相当于将Tab的this传给了obj,当调用obj时,就得到了tab的this
let mynew1 = new MyNew(Tab);
mynew1.product();//product...
let mynew2 = new MyNew(Tab);
mynew2.product();//product...
5.构造函数
- 构造函数要通过new来调用 this指向
- 约定俗成构造函数首字母大写
6.构造函数性能对比工厂模式
工厂模式:
//工厂模式
function Tab(){
let obj = {};
obj.name = "张三";
obj.hobby = function(){
console.log("打篮球");
}
return obj;
}
let tab1 = Tab();
console.log(tab1);//{name: "张三", hobby: ƒ}
构造函数:
//构造函数
function Tab(){
this.name = "张三";
this.hobby = function(){
console.log("打篮球");
}
}
let tab2 = new Tab();
console.log(tab2);//Tab {name: "张三", hobby: ƒ}
对比发现构造函数:
构造函数写法简单;性能更好(公共空间(原型prototype)存放公共方法)
为什么构造函数的性能会更好?见下一节原型prototype
7.构造函数原型——prototype原型
- - 通过new实例化出来的对象其属性和行为来自两个部分,一部分来自构造函数,另一部分来自原型。
- - 当声明一个函数的时候,同时也申明了一个原型 。
- - 原型本身是一个对象。
- - 对象属性方法查找规则;
- -本身的构造函数由构造函数本身和prototype构成,通过new实例化后,由实例化后的对象和__proto__构成。prototype和__proto__名字不一样但是地址是一样的,即Tab.prototype === tab1.__proto__返回true。
- 原型上有一个预定义属性即constructor指向构造函数本身
- 创建对象,可使用new Object() ,也可以通过Object.create({key:value});进行创建
- 函数也是对象,也会有自己的__proto__。而函数的原型也是一个对象,也会有自己对应的__proto__。即__proto__会不断往下查找__proto__对象,即所谓的原型链。如下图:
为什么构造函数的性能会更好?
- 工厂模式:发现工厂模式下,得到的两个对象tab1和tab2的内存地址是不一样的,所以tab1.hobby === tab2.hobby结果为false
function Tab(){
let obj = {};
obj.name = "张三";
obj.hobby = function(){
console.log("打篮球");
}
return obj;
}
let tab1 = Tab();
let tab2 = Tab();
console.log(tab1.hobby === tab2.hobby);//false
- 构造函数:发现构造函数实例化后得到的两个对象tab3和tab4也是存储在不同的内存空间,内存地址不一样。但是可以通过原型prototype将公共的内容存在公共空间下,这样就能节省内存空间,从而提升性能。
function Tab(){
this.name = "张三";
this.hobby = function(){
console.log("打篮球");
}
}
let tab3 = new Tab();
let tab4 = new Tab();
console.log(tab3.hobby === tab4.hobby);//false
- 使用原型后的tab5和tab6的hobby是存在同一内存空间下的,所以内存地址一致
function Tab(){
this.name = "张三";
}
// 将公共内容存储到共用空间即prototype原型下
Tab.prototype.hobby = function(){
console.log("打篮球");
};
let tab5 = new Tab();
let tab6 = new Tab();
console.log(tab5.hobby === tab6.hobby);//true
console.log(tab5.name === tab6.name);//true因为比较的本身就是基本数据类型,所以也返回true
- 原型上有一个预定义属性即constructor指向构造函数本身。所以,使用公共空间时,只能通过追加方式添加prototype原型,而不能直接覆盖。如果是直接添加 Tab.prototype = {}会将构造函数原有的其他属性或方法覆盖
如下,直接使用Tab.prototype = {}时,已经覆盖了原有的构造函数中的name属性,所以实例化后的tab7再去获取name时,发现已经不存在了。所以使用公共空间prototype原型时必须使用追加的方式
function Tab(){
this.name = "张三";
}
Tab.prototype = {
hobby(){
console.log("打篮球");
}
};
let tab7 = new Tab();
console.log(tab5.name);//发现会报错:Uncaught ReferenceError: tab5 is not defined
怎么理解原型上有一个预定义属性即constructor指向构造函数本身?
let prototype = {
//原型中的预定义属性constructor,执行构造函数本身
constructor:"构造函数"
};
//使用追加
prototype.hobby = function(){
console.log("打篮球");
}
//通过追加方式,会保留原有的constructor 构造函数
console.log(prototype);//{constructor: "构造函数", hobby: ƒ}
//直接覆盖
prototype = function(){
hobby = function(){
console.log("打篮球");
}
}
//会覆盖构造函数的原有属性或方法
console.log(prototype);//ƒ (){hobby = function(){console.log("打篮球");}}
Object.assign()如果是新的内容会进行追加,如果内容重复不会进行追加。
Object.create({key:value});进行创建对象:
//create创建对象
let obj = Object.create({
name:'lmf',
age:18
});
console.log(obj);
8.总结
案例tab选项卡切换:
- 面相过程写法
- 面相对象写法
工厂模式:共性内容方函数里;少数的通过传参(可以传配置也可以传对象)实现;个别的通过返还函数单独处理
使用构造方法改造多个选项卡且选项卡数量不一致的需求:
注意点:
- 构造方法声明时一般只写共用属性,共用方法一般写在原型里;
- 如果是每个实例化对象都需要调用的方法,可以再构造函数声明时就执行;
- forEach方法的返回值为undefined,如果需要循环获取返回值,不能使用forEach循环;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.cont div {
display: none;
}
.active {
background: red;
}
.cont .show {
display: block;
}
.next {
margin: 20px 0 50px 0;
}
</style>
</head>
<body>
<div id="wrap1">
<div class="btns">
<button class="active">选卡一</button>
<button>选卡二</button>
<button>选卡三</button>
</div>
<div class="cont">
<div class="show">内容一</div>
<div>内容二</div>
<div>内容三</div>
</div>
<button class="next">第一个选项卡实现下一页</button>
</div>
<div id="wrap2">
<div class="btns">
<button class="active">选卡一</button>
<button>选卡二</button>
<button>选卡三</button>
<button>选卡四</button>
<button>选卡五</button>
</div>
<div class="cont">
<div class="show">内容一</div>
<div>内容二</div>
<div>内容三</div>
<div>内容四</div>
<div>内容五</div>
</div>
<button class="play">开始轮播</button>
</div>
<script>
{
/*
构造方法实现多个不同数量的选项卡
思路:btns保留为Tab的属性
psFor保留为Tab原型上的方法
可以封装一个添加事件监听的方法去为元素添加各种事件
面向对象的基本思想:尽量少用函数嵌套,降低耦合;构造函数中一般只写属性或者初始化方法,其他方法一般都不写在构造函数里,以节省内存空间
*/
//需求:选项卡数量使用构造方法实现
//构造函数和原型里的this都指的是实例化后的tab1或者tab2对象
function Tab(btns,cont){
this.btns = btns;
this.cont = cont;
this.tabNum = btns.length;
//因为每个选项卡都需要循环,所以写在构造函数中
this.tabFor();
}
//循环选项卡
Tab.prototype.tabFor = function(){
this.btns.forEach((item,index)=>{
item.onclick = ()=>{
//因为都使用箭头函数,所以最后的this,指向最外层作用域的Tab
this.changeTab(index);
};
});
}
//切换选项卡时更改样式
Tab.prototype.changeTab = function(num){
this.btns.forEach((item,index)=>{
item.classList.remove("active");
this.cont[index].classList.remove("show");
});
this.btns[num].classList.add("active");
this.cont[num].classList.add("show");
}
//轮播或下一页时,获取点击切换的index
Tab.prototype.getIndex = function(){
//可以通过this获取到所有实例化后的对象,即可以得到所有选项卡,就能获取到被选中的index
//注意forEach循环的返回值是undefined,不能使用forEach循环返回
for (let i = 0; i < this.btns.length; i++) {
if(this.btns[i].classList.value === "active"){
return i;
}
}
}
//第一个选项卡私有方法:下一页
let wrap1 = document.querySelector("#wrap1");
let btns = wrap1.querySelectorAll(".btns button");
let cont = wrap1.querySelectorAll(".cont div");
let next = wrap1.querySelector(".next");
let tab1 = new Tab(btns,cont);
// console.log(tab1.getIndex());
let num1 = 0;
next.onclick = function(){
num1 = tab1.getIndex();
//这有选项卡1和2点击下一页或轮播私有,其他时候没有,所以不能写在原型上
num1++;
num1 = num1>tab1.tabNum-1?0:num1;
tab1.changeTab(num1);
};
//第一个选项卡私有方法:自动播放
let wrap2 = document.querySelector("#wrap2");
let btns2 = wrap2.querySelectorAll(".btns button");
let cont2 = wrap2.querySelectorAll(".cont div");
let tab2 = new Tab(btns2,cont2);
let playBtn = wrap2.querySelector(".play");
let timer = 0;
playBtn.onclick = function(){
let num2 = 0;
clearInterval(timer);
timer = setInterval(()=>{
num2 = tab2.getIndex();
num2++;
num2 = num2>tab2.tabNum-1?0:num2;
tab2.changeTab(num2);
},1000);
};
}
</script>
</body>
</html>