本文通过Tab选项卡案例,详细演示了如何将面向过程的JavaScript代码重构为面向对象形式。重点解析了构造函数中this指向的变化、事件绑定导致的this丢失问题,以及通过闭包或参数传递解决this指向错误的技巧,最终实现可复用的Tab组件。
本文通过一个具体的Tab选项卡实例,详细讲解如何利用JavaScript面向对象编程思想进行代码重构与实现,供读者参考学习。
Tab选项卡案例

首先展示一个基础的、面向过程的Tab选项卡实现方式。
<!DOCTYPE html>
<html>
<head>
<style>
#tabBox input {
background: #F6F3F3;
border: 1px solid #FF0000;
}
#tabBox .active {
background: #E9D4D4;
}
#tabBox div {
width:300px;
height:250px;
display:none;
padding: 10px;
background: #E9D4D4;
border: 1px solid #FF0000;
}
</style>
<meta charset="utf-8" />
<title>选项卡</title>
<script>
window.onload=function(){
var tabBox = document.getElementById('tabBox');
var tabBtn = tabBox.getElementsByTagName('input');
var tabDiv = tabBox.getElementsByTagName('div');
for(var i=0;i<tabBtn.length;i++){
tabBtn[i].index = i;
tabBtn[i].onclick = function (){
for(var j=0;j<tabBtn.length;j++){
tabBtn[j].className='';
tabDiv[j].style.display='none';
}
this.className='active';
tabDiv[this.index].style.display='block';
};
}
};
</script>
</head>
<body>
<div id="tabBox">
<input type="button" value="主页" class="active" />
<input type="button" value="说说" />
<input type="button" value="日志" />
<div style="display:block;">这是主页内容</div>
<div>这是说说内容</div>
<div>这是日志内容</div>
</div>
</body>
</html>
接下来,我们将逐步把这个过程式的代码改造成面向对象的结构。
第一步,将原本嵌套在函数内部的变量提取到全局作用域,确保没有函数嵌套,仅保留全局变量。经过这一步改写,代码的最终运行效果保持不变。
<script>
//将在嵌套函数里的变量提取到全局中
var tabBtn = null;
var tabDiv = null;
window.onload = function(){
var tabBox = document.getElementById('tabBox');
tabBtn = tabBox.getElementsByTagName('input');
tabDiv = tabBox.getElementsByTagName('div');
for(var i=0;i<tabBtn.length;i++){
tabBtn[i].index = i;
//此处调用函数即可
tabBtn[i].onclick = clickBtn;
}
};
//将嵌套函数提取到全局中
function clickBtn(){
for(var j=0;j<tabBtn.length;j++){
tabBtn[j].className='';
tabDiv[j].style.display='none';
}
this.className='active';
tabDiv[this.index].style.display='block';
};
</script>
第二步,将全局变量转换为对象的属性,将全局函数转换为对象的方法。同时,把window.onload中的初始化代码封装进构造函数,并在window.onload中实例化对象即可。(注:此时直接运行代码可能存在逻辑问题)。
需要特别注意:在构造函数Tab中,this的指向与之前不同。这里是通过new关键字创建对象,因此this指向的是当前实例化的对象(即var tab = new Tab()),而非之前的调用者。
上述代码存在一个关键问题:当我们在Tab的原型上定义clickBtn方法后,该方法内部的this本应指向Tab实例。然而,在语句this.tabBtn[i].onclick = this.clickBtn中,我们将clickBtn绑定到了input按钮元素上。此时,clickBtn的执行上下文(所属关系)从Tab对象变成了input按钮元素。
由于clickBtn现在属于input按钮,其内部的this自然指向该按钮元素。回顾clickBtn代码,它试图访问this.tabBtn和this.tabDiv,但input按钮元素上并不存在这两个属性,因此会导致错误。

<script>
window.onload = function(){
var tab = new Tab("tabBox");
}
/**
* 将之前window.onload里的代码提到一个构造函数里
* [可以将这个Tab构造函数想象成一个Tab类]
* @param {Object} id:选项卡id以参数的形式传入
*/
function Tab(id){
var tabBox = document.getElementById(id);
//将之前的全局变量变为对象的属性
this.tabBtn = tabBox.getElementsByTagName('input');
this.tabDiv = tabBox.getElementsByTagName('div');
for(var i=0;i<this.tabBtn.length;i++){
this.tabBtn[i].index = i;
//此处这种方式调用函数,已经将clickBtn的所属变成this.tabBtn[i]
this.tabBtn[i].onclick = this.clickBtn;
}
};
//将之前的全局函数添加到构造函数的原型里,作为对象的一个方法
Tab.prototype.clickBtn = function(){
alert(this); //HTMLInputElement
for(var j=0;j<this.tabBtn.length;j++){
this.tabBtn[j].className='';
this.tabDiv[j].style.display='none';
}
this.className='active';
this.tabDiv[this.index].style.display='block';
};
</script>
第三步,将clickBtn的执行包裹在一个函数中,以维持其所属关系不变。此时alert(this)会输出Object,说明clickBtn仍属于Tab对象。但新问题出现:clickBtn内部的this指向tab对象,而tab对象中并不包含className和index属性,依然会报错。因此需要进行第四步改造。

window.onload = function(){
var tab = new Tab("tabBox");
}
/**
* 选项卡
* @param {Object} id:选项卡id
*/
function Tab(id){
var tabBox = document.getElementById(id);
this.tabBtn = tabBox.getElementsByTagName('input');
this.tabDiv = tabBox.getElementsByTagName('div');
for(var i=0;i<this.tabBtn.length;i++){
this.tabBtn[i].index = i;
//将this保存成一个变量,就可以在下面代码中调用对象的方法了
var _this = this;
//此处这种方式调用函数,就不会改变clickBtn方法的所属关系
this.tabBtn[i].onclick = function(){
//注意此处不能直接使用this,this指向this.tabBtn[i]
_this.clickBtn();
};
}
};
//点击选项卡按钮
Tab.prototype.clickBtn = function(){
alert(this); //Object
for(var j=0;j<this.tabBtn.length;j++){
this.tabBtn[j].className='';
this.tabDiv[j].style.display='none';
}
this.className='active';
this.tabDiv[this.index].style.display='block';
};
第四步,通过参数形式将点击的按钮元素传入clickBtn方法中,以解决属性访问问题。
window.onload = function(){
var tab = new Tab("tabBox");
}
/**
* 选项卡
* @param {Object} id:选项卡id
*/
function Tab(id){
var tabBox = document.getElementById(id);
this.tabBtn = tabBox.getElementsByTagName('input');
this.tabDiv = tabBox.getElementsByTagName('div');
for(var i=0;i<this.tabBtn.length;i++){
this.tabBtn[i].index = i;
var _this = this;
this.tabBtn[i].onclick = function(){
//注意参数this代表的是this.tabBtn[i],即input按钮
_this.clickBtn(this);
};
}
};
//将点击的按钮以参数的形式传入
Tab.prototype.clickBtn = function(btn){
for(var j=0;j<this.tabBtn.length;j++){
this.tabBtn[j].className='';
this.tabDiv[j].style.display='none';
}
btn.className='active';
this.tabDiv[btn.index].style.display='block';
};
第五步,最终版本。将代码提取到独立的JS文件中,使用时直接引入即可。编写面向对象程序的核心目的正是为了实现代码复用和便捷调用。
Tab.js
/**
* 选项卡
* @param {Object} id 选项卡id
*/
function Tab(id){
var tabBox = document.getElementById(id);
this.tabBtn = tabBox.getElementsByTagName('input');
this.tabDiv = tabBox.getElementsByTagName('div');
for(var i=0;i<this.tabBtn.length;i++){
this.tabBtn[i].index = i;
var _this = this;
this.tabBtn[i].onclick = function(){
_this.clickBtn(this);
};
}
};
/**
* 为Tab原型添加点击选项卡方法
* @param {Object} btn 点击的按钮
*/
Tab.prototype.clickBtn = function(btn){
for(var j=0;j<this.tabBtn.length;j++){
this.tabBtn[j].className='';
this.tabDiv[j].style.display='none';
}
btn.className='active';
this.tabDiv[btn.index].style.display='block';
};
使用方法:在tab.html中引入上述JS文件,即可轻松创建多个选项卡实例。
<!DOCTYPE html>
<html>
<head>
<style>
.tab input {
background: #F6F3F3;
border: 1px solid #FF0000;
}
.tab .active {
background: #E9D4D4;
}
.tab div {
width:300px;
height:250px;
display:none;
padding: 10px;
background: #E9D4D4;
border: 1px solid #FF0000;
}
</style>
<meta charset="utf-8" />
<title>选项卡</title>
<!-- 引入tab.js -->
<script type="text/javascript" src="../js/tab.js" ></script>
<script>
window.onload = function(){
var tab1 = new Tab("tabBox1");
var tab2 = new Tab("tabBox2");
}
</script>
</head>
<body>
<div class="tab" id="tabBox1">
<input type="button" value="主页" class="active" />
<input type="button" value="说说" />
<input type="button" value="日志" />
<div style="display:block;">这是主页内容</div>
<div>这是说说内容</div>
<div>这是日志内容</div>
</div>
<br />
<div class="tab" id="tabBox2">
<input type="button" value="技术" class="active" />
<input type="button" value="工具" />
<input type="button" value="网站" />
<div style="display:block;">Js、Vue</div>
<div>VSCode</div>
<div>CSDN</div>
</div>
</body>
</html>

简要总结JS面向对象中的this指向问题。this通常在定时器回调和事件处理函数中容易出错,上述案例也印证了这一点。以下讨论仅针对构造函数内部的情况,其他场景下this通常指向调用者。
如示例所示,姓名未能正确显示。原因在于第14行代码this.name中的this指向了window对象,因为setInterval是window的方法,其回调函数中的this默认指向window。
<!DOCTYPE html>
<html>
<meta charset="UTF-8" />
<head>
<script>
function Person(name){
this.name = name;
//定时器
setInterval(this.showName, 3000);
}
Person.prototype.showName = function(){
alert(this); //window
alert("姓名:"+this.name);
}
var p1 = new Person("jiangzhou");
</script>
</head>
</html>
解决方法已在示例中给出:使用一个函数包裹待执行代码,以固定所属关系。注意在内部函数调用外部方法时,应使用外部变量(如'_this')来引用正确的this。事件处理的解决方案在前文已详细说明。
function Person(name){
this.name = name;
var _this = this;
setInterval(function(){
this.showName();
}, 3000);
}
Person.prototype.showName = function(){
alert(this); //[Object Object]
alert("姓名:"+this.name); //姓名:jianghzou
}
var p1 = new Person("jiangzhou");
感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码运行效果。
更多关于JavaScript相关内容感兴趣的读者可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》
希望本文所述对大家JavaScript程序设计有所帮助。