前面的系列文章,基本把JavaScript的核心知识点的基本语法、标准库等章节讲解完;
本章开始进入JavaScript核心知识点的高级部分——面向对象的程序设计,这一部分的内容将会对对象这一数据类型作进一步的深化理解,而且讲述几种建立对象的设计模式以及JavaScript独特的继承机制;javascript
"面向对象编程"(Object Oriented Programming,缩写为OOP)自己是一种编程的思惟模式,它把世界的一切看做是对象的集合,世界的运转就是靠一个个对象分工、合做的结果,体现一切皆“对象”思想;
而在程序设计当中,面向对象编程就能够看作编写各个具备特定功能的对象(模块)并将它们进行有机的分工合做,即目前的模块化编程就是面向对象的程序设计的实际应用;html
对象在前面的系列文章中曾经提到,从数据特征上看,对象是无序属性(键值对)的集合;
咱们可使用字面量和构造函数的方式去建立一个最为简单的对象:java
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); } }
一般建立一个简单的对象,都是采用字面量的方式;
上面的对象就是对现实对象的一种抽象表达;编程
前面章节中咱们使用delete命令能够删除一些对象的属性,有一些又不能够,使用Object.keys()方法只能遍历可枚举属性,那么对象的属性是否有一些特性是咱们还没有了解的呢?
ES5提供了一种只有内部才用的特性(attribute)去描述对象的属性(property)的各类特性,使用[[attribute]]
表示,在JavaScript中不能直接访问它们;
一个咱们很是熟悉的栗子就是Number构造函数构造出来的实例对象;设计模式
咱们没法直接访问num.[[PrimitiveValue]]
,这一属性,只能经过num.valueOf()
访问该值;数组
ES5中定义对象属性的两种特性,数据特性和访问器特性,对象属性能够兼备这两种特性;闭包
数据特性定义对象的属性值的特性,一个属性值能够包括如下四个数据特性:模块化
[[Value]]:存放属性值; [[Writable]]:是否可写属性; [[Enumerable]]:是否为可枚举属性; [[Configurable]]:是否可用delete命令删除;
访问器特性定义对象的属性在访问属性和设置属性时调用的两个函数getter和setter;函数
[[Get]]:访问属性时调用的函数; [[Set]]:设置属性时调用的函数;
下面以一个实例对象直接讲解这两个特性:this
//数据特性; 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能够一次性配置对象的多个属性;
上一节咱们对面向对象的程序设计思想和对象有了初步理解,这一节咱们深刻探讨一下对象的建立方式及其优缺点;
建立对象的不一样方式也能够简单的称做设计模式,不一样的设计模式在实际编程应用中起到不一样的做用;
单例模式就是产生一个类的惟一实例对象,它可以确保您只有一个对象实例可以实际派上用场;
单例模式下,建立对象方式以下:
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
单例模式只能创做单个实例对象,也就是说若是将该实例对象赋予多个变量时,会存在对象的引用问题,即修改其中一个变量会影响到另外一个变量;
有时咱们须要创造多个结构类似的对象,只有部分属性有所区别,这时候工厂模式派上用场;
工厂模式的设计思想就是可以像工厂同样批量生产出类似属性和方法的对象,使用工厂模式能解决多个类似的问题,例如创造多个弹窗(只是标题不一样);
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);
上述工厂模式:
优势:能批量生产结构相似的对象;封装建立对象的细节;
缺点:未能解决对象的类型,即由哪一个构造函数建立的;
构造函数能够建立特定类型的对象,相似以前的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操做符调用和普通函数是同样的;
每一个函数都有一个prototype原型属性,这个原型属性能够部署特定类型的实例共享的属性和方法;
function Person(){} Person.prototype.greet = function(){ return "hello "+this.name; }
将原来的greet函数部署在Person函数的prototype原型属性上,这样p1和p2能够共享该方法,而不像构造函数模式每建立一个实例就增长一个greet方法浪费内存;
【注】
关于原型对象的更多理解详见下一节——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);
混合模式是目前使用最普遍、认同度最高的一种建立自定义类型(类)的设计模式;
【注】
固然,设计模式不只仅上述所提到,还有更加精深能够参考《设计模式》一书以及以前小羊写的一篇文章《设计模式梗概》;
上一节咱们经过建立对象的不一样模式,隐式引出了原型对象的概念,这一节中咱们将详细了解一下原型对象、原型链及其实现的继承机制;
前面,咱们从数据特征上看,知道对象是无序属性(键值对)的集合;
如今,咱们能够从面向对象的角度看,任何对象都是更为抽象的对象的实例,能够理解为类的概念;
从这个角度理解,咱们如今能够从新定义一下对象和类的含义:
对象能够说是对现实事物的抽象,对象封装了属性和方法,属性值指的是对象的状态,方法指的是对象的行为;
类是提供一种模板的‘对象’,它是对象的抽象;
举个简单的栗子:
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中定义了类;
在上一节的原型模式中,咱们提到每一个函数都有一个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;
上面咱们只定义一个构造函数,实现一次继承;若是存在多个构造函数,它们之间也存在继承关系,那么就会造成一条关于继承的原型链;
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()方法;
遍历对象属性方法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版)》