一、初步实现选项卡
html
- 页卡一
- 页卡二
- 页卡三
内容一
内容二
内容三
css
* {margin: 0; padding: 0; font-family: "\5FAE\8F6F\96C5\9ED1", Helvetica, sans-serif; font-size: 14px;-webkit-user-select: none;}
ul,li {list-style: none;}
.box {width: 500px; margin: 20px auto;}
.box ul {position: relative; top: 1px; }
.box ul:after{content: ""; display: block; clear: both;}
.box ul li {float: left; margin-right: 15px; width: 100px; height: 30px; border: 1px solid green; line-height: 30px; text-align: center; cursor: pointer;}
.box ul li.selected {background: lightblue; border-bottom-color: lightblue}
.box div{height: 150px; line-height: 150px; background: lightblue; border: 1px solid green; text-align: center; display: none; }
.box div.selected{display: block;}
基础JavaScript
var tabFir = document.getElementById('tabFir'),
oLis = tabFir.getElementsByTagName('li'),
oDivs = tabFir.getElementsByTagName('div');
function changeTab(n) {
for (var i = 0; i < oLis.length; i++) {
oLis[i].className = null;
oDivs[i].className = null;
}
oLis[n].className = 'selected';
oDivs[n].className = 'selected';
}
错误绑定点击事件的方式
for (var i = 0; i < oLis.length; i++) {
oLis[i].onclick = function () {
changeTab(i);
}
}
以上方式绑定点击事件是达不到效果的,因为JavaScript中所有的事件绑定都是异步编程的,开始我们只是给元素的点击行为绑定了一个方法,但是需要手动点击才会执行这个方法,再次期间,不会干等着点击,会继续执行下一次循环,当点击的时候,循环早已结束。
在给元素绑定事件的时候,绑定的这个方法还只是定义部分,此时方法中存储的都是字符串,此时我们看到的i只是一个字符。
当点击的时候,执行对应的绑定方法,形成一个私有的作用域A,在A中会使用到变量i,而i不是自己私有的,是上级作用域window下的i,此时window下的i已经变为oLis.lenght。
解决方式一:使用自定义属性
要操作(获取/修改)当前元素的某个值,但是还不想受执行顺序和环境的影响,我们最简单的方式就是把其放在自己的自定义属性上。
for (var i = 0; i < oLis.length; i++) {
oLis[i].index = i;
oLis[i].onclick = function () {
changeTab(this.index);
}
}
解决方式二:使用闭包
for (var i = 0; i < oLis.length; i++) {
~ function (index) {
oLis[i].onclick = function () {
changeTab(index);
}
}(i);
}
或者
for (var i = 0; i < oLis.length; i++) {
oLis[i].onclick = (function (index) {
return function () {
changeTab(index);
}
})(i);
}
解决方式三:使用ES6中的let声明循环变量i
for (let i = 0; i < oLis.length; i++) {
oLis[i].onclick = function () {
changeTab(i);
}
}
使用let,声明的变量仅在块级作用域内有效。
上面代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。
二、深入剖析选项卡
以上的选项卡只能算是一个demo,是不能真正用于项目中的,因为获取切换的div用的是getElementsByTagName:
该方法是获取某个容器中的所有的div,如果在某个选项卡中也有div元素呢:
此时用就失效了,所以我们可以用到DOM操作和样式操作库的封装中封装的DOM库,并且可以处理的更优雅:
var tabFir = document.getElementById('tabFir'),
tabFirst = utils.firstChild(tabFir);
var oLis = utils.children(tabFirst);
for (var i = 0; i < oLis.length; i++) {
oLis[i].onclick = function () {
// --> 首先把兄弟元素的selected样式都移除掉
var curSiblings = utils.siblings(this);
for (var i = 0; i < curSiblings.length; i++) {
utils.removeClass(curSiblings[i], 'selected');
}
// --> 再让当前点击这个元素有选中的样式
utils.addClass(this, 'selected');
// --> 再让当前的这个li父亲元素的所有的弟弟元素中(三个div)和当前点击的这个li索引相同的有选中的样式,其余的移除选中样式
var index = utils.index(this);
var divList = utils.nextAll(this.parentNode);
for (i = 0; i < divList.length; i++) {
i === index ? utils.addClass(divList[i], 'selected') : utils.removeClass(divList[i], 'selected');
}
}
}
三、简单封装
假如一个页面中有多处用到选项卡:
这时候,如果是一个个的选项卡去处理就太麻烦了,也太冗余了,正确的做法是将选项卡的代码封装,让其变成通用的组件,可用于所有的符合结构的选项卡。
实现一个选项卡封装:我们可以分析出,只要多个选项卡的主体结构一样,那么每一个实现的思想都是一模一样的,唯一不一样的就是最外层的盒子不一样。
tab.js
~function () {
/**
* 封装一个选项卡的插件,只要大结构保持统一,以后实现选项卡的功能,只需要调取这个方法执行即可实现
* @param container 当前要实现选项卡的这个容器
* @param defaultIndex 默认选中项的索引
*/
function tabChange(container, defaultIndex) {
var tabFirst = utils.firstChild(container),
oLis = utils.children(tabFirst),
divList = utils.children(container, 'div');
// --> 让defaultIndex对应的页卡有选中的样式
defaultIndex = defaultIndex || 0;
utils.addClass(oLis[defaultIndex], 'selected');
utils.addClass(divList[defaultIndex], 'selected');
// --> 实现具体的切换功能
for (var i = 0; i < oLis.length; i++) {
oLis[i].onclick = function () {
var curSiblings = utils.siblings(this);
for (var i = 0; i < curSiblings.length; i++) {
utils.removeClass(curSiblings[i], 'selected');
}
utils.addClass(this, 'selected');
var index = utils.index(this);
for (i = 0; i < divList.length; i++) {
i === index ? utils.addClass(divList[i], 'selected') : utils.removeClass(divList[i], 'selected');
}
}
}
}
window.icemanTab = tabChange;
}();
使用:
var box1 = new icemanTab(boxList[0] , 0);
var box2 = new icemanTab(boxList[1] , 1);
var box3 = new icemanTab(boxList[2] , 2);
封装后的方法还加入默认选中。
四、利用事件委托再次封装
在上面的封装中,tab栏的点击是在每个li上加点击事件,我们这里使用事件委托的方式,可以让效率提高2~3倍。
// --> 使用事件委托来优化我们的点击操作
tabFirst.onclick = function (e) {
e = e || window.event;
e.target = e.target || e.srcElement;
// --> 说明我当前点击的是li标签
if (e.target.tagName.toLowerCase() === 'li') {
detailFn.call(e.target, oLis, divList);
}
}
function detailFn(oLis, divList) {
// this --> 当前点击的这个li
var index = utils.index(this);
utils.addClass(this, 'selected');
for (var i = 0; i < divList.length; i++) {
i === index ? utils.addClass(divList[i], 'selected') : (utils.removeClass(divList[i], 'selected'), utils.removeClass(oLis[i], 'selected'));
}
}
五、使用面向对象的方式最后封装
~function () {
function tabChange(container, defaultIndex) {
return this.init(container, defaultIndex);
}
tabChange.prototype = {
constructor: tabChange,
// 按照索引来设置默认选中的页卡
defaultIndexEven: function () {
utils.addClass(this.oLis[this.defaultIndex], 'selected');
utils.addClass(this.divList[this.defaultIndex], 'selected');
},
// 事件委托实现绑定切换
liveClick: function () {
var _this = this;
this.tabFirst.onclick = function (e) {
e = e || window.event;
e.target = e.target || e.srcElement;
// --> 说明我当前点击的是li标签
if (e.target.tagName.toLowerCase() === 'li') {
_this.detailFn(e.target);
}
}
},
detailFn: function (curEle) {
// this --> 当前点击的这个li
var index = utils.index(curEle);
utils.addClass(curEle, 'selected');
for (var i = 0; i < this.divList.length; i++) {
i === index ? utils.addClass(this.divList[i], 'selected') : (utils.removeClass(this.divList[i], 'selected'), utils.removeClass(this.oLis[i], 'selected'));
}
},
// 初始化,也是当前插件的唯一入口
init: function (container, defaultIndex) {
this.container = container || null;
this.defaultIndex = defaultIndex || 0;
this.tabFirst = utils.firstChild(this.container);
this.oLis = utils.children(this.tabFirst);
this.divList = utils.children(this.container, 'div');
this.defaultIndexEven();
this.liveClick();
return this;
},
};
window.icemanTab = tabChange;
}();
个人公众号(icemanFE):分享更多的前端技术和生活感悟