前言
本文将通过一个选项卡案例带读者了解面向对象是个什么概念,同时对于面向对象的各个知识点不会面面俱到,只是简单地从ES5和ES6去看看面向对象是怎么实现的、同时比较这个和面向过程有什么区别。
面向对象概念
面向对象有三要素:继承
、封装
、多态
(可以不用马上理解)
面向对象和面向过程有什么不同呢?
可以这么来理解,面向过程大概就是 你规定了做事情的各种方法;
而面向对象,则是 你规定属于一类的各对象的属性和做事情的方法,通过实例化对象使其与面向过程一样达到相同的目的;
那面向对象相对于面向过程有什么优势呢?
我认为面向对象最大的优势就是“大一统”,同时可以针对不同的特殊情况衍生出相应的变种;
就拿造汽车举个例子
面向过程:你指定一辆汽车装几个轮子、装几个座位、用什么材料的方向盘、用什么材料的灯、底盘有多高等具体参数;
面向对象:你指定一辆汽车有没有轮子、有没有座位、有没有挡风玻璃、有没有底盘,然后你还可以在实例化的时候传参告诉工人这些轮子有几个和用的什么材料、这个座位有几个和用的什么材料、挡风玻璃有几块还有用的什么材料等等;
所以现在你应该能看出来面向对象相对面向过程有什么好处了吧?如果用面向对象可以通过实例化造不同类型的车,但是如果用面向过程,写了一大堆代码最后只能造一种车;所以当考虑到代码的复用性和可拓展性时,就要用到面向对象这个技术了。
当然在js里面,并没有严格意义的面向对象,其本质还是原型链、对象的属性委托。
案例
本文讲的案例非常简单,可以说这是每个学js的都会做过的练习,就是如图的一个选项卡
相信大多数人遇到这个需求肯定想都不想就开始获取节点、绑定函数了。。
今天我们的目的是知道面向对象在js中怎么实现的,所以接下来就是要看看这个例子分别用ES5和ES6怎么实现
HTML
<div id="tabs" class="tabs">
<ul class="tabs-titles">
<li class="active">itemA</li>
<li>itemB</li>
<li>itemC</li>
</ul>
<div class="content-container">
<div class="content">
the content of itemA
</div>
<div class="content" style="display: none">
the content of itemB
</div>
<div class="content" style="display: none">
the content of itemC
</div>
</div>
</div>
ES5
window.onload = function () {
var tabs = new TabsBar("tabs");
tabs.init();
};
//去掉Element对象内的所有空白节点
function cleanWhitespace(node){
var loopIndex;
for (loopIndex = 0; loopIndex < node.childNodes.length; loopIndex++){
var currentNode = node.childNodes[loopIndex];
//如果是元素节点,则遍历该子元素的所有的子节点,用递归检查是否包含白节点
if (currentNode.nodeType == 1){
cleanWhitespace(currentNode);
}
//如果是文本节点,则检查是否是纯粹的空白节点,如果是,就将它从对象中删除
if (((/^\s+$/.test(currentNode.nodeValue))) && (currentNode.nodeType == 3)){
node.removeChild(node.childNodes[loopIndex--]);
}
}
}
function TabsBar(boxId) {
//公有的属性
this.boxObj = document.getElementById(boxId);
cleanWhitespace(this.boxObj);
//私有的属性
var _titles= this.boxObj.childNodes[0];
var _contents= this.boxObj.childNodes[1];
//公有的方法
this.init = function(){
bindTitlesEvent();
}
// 私有方法 事件绑定
function bindTitlesEvent() {
for (var j = 0; j < _titles.childNodes.length; j++) {
// console.log(1);
//第二步 绑定事件 A
(function (k) {
_titles.childNodes[k].onclick = function () {
changeContent(k);
};
})(j);
}
}
// 私有方法 显示不同类别的内容
function changeContent(n) {
for (var i = 0; i < _titles.childNodes.length; i++) {
_titles.childNodes[i].className = "";
_contents.childNodes[i].style.display = "none";
}
_titles.childNodes[n].className = "active";
_contents.childNodes[n].style.display = "block";
}
}
总结:在ES5中
- 一个“类”就是一个(构造)函数,通过 new 调用实例化
- 在构造函数中,通过
this
定义的属性是公共属性(构造函数本身和实例对象都能访问)、通过声明关键字var
来声明的属性是私有属性(只能在构造函数内部访问,实例对象不能访问) - 与属性类似,通过
this.函数名 = 函数
方法得到的函数是公共函数,而通过函数声明function
得到的函数是私有函数 - 有一个不成文的约定,私有属性以下划线
_
开头 - 另外一个不成文的约定,构造函数以大写字母开头
补充:
- 如果要声明“类”的静态方法,则通过构造函数的点操作进行声明,如
TabsBar.foo = function() {}
- 同样,声明“类”的静态属性也是通过构造函数的点操作,
如TabsBar.num = 1
- 静态方法不能访问构造函数内部
this.
变量
了解了什么是静态方法就知道第三点补充的原因了,至于静态属性和静态方法的内容这里不做过多描述,因为与本文主旨没有太大关系,本文重点是了解面向对象、而不是深入探讨一些细节。
ES6
window.onload = function () {
const tabsObj = new TabsBar('tabs');
tabsObj.init();
}
//去掉Element对象内的所有空白节点
function cleanWhitespace(node){
var loopIndex;
for (loopIndex = 0; loopIndex < node.childNodes.length; loopIndex++){
var currentNode = node.childNodes[loopIndex];
//如果是元素节点,则遍历该子元素的所有的子节点,用递归检查是否包含白节点
if (currentNode.nodeType == 1){
cleanWhitespace(currentNode);
}
//如果是文本节点,则检查是否是纯粹的空白节点,如果是,就将它从对象中删除
if (((/^\s+$/.test(currentNode.nodeValue))) && (currentNode.nodeType == 3)){
node.removeChild(node.childNodes[loopIndex--]);
}
}
}
// 私有属性的 Symbol
// const _titles = Symbol('titles');
// const _contents = Symbol('contents');
// 私有方法的 Symbol
const bindTitlesEvent = Symbol('bindTitlesEvent');
const changeContent = Symbol('changeContent');
class TabsBar{
#titles;
#contents;
constructor(id) {
this.tabsObj = document.getElementById(id);
cleanWhitespace(this.tabsObj);
// this[_titles] = this.tabsObj.childNodes[0];
// this[_contents] = this.tabsObj.childNodes[1];
this.#titles = this.tabsObj.childNodes[0];
this.#contents = this.tabsObj.childNodes[1];
}
init(){
this[bindTitlesEvent]();
}
// 给头部导航绑定事件
[bindTitlesEvent](){
this.#titles.childNodes.forEach((childNode, index)=>{
childNode.addEventListener('click', ()=>{
this[changeContent](index);
})
})
}
// 底部内容变更事件
[changeContent](index){
const titleNodes = this.#titles.childNodes;
const contentNodes = this.#contents.childNodes;
for (let i = 0; i < contentNodes.length; i++) {
if( i === index ){
contentNodes[i].style.display = 'block';
titleNodes[i].classList.add('active');
} else{
contentNodes[i].style.display = 'none';
titleNodes[i].classList.remove('active');
}
}
}
}
总结:在ES6中
- 使用
class
来声明一个"类“ - 在这里介绍两种声明私有属性的方法
Symbol
在class
的外部定义相关的Symbol
,然后在class
内部通过this[]
的方式来定义私有属性#
在构造器constructor
之前通过#
声明一个私有属性,访问时通过this.#属性名
来访问
- ES6声明私有函数的方法和声明私有属性的方法是一样的,不过在本文写作时我做了一下测试,通过
#
声明的私有方法在运行时会报错,我猜可能是浏览器问题?不管怎样,大家知道能用Symbol
和#
来声明私有函数就可以了
最后
本来想用最短的篇幅介绍一下面向对象的,写作的过程中已经尽量避免了很多额外的拓展了,但感觉篇幅还是比较长。希望不太明白面向对象的小可爱在读完本文后能对面向对象这个技术有一个比较清晰的认识吧,你们能有所收获就是我写作最大的意义!
喜欢本文的记得点赞、收藏,最好能点下关注喔!