JavaScript面向对象的程序设计(犯迷糊的小羊)

导语

前面的系列文章,基本把JavaScript的核心知识点的基本语法、标准库等章节讲解完;
本章开始进入JavaScript核心知识点的高级部分——面向对象的程序设计,这一部分的内容将会对对象这一数据类型做进一步的深化理解,并且讲述几种创建对象的设计模式以及JavaScript独特的继承机制;

1.理解对象和面向对象的程序设计

1.1 面向对象的程序设计

"面向对象编程"(Object Oriented Programming,缩写为OOP)本身是一种编程的思维模式,它把世界的一切看作是对象的集合,世界的运转就是靠一个个对象分工、合作的结果,体现一切皆“对象”思想;
而在程序设计当中,面向对象编程就可以看做编写各个具有特定功能的对象(模块)并将它们进行有机的分工合作,即目前的模块化编程就是面向对象的程序设计的实际应用;

1.2 理解对象

对象在前面的系列文章中曾经提到,从数据特征上看,对象是无序属性(键值对)的集合;
我们可以使用字面量和构造函数的方式去创建一个最为简单的对象:

var person = new Object();
person.name = "teren";
person.age = 18;
person.greet = function(){
   console.log("hello "+this.name);
}

var teren = {
 name:"teren",
 age:18,
 greet:function(){
   console.log("hello "+this.name);
 }
}

通常创建一个简单的对象,都是采用字面量的方式;
上面的对象就是对现实对象的一种抽象表达;

1.3 对象的属性类型

前面章节中我们使用delete命令可以删除一些对象的属性,有一些又不可以,使用Object.keys()方法只能遍历可枚举属性,那么对象的属性是否有一些特性是我们尚未了解的呢?
ES5提供了一种只有内部才用的特性(attribute)去描述对象的属性(property)的各种特性,使用[[attribute]]表示,在JavaScript中不能直接访问它们;
一个我们非常熟悉的栗子就是Number构造函数构造出来的实例对象;

我们无法直接访问num.[[PrimitiveValue]],这一属性,只能通过num.valueOf()访问该值;

ES5中定义对象属性的两种特性,数据特性和访问器特性,对象属性可以兼备这两种特性;

数据特性定义对象的属性值的特性,一个属性值可以包括以下四个数据特性:

[[Value]]:存放属性值;
[[Writable]]:是否可写属性;
[[Enumerable]]:是否为可枚举属性;
[[Configurable]]:是否可用delete命令删除;

访问器特性定义对象的属性在访问属性和设置属性时调用的两个函数getter和setter;

[[Get]]:访问属性时调用的函数;
[[Set]]:设置属性时调用的函数;

下面以一个实例对象直接讲解这两个特性:

//数据特性;
var teren = {};
Object.defineProperty(teren,{
  value:"teren",
  writable:false,
  enumerable:true,
  configurable:true
})

//访问器特性;
//html
<div id="name"></div>
//js
var obj = Object.defineProperty({},"name",{
set:function(name){
 document.getElementById('name').innerHTML=name
},
get:function(){
 console.log( document.getElementById('name').innerHTML
  )
 },
})
obj.name = "hello world"
obj.name

[demo]

Object.defineProperties可以一次性配置对象的多个属性;

2. 创建对象的方式

上一节我们对面向对象的程序设计思想和对象有了初步理解,这一节我们深入探讨一下对象的创建方式及其优缺点;

创建对象的不同方式也可以简单的称作设计模式,不同的设计模式在实际编程应用中起到不同的作用;

2.1 单例模式

单例模式就是产生一个类的唯一实例对象,它能够确保您只有一个对象实例能够实际派上用场;

单例模式下,创建对象方式如下:

var singleton = {
 attr:1,
 method:function(){
  return this.attr
 }
}
var ex1 = singleton;
var ex2 = singleton;
ex1 === ex2//true

上述创建单例的方式:

优点:使用非常简捷;
缺点:缺乏封装,成员暴露,初始化时占用资源;

可以使用闭包方式解决这一问题:

var substance = (function(){
 var unique;
 function init(){
  var type;
  return {
   setType:function(t){
    return type = t;
   }
  }
 }
 return {
  getInstance:function(){
   if(!unique){
    unique = init();
   }
   return unique;
  }
 }
})();

var Adam = substance.getInstance();
var Eve = substance.getInstance();
Adam === Eve
Adam.setType('Man')//Man

2.2 工厂模式

单例模式只能创作单个实例对象,也就是说如果将该实例对象赋予多个变量时,会存在对象的引用问题,即修改其中一个变量会影响到另一个变量;
有时我们需要创造多个结构相似的对象,只有部分属性有所区别,这时候工厂模式派上用场;

工厂模式的设计思想就是能够像工厂一样批量生产出相似属性和方法的对象,使用工厂模式能解决多个相似的问题,例如创造多个弹窗(只是标题不同);

function person(name,age){
 var obj = new Object();
 obj.name = name;
 obj.age = age;
 obj.greet = function(){
   return "hello "+this.name;
 };
 return obj
}
var Adam = person("Adam",18);
var Eve = person("Eve",20);

上述工厂模式:

优点:能批量生产结构类似的对象;封装创建对象的细节;
缺点:未能解决对象的类型,即由哪个构造函数创建的;

2.3 构造函数模式

构造函数可以创建特定类型的对象,类似之前的Array、RegExp等原生对象都能创造特定类型的实例对象;

function Person(name,age){
 this.name = name;
 this.age = age;
 this.greet = function(){
   return "hello "+this.name;
 }
}
var p1 = new Person('Adam',18);
var p2 = new Person('Eve',20);

使用构造函数模式就能够解决实例对象由谁创建的问题;

上述代码和工厂模式的区别在于:

1.没有显示创建新对象;
2.直接将属性和方法赋给this对象;
3.没有return语句;
4.函数名开头大写以区别普通函数;
5.使用new操作符去创建对象实例;

new操作符的原理

使用new操作符去调用函数和直接调用函数不同,其new操作符的运行函数的过程为:

创建一个新对象;
将构造函数的作用域赋给新对象并执行构造函
内的代码;
返回新对象;
使用代码表示如下:

function Person(name,age){
 this.name = name;
 this.age = age;
 this.greet = function(){
   return "hello "+this.name;
 }
}
function createPerson(name,age){
  var o = new Object();
  Person.call(o,name,age);
  return o;
}
var p1 = createPerson('Adam',18);
var p2 = createPerson('Eve',20);

使用构造函数模式创建对象的优缺点在于:

优点:能够识别对象属于的构造函数;
缺点:如果存在不同实例对象共享的属性和方法,使用构造函数模式则会浪费内存;

【注】
关于this关键字的更多知识点可以参见【what's this】;
构造函数如果不用new操作符调用和普通函数是一样的;

2.4 原型模式

每个函数都有一个prototype原型属性,这个原型属性可以部署特定类型的实例共享的属性和方法;

function Person(){}
}
Person.prototype.greet = function(){
 return "hello "+this.name;

将原来的greet函数部署在Person函数的prototype原型属性上,这样p1和p2可以共享该方法,而不像构造函数模式每创建一个实例就增加一个greet方法浪费内存;

【注】
关于原型对象的更多理解详见下一节——JavaScript的继承机制;

使用原型模式创建对象的优缺点在于:

优点:对于每个实例的共享属性和方法能够较好实现;
缺点:单独采用原型模式将无法区分不同实例的私有属性;

2.5 混合模式

混合模式,就是综合构造函数模式和原型模式的优缺点,构造函数模式部署实例的私有属性,原型模式部署实例的公有属性;

function Person(name,age){
  this.name = name;
  this.age = age;
}
Person.prototype.greet = function(){
  return "hello "+this.name;
}
var p1 = new Person("Adam",18);
var p2 = new Person("Eve",20);

混合模式是目前使用最广泛、认同度最高的一种创建自定义类型(类)的设计模式;

【注】
当然,设计模式不仅仅上述所提到,还有更加精深可以参考《设计模式》一书以及之前小羊写的一篇文章《设计模式梗概》;

3.JavaScript的继承机制

上一节我们通过创建对象的不同模式,隐式引出了原型对象的概念,这一节中我们将详细了解一下原型对象、原型链及其实现的继承机制;

前面,我们从数据特征上看,知道对象是无序属性(键值对)的集合;
现在,我们可以从面向对象的角度看,任何对象都是更为抽象的对象的实例,可以理解为类的概念;
从这个角度理解,我们现在可以重新定义一下对象和类的含义:
对象可以说是对现实事物的抽象,对象封装了属性和方法,属性值指的是对象的状态,方法指的是对象的行为;
类是提供一种模板的‘对象',它是对象的抽象;
举个简单的栗子:

function Person(name,age){
  this.name = name;
  this.age = age;
}
Person.prototype.greet = function(){
  return "hello "+this.name;
}
var p1 = new Person("Adam",18);
var p2 = new Person("Eve",20);

上述代码表明,p1和p2两个实例是对现实Adam和Eve的抽象,而“类”Person又是对2个实例的抽象,为创建相似结构的人提供标准的模板;
[注]ES6之前JavaScript中没有类,在ES6中定义了类;

3.1 原型对象

在上一节的原型模式中,我们提到每个函数都有一个prototype属性,这个属性指向函数的原型对象,可以部署特定类型的实例共享的属性和方法;

更为深入理解prototype原型对象,prototype原型对象不仅可以部署特定类型的实例共享的属性和方法,而且还是实现JavaScript继承的关键;

只要创建一个新函数就会为该函数创建一个prototype属性,每个prototype属性自动获得一个constructor属性,该属性指向prototype属性所在的函数指针;

当使用构造函数创建一个实例时,该实例内部包含一个内部属性__proto__指向构造函数的原型对象;

由此,一个简单的继承便产生了;
以下面代码为例:

function Person(name,age){
  this.name = name;
  this.age = age;
}
Person.prototype.greet = function(){
  return "hello "+this.name;
}
var p1 = new Person("Adam",18);
var p2 = new Person("Eve",20);

构造函数创建之后,自动创建一个prototype属性,prototype原型对象下有一个默认的constructor属性指向prototype属性所在的函数Person中;
在prototype原型对象上部署greet方法,实例p1的内部属性__proto__指向构造函数Person.prototype,由此继承了构造函数的原型对象上的greet方法;

【注意】

  • 实例的__proto__指向构造函数的prototype原型对象实现继承,这种联系存在于实例与构造函数的原型对象之间而不是构造函数之间;
  • 当js引擎解析对象的属性时,先会搜索对象本身的属性,如果没有则会去搜索__proto__指向的原型对象上的属性,直到找到为止,如果在对象本身定义的属性和原型对象上的具有相同属性名,则在读取该属性时,自身属性会屏蔽原型对象上的属性;
function Person(name,age){
  this.name = name;
  this.age = age;
}
Person.prototype.greet = function(){
  return "hello "+this.name;
}
var p1 = new Person("Adam",18);
p1.greet()//hello Adam;
p1.greet = function(){
  return "hello world"
}

修改构造函数的原型对象可以直接使用点操作,效果是直接在原来的原型对象上增加属性,有时需要增加的属性太多是,点操作就显得太麻烦,可以采用重置原型对象的方法:

function Person(name,age){
  this.name = name;
  this.age = age;
}
Person.prototype = {
 constructor:Person,
 greet1:function(){},
 greet2:function(){},
 greet3:function(){}
};
var p1 = new Person("Adam",18);
Person.prototype.constructor.name//"Object"

需要注意的是,重置原型对象后,要重新为原型对象的constructor的属性指回Person构造函数;
如果不重置constructor的话,那么此时的Person.prototype是由字面量创建的对象,字面量创建的对象默认的构造函数是Object;

3.2 原型链

上面我们只定义一个构造函数,实现一次继承;如果存在多个构造函数,它们之间也存在继承关系,那么就会形成一条关于继承的原型链;

function SuperType(name,age){
  this.name = name;
  this.age = age
}
SuperType.prototype.greet = function(){
  return "hello "+this.name
}
function SubType(name,age,height){
  SuperType.call(this,name,age);
  this.height = height;
}
SubType.prototype = Object.create(SuperType.prototype);
SubType.prototype.constructor = SubType;
SubType.prototype.method = function(){return 1;}

var instance = new SubType('teren',18,180)

上面就是一个最为常用的实现多个类间继承的设计模式;
使用Object.create(SuperType.prototype)的优缺点在于:

优点:能够创建一个新的SuperType.prototype对象赋给SubType.prototype,修改SubType.prototype这个而不影响原来构造函数SuperType.prototype;
缺点:虽然拥有子类的prototype和父类的prototype值是相同的,但内存不同,从而切断子类和父类之间的类型;

还可以使用SubType.prototype =new SuperType()实现相同效果,其优缺点在于:

优点:能够体现子类和父类的继承关系;
缺点:子类具有父类的私有属性;

所以,一般在实际实现原型链时使用Object.create()方法,而理解原型链时使用new SuperType()方法;

3.3 与原型对象相关的方法

遍历对象属性方法
Object.keys()Object.getOwnPropertyNames()用于遍历对象自身而不是继承的属性名,返回一个数组,其中Object.keys()只返回可枚举属性;

in用于检查一个对象是否具有某个属性。它不区分该属性是对象自身的属性,还是继承的属性;
for...in用于遍历对象的所有可枚举属性(不管是自身的还是继承的)

如果只遍历自身的属性,可以使用如下代码:

for (var key in instance){
 if(instance.hasOwnProperty(key)){
   console.log(key)
 }
}

判断属性是否为自身的方法

Object.prototype.hasOwnProperty()返回一个布尔值,用于判断某个属性定义在对象自身,还是定义在原型链上;

设置和获取实例对象的原型对象的方法

Object.getPropertyOf()返回一个实例对象的原型对象;
Object.setPropertyOf(obj,prototype)可传两个参数,第1个为现有参数,第2个为原型对象;

判断一个对象是否为另一个对象的原型对象

Object.prototype.isPrototypeOf()用于判断一个对象是否是另一个对象的原型;

小结

通读本文,我们可以知道:

  • 面向对象编程时一种思维模式,它把世界的一切看做是对象的集合,世界的运作是一个个对象分工合作的结果;映射到程序设计中,就是编写各个具有特定功能的对象(模块),并将它们有机整合使程序得以运作;
  • 对象从数据特征角度看,是无序键值对的集合,对象的属性具有两种特性——数据特性和访问器特性;
  • 创建对象的不同方式可以称为设计模式,本文简单讲解了单例模式、工厂模式、构造函数模式、原型模式、混合模式等;
  • 从面向对象角度看,对象可以说是对现实事物的抽象,类是对对象的抽象;
  • 每个函数都有一个原型对象prototype,既可以部署特定类型实例的共享属性和方法,也是JavaScript实现继承的关键;
  • prototype原型对象有一个constructor属性,该属性指向prototype所在的函数指针;
  • 每当使用构造函数创建一个实例时,该实例内部包含一个内部属性__proto__指向构造函数的原型对象,由此实现简单的继承;
  • 当A构造函数是B构造函数的实例时,由此就会形成一条原型链,即
    A构造函数的实例对象C的__proto__指向A构造函数的原型对象prototype,A构造函数prototype的__proto__指向B构造函数的原型对象prototype,B构造函数prototype的__proto__指向Function构造函数的prototype,Function的prototype的__proto__指向Object的prototype;
  • 与原型对象的相关方法包括:Object.keys()和Object.getPropertyNames()、for...in方法,Object.getPrototypeOf()和Object.setPrototypeOf()方法,Object.prototype.hasOwnProperty()和Object.prototype.isPrototypeOf()方法;

参考资料

《JavaScript高级程序设计(第3版)》
《JavaScript标准参考教程》——阮一峰

(0)

相关推荐

  • 详解JS面向对象编程

    因为JavaScript是基于原型(prototype)的,没有类的概念(ES6有了,这个暂且不谈),我们能接触到的都是对象,真正做到了一切皆为对象 所以我们再说对象就有些模糊了,很多同学会搞混类型的对象和对象本身这个概念,我们在接下来的术语中不提对象,我们使用和Java类似的方式,方便理解 方式一 类(函数模拟) function Person(name,id){ //实例变量可以被继承 this.name = name; //私有变量无法被继承 var id = id; //私有函数无法被继

  • JS 面向对象之继承---多种组合继承详解

    这一次要讲 组合.原型式.寄生式.寄生组合式继承方式. 1. 组合继承:又叫伪经典继承,是指将原型链和借用构造函数技术组合在一块的一种继承方式. 下面来看一个例子: function SuperType(name) { this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function() { alert(this.n

  • JavaScript求一组数的最小公倍数和最大公约数常用算法详解【面向对象,回归迭代和循环】

    本文实例讲述了JavaScript求一组数的最小公倍数和最大公约数常用算法.分享给大家供大家参考,具体如下: 方法来自求多个数最小公倍数的一种变换算法(详见附录说明) 最小公倍数的算法由最大公约数转化而来.最大公约数可通过如下步骤求得: (1) 找到a1,a2,..,an中的最小非零项aj,若有多个最小非零项则任取一个 (2) aj以外的所有其他非0项ak用ak mod aj代替:若没有除aj以外的其他非0项,则转到(4) (3) 转到(1) (4) a1,a2,..,an的最大公约数为aj 写

  • JS面向对象的程序设计相关知识小结

    面向对象的语言有一个标志,即拥有类的概念,抽象实例对象的公共属性与方法,基于类可以创建任意多个实例对象,一般具有封装.继承.多态的特性!但JS中对象与纯面向对象语言中的对象是不同的,ECMA标准定义JS中对象:无序属性的集合,其属性可以包含基本值.对象或者函数.可以简单理解为JS的对象是一组无序的值,其中的属性或方法都有一个名字,根据这个名字可以访问相映射的值(值可以是基本值/对象/方法). 一.理解对象: 第一种:基于Object对象 var person = new Object(); pe

  • 详解JavaScript基于面向对象之创建对象(2)

    接着上文<详解JavaScript基于面向对象之创建对象(1)>继续学习. 4.原型方式        我们创建的每个函数都有一个通过prototype(原型)属性,这个属性是一个对象,它的用途是包含可以由特定类型的所有实例共享的属性和方法.逻辑上可以这么理解:prototypt通过条用构造函数而创建的那个对象的原型对象.使用原型的好处就是可以让所有对象实例共享它所包含的属性和方法.也就是说,不必在构造函数中定义对象信息,而是直接将这些信息添加到原型中.        原型方式利用了对象的pr

  • javascript 面向对象function详解及实例代码

    javascript 面向对象function详解     js中的函数有三种表示方式: //函数的第一种表示方式:函数关键字的方式 function f1() { alert("f1"); } //函数的第二种表示方式:函数字面量的方式 var f2 = function() { alert("f2"); } //函数的第三种表示方式:构造函数的方式 var f3 = new Function('var a = 100; b = 200; return a+b;'

  • 详解JavaScript基于面向对象之创建对象(1)

    这一次我们深入的学习一下JavaScript面向对象技术,在学习之前,必要的说明一下一些面向对象的一些术语.这也是所有面对对象语言所拥有的共同点.有这样几个面向对象术语: 一.对象        ECMA-262把对象(object)定义为"属性的无序集合,每个属性存放一个原始值.对象或函数".严格来说,这意味着对象是无特定顺序的值的数组.尽管ECMAScript如此定义对象,但它更通用的定义是基于代码的名词(人.地点或事物)的表示. 二.类        每个对象都由类定义,可以把类

  • JS Pro-深入面向对象的程序设计之继承的详解

    原型链(prototype chaining): 利用原型来继承属性和方法.回顾一下构造函数(constructor),原型对象(prototype)和实例(instance)的关系.每一个构造函数都有一个prototype属性,该属性指向一个prototype对象:prototype对象也有constructor属性,指向该函数:而实例也有一个内部指针(__proto__)指向这个prototype对象.如果这个prototype对象是另外一个对象的实例会是怎样的呢?这样该prototype对

  • 详解JavaScript基于面向对象之继承实例

    javascript面向对象继承的简单实例: 作为一门面向对象的语言,继承自然是它的一大特性,尽管javascript的面向对象的实现机制和和c#和java这样典型的面向对象不同,但是继承的基本特点还是具有的,简单的说就是获得父级的方法和属性,下面是一段简单的实例,大家有兴趣可以分析一下: window.onload = function(){ function parent(age,name){ this.age = age; this.name = name; } parent.protot

  • 详解JavaScript基于面向对象之继承

    一.面相对象继承机制       这个实例使用UML很好的解释了继承机制.       说明继承机制最简单的方式是,利用一个经典的例子就是几何形状.实际上,几何形状只有两种,即椭圆形(是圆形的)和多边形(具有一定数量的边).圆是椭圆的一种,它只有一个焦点.三角形.矩形和五边形都是多边形的一种,具有不同数量的边.正方形是矩形的一种,所有的边等长.这就构成了一种完美的继承关系,很好的解释了面向对象的继承机制.        在这个例子中,形状是椭圆形和多边形的基类(通常我们也可以叫它父类,所有类都由

  • js面向对象之公有、私有、静态属性和方法详解

    现下,javascript大行其道,对于网站开发人员来说,javascript是必需掌据的一门语言,但随着jquery等框架的流行和使用,许多人对于原生javascript缺乏深入的理解,习惯了函数式的编辑风格,对于闭包.原型总是说不清道不明.对于js面向对象蹩脚的用着,而要了解js面向对象,就必需先了解js中什么是公有方法.特权方法.静态方法 方法/步骤 1.公有属性和公有方法 function User(name,age){ this.name = name;//公有属性 this.age

  • JAVASCRIPT THIS详解 面向对象

    虽然在开始学习的时候觉得比较难,但只要理解了,用起来是非常方便和意义确定的.JavaScript也提供了这个this关键字,不过用起来就比经典OO语言中要"混乱"的多了.下面就来看看,在JavaScript中各种this的使用方法有什么混乱之处? 1.在HTML元素事件属性中inline方式使用this关键字: <div onclick=" // 可以在里面使用this ">division element</div> 我们一般比较常用的方法

  • JS面向对象编程详解

    序言 在JavaScript的大世界里讨论面向对象,都要提到两点:1.JavaScript是一门基于原型的面向对象语言 2.模拟类语言的面向对象方式.对于为什么要模拟类语言的面向对象,我个人认为:某些情况下,原型模式能够提供一定的便利,但在复杂的应用中,基于原型的面向对象系统在抽象性与继承性方面差强人意.由于JavaScript是唯一一个被各大浏览器支持的脚本语言,所以各路高手不得不使用各种方法来提高语言的便利性,优化的结果就是其编写的代码越来越像类语言中的面向对象方式,从而也掩盖了JavaSc

随机推荐