最近刚步入es6,上来就给我整面向对象,梦回java实验。。。
功能需求:
- 点击nav栏的各个版块可以切换,同时下面的内容也在跟随变化。
- 增加nav的版块功能,点击右侧的加号即可实现。
- 删除nav的版块功能,点击版块右上角的x号就可以删除。
- 编辑功能。双击nav和下方文本区都可以进行修改。
为什么是面向对象?
实际上,面向对象的好处不用多说,可维护性高,代码复用性强,低耦合......
但是刚刚接触写起来还是挺麻烦的。。。这也是js的第一个面向对象案例。
之前的所有案例都是面向过程的写法,怎么说呢,写过c的懂得都懂,一大串,根本没有模块化。
采用面向对象,就带来一个很棘手的问题,this的指向,接下来每一个迷惑的指向我都会分析一下。
接下来我会分为问题来循序渐进的进行整个过程的梳理
问题一:用class声明类并且完成constructor构造方法
在html中我们用的id选择器给整个tab栏命名的,所以实例化对象我们传进去的也是id。
var tab = new Tab('#tab');
接着我们的构造器constructor(id){ }
我们是在构造器里面获得dom元素的,因为当new的那一刻,就会调用构造器,我们就要获得这些dom元素。
所以你会看到接下来的获取dom元素:
constructor(id) {
that = this;
// 通过id获取元素
this.main = document.querySelector(id);
this.add = this.main.querySelector('.tabadd');
// 获取li的父元素ul
this.ul = this.main.querySelector('.fisrstnav ul')
// 获取section的父元素 fsection
this.fsection = this.main.querySelector('.tabscon');
this.init();
}
你很好奇,为什么还有that?为什么全都是this?为什么还有init() ?
接下来来到下面的问题
问题二:为什么全都是this?
js中,在类的声明内部,只要是公共的属性和方法,都必须加上this,否则就会报错。
很明显,这里的this是在constructor里面,因此指向实例化对象,就是当前的tab,这样就好理解了。
为什么我要强调this的指向?
关于类中this的指向,大致分为 在构造方法中this指向将要实例化的对象 和 在方法中this指向方法的调用者。
后面有的地方,你需要的不是调用这个方法的元素,而是实例化对象,那么就不能用this,否则会报错的。
这就引出下一个问题
问题三:为什么有that?
实际上这个that,大家也看到了,是在类的constructor的第一句赋值的:
that = this;
但是这个that的声明 var that; 是在js的第一句,因此是全局变量。它也必须是全局变量,因为后面要很频繁的用它。
之后再用到that的时候,说明我们想要的是实例化对象,即整个tab,而不是指向调用当前方法的这个元素
问题四:init( ) 函数是干嘛的
顾名思义,是用来初始化操作的。
对于这个tab栏,有很多的状态栏要点击,里面有很多的span要点击,有很多的x号,有很多的section要点击,所以我们在constructor里面获取的时候,获取的都是伪数组,因此我们要用for循环给每一个都添加我们想要的事件,比如onclick,并且绑定相应的方法。
通俗来说,就是把for循环的事件绑定封装在了这个函数里面,方便调用。
注意,这个init属于谁呢?当然是属于实例化对象,整个tab,所以其他地方在调用的时候,都是that.init( ) !!!
init() {
this.updateNode();
for (var i = 0; i < this.lis.length; i++) {
this.lis[i].index = i;
this.lis[i].onclick = this.toggleTab;
this.removes[i].onclick = this.delTab;
this.spans[i].ondblclick = this.editTab;
this.sections[i].ondblclick = this.editTab;
}
// 点击加号 执行addtab方法
this.add.onclick = this.addTab;
}
问题五:nav栏的切换 toggleTab()
我们用toggleTab( ) 来实现切换功能,本质上就是当前的栏改变样式,并且下面的section的内容也随着变化。
这就要求我们给每一个 li 一个专门的索引号,好比自定义属性。这个在init函数里面就已经实现了。
注意,是谁调用toggleTab( )? 是每一个小li
所以在toggleTab里面,我们利用排他思想,把其他元素的样式给清楚掉,留下我自己。
// 1. 切换tab栏功能
toggleTab() {
that.clearClass(); // 让实例对象调用清除样式 如果是this,就是当前li调用了,显然不对
this.className = 'liactive';
that.sections[this.index].className = 'conactive';
};
clearClass() { //
for (var i = 0; i < this.lis.length; i++) {
this.lis[i].className = '';
that.sections[i].className = '';
}
}
可以看到,调用sections的是that,因为只有实例化对象tab可以调用sections,相当于父子关系。
然后再通过this.index 获得当前li的索引index值,把当前的liactive和conactive样式给它们。
这里封装clearclass,也就体现了面向对象的封装特性,细节决定成败。
问题六:nav栏的增加 addTab( )
同样的,我们把增加功能封装到addTab( )里面实现。
我们每点击一次右边的加号,就要创建一个新的版块,并且新的板块有liactive和conactive样式。
这要求我们给加号绑定点击事件,然后调用addTab( )。
然后我们写addTab()里面的内容。
首先还是clearclass,把其他元素的样式清掉,确保聚焦在新添加的元素身上。
然后调用insertAdjacentHTML这个API,不知道为什么这么长vscode还不提示?
这个API的好处就是,以往可能是document.createElement(),然后用innerHTML往里面添加内容,但是对于这个案例呢,里面内容比较多,不好添加。而这个API,就可以用字符串的形式,把整个html样式复制过来就可以了。
在复制的同时呢,就已经加上了liactive和conactive。然后采用beforeend的添加方式,放到父元素里面子元素的最后一个。
API的相关解释可以查阅mdn。
最后一句话点睛之笔:that.init( );
为什么呢?
因为当我们添加了新的板块之后,原先的直接在constructor里面获取的lis伪数组length已经不够了!
所以我们要不断地更新这些涉及到增删的元素的数目。
因此引出了我们的新的封装函数updateNode()
// 因为我们点击删除和添加,是动态的,所以需要一直获得最新的元素个数,对于这样的元素我们放到update函数里面
updateNode() {
this.lis = this.main.querySelectorAll('li');
this.sections = this.main.querySelectorAll('section');
this.removes = this.main.querySelectorAll('.icon-guanbi');
this.spans = this.main.querySelectorAll('.fisrstnav li span:first-child');
}
可以看到,涉及到增加和删除的lis和sections的dom获取,都在updateNode里面。
那怎么实现更新效果呢?
简单啊,把updateNode放在init里面,因为init是给这些lis什么的绑定事件的,他俩放一起正好!
然后addTab()函数结束时,在最后一句写上that.init(),就达到了更新lis等数目的效果!
妙不可言~
接下来的删除原理也就明白了。
问题七:nav栏的删除 delTab( )
当点击nav栏每个板块的右上角的x号,就可以完成删除。
但我们不仅要完成删除功能,还有三个超级无敌细的细节:
1 > 当我们删除当前li之后,前一个li自动变成选定状态。
2 > 当我们删除的 li 不与当前 li 紧挨的时候,不执行上述效果。
3 > 点击x号,不会导致当前版块被选中
对于问题3>,这实际上是一个,不怎么容易被发现的冒泡问题。
x号是一个span,它是 li 的孩子,而 li 也有点击事件,因此你点击x号,自然就会被 li 捕捉,从而当前 li 被选中,所以我们要阻止冒泡:e.stopPropagation(); 用的是事件对象e
开始删除工作:
谁调用的delTab()?是x号这个小span,我想获得我要删除的li的序号,怎么做?
没错,还是dom操作,var index = this.parentNode.index 即可。
然后调用remove()方法,就可以删除指定index的元素了。
别忘了!!!调用that.init( ) ,更新当前的数目。
对于问题1>,想的巧一点,既然我们获得了当前的index,我们想对前一个进行操作,就index--;
然后调用前一个的点击事件不就好了吗???
但是总有index < 0 的时候,不加判断就会出错。所以我们用一个&&运算
that.lis[ index ] && that.lis[ index ].click( );
如果存在,就调用click()。
对于问题2>,想一下,如果删除之后,页面还有选中的,我就不要这个问题1的效果了;否则,我就要。那一句话就可以解决了
if( document.querySelector( '.liactive' ) ) return;
// 3. 删除tab栏功能
delTab(e) {
e.stopPropagation(); // 阻止冒泡,防止点击span导致父亲li捕获孩子span的点击事件
var index = this.parentNode.index;
// console.log(index);
that.lis[index].remove();
that.sections[index].remove();
that.init(); // 删除之后 更新当前元素个数
// !!! 当我们删除了选中的li之后,让它的前一个自动处于选定状态,通过调用前一个的手动点击事件
// 但是这时候有个bug,如果我删除的不与当前选定框相邻,我不希望执行这个效果,所以需要判断一下。
// 如果删除之后,当前页面有选中的,那就不执行,return就可以;否则就执行
if (document.querySelector('.liactive')) return;
index--;
that.lis[index] && that.lis[index].click();
};
问题八:nav栏和下方section的内容修改
如果不给input文本输入,那nav栏和下面的section内容都是纯文字,双击只会选中。
所以我们要双击的时候,给它一个input,光标离开的时候,再把input里面的内容给innerHTML。
注意,这里有一个我想不太懂的地方,给了span一个input,就变成它的子元素啦?
editTab() {
var str = this.innerHTML; // 把原先的文本框内容存下来
this.innerHTML = '<input type = "text">';
var input = this.children[0];
input.value = str;
input.select(); // 让里面的文字自动处于选定状态
// 失去光标时,把当前input的value给span的innerhtml即可
input.onblur = function() {
this.parentNode.innerHTML = this.value;
}
// 按下回车也可以实现把value值给span的innerhtml
input.onkeyup = function(e) {
if (e.keyCode === 13) { // 注意 keycode中的c 要大写!!!
this.blur();
}
}
};
这样整个tab栏的制作就完成了。