JavaScript面向对象编程再讲
JavaScript面向对象编程再讲
JavaScript支持的面向对象比较复杂,和其他编程语言又有其独特之处。本文是对以前博文 JavaScript的面向对象编程 https://blog.csdn.net/cnds123/article/details/109763357 补充。
概述
这部分是JavaScript面向对象的概括,便于从总体上了解JavaScript面向对象情况,初学者先大体了解即可,不必心忧看不懂,等学习实践过一段时间后,再回过头来看,就容易理解掌握了。
JavaScript中对象(object)
JavaScript中的对象(object)是相关数据和/或功能的集合。这些通常由几个变量和函数组成(当它们位于对象中时称为属性[properties]和方法[methods])。【见https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Basics对象基础(Object basics)节】
在 JavaScript中一个对象由许多的成员(members)组成,包括:
用Property / properties描述对象的 状态、性质、特征(features)数据。
用 method 描述是对象的 动作、行为、操作。
【如果你接触过其他语言的面向对象编程,请注意Property这个词在JavaScript面向对象编程中的含义。
在Web文档资料中(包括权威的https://developer.mozilla.org/zh-CN/docs/Web 支持多语言包括中英文切换),一般而言,在HTML部分将的attribute译为属性,在CSS部分将Property译为属性,在JavaScript部分将Property译为属性、attribute译为特性。】
早期(ES5标准及其之前)JavaScript的面向对象编程和大多数其他语言如Java、C#的面向对象编程都不太一样,没有class 关键字定义类(classes)。
Java或C#面向对象的两个基本概念:
类: 类是对象的类型模板,例如,定义Student类来表示学生,类 (classes)本身是一种类型(type),如Student表示学生类型,但不表示任何具体的某个学生。
对象:实例是根据类创建的对象,例如,根据Student类可以创建出多个实例,每个实例表示一个具体的学生,他们全都属于Student类型。
在JavaScript中需要大家换一下思维方式!JavaScript不区分类和实例的概念,通过原型(prototype)来实现对象继承特征(inherit features)【https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Objects/Object_prototypes 】。
后来【2015年6月发布的ES6标准中】JavaScript 还提供了更接近经典 OOP 概念的特征(features )。注意,这里描述的特征并不是一种继承对象的新方式:在底层,使用的仍是原型。这只是一种更容易的创建原型链(prototype chain)的方法【https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Objects/Classes_in_JavaScript 】。
在 ES6中,类 (classes) 作为对象的模板被引入,可以通过 class 关键字定义类。以# 开头的属性(properties)和方法(methods)是私有的,必须在类(class)中声明和使用,如果在类的外部尝试访问,浏览器将会抛出错误:SyntaxError。
类是用于创建对象的模板。他们用代码封装数据(encapsulate data)以处理这些数据。JS 中类建立在原型(prototype)上,但也有一些独特的语法和语义(与 ES5相比)。
类可以用两种方式定义:类表达式(class expression )或类声明( class declaration)。
// Declaration
class Rectangle {constructor(height, width) {this.height = height;this.width = width;}
}// Expression; the class is anonymous but assigned to a variable
const Rectangle = class {constructor(height, width) {this.height = height;this.width = width;}
};// Expression; the class has its own name
const Rectangle = class Rectangle2 {constructor(height, width) {this.height = height;this.width = width;}
};
【https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Classes 】。
早期的JavaScript面向对象的实现
先看看早期的JavaScript面向对象的实现
早期的JavaScript,生成实例对象的传统方法是通过构造函数。
在JavaScript中,每个函数(function)其实都是一个Function对象。其他对象一样具有属性(property)和方法(method)。
可以用作构造函数(constructor)的函数实例具有“prototype”属性(property)。每个由用户定义的函数都会有一个 prototype 属性。
一个 Function 对象在使用 new 运算符来作为构造函数(constructor)时,会用到它的 prototype 属性(property),它将成为新对象的原型(prototype)。
这是JavaScriptde 面向对象实现的经典方法,它用构造函数模拟"类",在其内部用this关键字指代实例对象。如:
function Point(x, y) {this.x = x;this.y = y;this.explain = "这是一个点的位置";this.position = function(){return '(' + this.x + ', ' + this.y + ')';};
}//生成实例对象
let p1 = new Point(1, 2);
let p2 = new Point(3, 5);console.log(p1.x) //输出:1
console.log(p1.explain) //输出:这是一个点的位置
console.log(p1.position()) //输出:(1, 2)
上面的例子——构造函数模式,表面上好像没什么问题,但是实际上这样做,有一个很大的弊端。那就是对于每一个实例对象,explain属性和position()方法都是一模。每一次生成一个实例,都必须为重复的内容,多占用一些内存,缺乏效率。
Javascript生成实例对象时,会自动含有一个constructor属性,指向它们的构造函数,每一个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。类的属性和方法,可以定义在构造函数的prototype对象之上。这意味着,我们可以把那些不变的属性和方法,直接定义在prototype对象上。因此可以将上例改造为:
function Point(x, y) {this.x = x;this.y = y;
}
Point.prototype.explain = "这是一个点的位置";
Point.prototype.position = function () {return '(' + this.x + ', ' + this.y + ')';
};//生成实例对象
let p1 = new Point(1, 2);
let p2 = new Point(3, 5);console.log(p1.x)//输出:1
console.log(p1.explain) //输出:这是一个点的位置
console.log(p1.position()) //输出:(1, 2)
这时所有实例的explain属性和position()方法,其实都是同一个内存地址,指向prototype对象。因此节省了内存提高了运行效率。这称为原型模式。
早期面向对象的设计模式,除上面介绍的构造函数模式和原型模式,还有
单例模式
工厂模式
在此,就不具体介绍了。
ES6中新增class关键字的使用
下面重点介绍ES6中新增class关键字后的情况。
在ES6中新增了类的概念,可用class关键字声明一个类,之后用该类实例化对象,这样更像面向对象编程的语法。
类和对象关系:
类抽象了对象的公共部分,它泛指某一大类(class);
对象特指某一个,通过类实例化一个具体的对象;
下面给出示例源码:
<script>
//ES6 之后===
// 定义一个学生的类
class Student{ constructor(name){this.name = name;}hello(){console.log(this.name + '你好啊!')}
}// PupilStudent子类继承父类Student
class PupilStudent extends Student{constructor(name,grade){super(name); //super关键字this.grade = grade;}myGrade(){console.log(this.name +'是' +this.grade + '年级学生')}
}//创建实例对象
let LiJun = new Student("李军");
let XiaoMing = new PupilStudent("小明",1);// 通过实例调用方法
LiJun.hello(); //输出:李军你好啊!
XiaoMing.myGrade(); //输出:小明是1年级学生</script>
将上面代码保存文件名为:class关键字示例.html
几点说明:
☆类中函数(方法)不需要写function。
☆this关键字总是指向函数所在的当前对象,类里面共有的属性和方法一定要加this使用;构造函数中的this 指向的是创建的实例对象;谁调用类中的方法,this就指向谁。
☆必须要先定义类,才能通过类实例化对象;类必须使用new实例化对象。
☆constructor()方法是类的构造函数(默认方法),用于传递参数,返回实例对象,通过new命令生成对象实例,自动调用该方法——constructor()方法,也称为constructor() 函数。若没有显示定义,类内部会自动给我们创建一个constructor()。
用浏览器打开“class关键字示例.html”,什么也看不到?
打开浏览器的“控制台”(console) 面板,就看到了
【如何打开浏览器的“控制台”(console) 面板?
打开浏览器后,按下 F12键 【或 按 Ctrl+Shift+J (Windows、Linux) 或 Command+Option+J (macOS)】,然后单击 “控制台”(console) 面板,就进入了控制台。
顺便简要介绍浏览器“控制台”的使用
在浏览器地址栏输入about:blank回车,将打开浏览器空白页的命令——about:blank是内置在浏览器中的命令,可以打开浏览器空白页(没有任何内容)。进入控制台以后,就可以在提示符(> 符号)后输入代码,然后按回车(Enter键),代码就会执行。如果按Shift + Enter键,就是代码换行,不会触发执行。执行结果显示在<符号之后。
以win10的Microsoft Edge浏览器为例,参见下图:
】
上面代码显示效果如下:
下面查看一下上述代码XiaoMing对象原型(prototype):
在“控制台”(console)输入
console.log(XiaoMing)
参见下图:
【为什么浏览器控制台(Console)运行JavaScript代码有时会出现“undefined”?可见https://blog.csdn.net/cnds123/article/details/128014970】
继承是指子类可以继承父类的一些属性和方法。
子类继承父类的语法如下:
class 父类 extends 子类{
……
}
具体示例可见上例的
// PupilStudent子类继承父类Student
class PupilStudent extends Student{
……
}
部分。
需要注意的是,子类要继承父类中的参数和方法,需要super关键字。
子类在构造函数中使用super,必须放到this前面(即必须先调用父类的构造方法,再使用子类的构造方法)。否则报错。
将上例中
super(name); //super关键字
改为
this.name = name;
运行报错,参见下图:
【报错:
Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
at ……
大意是,未捕获的ReferenceError:在访问“this”或从派生构造函数返回之前,必须调用派生类中的super构造函数。】
super关键字
关键字super,指向当前对象的原型对象,它用于访问和调用对象父类上的函数,可以调用父类的构造函数,也可以调用父类的普通函数。
子类在构造函数中使用super,必须放到this前面(即必须先调用父类的构造方法,再使用子类的构造方法)。上面示例中子类在构造函数
super(name); //super关键字
this.grade = grade;
的两句,若改为
this.grade = grade;
super(name); //super关键字
将报和前面相似的错误。你可以试试。
面向对象的的三大特征(feature)简介
封装(encapsulation):封装即信息隐蔽,在确定系统的某一部分内容时,应考虑到其它部分的信息及联系都在这一部分的内部进行,外部各部分之间的信息联系应尽可能的少。目的是尽量做到“高内聚,低耦合”,高内聚就是类的内部数据操作细节自己完成,不允许外部干涉;低耦合:仅暴露少量的方法给外部使用。例如,将对应的行为抽取为方法,状态数据抽取为属性,创建良好的类。
继承(Inheritance):继承是类和类之间的一种关系。子类(subclass)继承超类(superclass,也叫父类)的属性和方法。
多态(polymorphism):当一个方法拥有相同的函数名,但是在不同的类中可以具有不同的实现时,我们称这一特性为多态。当子类中的方法替换超类的实现时,我们说子类重写(override)超类中的版本。
下面展开介绍。
创建类的简明语法:
class ClassName {
// 类体
}
创建实例:
xx = new ClassName r();
注意语法规范,如:创建类 类名后面不要加小括号,类中的函数不需要加 function。
类里面的共有的属性和方法一定要加this使用。
在 ES6 中类没有变量提升,所以必须先定义类,才能通过类实例化对象。
请留意类里面的this指向问题。constructor 里面的this指向实例对象, 方法里面的this 指向这个方法的调用者。
下面给出示例源码:
<script>// 1. 创建类 class 创建一个 Star类class Star {//在类中定义constructor函数constructor(uname, age) {this.uname = uname;this.age = age;}//在类中定义普通函数,这里是 sing(song)sing(song) {console.log(this.uname + "经典歌曲:" + song);}}// 2. 利用类创建对象 newvar ldh = new Star("刘德华", 28);var zxy = new Star("张学友", 27);ldh.sing("忘情水"); //输出:刘德华经典歌曲:忘情水zxy.sing("吻别"); //输出:张学友经典歌曲:吻别
</script>
将上面代码保存文件名为:JS类示例测试1.html,用浏览器打开它,再按下 F12键打开浏览器的“控制台”(console) 面板,显示效果如下:
以#开头的属性(properties)和方法(methods)是私有的,必须在类(class)中声明和使用,如果在类的外部尝试访问,浏览器将会抛出错误:SyntaxError。
示例源码如下:
<script>
class Example {hiA='Hello';#hiB='Hello';somePublicMethod() {this.#somePrivateMethod();}#somePrivateMethod() {console.log('You called me?');}
}const myExample = new Example();
console.log(myExample.hiA) //输出:Hello
//console.log(myExample.#hiB) // 若不注释掉本句将报错SyntaxError
myExample.somePublicMethod(); //输出:You called me?
//myExample.#somePrivateMethod(); // 若不注释掉本句将报错SyntaxError</script>
你可以测试运行试试。
面向对象重要特征(feature):继承性(inheritance)。
继承是指子类可以继承父类的一些属性和方法。创建继承的简明语法:
// 父类
class FatherName {
// 父类体
}
// 子类继承父类
class SonV extends FatherName {
// 子类体
}
super关键字
super关键字用于访问和调用对象父类上的函数,可以调用父类的构造函数,也可以调用父类的普通函数。
注意:子类在构造函数中使用 super,必须放到 this 前面(必须先调用父级的构造方法,在使用子类的构造方法)
下面给出示例源码:
<script>//定义父类class Father {constructor(x, y) {this.x = x;this.y = y;}sum() {console.log(this.x + this.y);}}//定义子类,子类继承父类加法方法 同时 扩展减法方法class Son extends Father {constructor(x, y) {// 利用 super 调用父类的构造函数// super 必须在字类this之前调用super(x, y);this.x = x;this.y = y;}sub() {console.log(this.x - this.y);}}//利用类创建对象 newvar son = new Son(5, 3);son.sum(); //输出:8son.sub(); //输出:6
</script>
将上面代码保存文件名为:JS类示例测试2.html,用浏览器打开它,再按下 F12键打开浏览器的“控制台”(console) 面板,显示效果如下:
面向对象重要特征(feature):多态(polymorphism)
多态指子类重写父类的方法。
下面给出重写(子类重写父类的方法)示例源码:
<script>
class Person{constructor(name){// var sex = '男'this.name = name}//say()方法say(){console.log(this.name+'哈哈哈哈');}
}
class Son extends Person{constructor(name,age){super() //调用Person的constructorthis.name = namethis.age = age}//重写say()方法say(){console.log(this.name+'嘻嘻嘻嘻');}}var person = new Person('刘德华')
person.say()//刘德华哈哈哈哈var son = new Son('张学友')
son.say() //张学友嘻嘻嘻嘻</script>
将上面代码保存文件名为:JS重写示例.html,用浏览器打开它,再按下 F12键打开浏览器的“控制台”(console) 面板,显示效果如下:
应用例子
例1、下面给出一个按钮的示例,单击按钮,将弹出提示消息,源码如下:
<body><p>单击下面按钮,将弹出提示消息</p><button>点击</button><script>// 1. 创建类 class 创建一个 C类class C{constructor() {// constructor 里面的this 指向的是 创建的实例对象this.btn = document.querySelector('button');//按钮调用fn函数this.btn.onclick = this.fn; //注意:此处fn后面不要加(),想要点击完调用,不需要立即调用//加括号:代表立即执行,也代表该函数的返回值//不加括号:代表函数体本身(Function类型)}//在类中定义普通函数,这里是 fn()fn() {//console.log("你单击了“点击”按钮");alert("你单击了“点击”按钮");} }// 2. 利用类创建对象 newvar c = new C(); </script>
</body>
将上面代码保存文件名为:JS类示例测试3.html,用浏览器打开它,再单击了页面上“点击”按钮试试,显示效果如下:
例2、一个比较大的示例——使用类的tab 栏切换。
可以实现tab栏的动态切换、添加、删除、编辑。双击tab的标题可以编辑tab标题,双击tab的页面可以输入编辑页面内容。
先给出效果图:
此项目参考自网络。
项目包含的文件,为简便使用放到同一个目录中,我这里目录名是“使用class版tab栏”,含有三个文件,参见下图:
tab.js文件的内容如下:
var that;
class Tab {constructor(id){// 获取元素that = this;// tab栏盒子this.main = document.querySelector(id);// 加号this.add = this.main.querySelector('.tabadd');// li的父元素this.ul = this.main.querySelector('.firstnav ul:first-child');// section的父元素this.fsection = this.main.querySelector('.tabscon');// 初始化操作让相关的元素绑定事件this.init();}init(){this.updateNode();// 初始化操作让相关的元素绑定事件this.add.onclick = this.addTab;for(var i = 0; i<this.lis.length;i++){this.lis[i].index=i;this.lis[i].onclick = this.toggleTab;this.remove[i].onclick = this.removeTab;this.lis[i].ondblclick = this.editTab;this.sections[i].ondblclick = this.editTab;}}// 因为我们动态添加元素 需要从新获取对应的元素updateNode(){// 所有的lithis.lis = this.ul.querySelectorAll('li');// 所有的sectionthis.sections = this.fsection.querySelectorAll('section');// 所有的X删除this.remove = this.ul.querySelectorAll('li span')}//1. 切换功能toggleTab(){//排他思想// 清除所有的li和section的类that.clearClass()// 给当前的li和section加上类this.className='liactive'that.sections[this.index].className='conactive'}// 清除所有的li和section的类clearClass(){for(var i=0;i<this.lis.length;i++){this.lis[i].className='';this.sections[i].className='';}}// 2. 添加功能addTab(){that.clearClass()// (1) 创建li元素和section元素var li = '<li class="liactive">新增选项卡<span >X</span></li>';var section = '<section class="conactive">新增选项卡</section>';// (2) 把这两个元素追加到对应的父元素里面that.ul.insertAdjacentHTML('beforeend',li);that.fsection.insertAdjacentHTML('beforeend',section);that.init();}// 3. 删除功能removeTab(e){e.stopPropagation(); // 阻止冒泡 防止触发li 的切换点击事件var index = this.parentNode.index;// 根据索引号删除对应的li 和section remove()方法可以直接删除指定的元素that.lis[index].remove();that.sections[index].remove();that.init();// 当我们删除的不是选中状态的li 的时候,原来的选中状态li保持不变if (document.querySelector('.liactive')) return;// 当我们删除了选中状态的这个li 的时候, 让它的前一个li 处于选定状态(that.lis[index] && that.lis[index].click())||(that.lis[--index] && that.lis[index].click())}// 4. 修改功能editTab(e){var str = this.innerHTML;var isnav =(str.indexOf('<span>X</span>')!==-1); // 是否是tab 标签if(isnav) str = this.innerHTML.replace('<span>X</span>','')// 双击禁止选定文字window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty();this.innerHTML = '<input type="text" />';var input = this.children[0];input.value = str;input.select();// 文本框里面的文字处于选定状态// 当我们离开文本框就把文本框里面的值给spaninput.onblur = function(){if(isnav){this.parentNode.innerHTML = this.value + '<span>X</span>'}else{this.parentNode.innerHTML = this.value;}}// 按下回车也可以把文本框里面的值给spaninput.onkeyup = function(e){if(e.keyCode === 13){// 手动调用表单失去焦点事件 不需要鼠标离开操作this.blur();}}}}new Tab('#tab')
tab.css文件的内容如下:
* {margin: 0;padding: 0;
}ul li {list-style: none;
}main {width: 600px;height: 400px;border-radius: 10px;margin: 50px auto;
}main h4 {/*height: 100px;*//*line-height: 100px;*/text-align: center;
}.tabsbox {width: 600px;margin: 0 auto;height: 400px;border: 5px solid lightsalmon;position: relative;
}nav ul {overflow: hidden;
}nav ul li {float: left;width: 100px;height: 50px;line-height: 50px;text-align: center;border-right: 3px solid #ccc;position: relative;
}nav ul li.liactive {border-bottom: 2px solid #fff;z-index: 9;
}#tab input {width: 80%;height: 60%;
}nav ul li span:last-child {position: absolute;user-select: none;font-size: 12px;top: -18px;right: 0;display: inline-block;height: 20px;
}.tabadd {position: absolute;/* width: 100px; */top: 0;right: 0;
}.tabadd span {display: block;width: 20px;height: 20px;line-height: 20px;text-align: center;border: 1px solid #ccc;float: right;margin: 10px;user-select: none;
}.tabscon {width: 100%;height: 300px;position: absolute;padding: 30px;top: 50px;left: 0px;box-sizing: border-box;border-top: 2px solid #ccc;
}.tabscon section,
.tabscon section.conactive {display: none;width: 100%;height: 100%;
}.tabscon section.conactive {display: block;
}
index.html文件的内容如下:
<!doctype html>
<html lang="zh">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width" /><title>Document</title><link rel="stylesheet" href="./tab.css">
</head>
<body><main><h4>面向对象 动态添加标签页</h4><div class="tabsbox" id="tab"><!--tab 标签--><nav class="firstnav"><ul><li class="liactive">测试1<span >X</span></li><li>测试2<span>X</span></li><li>测试3<span>X</span></li></ul><div class="tabadd"><span>+</span></div></nav><!--tab 内容--><div class="tabscon"><section class="conactive">测试1</section><section>测试2</section><section>测试3</section></div></div></main><script src="./tab.js"></script>
</body>
</html>
用浏览器打开index.html文件,就可以看到效果了。
附录、编程语言和面向对象浅谈 https://blog.csdn.net/cnds123/article/details/128998309
参考
https://blog.csdn.net/ks795820/article/details/122487046
https://www.freecodecamp.org/chinese/news/object-oriented-javascript-for-beginners/