【前端系列教程之JavaScript】15_JavaScript面向对象编程
JavaScript面向对象编程
面向对象概述
什么是对象
Everything is object (万物皆对象)
对象到底是什么,我们可以从两次层次来理解。
(1) 对象是单个事物的抽象。
一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个与远程服务器的连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。
(2) 对象是一个容器,封装了属性(property)和方法(method)。
属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal对象,使用“属性”记录具体是那一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。
在实际开发中,对象是一个抽象的概念,可以将其简单理解为:数据集或功能集。
ECMAScript-262 把对象定义为:无序属性的集合,其属性可以包含基本值、对象或者函数。
严格来讲,这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。
提示:每个对象都是基于一个引用类型创建的,这些类型可以是系统内置的原生类型,也可以是开发人员自定义的类型。
什么是面向对象
面向对象不是新的东西,它只是过程式代码的一种高度封装,目的在于提高代码的开发效率和可维 护性。
面向对象编程 —— Object Oriented Programming,简称 OOP ,是一种编程开发思想。
它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。
在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。
因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。
面向对象与面向过程:
面向过程就是亲力亲为,事无巨细,面面俱到,步步紧跟,有条不紊
面向对象就是找一个对象,指挥得结果
面向对象将执行者转变成指挥者
面向对象不是面向过程的替代,而是面向过程的封装
面向对象的特性:
封装性
继承性
[多态性]抽象
扩展阅读:
维基百科 - 面向对象程序设计
知乎:如何用一句话说明什么是面向对象思想?
知乎:什么是面向对象编程思想?
程序中面向对象的基本体现
在 JavaScript 中,所有数据类型都可以视为对象,当然也可以自定义对象。
我们以一个例子来说明面向过程和面向对象在程序流程上的不同之处。
假设我们要处理学生的成绩表,为了表示一个学生的成绩,面向过程的程序可以用一个对象表示:
var stu1 = {name: 'zs', subject: '语文', score: 90};
var stu2 = {name: 'ls', subject: '语文', score: 80};
而处理学生成绩可以通过函数实现,比如打印学生的成绩:
console.log(stu1.name, stu1.subject, stu1.score);console.log(stu2.name, stu2.subject, stu2.score);
如果采用面向对象的程序设计思想,我们首选思考的不是程序的执行流程,而是 Student
这种数据类型应该被视为一个对象,这个对象拥有 name
和 score
这两个属性(Property)。
如果要打印一个学生的成绩,首先必须创建出这个学生对应的对象,然后,给对象发一个 printScore
消息,让对象自己把自己的数据打印出来。
抽象数据行为模板(Class):
function Student(name, score) {this.name = name;this.score = score;this.printScore = function() {console.log('姓名:' + this.name + ' ' + '成绩:' + this.score);}
}
根据模板创建具体实例对象(Instance):
var std1 = new Student('Michael', 98)
var std2 = new Student('Bob', 81)
实例对象具有自己的具体行为(给对象发消息):
std1.printScore() // => 姓名:Michael 成绩:98
std2.printScore() // => 姓名:Bob 成绩 81
面向对象的设计思想是从自然界中来的,因为在自然界中,类(Class)和实例(Instance)的概念是很自然的。
Class 是一种抽象概念,比如我们定义的 Class——Student ,是指学生这个概念,而实例(Instance)则是一个个具体的 Student ,比如, Michael 和 Bob 是两个具体的 Student 。
所以,面向对象的设计思想是:
抽象出 Class(构造函数)
根据 Class(构造函数) 创建 Instance
指挥 Instance 得结果
面向对象的抽象程度又比函数要高,因为一个 Class 既包含数据,又包含操作数据的方法。
创建对象
new Object()创建对象
var person = new Object()
person.name = 'Jack'
person.age = 18person.sayName = function () {console.log(this.name)
}
每次创建通过 new Object()
比较麻烦,所以可以通过它的简写形式对象字面量来创建:
对象字面量
var person = {name: 'Jack',age: 18,sayName: function () {console.log(this.name)}
}
对于上面的写法固然没有问题,但是假如我们要生成两个 person
实例对象呢?代码太过冗余,重复性太高。
工厂函数创建对象
我们可以写一个函数,解决代码重复问题:
function createPerson (name, age) {return {name: name,age: age,sayName: function () {console.log(this.name)}}
}
然后生成实例对象:
var p1 = createPerson('Jack', 18)
var p2 = createPerson('Mike', 18)
这样封装确实爽多了,通过工厂模式我们解决了创建多个相似对象代码冗余的问题, 但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
构造函数
一、构造函数语法
一种更优雅的工厂函数就是下面这样,构造函数:
function Person (name, age) {this.name = namethis.age = agethis.sayName = function () {console.log(this.name)}
}var p1 = new Person('Jack', 18)
p1.sayName() // => Jackvar p2 = new Person('Mike', 23)
p2.sayName() // => Mike
二、构造函数分析
在上面的示例中,Person()
函数取代了 createPerson()
函数,但是实现效果是一样的。 这是为什么呢?
我们注意到,Person()
中的代码与 createPerson()
有以下几点不同之处:
没有显示的创建对象
直接将属性和方法赋给了
this
对象没有
return
语句函数名使用的是大写的
Person
而要创建 Person
实例,则必须使用 new
操作符。
以这种方式调用构造函数会经历以下 4 个步骤:
创建一个新对象
将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)
执行构造函数中的代码
返回新对象
下面是具体的伪代码:
function Person (name, age) {// 当使用 new 操作符调用 Person() 的时候,实际上这里会先创建一个对象// var instance = {}// 然后让内部的 this 指向 instance 对象// this = instance// 接下来所有针对 this 的操作实际上操作的就是 instancethis.name = namethis.age = agethis.sayName = function () {console.log(this.name)}// 在函数的结尾处会将 this 返回,也就是 instance// return this
}
三、构造函数和实例对象的关系
使用构造函数的好处不仅仅在于代码的简洁性,更重要的是我们可以识别对象的具体类型了。
在每一个实例对象中同时有一个 constructor
属性,该属性指向创建该实例的构造函数:
console.log(p1.constructor === Person) // => true
console.log(p2.constructor === Person) // => true
console.log(p1.constructor === p2.constructor) // => true
对象的 constructor
属性最初是用来标识对象类型的, 但是,如果要检测对象的类型,还是使用 instanceof
操作符更可靠一些:
console.log(p1 instanceof Person) // => true
console.log(p2 instanceof Person) // => true
总结:
构造函数是根据具体的事物抽象出来的抽象模板
实例对象是根据抽象的构造函数模板得到的具体实例对象
每一个实例对象都具有一个
constructor
属性,指向创建该实例的构造函数注意:
constructor
是实例的属性的说法不严谨,具体后面的原型会讲到
可以通过实例的
constructor
属性判断实例和构造函数之间的关系注意:这种方式不严谨,推荐使用
instanceof
操作符,后面学原型会解释为什么
四、构造函数的问题
使用构造函数带来的最大的好处就是创建对象更方便了,但是其本身也存在一个浪费内存的问题:
function Person (name, age) {this.name = namethis.age = agethis.type = 'human'this.sayHello = function () {console.log('hello ' + this.name)}
}var p1 = new Person('Tom', 18)
var p2 = new Person('Jack', 16)
在该示例中,从表面上好像没什么问题,但是实际上这样做,有一个很大的弊端。 那就是对于每一个实例对象,type
和 sayHello
都是一模一样的内容, 每一次生成一个实例,都必须为重复的内容,多占用一些内存,如果实例对象很多,会造成极大的内存浪费。
console.log(p1.sayHello === p2.sayHello) // => false
对于这种问题我们可以把需要共享的函数定义到构造函数外部:
function sayHello () {console.log('hello ' + this.name)
}function Person (name, age) {this.name = namethis.age = agethis.type = 'human'this.sayHello = sayHello
}var p1 = new Person('Top', 18)
var p2 = new Person('Jack', 16)console.log(p1.sayHello === p2.sayHello) // => true
至此,我们利用自己的方式基本上解决了构造函数的内存浪费问题。
但是代码看起来还是那么的格格不入,那有没有更好的方式呢?
原型
更好的解决方案: prototype
JavaScript 规定,每一个构造函数都有一个 prototype
属性,指向另一个对象。
这个对象的所有属性和方法,都会被构造函数的所拥有。
这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在 prototype
对象上。
function Person (name, age) {this.name = namethis.age = age
}console.log(Person.prototype)Person.prototype.type = 'human'Person.prototype.sayName = function () {console.log(this.name)
}var p1 = new Person(...)
var p2 = new Person(...)console.log(p1.sayName === p2.sayName) // => true
这时所有实例的 type
属性和 sayName()
方法,其实都是同一个内存地址,指向 prototype
对象,因此就提高了运行效率。
构造函数、实例、原型三者之间的关系
任何函数都具有一个 prototype
属性,该属性是一个对象。
function F () {}
console.log(F.prototype) // => objectF.prototype.sayHi = function () {console.log('hi!')
}
构造函数的 prototype
对象默认都有一个 constructor
属性,指向 prototype
对象所在函数。
console.log(F.prototype.constructor === F) // => false
通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype
对象的指针 __proto__
。
var instance = new F()
console.log(instance.__proto__ === F.prototype) // => true
`__proto__` 是非标准属性。
实例对象可以直接访问原型对象成员。
instance.sayHi() // => hi!
总结:
任何函数都具有一个
prototype
属性,该属性是一个对象构造函数的
prototype
对象默认都有一个constructor
属性,指向prototype
对象所在函数通过构造函数得到的实例对象内部会包含一个指向构造函数的
prototype
对象的指针__proto__
所有实例都直接或间接继承了原型对象的成员
属性成员的搜索原则:原型链
了解了 构造函数-实例-原型对象 三者之间的关系后,接下来我们来解释一下为什么实例对象可以访问原型对象中的成员。
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性
搜索首先从对象实例本身开始
如果在实例中找到了具有给定名字的属性,则返回该属性的值
如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性
如果在原型对象中找到了这个属性,则返回该属性的值
也就是说,在我们调用 person1.sayName()
的时候,会先后执行两次搜索:
首先,解析器会问:“实例 person1 有 sayName 属性吗?”答:“没有。
”然后,它继续搜索,再问:“ person1 的原型有 sayName 属性吗?”答:“有。
”于是,它就读取那个保存在原型对象中的函数。
当我们调用 person2.sayName() 时,将会重现相同的搜索过程,得到相同的结果。
而这正是多个对象实例共享原型所保存的属性和方法的基本原理。
总结:
先在自己身上找,找到即返回
自己身上找不到,则沿着原型链向上查找,找到即返回
如果一直到原型链的末端还没有找到,则返回
undefined
例对象读写原型对象成员
读取:
先在自己身上找,找到即返回
自己身上找不到,则沿着原型链向上查找,找到即返回
如果一直到原型链的末端还没有找到,则返回
undefined
值类型成员写入(实例对象.值类型成员 = xx
):
当实例期望重写原型对象中的某个普通数据成员时实际上会把该成员添加到自己身上
也就是说该行为实际上会屏蔽掉对原型对象成员的访问
引用类型成员写入(实例对象.引用类型成员 = xx
):
同上
复杂类型修改(实例对象.成员.xx = xx
):
同样会先在自己身上找该成员,如果自己身上找到则直接修改
如果自己身上找不到,则沿着原型链继续查找,如果找到则修改
如果一直到原型链的末端还没有找到该成员,则报错(
实例对象.undefined.xx = xx
)
function Person(name, age) {this.name = namethis.age = age
}console.log(Person.prototype)Person.prototype.type = 'human'
Person.prototype.evr = {breathe: "氧气",drink: "水"
}Person.prototype.sayName = function () {console.log(this.name)
}var p1 = new Person("zhangsan", 12);
var p2 = new Person("lis", 11);p1.type = "cat"
console.log(p1.type); // cat
console.log(p1.__proto__.type) // human// p1.evr = "xx";
// console.log(p1.evr); // xx
// console.log(p1.__proto__.evr) // {breathe: "氧气", drink: "水"}p1.evr.drink = "纯净水";
console.log(p1.evr); // {breathe: "氧气", drink: "纯净水"}
console.log(p1.__proto__.evr) // {breathe: "氧气", drink: "纯净水"}
更简单的原型语法
我们注意到,前面例子中每添加一个属性和方法就要敲一遍 Person.prototype
。 为减少不必要的输入,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象:
function Person (name, age) {this.name = namethis.age = age
}Person.prototype = {type: 'human',sayHello: function () {console.log('我叫' + this.name + ',我今年' + this.age + '岁了')}
}
在该示例中,我们将 Person.prototype
重置到了一个新的对象。
这样做的好处就是为 Person.prototype
添加成员简单了,但是也会带来一个问题,那就是原型对象丢失了 constructor
成员。
所以,我们为了保持 constructor
的指向正确,建议的写法是:
function Person (name, age) {this.name = namethis.age = age
}Person.prototype = {constructor: Person, // => 手动将 constructor 指向正确的构造函数type: 'human',sayHello: function () {console.log('我叫' + this.name + ',我今年' + this.age + '岁了')}
}
原生对象的原型
所有函数都有 prototype 属性对象。
Object.prototype
Function.prototype
Array.prototype
String.prototype
Number.prototype
Date.prototype
...
练习:为数组对象和字符串对象扩展原型方法。
var array = [5, 4, 1, 8];Array.prototype.getSum = function () {// 求数组中所有偶数的和var sum = 0;for (var i = 0; i < this.length; i++) {if (this[i] % 2 === 0) {sum += this[i];}}return sum;
}
// 数组或者String 中的prototype是不可以修改的
// Array.prototype = {
// getSum: function () {
// // 求数组中所有偶数的和
// var sum = 0;
// for (var i = 0; i < this.length; i++) {
// if (this[i] % 2 === 0) {
// sum += this[i];
// }
// }
// return sum;
// }
// }console.log(array.getSum());
原型对象使用建议
私有成员(一般就是非函数成员)放到构造函数中
共享成员(一般就是函数)放到原型对象中
如果重置了
prototype
记得修正constructor
的指向
继承
什么是继承
继承是面向对象编程中的一个重要概念,通过继承可以使子类的实例使用在父类中定义的属性和方法。
继承的实现方式
对象字面量继承
var wjl = {name: '王健林',money: 10000000,cars: ['玛莎拉蒂', '特斯拉'],houses: ['别墅', '大别墅'],play: function () {console.log('打高尔夫');}
}var wsc = {name: '王思聪'
}// // 复制对象的成员给另一个对象
// for (var key in wjl) {
// // 不给wsc复制同名的属性
// if (wsc[key]) {
// continue;
// }
// wsc[key] = wjl[key];
// }// console.log(wsc);// 对象的拷贝
// 复制对象的成员给另一个对象
function extend(parent, child) {for (var key in parent) {// 不给wsc复制同名的属性if (child[key]) {continue;}child[key] = parent[key];}
}extend(wjl, wsc);console.log(wsc);
构造函数的属性继承:借用构造函数
function Person (name, age) {this.type = 'human'this.name = namethis.age = age
}function Student (name, age) {// 借用构造函数继承属性成员 // call()是改变函数中的this,直接调用函数Person.call(this, name, age)
}var s1 = new Student('张三', 18)
console.log(s1.type, s1.name, s1.age) // => human 张三 18
构造函数的原型方法继承:拷贝继承(for-in)
function Person(name, age) {this.type = 'human'this.name = namethis.age = age
}Person.prototype.sayName = function () {console.log('hello ' + this.name)
}function Student(name, age) {Person.call(this, name, age)
}// 原型对象拷贝继承原型对象成员
for (var key in Person.prototype) {Student.prototype[key] = Person.prototype[key]
}var s1 = new Student('张三', 18)s1.sayName() // => hello 张三
另一种继承方式:原型继承
function Person(name, age) {this.type = 'human'this.name = namethis.age = age}Person.prototype.sayName = function () {console.log('hello ' + this.name)
}function Student(name, age) {Person.call(this, name, age)
}// 利用原型的特性实现继承
Student.prototype = new Person();
Student.prototype.constructor = Student;var s1 = new Student('张三', 18)console.log(s1.type) // => humans1.sayName() // => hello 张三
【前端系列教程之JavaScript】15_JavaScript面向对象编程相关推荐
- 【前端系列教程之JavaScript】08_BOM编程
一.BOM概述 1.1.Web API API的概念 API(Application Programming Interface,应用程序接口)是一些预先定义的函数,或指软件系统不同组成部分衔接的约定 ...
- 【前端系列教程之JavaScript】01_JavaScript概述和引入方式
JavaScript概述 JavaScript可以做什么 页面的各种动画效果 页面的点击/移入响应 对客户端数据进行验证 各种页面小游戏 用途总结 嵌入动态文本于HTML页面. 对浏览器事件做出响应( ...
- 【前端系列教程之JavaScript】16_JavaScript函数进阶
函数进阶 函数回顾 函数的定义方式 函数声明 function foo () {} 函数表达式 var foo = function () {} 函数声明与函数表达式的区别 函数声明必须有名字 函数声 ...
- 【前端系列教程之HTML5】01_HTML概述
一.Web前端开发工具 1.1.常用前端开发工具介绍 软件开发工具是用于辅助软件生命周期过程的基于计算机的工具.通常可以设计并实现工具来支持特定的软件工程方法,减少手工方式管理的负担. 作为一个前端开 ...
- 【前端系列教程之CSS3】01_CSS概述、引入规则和三大机制
前言 最准确的网页设计思路是把网页分成三个层次,即:结构层.表示层.行为层. HTML:结构层,Hyper Text Markup Language,超文本标记语言 CSS:表现层,Cascading ...
- 【前端系列教程之HTML5】02_HTML文档结构
一.HTML文件(文档)定义 试想:我们上网访问到底做了什么? 上一节提到URL问题,组成部分由HTTP,服务器地址,端口号,资源路径;这些东西组成了我们上网访问所谓的网址,其中有服务器地址和路径,从 ...
- 【前端系列教程之CSS3】04_CSS定位和浮动详解
一.CSS 定位(重点) 1.1 CSS 定位属性 position 属性指定了元素的定位类型. position 属性的五个值: static 静态定位,无特殊定位,对象遵循正常文档流.top,ri ...
- 【前端系列教程之HTML5】06_HTML5新标签
一.HTML5新布局标签 许多现有网站都包含以下HTML代码: <div id="nav">, <div class="header"> ...
- 【前端系列教程之CSS3】06_CSS3边框、渐变、文本效果等
一.CSS3边框(重要) 用 CSS3,你可以创建圆角边框,添加阴影框,并作为边界的形象而不使用设计程序,如 Photoshop. 1.1 CSS3边框圆角 在 CSS2 中添加圆角棘手.我们不得不在 ...
最新文章
- 高并发高流量网站架构详解--转载
- 并发基础篇(一): Java 并发性和多线程
- [HDU 1015] Safecracker
- linux 设备 major 253,redhat5.5测试环境中使用udev配置raw设备
- 数据仓库之电商数仓-- 4、可视化报表Superset
- 虚方法的使用 c# 1613719803
- 网络安全实验报告 第一章
- 一招解决4道leetcode hard题,动态规划在字符串匹配问题中的应用
- oracle中sql语句排序,Oracle SQL排序方式与case语句
- timimg学习数据删了_如何评价Timing这个督促人学习的软件?
- python中的作用域_python中作用域
- MybatisPlus懒人代码生成器(附源码)
- android fmod,Android采用fmod库实现变声效果
- pb9连接mysql
- 【CSDN】博文导入微信公众号
- 域名如何转移?域名转移流程有哪些?
- Android模仿通讯录
- 模电——极性电容与非极性电容的异同
- eclipse改成护眼背景色
- 服务器如何多个网站和数据库,网站和数据库分两个服务器
热门文章
- C++ ostream源码
- 蓝桥杯:C语言,十六进制转十进制
- 从千篇一律到独树一帜,TOOM舆情监测系统助你成功!
- 边缘计算在物联网行业的应用
- Web前端20~45
- 湖北省计算机学院排名,湖北省大学名次:华中师大没进前三,7所全国百强大学...
- 一些喜欢的歌词热评(网易云/酷狗/酷我/qq)
- Android Studio 自带的虚拟机上网解决方案,100%成功
- [Python3]编写简单的登陆认证程序
- Shell 语言--字符提取