Kivi

没有什么远大理想,只是永远都不会满足而已


  • 首页

  • 关于

  • 标签

  • 归档

javascript面向对象编程实践

发表于 2016-10-08 更新于 2017-07-02 分类于 javascript 阅读次数:
本文字数: 8.9k 阅读时长 ≈ 8 分钟

什么是面向对象编程

面向对象程序设计(英语:Object-oriented programming,缩写:OOP)是种具有对象概念的程序编程范型,同时也是一种程序开发的方法。它可能包含数据、属性、代码与方法。对象则指的是类的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性,对象里的程序可以访问及经常修改对象相关连的数据。在面向对象程序编程里,计算机程序会被设计成彼此相关的对象。

面向对象编程的基本概念以及js实践

类和对象


概念

  • 类:定义了一件事物的抽象特点,类的定义包含了数据的形式(属性)以及对数据的操作(方法)
  • 对象:类的实例(简单来说),在面向对象程序设计中,对象是程序的基本单元
  • 属性:对象的特征,比如颜色、尺寸等
  • 方法:对象的行为,比如行走、说话等
  • 构造函数:对象初始化的瞬间被调用的方法

实践

1. 类在面向对象编程中的作用(个人理解)

  • 面向对象编程概念中抽象,封装, 继承的实现
  • 生成对象

2. 类的使用(或者说是如何在js中实现类的功能)

javascript是一种基于对象(object-based)的语言,你遇到的所有东西几乎都是对象。但是,它又不是一种真正的面向对象编程(OOP)语言,因为它的语法中没有class(类)(es6之前)。

  • 面向对象编程概念中抽象,封装, 继承的实现
    因为上述原因的存在,在js中可能没有办法像传统OOP语言(C++, java)那样使用关键字class,或者用传统的思路进行OOP,那么需要做的就是寻求替代class应有功能的解决方案。
    这部分内容,在下文中抽象,封装,继承的章节中会有体现

  • 对象的生成
    1.对象字面量语法

    如果你从来没有接触过对象字面量的写法,可能会感觉怪怪的。但越到后来你就越喜欢它。本质上讲,对象字面量语法包括:
    1. 将对象主体包含在一对花括号内({ 和 })。
    2. 对象内的属性或方法之间使用逗号分隔。最后一个名值对后也可以有逗号,但在IE下会报错,所以尽量不要在最后一个属性或方法后加逗号。
    3. 属性名和值之间使用冒号分隔。
    4. 如果将对象赋值给一个变量,不要忘了在右括号}之后补上分号。

    2.构造函数创建对象/自定义构造函数/构造函数的返回值

    3.new Object()方法(不推荐使用这种方法)
    创建实例对象时能用对象字面量就不要使用new Object()构造函数,但有时你可能是在别人写的代码基础上工作,这时就需要了解构造函数的一个“特性”(也是不使用它的另一个原因),就是Object()构造函数可以接收参数,通过这个参数可以把对象实例的创建过程委托给另一个内置构造函数,并返回另外一个对象实例,而这往往不是你想要的。

以上是创建对象的三种基本的思想(方法),可以进行多种变形。如果对字面量或者是对象创建过程比较感兴趣,可以参考这篇文章第三章 字面量和构造函数

抽象


概念

抽象是人们认识事物的常用方法,比如地图的绘制。抽象的过程就是如何简化、概括所观察到的现实世界,并为人们所用的过程。

抽象是软件开发的基础。软件开发离不开现实环境,但需要对信息细节进行提炼、抽象,找到事物的本质和重要属性。

抽象包括两个方面:过程抽象和数据抽象。过程抽象把一个系统按功能划分成若干个子系统,进行”自顶向下逐步求精”的程序设计。数据抽象以数据为中心,把数据类型和施加在该类型对象上的操作作为一个整体(对象)来进行描述,形成抽象数据类型ADT。

所有编程语言的最终目的都是提供一种”抽象”方法。一种较有争议的说法是:解决问题的复杂程度直接取决于抽象的种类及质量。其中,”种类”是指准备对什么进行”抽象”。汇编语言是对基础机器的少量抽象。后来的许多”命令式”语言(如FORTRAN、BASIC和C)是对汇编语言的一种抽象。与汇编语言相比,这些语言已有了较大的进步,但它们的抽象原理依然要求程序设计者着重考虑计算机的结构,而非考虑问题本身的结构。在机器模型(位于”方案空间”)与实际解决的问题模型(位于”问题空间”)之间,程序员必须建立起一种联系。这个过程要求人们付出较大的精力,而且由于它脱离了编程语言本身的范围,造成程序代码很难编写,而且要花较大的代价进行维护。由此造成的副作用便是一门完善的”编程方法”学科。

为机器建模的另一个方法是为要解决的问题制作模型。对一些早期语言来说,如LISP和APL,它们的做法是”从不同的角度观察世界”、”所有问题都归纳为列表”或”所有问题都归纳为算法”。PROLOG则将所有问题都归纳为决策链。对于这些语言,可以认为它们一部分是面向基于”强制”的编程,另一部分则是专为处理图形符号设计的。每种方法都有自己特殊的用途,适合解决某一类的问题。但只要超出了它们力所能及的范围,就会显得非常笨拙。

面向对象的程序设计在此基础上则跨出了一大步,程序员可利用一些工具来表达问题空间内的元素。由于这种表达非常普遍,所以不必受限于特定类型的问题。人们将问题空间中的元素以及它们在方案空间的表示物称作”对象”。当然,还有一些在问题空间没有对应体的其他对象。通过添加新的对象类型,程序可进行灵活的调整,以便与特定的问题配合。所以在阅读方案的描述代码时,会读到对问题进行表达的话语。与以前的方法相比,这无疑是一种更加灵活、更加强大的语言抽象方法。

总之,OOP允许人们根据问题,而不是根据方案来描述问题。然而,仍有一个联系途径回到计算机。每个对象都类似一台小计算机;它们有自己的状态,而且可要求它们进行特定的操作。与现实世界的”对象”或者”物体”相比,编程”对象”与它们也存在共通的地方:它们都有自己的特征和行为。

总结

我的理解,抽象就是现实世界到计算机程序的映射,就是想办法用计算机的方式去描述现实世界的过程。
输入现实世界,输出计算机程序可以理解的概念,像“类和对象”里面的例子,输入的是现实世界的猫,输出的是一个有“名字”,“颜色”,“类型”等属性的,有“吃”这个方法的一个计算机程序概念,然后在把这个概念封装成为类,实例化成对象,运行在面向对象程序中。这个输入输出的过程,叫做抽象。

封装


概念

封装是面向对象编程的特征之一,也是类和对象的主要特征。封装将数据以及加在这些数据上的操作组织在一起,成为有独立意义的构件。外部无法直接访问这些封装了的数据,从而保证了这些数据的正确性。如果这些数据发生了差错,也很容易定位错误是由哪个操作引起的。

如果外部需要访问类里面的数据,就必须通过接口(Interface)进行访问。接口规定了可对一个特定的对象发出哪些请求。当然,必须在某个地方存在着一些代码,以便满足这些请求。这些代码与那些隐藏起来的数据叫作”隐藏的实现”。站在过程化程序编写(Procedural Programming)的角度,整个问题并不显得复杂。一种类型含有与每种可能的请求关联起来的函数。一旦向对象发出一个特定的请求,就会调用那个函数。通常将这个过程总结为向对象”发送一条消息”(提出一个请求)。对象的职责就是决定如何对这条消息作出反应(执行相应的代码)。

若任何人都能使用一个类的所有成员,那么可对这个类做任何事情,则没有办法强制他们遵守任何约束–所有东西都会暴露无遗。

有两方面的原因促使了类的编制者控制对成员的访问。第一个原因是防止程序员接触他们不该接触的东西–通常是内部数据类型的设计思想。若只是为了解决特定的问题,用户只需操作接口即可,无需明白这些信息。类向用户提供的实际是一种服务,因为他们很容易就可看出哪些对自己非常重要,以及哪些可忽略不计。进行访问控制的第二个原因是允许库设计人员修改内部结构,不用担心它会对客户程序员造成什么影响。例如,编制者最开始可能设计了一个形式简单的类,以便简化开发。以后又决定进行改写,使其更快地运行。若接口与实现方法早已隔离开,并分别受到保护,就可放心做到这一点,只要求用户重新链接一下即可。

封装考虑的是内部实现,抽象考虑的是外部行为。符合模块化的原则,使得软件的可维护性、扩充性大为改观。

实践

构造函数模式

为了解决从原型对象生成实例的问题,Javascript提供了一个构造函数(Constructor)模式。
所谓”构造函数”,其实就是一个普通函数,但是内部使用了this变量。对构造函数使用new运算符,就能生成实例,并且this变量会绑定在实例对象上(简单来说就是this对象所有的属性,方法,生成的对象都会有)。例如

1
2
3
4
5
6
7
8
function Cat(name,color) {
this.name = name;
this.color = color;
this.type = '猫科动物';
this.eat = function(){
console.log("吃老鼠");
};
}

这就是一个构造函数,生成实例对象:

1
2
3
4
var cat1 = new Cat("大毛","黄色");
var cat2 = new Cat("二毛","黑色");
alert(cat1.name); // 大毛
alert(cat1.color); // 黄色

缺点:

1
alert(cat1.eat == cat2.eat); //false

从上面的代码可以看出,eat方法被生成了2次,但却是一模一样的内容,所以,构造函数方法实现封装的问题是造成内存浪费

这里补充说明一点:new语句执行的过程
1. 一个新对象被创建。它继承自Cat.prototype.
2. 构造函数 Cat 被执行。执行的时候,相应的传参会被传入,同时上下文(this)会被指定为这个新实例。
3. 如果构造函数返回了一个“对象”,那么这个对象会取代整个new出来的结果。如果构造函数没有返回对象,那么new出来的结果为步骤1创建的对象,ps:一般情况下构造函数不返回任何值,不过用户如果想覆盖这个返回值,可以自己选择返回一个普通对象来覆盖。当然,返回数组也会覆盖,因为数组也是对象。

prototype模式

javascript规定,每一个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。
这意味着,我们可以把那些不变的属性和方法,直接定义在prototype对象上。

1
2
3
4
5
6
function Cat(name,color) {
this.name = name;
this.color = color;
}
Cat.prototype.type = "猫科动物";
Cat.prototype.eat = function(){ alert("吃老鼠") };

js常用方法(面向对象相关)

  1. constructor 获取对象的构造函数
  2. instanceof 左边是对象,右边是构造函数,验证原型对象与实例对象之间的关系
  3. isPrototypeOf 这个方法用来判断,某个proptotype对象和某个实例之间的关系
  4. hasOwnProperty 来判断某一个属性到底是本地属性,还是继承自prototype对象的属性
  5. in 判断某个实例是否含有某个属性,不管是不是本地属性

最佳实践

动态属性或方法通过构造函数封装,静态的属性或者方法通过原型封装

继承


概念

继承是一种联结类的层次模型,并且允许和鼓励类的重用,它提供了一种明确表述共性的方法。对象的一个新类可以从现有的类中派生,这个过程称为类的继承。新类继承了原始类的特性,新类称为原始类的派生类(子类),而原始类称为新类的基类(父类)。派生类可以从它的基类那里继承方法和实例变量,并且派生类可以修改或增加新的方法使之更适合特殊的需求。这也体现了大自然中一般与特殊的关系。继承性很好地解决了软件的可重用性问题。比如说,所有的Windows应用程序都有一个窗口,它们可以看作都是从一个窗口类派生出来的。但是有的应用程序用于文字处理,有的应用程序用于绘图,这是由于派生出了不同的子类,各个子类添加了不同的特性。

实践

继承实践会随着封装的方式在策略上有所不同

  • 构造函数封装,继承方式一定要是以下构造函数继承中的一种
  • 原型封装,继承的时候一定要复制父类原型属性方法
  • 构造函数混合原型封装,要保证原型和构造函数的属性方法都要能继承

构造函数继承

1.构造函数绑定

1
2
3
4
5
6
7
8
9
10
11
function Animal(){
this.species = "动物";
}

function Cat(name,color) {
Animal.apply(this, arguments); // 构造函数绑定重点
this.name = name;
this.color = color;
}
var cat1 = new Cat("大毛","黄色");
console.log(cat1.species);

小结:本质就是在子类的构造函数中调用父类的构造函数,这种继承方式的缺点是,父类原型上自定义的属性方法,通过构造函数是没有办法继承得到的。

2.prototype

1
2
3
4
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat1 = new Cat("大毛","黄色");
alert(cat1.species); // 动物

小结:用父类的实例,替换子类的prototype,并手动纠正构造函数。实例对象拥有构造函数原型的所有属性方法的特点,实现继承。注意,手动纠正constructor的那行代码很重要,如果不写,会造成继承紊乱

3.直接继承prototype

1
2
3
4
5
6
7
function Animal(){ }
Animal.prototype.species = '动物';

Cat.prototype = Animal.prototype;
Cat.prototype.constructor = Cat;
var cat1 = new Cat('大毛', '黄色');
alert(cat1.species); // 动物

小结:这种方法有优点,不用多实例化父类一次了,节省了资源。缺点是,一方面,父类的非静态属性(父类构造函数中添加的属性)没有办法得到继承,另一方面,父类和子类的prototype指向了同一个对象,任何对子类原型的修改都会反映到父类原型上。而且

1
2
Cat.prototype.constructor = Cat;
alert(Animal.prototype.constructor); // Cat

实际上上面的代码已经将父类的构造函数改掉了,所以个人不推荐这种方法

4.利用空对象作为中介

1
2
3
4
5
var F = function() {};
F.prototype = Animal.prototype;
Cat.prototype = new F();
Cat.prototype.constructor = Cat;
alert(Animal.prototype.constructor); // Animal

F是空对象,所以几乎不占内存。这时,修改Cat的prototype对象,就不会影响到Animal的prototype对象。
我们将上面的方法,封装成一个函数,便于使用。

1
2
3
4
5
6
7
8
9
10
11
function extend(Child, Parent) {
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.uber = Parent.prototype;
}

extend(Cat,Animal);
var cat1 = new Cat("大毛","黄色");
alert(cat1.species); // 动物

另外,说明一点,函数体最后一行意思是为子对象设一个uber属性,这个属性直接指向父对象的prototype属性。(uber是一个德语词,意思是”向上”、”上一层”。)这等于在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。

小结:修复了3中的问题,也一定程度上解决了内存占用的问题

5.拷贝继承
上面是采用prototype对象,实现继承。我们也可以换一种思路,纯粹采用”拷贝”方法实现继承。简单说,如果把父对象的所有属性和方法,拷贝进子对象,不也能够实现继承吗?这样我们就有了第五种方法。

首先,还是把Animal的所有不变属性,都放到它的prototype对象上。

1
2
3
4
5
6
7
8
9
10
11
function Animal() {}
Animal.prototype.species = "动物";

function extend2(Child, Parent) { // 浅拷贝
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
}

小结:自己复制父类原型所有属性方法到子类原型实现继承

非构造函数继承

1.解决问题的场景:两个对象,希望一个对象能够继承另外一个对象的属性

1
2
3
4
5
6
7
let Chinese = {
nation: '中国'
};

let Doctor = {
career: '医生'
};

让医生继承中国

1
2
3
var Doctor = object(Chinese);
Doctor.career = '医生';
alert(Doctor.nation); //中国

小结:原理就是把子对象的prototype属性,指向父对象,从而使得父对象和子对象关联起来。

2.浅拷贝

1
2
3
4
5
6
7
8
9
10
11
12
function extendCopy(p) {
var c = {};
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
return c;
}

var Doctor = extendCopy(Chinese);
Doctor.career = '医生';
alert(Doctor.nation); // 中国

小结:浅拷贝有一个问题。那就是,如果父对象的属性等于数组或另一个对象,那么实际上,子对象获得的只是一个内存地址,而不是真正拷贝,因此存在父对象被篡改的可能。

3.深拷贝

1
2
3
4
5
6
7
8
9
10
11
12
function deepCopy(p, c) {
var c = c || {};
for (var i in p) {
if (typeof p[i] === 'object') {
c[i] = (p[i].constructor === Array) ? [] : {};
deepCopy(p[i], c[i]);
} else {
c[i] = p[i];
}
}
return c;
}

小结:深拷贝解决了浅拷贝的问题

总结

  • 继承构造函数中封装的属性方法:
    1. 构造函数绑定
    2. 修改子类原型为父类实例对象
  • 继承原型上封装的属性方法:
    1. 利用空对象复制父类原型
    2. 遍历拷贝父类原型

多态


概念

多态性是指允许不同类的对象对同一消息作出响应。比如同样的加法,把两个时间加在一起和把两个整数加在一起肯定完全不同。又比如,同样的选择”编辑”、”粘贴”操作,在字处理程序和绘图程序中有不同的效果。多态性包括参数化多态性和运行时多态性。多态性语言具有灵活、抽象、行为共享、代码共享的优势,很好地解决了应用程序函数同名问题。

实践

1. 参数化多态性
参数化多态性的实现,需要结束function的arguments属性来实现
2. 运行时多态性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var makeSound = function(animal) {
animal.sound();
}

var Duck = function () {}
Duck.prototype.sound = function() {
console.log('嘎嘎嘎')
}
var Chiken = function () {};
Chiken.prototype.sound = function() {
console.log('咯咯咯')
}

makeSound(new Chicken());
makeSound(new Duck());

注意,这里的多态性的实现,并不遵循多态定义中子类重写父类方法,并且子类示可任意例替代父类实例的调用的概念,因为js是动态语言的缘故。

参考文献

  • Javascript 面向对象编程(一):封装
  • Javascript面向对象编程(二):构造函数的继承
  • Javascript面向对象编程(三):非构造函数的继承
  • 维基百科-面向对象程序设计
  • 深入解读JavaScript面向对象编程实践
  • 面向对象的三个基本特征 和 五种设计原则
  • JavaScript reference MDN
  • 字面量和构造函数
# javascript
Node.js生产环境部署监控方案:pm2+keymetrics
web端用户行为分析初探
  • 文章目录
  • 站点概览
kivi

kivi

nodejs | server
58 日志
17 分类
32 标签
RSS
  1. 1. 什么是面向对象编程
  2. 2. 面向对象编程的基本概念以及js实践
    1. 2.1. 类和对象
      1. 2.1.1. 概念
      2. 2.1.2. 实践
    2. 2.2. 抽象
      1. 2.2.1. 概念
      2. 2.2.2. 总结
    3. 2.3. 封装
      1. 2.3.1. 概念
      2. 2.3.2. 实践
      3. 2.3.3. 最佳实践
    4. 2.4. 继承
      1. 2.4.1. 概念
      2. 2.4.2. 实践
      3. 2.4.3. 总结
    5. 2.5. 多态
      1. 2.5.1. 概念
      2. 2.5.2. 实践
  3. 3. 参考文献
© 2019 kivi | 173k | 2:37
由 Hexo 强力驱动 v3.9.0
|
主题 – NexT.Pisces v7.3.0
|