面向对象简介
面向对象编程(Object Oriented Programming,缩写为 OOP)是目前主流的编程范式
为什么要使用面向对象
《大话设计模式》中大鸟给小菜讲的故事非常经典:
“话说三国时期,曹操带领百万大军攻打东吴,大军在长江赤壁驻扎,军船连成一片,眼看就要灭掉东吴,统一天下,曹操大悦,于是大宴众文武,在酒席间,曹操诗性大发,不觉吟道:‘喝酒唱歌,人生真爽……’众文武齐呼:‘丞相好诗!’于是一臣子速命印刷工匠刻版印刷,以便流传天下。”“样张出来给曹操一看,曹操感觉不妥,说道:‘喝与唱,此话过俗,应改为‘对酒当歌’较好!’于是此臣就命工匠重新来过。工匠眼看连夜刻版之工,彻底白费,心中叫苦不迭。只得照办。”
“样张再次出来请曹操过目,曹操细细一品,觉得还是不好,说:‘人生真爽‘太过直接,应改问语才够意境,因此应改为‘对酒当歌,人生几何……’当臣子转告工匠之时,工匠晕倒……”
大鸟:“小菜你说,这里面问题出在哪里?”
小菜:“是不是因为三国时期活字印刷还未发明,所以要改字的时候,就必须要整个刻板全部重新刻。”
大鸟:“说得好!如果是有了活字印刷,则只需更改四个字就可,其余工作都未白做。岂不妙哉。
一、要改,只需更改要改之字,此为可维护;
二、这些字并非用完这次就无用,完全可以在后来的印刷中重复使用,此乃可复用;
三、此诗若要加字,只需另刻字加入即可,这是可扩展;
四、字的排列其实可能是竖排可能是横排,此时只需将活字移动就可做到满足排列需求,此是灵活性好。”
“而在活字印刷术出现之前,上面的四种特性都无法满足,要修改,必须重刻,要加字,必须重刻,要重新排列,必须重刻,印完这本书后,此版已无任何可再利用价值。”
小菜:“是的,小时候我一直奇怪,为何火药、指南针、造纸术都是从无到有,从未知到发现的伟大发明,而活字印刷仅仅是从刻版印刷到活字印刷的一次技术上的进步,为何不是评印刷术为四大发明
之一呢?原来活字印刷是思想的成功,面向对象的胜利。”
对象是什么
要了解面向对象编程,要先了解对象是什么?
对象其实是一个抽象概念的具体实例,例如:
人类:张三
动物:猫
抽象概念:人类、动物 特别常见称呼:类、模板
对象:张三、猫 特别常见称呼:实例对象
生成对象
对象是一个抽象概念的具体实例,那么我们要生成一个对象,需要有一个模板,表示某一类实物的共同特征,然后对象根据这个模板生成。
JavaScript语言使用构造函数(constructor)作为对象的模板。所谓“构造函数”,就是专门用来生成实例对象的函数。
构造函数就是一个普通的函数,但具有自己的特征和用法。
function People(){
this.name = "张三"
}
温馨提示
构造函数的首字母大写:例如People
中的P
是大写的
构造函数的特点有两个
1 函数体内部使用了
this
关键字,代表了所要生成的对象实例
2 生成对象的时候,必须使用new
命令
function People(){
this.name = "张三"
}
var p = new People();
p.name // 张三
new 命令
new
命令的作用,就是执行构造函数,返回一个实例对象
function People(){
this.name = "张三"
}
var p = new People();
p.name // 张三
使用 new
命令时,根据需要,构造函数也可以接受参数
function People(name) {
this.name = name;
}
var p1 = new People("张三");
var p2 = new People("李四");
p1.name // 张三
p2.name // 李四
this
关键字
在构造函数中的this
指向当前实例对象
prototype原型
构造函数的缺点
JavaScript通过构造函数生成新对象,因此构造函数可以视为对象的模板。实例对象的属性和方法,可以定义在构造函数内部
function People(name,age) {
// 属性
this.name = name;
this.age = age;
// 方法;
this.sayHello = function(){
console.log("Hello");
}
}
var p1 = new People("张三",20);
var p2 = new People("李四",30);
通过构造函数为实例对象定义属性和方法,虽然很方便,但是有一个缺点。同一个构造函数的多个实例之间,无法共享属性和方法,从而造成对系统资源的浪费
console.log(p1.sayHello === p2.sayHello); //false
这个问题的解决方法,就是JavaScript的原型对象(prototype)
prototype原型的作用
prototype
原型对象的所有属性和方法,都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,节省了内存。
怎么为对象指定原型呢,JavaScript规定,每个函数都有一个prototype
属性,指向一个对象
function People() { }
console.log(People.prototype);
对于普通函数来说,该属性基本无用。但是,对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型。
function People(name,age) {
// 属性
this.name = name;
this.age = age;
}
// 原型属性
People.prototype.color = "黄种人";
// 原型方法
People.prototype.sayHello = function(){
console.log("Hello");
}
var p1 = new People("张三",20);
var p2 = new People("李四",30);
console.log(p1.sayHello === p2.sayHello); //true
console.log(p1.color === p2.color); // true
p1
和 p2
共享了原型属性和方法
原型对象的属性不是实例对象自身的属性。只要修改原型对象,变动就立刻会体现在所有实例对象上。
People.prototype.color = "白种人";
p1.color // 白种人
p2.color // 白种人
如果实例对象自身就有某个属性或方法,它就不会再去原型对象寻找这个属性或方法
p1.color = "白种人"
console.log(p1.color); // "白种人"
总结一下,原型对象的作用,就是定义所有实例对象共享的属性和方法。
实例、静态方法与属性
实例方法和静态方法
在JavaScript中有静态方法和实例方法,静态方法是函数自己定义的,而实例方法是通过原型来定义。它们的区别是静态方法是可以直接用 类名.方法名
去调用的,而实例方法是不可以的,它必须要用实例才可以去调用 实例.方法名
。
function Person(name){
this.name = name;
}
// 实例方法
Person.prototype.getName = function(){
console.log(this.name);
}
// 静态方法
Person.getAge = function(age){
console.log(age);
}
var person = new Person("itbaizhan");
person.getName();
Person.getAge(10);
JavaScript内置的Array方法则分为实例方法和静态方法
var arr = [10,20,30];
// 实例方法
arr.push(40);
// 静态方法
Array.isArray(arr);
实例属性和静态属性
实例属性和静态属性与方法类似,实例属性是通过实例对象调用的实例对象.属性
,静态属性是通过类名调用的 类名.属性
function Person(name){
// 实例属性
this.name = name;
}
// 静态属性
Person.age = 20;
var person = new Person("itbaizhan");
console.log(person.name);
console.log(Person.age);
__ proto__ 属性
实例对象的 __proto__
属性(前后各两个下划线),返回该对象的原型。该属性可读写
function People(){}
var p = new People();
console.log(p.__proto__);
对象的 prototype
属性等同于实例对象的__proto__
属性
function People(){}
var p = new People();
console.log(p.__proto__ === People.prototype); // true
温馨提示
根据语言标准,__proto__
属性只有浏览器才需要部署,其他环境可以没有这个属性
constructor 属性
prototype
对象有一个 constructor
属性,默认指向 prototype
对象所在的构造函数
function Person(){}
console.log(Person.prototype.constructor === Person); // true
constructor 属性的作用是什么呢
通俗的讲,就是为了将实例原型对象暴露出来, 比如你写了一个插件,别人得到的都是你实例化后的对象, 如果别人想扩展下对象,就可以用constructor.prototype
去修改或扩展原型对象
var person;
(function(){
function Person(){
this.name = "张三";
this.age = 20;
}
Person.prototype.getName = function(){
console.log(this.name);
}
person = new Person();
})();
// 通过实例对象扩展方法
person.constructor.prototype.getAge =
function(){
console.log(this.age);
}
person.getName();
person.getAge();
原型链
JavaScript 规定,所有对象都有自己的原型对象(prototype)。一方面,任何一个对象,都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”(prototype chain):对象到原型,再到原型的原型…
如果一层层地上溯,所有对象的原型最终都可以上溯到Object.prototype
,即 Object
构造函数的 prototype
属性。也就是说,所有对象都继承了 Object.prototype
的属性。这就是所有对象都有 valueOf
和toString
方法的原因,因为这是从 Object.prototype
继承的。
function Person(){}
console.log(Person.prototype.__proto__);
console.log(Object.prototype);
那么, Object.prototype
对象有没有它的原型呢?回答是 Object.prototype
的原型是 null
。 null
没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是 null
。
console.log(Object.prototype.__proto__); //null
读取对象的某个属性时,JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的 Object.prototype
还是找不到,则返回undefined
。如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”(overriding
)
function Person(){}
Person.prototype.toString = function(){
return "这是Person的toString方法"
}
var person = new Person();
person.toString()
instanceof 运算符
instanceof
运算符返回一个布尔值,表示对象是否为某个构造函数的实例
function Person(){}
var person = new Person();
console.log(person instanceof Person);
由于 instanceof
检查整个原型链,因此同一个实例对象,可能会对多个构造函数都返回 true
。
var d = new Date();
d instanceof Date // true
d instanceof Object // true
instanceof
运算符的一个用处,是判断值的类型
var arr = [10,20,30];
var obj = {};
console.log(arr instanceof Array);
console.log(obj instanceof Object);
温馨提示
instanceof
运算符只能用于对象,不适用原始类型的值。
var str = 'itbaizhan';
str instanceof String // false
Object 对象的相关方法
Object.getPrototypeOf()
Object.getPrototypeOf
方法返回参数对象的原型。这是获取原型对象的标准方法
var arr = [];
console.log(Object.getPrototypeOf(arr));
Object.setPrototypeOf()
Object.setPrototypeOf
方法为参数对象设置原型,返回该参数对象。它接受两个参数,第一个是现有对象,第二个是原型对象
var sxt = {};
var itbaizhan = { teacher: "iwen" };
Object.setPrototypeOf(sxt, itbaizhan);
sxt.teacher // iwen
Object.create()
JavaScript 提供了 Object.create()
方法,让一个对象继承另一个对象的属性和方法
var sxt = {
teacher: function () {
console.log('hello');
}
};
var itbaizhan = Object.create(sxt);
itbaizhan.teacher() // hello
Object.create()
方法生成的新对象,动态继承了原型。在原型上添加或修改任何方法,会立刻反映在新对象之上
var sxt = { t: 10 };
var itbaizhan = Object.create(sxt);
sxt.t = 20;
itbaizhan.t // 20
除了对象的原型, Object.create()
方法还可以接受第二个参数。该参数是一个属性描述对象,它所描述的对象属性,会添加到实例对象,作为该对象自身的属性
var sxt = {
java: {
value: "全体系"
},
web: {
value: '大前端'
}
};
var itbaizhan = Object.create(sxt, {
python:{
value:"全方向"
}
});
console.log(itbaizhan.java.value);
console.log(itbaizhan.python);
Object.getOwnPropertyNames()
Object.getOwnPropertyNames
方法返回一个数组,成员是参数对象本身的所有属性的键名,不包含继承的属性键名
Object.getOwnPropertyNames(Array)
// ['length', 'name', 'prototype', 'isArray','from', 'of']
Object.prototype.hasOwnProperty()
对象实例的 hasOwnProperty
方法返回一个布尔值,用于判断某个属性定义在对象自身,还是定义在原型链上
Date.hasOwnProperty('length') // true
Date.hasOwnProperty('toString') // false
对象的继承
面向对象编程很重要的一个方面,就是对象的继承。A对象通过继承B对象,就能直接拥有B对象的所有属性和方法。这对于代码的复用是非常有用的。
大部分面向对象的编程语言,都是通过“类”(class)实现对象的继承。传统上,JavaScript语言的继承不通过class,而是通过“原型对象”(prototype)实现
温馨提示
ES6版本已经提供了 class 语法,我们会在讲解ES6的时候单独讲解使用方式
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getName = function () {
console.log(this.name);
}
Person.prototype.getAge = function(){
console.log(this.age);
}
function Student(name, age, major) {
//调用父类的构造函数
Person.call(this, name, age);
this.major = major;
}
// 继承父类原型中的方法
for (var p in Person.prototype) {
Student.prototype[p] = Person.prototype[p];
}
Student.prototype.showMajor = function () {
console.log(this.major);
}
Student.prototype.getName = function(){
console.log("Student:" + this.name);
}
var student = new Student("itbaizhan", "20","it");
student.showMajor();
student.getName();
student.getAge();
多重继承
JavaScript 不提供多重继承功能,即不允许一个对象同时继承多个对象。但是,可以通过变通方法,实现这个功能
function Sxt() {
this.hello = 'hello';
}
function Itbaizhan() {
this.world = 'world';
}
function Sum() {
Sxt.call(this);
Itbaizhan.call(this);
}
// 继承 Sxt
Sum.prototype = Object.create(Sxt.prototype);
// 继承链上加入 Itbaizhan
Object.assign(Sum.prototype, Sum.prototype);
// 指定构造函数
Sum.prototype.constructor = Sum;
var s = new Sum();
console.log(s.hello); // 'hello'
console.log(s.world); // 'world'
严格模式
除了正常的运行模式,JavaScript 还有第二种运行模式:严格模式(strict mode)。顾名思义,这种模式采用更加严格的 JavaScript语法。
同样的代码,在正常模式和严格模式中,可能会有不一样的运行结果。一些在正常模式下可以运行的语句,在严格模式下将不能运行。
设计目的
早期的JavaScript语言有很多设计不合理的地方,但是为了兼容以前的代码,又不能改变老的语法,只能不断添加新的语法,引导程序员使用新语法。
严格模式是从ES5进入标准的,主要目的有以下几个。
- 明确禁止一些不合理、不严谨的语法,减少JavaScript语言的一些怪异行为。
- 增加更多报错的场合,消除代码运行的一些不安全之处,保证代码运行的安全。
- 提高编译器效率,增加运行速度。
- 为未来新版本的javaScript语法做好铺垫。
总之,严格模式体现了JavaScript更合理、更安全、更严谨的发展方向。
启用方法
进入严格模式的标志,是一行字符串use strict
'use strict';
可以放在两个位置:
1
use strict
放在脚本文件的第一行,整个脚本都将以严格模式运行
2use strict
放在函数体的第一行,则整个函数以严格模式运行
<script>
'use strict';
console.log('这是严格模式');
</script>
function strict() {
'use strict';
return '这是严格模式';
}
显式报错
eval、arguments
不可用作标识名
严格模式下,使用 eval
或者 arguments
作为标识名,将会报错。下面的语句都会报错
'use strict';
var eval = 17;
var arguments = 17;
函数不能有重名的参数
正常模式下,如果函数有多个重名的参数,可以用 arguments[i]
读取。
严格模式下,这属于语法错误
function f(a, a, b) {
'use strict';
return a + b;
}
全局变量显式声明
正常模式中,如果一个变量没有声明就赋值,默认是全局变量。严格模式禁止这种用法,全局变量必须显式声明
'use strict';
v = 1; // 报错,v未声明
for (i = 0; i < 2; i++) { // 报错,i 未声明
// ...
}
function f() {
x = 123;
}
f() // 报错,未声明就创建一个全局变量
禁止 this
关键字指向全局对象
禁止使用 with
语句
arguments
不再追踪参数的变化
保留字
为了向将来 JavaScript 的新版本过渡,严格模式新增了一些保留字(implements、interface、let、package、private、protected、public、static、yield
等)。使用这些词作为变量名将会报错
function package(protected) { // 语法错误
'use strict';
var implements; // 语法错误
}
实操-选项卡-回顾
<!DOCTYPE html>
<html>
<head>
<style>
#tabBox input {
background: #f1f1f1;
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="web"class="active" />
<input type="button" value="Java" />
<input type="button" value="Python"/>
<div style="display:block;">React、Vue</div>
<div>SpringBoot、SpringMVC</div>
<div>Flask、Django</div>
</div>
</body>
</html>
实操-选项卡-面向对象
<!DOCTYPE html>
<html>
<head>
<style>
.tab input {
background: #F1F1F1;
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>
<script>
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.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';
};
</script>
<script>
window.onload = function(){
var tab1 = new Tab("tabBox1");
var tab2 = new Tab("tabBox2");
}
</script>
</head>
<body>
<div id="tabBox1" class="tab">
<input type="button" value="web"class="active" />
<input type="button" value="Java" />
<input type="button" value="Python"/>
<div style="display:block;">React、Vue</div>
<div>SpringBoot、SpringMVC</div>
<div>Flask、Django</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;">Java、WEB</div>
<div>vsCode、WebStorm</div>
<div>百战程序员、尚学堂</div>
</div>
</body>
</html>
实操-选项卡-面向对象-继承
function ItbaizhanTab(id, effect) {
Tab.call(this, id);
this.effect = effect
}
for (var p in Tab.prototype) {
ItbaizhanTab.prototype[p] = Tab.prototype[p];
}
ItbaizhanTab.prototype.sayEffect = function() {
console.log(this.effect);
}
window.onload = function () {
var tab1 = new ItbaizhanTab("tabBox1","专业");
var tab2 = new ItbaizhanTab("tabBox2","参考");
tab1.sayEffect();
tab2.sayEffect();
}