JavaScript中的面向对象(一)

时间:2021-07-29 16:30:19

面向对象编程

面向对象英文全称叫做ObjectOriented,简称 OO。OO其实包括 OOA(ObjectOrientedAnalysis,面向对象分析 ) 、OOD(ObjectOrientedDesign,面向对象设计 )和 OOP(ObjectOrientedProgramming,面向对象的程序设计 ) 。
面向对象的语法只对应 OOP,只是 OO的一部分 。

一个典型的 OO编程过程应该是先整理需求 ,根据需求进行 OOA ,将真实世界的客观物件抽象成程序中的类或对象 ,这个过程经也称建模 , OOA的输出结果是一个个类或对象的模型图 。接下来要进行 OOD ,这一步的目的是处理类之间的耦合关系 ,设计类或对象的接口 ,此时会用到各种设计模式 ,例如观察者模式 、责任链模式等 。 OOA和 OOD是个反复迭代的过程 ,它们本身也没有非常清晰的边界 ,是相互影响 、制约的 。等 OOA和OOD结束之后 ,才到 OOP ,进行实际的编码工作 。OOA和 OOD是面向对象编程的思想和具体语言无关 ,而 OOP是面向对象编程的工具 ,和选用的语言相关 。 OOP是 OOA和 OOD的底层 ,不同语言的语法不同 ,所以 OOP不同 ,但 OOA和 OOD与具体要求语言无关 ,一般情况下可以轻易跨语言重用 。比如 ,我们经常会看到设计模式方面的书会声明是基于 Java或者基于 C #描述的 ,设计模式和具体语言无关 ,无论哪种支持面向对象的语言都可以使用设计模式 。

OOP是使用面向对象技术的基础 ,面向对象的思维最后是要通过 OOP来实施的 。但 OOP并不等于 OO ,甚至并不是 OO。OOA才是要重要 、最关键的部分 ,绝大多数情况下 ,我们的程序可能用不上很复杂的 OOD设计 , OOA会占据大部分的开发时间 , OOP反而只占很少量的时间 。OOA的熟练度直接关系到开发的速度 ,而 OOA能力的提升很需要经验的积累 。

面向过程和面向对象编程

写一个简单的电话本程序,实现简单的增删查改

//定义电话本
var phonebook = [
{name : "张三" , tel : 123456} ,
{name : "李四" , tel : 234561} ,
{name : "王五" , tel : 345612} ,
{name : "赵六" , tel : 456123} ,
{name : "小七" , tel : 561234}
]
//增
function add(oBook,oName,oTel) {
oBook.push({name : oName , tel : oTel})
}
//删
function remove(oBook,oName,oTel) {
var n;
for (var i=0; i<oBook.length; i++) {
if(oBook[i].name == oName) {
n = i;
break;
}
}
if (n != undefined) {
oBook.splice(n,1)
}
}
//查
function getTel(oBook,oName) {
var tel = "";
for (var i=0; i<oBook.length; i++) {
if(oBook[i].name == oName) {
tel = oBook[i].tel;
break;
}
}
return tel;
}
//改
function modify(oBook,oName,oTel) {
for (var i=0; i<oBook.length; i++) {
if(oBook[i].name == oName) {
oBook[i].tel = oTel;
break;
}
}
}

//使用程序
//从电话本中找到张三的号码
var str = getTel(phonebook,"张三");
console.log(str)

这种编程方式就是一种面向过程编程。程序由数据和函数组成,如果要执行什么操作
就把数据传给相应的’处理函数’,经过处理之后返回我们想要的结果。

面向过程存在的问题:
(1)数据和处理函数没有直接的联系,在执行操作时,我们不但要选择相应的处理函数还要自己准备处理函数需要的数据,也就是说需要同时关注处理函数和数据。
(2) 数据和函数都暴露在同一个作用域里面,没有私有和共有的概念,整个程序中所有的数据和处理函数都可以互相访问,在开发阶段的初期也许开发速度很快但是到后期的维护阶段,由于整个程序的耦合非常紧,任何一个处理函数和数据都有可能关联到其他地方,容易牵一发而动全身,加大维护难度。
(3) 其实面向过程的思维方式是典型的计算机思维方式———输入数据给处理器,
处理器内部执行运算,处理器返回结果。在计算机的世界里只能识别0和1,而现实生活中,我们思路并不是这样的简单,所有的东西在我们脑海中都是有状态有动作的物件。用面向过程的思维我们无法描绘客观世界的事物,因为无法直接使用生活中的思维方式。

面向过程的方式描述一个人与面向对象的方式描述一个人。

面向过程
var name = "Haha";
var state = "awake";
var say = function(oName) {
console.log("I'm " + oName);
};
var sleep = function(oState) {
oState = "asleep"
};
say(name);
sleep(state);

面向对象
var person = {
name : "Haha",
state : "awake",
say : function(oName) {
console.log("I'm " + this.name)
},
sleep = function() {
this.state = "asleep"
}
};
person.say();
person.sleep();

以上代码中,程序由变量和函数组成,而变量和函数无法联系起来共同描述同一个物件。
用计算机的思维进行编程其实是很难描述的,因为面向过程的思维方式是在描述一个个”动作”,
有动作的起点(初始数据),有动作的过程(初始数据传给处理函数进行处理),
有动作的终点(处理函数返回处理结果),而客观世界中存在的是一个个”物件”,
物件有状态,有动作,物件本身只是一个客观存在,它没有起点,没有终点。
能用面向过程思维描述的只是物件的动作,例如我开始睡觉(起点),意识逐渐模糊(过程),
睡着了(终点)。用面向过程的思维方式是无法描绘客观世界的事物的,
因为编程时无法直接使用生活中的思维方式。

而面向对象编程就是抛开计算机思维,使用生活中的思维进行编程的编程方式。
面向过程的思维就是描述一个个”动作”,面向对象的思维则是描述一个个”物件”,
客观生活中的物件,都可以通过面向对象思维映射到程序中。
在程序中我们管”物件”叫做”对象”,对象由两部分组成:”属性”和”行为”,
对应客观世界中物件的”状态”和”动作”。属性本质其实是个变量相当于面向过程中的数据,
而行为的本质其实是函数相当于面向过程中的处理函数。

面向对象改写电话本程序
//定义电话本类
function Phonebook(opt){
this._phonebook = opt;
}
PhoneBook.prototype = {
//增
add : function(oName,oTel) {
this._phonebook.push({name : oName , tel : oTel})
},
//删
remove : function(oName) {
var n;
for (var i=0; i<this._phonebook.length; i++) {
if(this._phonebook[i].name == oName) {
n = i;
break;
}
}
if (n != undefined) {
this._phonebook.splice(n,1)
}
},
//查
getTel : function(oName) {
var tel = "";
for (var i=0; i<this._phonebook.length; i++) {
if(this._phonebook[i].name == oName) {
tel = this._phonebook[i].tel;
break;
}
}
return tel;
},
//改
modify : function(oName,oTel) {
for (var i=0; i<this._phonebook.length; i++) {
if(this._phonebook[i].name == oName) {
this._phonebook[i].tel = oTel;
break;
}
}
}

}

//使用
var book1 = new Phonebook([
{name : "张三" , tel : 123456} ,
{name : "李四" , tel : 234561} ,
{name : "王五" , tel : 345612} ,
{name : "赵六" , tel : 456123} ,
{name : "小七" , tel : 561234}
]);

var result = book1.getTel("张三");
console.log(result)

ECMAScript三种开发模式

  • 面向过程
  • 面向对象
  • 函数式编程

javascript中的面向对象

和java这种基于类(class-base)的面向对象的编程语言不同,
javascript没有类这样的概念,但是javascript也是面向对象的语言,
这种面向对象的方式成为 基于原型(prototype-base)的面向对象。
虽然说ES6已经引入了类的概念来作为模板,通过关键字 “class” 可以定义类,
但ES6的这种写法可以理解为一种语法糖,它的绝大部分功能,ES5都可以做到,
新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
如果要理解基于原型实现面向对象的思想,那么理解javascript中的
三个重要概念: 构造函数(constructor)、原型(prototype)、原型链(prototype chain)
对帮助理解基于原型的面向对象思想就显得尤为重要。

构造函数

function Student(name, age){
this.name = name;
this.age = age;
this.sayName = function(){alert(this.name);};
}
var s1 = new Student("王铁锤", 18);

这种方式调用构造函数实际上会经历以下4个步骤:

  • 创建一个新对象( 在内部隐式调用了new Object() )
  • 将构造函数的作用域赋给新对象(把this绑定到实例对象)
  • 执行构造函数中的代码(为这个新对象添加属性)
  • 返回新对象。

构造函数与普通函数的区别

唯一区别:调用方式不同。构造函数首字母一定要大写。

任何函数,只要通过new操作符来调用,它就可以作为构造函数;
而任何构造函数,如果不通过new 操作符来调用,那它跟普通函数无区别。

this的指向

  • 用new关键字执行:this指向生成的实例对象
  • 普通函数执行:this指向调用函数的对象

原型对象prototype

  • 我们创建的每个函数都有一个prototype属性,这个属性值指向原型对象
  • 原型对象默认包含一个constructor属性,指向构造函数
  • 任何写在原型对象中的属性和方法都可以让所有对象实例共享

实例

用new关键字生成的对象称为实例,实例会复制构造函数内所有的属性和方法。
当调用构造函数创建一个新实例后,该实例的内部将包含一个内部属性[[prototype]],指向原型对象。
这个内部属性可以通过以下方式获取(通过实例得到原型对象):
在FF,Chrome等浏览器可以通过私有属性__proto__得到;
通过ES5方式去获取:Object.getPrototypeOf(实例);
function Student(name, age){
this.name = name;
this.age = age;
this.sayName = function(){alert(this.name);};
}
var s1 = new Student("王铁锤", 18);
s1就称作构造函数Student的实例

构造函数、原型对象和实例的关系

1,每个构造函数都有一个原型对象(prototype)

2,原型对象都包含一个指向构造函数的指针(constructor)

3,而实例都包含一个指向原型对象的内部指针([[prototype]])

判断原型和实例的关系(返回布尔值)
constructor: 得到构造函数的引用,一般用于判断该实例是否由某一构造函数生成
实例.constructor == Student //true
instanceof: 检测某个对象是不是某一构造函数的实例,适用于原型链中的所有实例
实例 instanceof Student //true
实例 instanceof Object //true
isPrototypeOf: 判断当前对象是否为实例的原型
原型对象.isPrototypeOf(实例) //true

构造函数方法很好用,但是单独使用存在一个浪费内存的问题(所有的实例会复制所有构造函数中的属性/方法)。
这样既不环保,也缺乏效率。通常的做法是:使用构造函数添加私有属性和方法,使用原型添加共享的属性和方法。

原型链

实例与Object原型对象之间的链条称为原型链

原型链搜索机制

  • 读取实例对象的属性时,先从实例对象本身开始搜索。如果在实例中找到了这个属性,则返回该属性的值
  • 如果没有找到,则继续搜索实例的原型对象,如果在原型对象中找到了这个属性,则返回该属性的值
  • 如果还是没找到,则向原型对象的原型对象查找,依此类推,直到Object的原型对象(最顶层对象)
  • 如果再Object的原型对象中还搜索不到,则抛出错误

重置原型对象

重置原型对象,可以一次性给原型对象添加多个方法

function Popover(){}
Popover.prototype = {
constructor:Popover,
show:function(){},
hide:function(){}
}
记得要加这一个属性constructor:Popover来解决原型对象与构造函数之家的识别问题。

内置原型对象

使用内置原型可以给已有构造函数添加方法

例如:给所有的字符串添加一个获取ascii码的方法
String.prototype.toascii = function() {
var result = [];
for (var i=0; i<this.length; i++) {
result.push(this[i].charCodeAt())
}
return result;
}

var str = 'abcd';
str = str.toascii();
console.log(str);// [97, 98, 99, 100]

使用new实例化的原型

每个被new实例化的对象都会包含一个proto 属性,它是对构造函数 prototype 的引用。

  function Foo(){};
var foo = new Foo();
console.log(foo.__proto__ === Foo.prototype); // ture

function Foo(){};
console.log(Foo.prototype.__proto__ === Object.prototype); // true

上面返回true 的原因是Foo.prototype 是Object预创建的一个对象,是Object创建的一个实例,
所以,Foo.prototype._proto 是Object.prototype的引用。

函数(function)对象的原型

在javascript中,函数是一种特殊的对象,所有的函数都是构造函数 Function 的实例。
所以,函数的原型链与new操作符实例化对象的原型链会不同。

function Foo() {}
console.log(Foo.__proto__ === Object.prototype);
// false
console.log(Foo.__proto__ === Function.prototype); // true

从上面代码可以看出,函数Foo的proto属性并不是指向到Object.prototype,
而是指向到Function.prototype,这就说明函数Foo是Function的一个实例。

console.log(Function.__proto__ === Function.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true

上面代码可以看出,函数Function自己本身也是构造函数Function的一个实例,这段读起来非常拗口,看下面的图:
JavaScript中的面向对象(一)
由此可见,Object、Function、Array等等这些函数,都是构造函数Function的实例。

instanceof运算符

instanceof运算符返回一个指定的对象是否一个类的实例,格式如:A instanceof B。其中,左操作数必须是一个对象,右操作数必须是一个类(构造函数)。判断过程:如果函数B在对象A的原型链(prototype chain)中被发现,那么instanceof操作符将返回true,否则返回false。
对照上文中的原型链图,看下面的代码:

function Foo() {}
var foo = new Foo();
console.log(foo instanceof Foo); // true
console.log(foo instanceof Object); // true
console.log(foo instanceof Function); // false,foo原型链中没有Function.prototype
console.log(Foo instanceof Function); // true
console.log(Foo instanceof Object); // true
console.log(Function instanceof Function); // true
console.log(Object instanceof Function); // true
console.log(Function instanceof Object); // true

javascript对象结构图

JavaScript中的面向对象(一)
通过图片可以看出,原型链的搜索机制。首先定义一个实例var s1 = new Foo(),s1在带调用某个方法时首先会在本身查找调用的方法,如果自身没有定义则通过proto 找到Foo的原型对象;Foo的原型对象包含着所有构造函数function Foo()的方法,如果在Foo的原型对象没有找到该方法则继续向原型对象的原型对象查找,依此类推,直到Object的原型对象,如果找到了则返回,否则抛出错误。