【修真院web小课堂】简述js中的面向对象编程
小课堂
大家好,我是IT修真院郑州分院第十期的学员金俊,一枚正直纯洁善良的程序员 今天给大家分享一下,修真院官网前端任务js6,深度思考中的知识点——面向对象编程
一、背景知识:
究竟什么是面向对象编程?
以上这些,都是本次小课堂要讲解的重点!
二、知识剖析
编程范式
编程范式的英语是 programming paradigm,范即模范之意,范式即模式、方法,是一类典型的编程风格,是指从事软件工程的一类典型的风格(可以对照“方法学”一词)。编程语言发展到今天,出现了好多不同的代码编写方式,但不同的方式解决的都是同一个问题 。
下面是一张编程范式的图
https://www.info.ucl.ac.be/~pvr/paradigmsDIAGRAMeng108.pdf
不过总体说来,我们可以简单地把这世界上纷乱的编程范式,分成这几类:声明式、命名式、逻辑的、函数式、面向对象的、面向过程的。
面向对象程序设计(英语:Object-oriented programming,缩写:OOP)是种具有对象概念的程序编程典范,同时也是一种程序开发的抽象方针。它可能包含数据、属性、代码与方法。对象则指的是类的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性,对象里的程序可以访问及经常修改对象相关连的数据。在面向对象程序编程里,计算机程序会被设计成彼此相关的对象1。
面向对象程序设计可以看作一种在程序中包含各种独立而又互相调用的对象的思想,这与传统的思想刚好相反:传统的程序设计主张将程序看作一系列函数的集合,或者直接就是一系列对电脑下达的指令。面向对象程序设计中的每一个对象都应该能够接受数据、处理数据并将数据传达给其它对象,因此它们都可以被看作一个小型的“机器”,即对象。目前已经被证实的是,面向对象程序设计推广了程序的灵活性和可维护性,并且在大型项目设计中广为应用。此外,支持者声称面向对象程序设计要比以往的做法更加便于学习,因为它能够让人们更简单地设计并维护程序,使得程序更加便于分析、设计、理解。
面向对象程序的特征:
分享非面向对象程序前身语言
面向对象编程通常共享高阶编程语言的低级功能。可用于建构一个程序的基本工具包括:
类与对象
支持面向对象编程语言通常利用继承其他类达到代码重用和可扩展性的特性。而类有两个主要的概念:
类(Class):定义了一件事物的抽象特点。类的定义包含了数据的形式以及对数据的操作。对象:是类的实例。其中类(Class)定义了一件事物的抽象特点。类的定义包含了数据的形式以及对数据的操作。举例来说,“狗”这个类会包含狗的一切基础特征,即所有“狗”都共有的特征或行为,例如它的孕育、毛皮颜色和吠叫的能力。类可以为程序提供模版和结构。一个类的方法和属性被称为“成员”。 我们来看一段伪代码:
类狗 开始 公有成员: 吠叫(): 私有成员: 毛皮颜色: 孕育: 结束
在这串代码中,我们声明了一个类,这个类具有一些狗的基本特征。关于公有成员和私有成员,请参见下面的继承性一节。
对象(Object)是类的实例。对象有时会对应到现实世界中的事物,举例来说,一个图形程序可能有圆形、矩形与画面等对象,一个线上购物系统可能有购物车、顾客与产品等类。[7]。有时对象会表示更抽象的实体,比如一个被打开的文件或是一个提供美国惯用量测转换的服务。每个对象就是一个特定类的实例(例如,名称是“玛丽”的对象可能是类雇员的一个实例)。程序在面向对象编程当中被视为方法,变量被视为成员或属性。例如,“狗”这个类列举狗的特点,从而使这个类定义了世界上所有的狗。而莱丝这个对象则是一条具体的狗,它的属性也是具体的。狗有皮毛颜色,而莱丝的皮毛颜色是棕白色的。因此,莱丝就是狗这个类的一个实例。一个具体对象属性的值被称作它的“状态”。(系统给对象分配内存空间,而不会给类分配内存空间。这很好理解,类是抽象的,系统不可能给抽象的东西分配空间,而对象则是具体的。)
假设我们已经在上面定义了狗这个类,我们就可以用这个类来定义对象:
定义莱丝是狗 莱丝.毛皮颜色 : 棕白色 莱丝.吠叫()
我们无法让狗这个类去吠叫,但是我们可以让对象“莱丝”去吠叫,正如狗可以吠叫,但没有具体的狗就无法吠叫。
动态配置与消息传递机制
定义上动态配置是指方法会随着实例动态的改变。而消息传递机制(Message Passing)是指一个对象通过接受消息、处理消息、传出消息或使用其他类的方法来实现一定功能。如:莱丝可以通过吠叫引起人的注意,从而导致一系列的事发生。
封装性
具备封装性(Encapsulation)的面向对象编程隐藏了某一方法的具体运行步骤,取而代之的是通过消息传递机制发送消息给它。封装是通过限制只有特定类的对象可以访问这一特定类的成员,而它们通常利用接口实现消息的传入传出。举个例子,接口能确保幼犬这一特征只能被赋予狗这一类。通常来说,成员会依它们的访问权限被分为3种:公有成员、私有成员以及保护成员。有些语言更进一步:Java可以限制同一包内不同类的访问;C#和VB.NET保留了为类的成员聚集准备的关键字:internal(C#)和Friend(VB.NET);Eiffel语言则可以让用户指定哪个类可以访问所有成员。
具备封装性(Encapsulation)的面向对象程序设计隐藏了某一方法的具体执行步骤,取而代之的是通过消息传递机制传送消息给它。因此,举例来说,“狗”这个类有“吠叫()”的方法,这一方法定义了狗具体该通过什么方法吠叫。但是,莱丝的朋友并不知道它到底是如何吠叫的。
从实例来看:
/* 一个面向过程的程序会这样写: */ 定义莱丝 莱丝.设置音调(5) 莱丝.吸气() 莱丝.吐气() /* 而当狗的吠叫被封装到类中,任何人都可以简单地使用: */ 定义莱丝是狗 莱丝.吠叫()
继承
继承性(Inheritance)是指,在某种情况下,一个类会有“子类”。子类比原本的类(称为父类)要更加具体化。例如,“狗”这个类可能会有它的子类“牧羊犬”和“吉娃娃犬”。在这种情况下,“莱丝”可能就是牧羊犬的一个实例。子类会继承父类的属性和行为,并且也可包含它们自己的。我们假设“狗”这个类有一个方法(行为)叫做“吠叫()”和一个属性叫做“毛皮颜色”。它的子类(前例中的牧羊犬和吉娃娃犬)会继承这些成员。这意味着程序员只需要将相同的代码写一次。
在伪代码中我们可以这样写:
类牧羊犬 : 继承狗 定义莱丝是牧羊犬 莱丝.吠叫() /* 注意这里调用的是狗这个类的吠叫方法。*/
回到前面的例子,“牧羊犬”这个类可以继承“毛皮颜色”这个属性,并指定其为棕白色。而“吉娃娃犬”则可以继承“吠叫()”这个方法,并指定它的音调高于平常。子类也可以加入新的成员,例如,“吉娃娃犬”这个类可以加入一个方法叫做“颤抖()”。设若用“牧羊犬”这个类定义了一个实例“莱丝”,那么莱丝就不会颤抖,因为这个方法是属于吉娃娃犬的,而非牧羊犬。事实上,我们可以把继承理解为“是”或“属于”。莱丝“是”牧羊犬,牧羊犬“属于”狗类。因此,莱丝既得到了牧羊犬的属性,又继承了狗的属性。 我们来看伪代码:
类吉娃娃犬 : 继承狗 开始 公有成员: 颤抖() 结束 类牧羊犬 : 继承狗 定义莱丝是牧羊犬 莱丝.颤抖() /* 错误:颤抖是吉娃娃犬的成员方法。 */
当一个类从多个父类继承时,我们称之为“多重继承”。如一只狗既是吉娃娃犬又是牧羊犬(虽然事实上并不合逻辑)。多重继承并不总是被支持的,因为它很难理解,又很难被好好使用。
多态
多态(Polymorphism)是指由继承而产生的相关的不同的类,其对象对同一消息会做出不同的响应[8]。例如,狗和鸡都有“叫()”这一方法,但是调用狗的“叫()”,狗会吠叫;调用鸡的“叫()”,鸡则会啼叫。 我们将它体现在伪代码上:
类狗 开始 公有成员: 叫() 开始 吠叫() 结束 结束 类鸡 开始 公有成员: 叫() 开始 啼叫() 结束 结束 定义莱丝是狗 定义鲁斯特是鸡 莱丝.叫() 鲁斯特.叫()
这样,虽然同样是做出叫这一种行为,但莱丝和鲁斯特具体做出的表现方式将大不相同。多态性的概念可以用在运算符重载上,本文不再赘述。
抽象性
抽象(Abstraction)是简化复杂的现实问题的途径,它可以为具体问题找到最恰当的类定义,并且可以在最恰当的继承级别解释问题。举例说明,莱丝在大多数时候都被当作一条狗,但是如果想要让它做牧羊犬做的事,你完全可以调用牧羊犬的方法。如果狗这个类还有动物的父类,那么你完全可以视莱丝为一个动物。
优缺点
不过,我们也需要知道面向对象的优缺点。
优点
能和真实的世界交相辉映,符合人的直觉。
面向对象和数据库模型设计类型,更多地关注对象间的模型设计。
强调于“名词”而不是“动词”,更多地关注对象和对象间的接口。
根据业务的特征形成一个个高内聚的对象,有效地分离了抽象和具体实现,增强了可重用性和可扩展性。
拥有大量非常优秀的设计原则和设计模式。
S.O.L.I.D(单一功能、开闭原则、里氏替换、接口隔离以及依赖反转,是面向对象设计的五个基本原则)、IoC/DIP……
缺点
代码都需要附着在一个类上,从一侧面上说,其鼓励了类型。代码需要通过对象来达到抽象的效果,导致了相当厚重的“代码粘合层”。因为太多的封装以及对状态的鼓励,导致了大量不透明并在并发下出现很多问题。
三、常见问题
基于原型的编程范式?
四、解决方案
-
基于原型的编程范式
基于原型(prototype)的编程其实也是面向对象编程的一种方式。没有 class 化的,直接使用对象。又叫,基于实例的编程。其主流的语言就是 JavaScript。与传统的面对象编程的比较如下。
与基于类编程的比较
在基于类编程当中,对象总共有两种类型。类定义了对象的基本布局和函数特性,而接口是“可以使用的”对象,它基于特定类的样式。在此模型中,类表现为行为和结构的集合,就接口持有对象的数据而言,对所有接口来说是相同的。区分规则因而首先是基于结构和行为,而后是状态。
原型编程的主张者经常争论说基于类的语言提倡使用一个关注分类和类之间关系开发模型。与此相对,原型编程看起来提倡程序员关注一系列对象实例的行为,而之后才关心如何将这些对象划分到最近的使用方式相似的原型对象,而不是分成类。因为如此,很多基于原型的系统提倡运行时原型的修改,而只有极少数基于类的面向对象系统(比如第一个动态面向对象的系统 Smalltalk)允许类在程序运行时被修改。
原型编程常与认知心理学的特定思想流派有关,同样强调prototypes 和 exemplars 作为学习过程中的关键词。
考虑到绝大多数基于原型的系统是基于解释性的和动态类型程序语言,这里要重点指出的是静态类型语言实现基于原型从技术上是可行的。用基于原型编程描述的 Omega 语言就是这样系统的一个例子。尽管根据 Omega 网站所述,Omega 也不是完全的静态,但是可能的时候,它的编译器有时会使用静态绑定来改进程序的效率。
对象构造
在基于原型的系统中构造对象有两种方法,通过复制已有的对象或者通过扩展空对象创建。
在基于类的语言中,一个新的实例通过类构造器和构造器可选的参数来构造,结果实例由类选定的行为和布局创建模型。在基于原型的系统中构造对象有两种方法,通过复制已有的对象 或者通过扩展 nihilo(空的)对象创建,因为大多数系统提供了不同的复制方法,扩展 nihilo 对象的方式鲜为人知。
提供扩展 nihilo 对象创建的系统允许对象从空白中创建而无需从已有的原型中复制。这样的系统提供特殊的文法用以指定新对象的行为和属性,无须参考已存在的对象。在很多原型语言中,通常有一个 Object 原型,其中有普遍需要的方法。它被用作所有其它对象的最终原型。扩展 nihilo 对象创建可以保证新对象不会被顶级对象的名字空间污染。(在JavaScript中,可以利用null原型来做到, i.e. Object.create(null))。
Cloning 指一个新对象通过复制一个已经存在的对象(就是他的原型)来构造自己的过程。于是新的对象拥有原来对象的所有属性,从这一点出发新对象的属性可以被修改。在某些系统中,子对象持有一个到它原型的直接链接(经由授权或类似方式)。并且原型的改变同样会导致它的副本的变化。其他系统中,如类 Forth 的程序语言,Kevo 在此情况下不传播原型的改变,而遵循一个更加连续的模型,其中被复制的对象改变不会通过他的副本传播。
JavaScript的原型概念
由于JavaScript缺乏类,它用构造函数和原型对象来给对象带来与类相似的功能。但是,这些相似的功能并不一定 "表现的跟类完全一致。在本章中,你会详细看到JavaScript如何使用构造函数和原型对象来创建对象。
我们先来看一个示例。
var foo = {name: "foo", one: 1, two: 2}; var bar = {three: 3};
每个对象都有一个 proto 的属性,这个就是“原型”。对于上面的两个对象,如果我们把 foo 赋值给 bar.proto,那就意味着,bar 的原型就成了 foo的。
bar.__proto__ = foo; // foo is now the prototype of bar.
于是,我们就可以在 bar 里面访问 foo 的属性了。
// If we try to access foo's properties from bar // from now on, we'll succeed. bar.one // Resolves to 1. // The child object's properties are also accessible. bar.three // Resolves to 3. // Own properties shadow prototype properties bar.name = "bar"; foo.name; // unaffected, resolves to "foo" bar.name; // Resolves to "bar"
需要解释一下 JavaScript 的两个东西,一个是 proto,另一个是 prototype,这两个东西很容易混淆。
这里说明一下:
_proto__ 主要是安放在一个实际的对象中,用它来产生一个链接,一个原型链连,用于寻找方法名或属性,等等。
prototype 是用 new 来创建一个对象时构造 proto 用的。它是构造函数的一个属性。
在 JavaScript 中,对象有两种表现形式, 一种是 Object (ES5 关于 Object 的文档),一种是 Function (ES5 关于 Function 的文档)。我们可以简单地认为,proto 是所有对象用于链接原型的一个指针,而 prototype 则是 Function 对象的属性,其主要是用来当需要 new 一个对象时让 proto 指针所指向的地方。 对于超级对象 Function 而言, Function.proto 就是 Function.prototype。
比如我们有如下的代码:
var a = { x: 10, calculate: function (z) { return this.x + this.y + z; } }; var b = { y: 20, __proto__: a }; var c = { y: 30, __proto__: a }; // call the inherited method b.calculate(30); // 60 c.calculate(40); // 80
其中的“原型链”如下所示:
注意:ES5 中,规定原型继承需要使用 Object.create() 函数。如下所示:
var b = Object.create(a, {y: {value: 20}}); var c = Object.create(a, {y: {value: 30}});
好了,我们再来看一段代码:
// 一种构造函数写法 function Foo(y) { this.y = y; } // 修改 Foo 的 prototype,加入一个成员变量 x Foo.prototype.x = 10; // 修改 Foo 的 prototype,加入一个成员函数 calculate Foo.prototype.calculate = function (z) { return this.x + this.y + z; }; // 现在,我们用 Foo 这个原型来创建 b 和 c var b = new Foo(20); var c = new Foo(30); // 调用原型中的方法,可以得到正确的值 b.calculate(30); // 60 c.calculate(40); // 80
那么,在内存中的布局是怎么样的呢?大概是下面这个样子。
这个图应该可以让你很好地看明白 proto 和 prototype 的差别了。
我们可以测试一下:
b.__proto__ === Foo.prototype, // true c.__proto__ === Foo.prototype, // true b.constructor === Foo, // true c.constructor === Foo, // true Foo.prototype.constructor === Foo, // true b.calculate === b.__proto__.calculate, // true b.__proto__.calculate === Foo.prototype.calculate // true
这里需要说明的是——
Foo.prototype 自动创建了一个属性 constructor,这是一个指向函数自己的一个 reference。
这样一来,对于实例 b 或 c 来说,就能访问到这个继承的 constructor 了。有了这些基本概念,我们就可以讲一下 JavaScript 的面向对象编程了。
注: 上面示例和图示来源于 JavaScript, The Core 一文。JavaScript 原型编程的面向对象我们再来重温一下上面讲述的内容:
function Person(){} var p = new Person(); Person.prototype.name = "Hao Chen"; Person.prototype.sayHello = function(){ console.log("Hi, I am " + this.name); } console.log(p.name); // "Hao Chen" p.sayHello(); // "Hi, I am Hao Chen"
在上面这个例子中:
我们先生成了一个空的函数对象 Person();
然后将这个空的函数对象 new 出另一个对象,存在 p 中;
这时再改变 Person.prototype,让其有一个 name 的属性和一个 sayHello() 的方法;
我们发现,另外那个 p 的对象也跟着一起改变了。
注意一下,
当创建 function Person(){} 时,Person.proto 指向 Function.prototype;
当创建 var p = new Person() 时,p.proto 指向 Person.prototype;
当修改了 Person.prototype 的内容后,p.proto 的内容也就被改变了。好了,我们再来看一下“原型编程”中面向对象的编程玩法。首先,我们定义一个 Person 类。
//Define human class var Person = function (fullName, email) { this.fullName = fullName; this.email = email; this.speak = function(){ console.log("I speak English!"); }; this.introduction = function(){ console.log("Hi, I am " + this.fullName); }; }
上面这个对象中,
包含了:
属性: fullName 和 email;
方法: speak() 和 introduction()。
其实,所谓的方法也是属性。然后,我们可以定义一个 Student 对象。
//Define Student class var Student = function(fullName, email, school, courses) { Person.call(this, fullName, email); // Initialize our Student properties this.school = school; this.courses = courses; // override the "introduction" method this.introduction= function(){ console.log("Hi, I am " + this.fullName + ". I am a student of " + this.school + ", I study "+ this.courses +"."); }; // Add a "exams" method this.takeExams = function(){ console.log("This is my exams time!"); }; };
在上面的代码中:
使用了 Person.call(this, fullName, email),call() 或 apply() 都是为了动态改变 this 所指向的对象的内容而出现的。这里的 this 就是 Student。
上面的例子中,我们重载了 introduction() 方法,并新增加了一个 takeExams()的方法。
虽然,我们这样定义了 Student,但是它还没有和 Person 发生继承关系。
为了要让它们发生关系,我们就需要修改 Student 的原型。我们可以简单粗暴地做赋值:Student.proto = Person.prototype ,但是,这太粗暴了。
我们还是使用比较规范的方式:
先用 Object.create() 来将Person.prototype 和 Student.prototype 关联上。
然后,修改一下构造函数 Student.prototype.constructor = Student;。
// Create a Student.prototype object that inherits // from Person.prototype. Student.prototype = Object.create(Person.prototype); // Set the "constructor" property to refer to Student Student.prototype.constructor = Student;
这样,我们就可以这样使用了。
var student = new Student("Hao Chen", "[email protected]", "XYZ University", "Computer Science"); student.introduction(); student.speak(); student.takeExams(); // Check that instanceof works correctly console.log(student instanceof Person); // true console.log(student instanceof Student); // true
上述就是基于原型的面向对象编程的玩法了。注:在 ECMAScript 标准的第四版开始寻求使 JavaScript 提供基于类的构造,且 ECMAScript 第六版有提供 "class"(类) 作为原有的原型架构之上的语法糖,提供构建对象与处理继承时的另一种语法。
五、编码实战
如上
六、拓展思考
我们可以看到,这种玩法就是一种委托的方式。在使用委托的基于原型的语言中,运行时语言可以仅仅通过循着一个序列的指针直到找到匹配这样的方式来定位属性或者寻找正确的数据。所有这些创建行为共享的行为需要的是委托指针。
不像是基于类的面向对象语言中类和接口的关系,原型和它的分支之间的关系并不要求子对象有相似的内存结构,因为如此,子对象可以继续修改而无需像基于类的系统那样整理结构。还有一个要提到的地方是,不仅仅是数据,方法也能被修改。因为这个原因,大多数基于原型的语言把数据和方法提作“slots”。
这种在对象里面直接修改的玩法,虽然这个特性可以带来运行时的灵活性,我们可以在运行时修改一个 prototype,给它增加甚至删除属性和方法。但是其带来了执行的不确定性,也有安全性的问题,而代码还变得不可预测,这有点黑科技的味道了。因为这些不像静态类型系统,没有一个不可变的契约对代码的确定性有保证,所以,需要使用者来自己保证。
七、参考文献
http://www.cnblogs.com/dolphinX/p/3348469.html
https://baike.so.com/doc/4477793-4686855.html
https://blog.****.net/xtzz92/article/details/51668644
八、更多讨论
1.面向对象优点:
优点
能和真实的世界交相辉映,符合人的直觉。
面向对象和数据库模型设计类型,更多地关注对象间的模型设计。
强调于“名词”而不是“动词”,更多地关注对象和对象间的接口。
根据业务的特征形成一个个高内聚的对象,有效地分离了抽象和具体实现,增强了可重用性和可扩展性。
拥有大量非常优秀的设计原则和设计模式。
S.O.L.I.D(单一功能、开闭原则、里氏替换、接口隔离以及依赖反转,是面向对象设计的五个基本原则)、IoC/DIP……
2.面向对象缺点:
代码都需要附着在一个类上,从一侧面上说,其鼓励了类型。代码需要通过对象来达到抽象的效果,导致了相当厚重的“代码粘合层”。因为太多的封装以及对状态的鼓励,导致了大量不透明并在并发下出现很多问题。
#### 3.多态是什么
多态(Polymorphism)是指由继承而产生的相关的不同的类,其对象对同一消息会做出不同的响应[8]。例如,狗和鸡都有“叫()”这一方法,但是调用狗的“叫()”,狗会吠叫;调用鸡的“叫()”,鸡则会啼叫。
今天的分享就到这里啦,欢迎大家点赞、转发、留言、拍砖~