怎麼理解js的面向對象編程

面向對象的語言有一個標誌,即擁有類的概念,抽象實例對象的公共屬性與方法,基於類能夠建立任意多個實例對象,通常具備封裝、繼承、多態的特性!但JS中對象與純面嚮對象語言中的對象是不一樣的,ECMA標準定義JS中對象:無序屬性的集合,其屬性能夠包含基本值、對象或者函數。能夠簡單理解爲JS的對象是一組無序的值,其中的屬性或方法都有一個名字,根據這個名字能夠訪問相映射的值(值能夠是基本值/對象/方法)。編程

官方解釋瀏覽器

原型模式如類模式同樣,都是是一種編程泛型,即編程的方法論。另外最近大紅大紫的函數編程也是一種編程泛型。JavaScript之父Brendan Eich在設計JavaScript時,從一開始就沒打算爲其加入類的概念,而是借鑑了另外兩門基於原型的的語言:Self和Smalltalk。less

  既然同爲面嚮對象語言,那就得有建立對象的方法。在類語言中,對象基於模板來建立,首先定義一個類做爲對現實世界的抽象,而後由類來實例化對象;而在原型語言中,對象以克隆另外一個對象的方式建立,被克隆的母體稱爲原型對象。函數

1、理解對象:this

第一種:基於Object對象spa

var person = new Object();
person.name = 'My Name';
person.age = 18;
person.getName = function(){
return this.name;
}

第二種:對象字面量方式(比較清楚的查找對象包含的屬性及方法)prototype

var person = {
    name : 'My name',
    age : 18,
    getName : function(){
        return this.name;
    }
}

JS的對象可使用‘.’操做符動態的擴展其屬性,可使用’delete’操做符或將屬性值設置爲’undefined’來刪除屬性。以下:設計

person.newAtt=’new Attr’;//添加屬性
alert(person.newAtt);//new Attr
delete person.age;
alert(person.age);//undefined(刪除屬性後值爲undefined);

2、對象屬性類型指針

ECMA-262第5版定義了JS對象屬性中特徵(用於JS引擎,外部沒法直接訪問)。ECMAScript中有兩種屬性:數據屬性和訪問器屬性code

一、數據屬性:

數據屬性指包含一個數據值的位置,可在該位置讀取或寫入值,該屬性有4個供述其行爲的特性:

[[configurable]]:表示可否使用delete操做符刪除從而從新定義,或可否修改成訪問器屬性。默認爲true;

[[Enumberable]]:表示是否可經過for-in循環返回屬性。默認true;

[[Writable]]:表示是否可修改屬性的值。默認true;

[[Value]]:包含該屬性的數據值。讀取/寫入都是該值。默認爲undefined;如上面實例對象person中定義了name屬性,其值爲’My name’,對該值的修改都反正在這個位置

要修改對象屬性的默認特徵(默認都爲true),可調用Object.defineProperty()方法,它接收三個參數:屬性所在對象,屬性名和一個描述符對象(必須是:configurable、enumberable、writable和value,可設置一個或多個值)。

以下:(瀏覽器支持:IE9+、Firefox 4+、Chrome、Safari5+)

var person = {};
Object.defineProperty(person, 'name', {
configurable: false,
writable: false,
value: 'Jack'
});
alert(person.name);//Jack
delete person.name;
person.name = 'lily';
alert(person.name);//Jack

能夠看出,delete及重置person.name的值都沒有生效,這就是由於調用defineProperty函數修改了對象屬性的特徵;值得注意的是一旦將configurable設置爲false,則沒法再使用defineProperty將其修改成true(執行會報錯:can't redefine non-configurable property);

二、訪問器屬性:

它主要包括一對getter和setter函數,在讀取訪問器屬性時,會調用getter返回有效值;寫入訪問器屬性時,調用setter,寫入新值;該屬性有如下4個特徵:

[[Configurable]]:是否可經過delete操做符刪除從新定義屬性;

[[Numberable]]:是否可經過for-in循環查找該屬性;

[[Get]]:讀取屬性時調用,默認:undefined;

[[Set]]:寫入屬性時調用,默認:undefined;

訪問器屬性不能直接定義,必須使用defineProperty()來定義,以下:

var person = {
_age: 18
};
Object.defineProperty(person, 'isAdult', {
get: function () {
if (this._age >= 18) {
return true;
} else {
return false;
}
}
});


alert(person.isAdult?'成年':'未成年');//成年
從上面可知,定義訪問器屬性時getter與setter函數不是必須的,而且,在定義getter與setter時不能指定屬性的configurable及writable特性;

此外,ECMA-262(5)還提供了一個Object.defineProperties()方法,能夠用來一次性定義多個屬性的特性:

var person = {};
Object.defineProperties(person,{
_age:{
value:19
},
isAdult:{
get: function () {
if (this._age >= 18) {
return true;
} else {
return false;
}
}
}
});

alert(person.isAdult?'成年':'未成年');//成年

上述代碼使用Object.defineProperties()方法同時定義了_age及isAudlt兩個屬性的特性

此外,使用Object.getOwnPropertyDescriptor()方法能夠取得給定屬性的特性:

var descriptor = Object.getOwnPropertyDescriptor(person,'_age');
alert(descriptor.value);//19
對於數據屬性,能夠取得:configurable,enumberable,writable和value;

對於訪問器屬性,能夠取得:configurable,enumberable,get和set

3、建立對象

使用Object構造函數或對象字面量均可以建立對象,但缺點是建立多個對象時,會產生大量的重複代碼,所以下面介紹可解決這個問題的建立對象的方法

一、工廠模式

function createPerson(name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.getName = function () {
return this.name;
}
return o;//使用return返回生成的對象實例
}
var person = createPerson('Jack', 19, 'SoftWare Engineer');

建立對象交給一個工廠方法來實現,能夠傳遞參數,但主要缺點是沒法識別對象類型,由於建立對象都是使用Object的原生構造函數來完成的。

二、構造函數模式

function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.getName = function () {
return this.name;
}
}
var person1 = new Person('Jack', 19, 'SoftWare Engineer');

var person2 = new Person('Liye', 23, 'Mechanical Engineer');

使用自定義的構造函數(與普通函數同樣,只是用它來建立對象),定義對象類型(如:Person)的屬性和方法。它與工廠方法區別在於:

沒有顯式地建立對象
直接將屬性和方法賦值給this對象;
沒有return語句;
此外,要建立Person的實例,必須使用new關鍵字,以Person函數爲構造函數,傳遞參數完成對象建立;實際建立通過如下4個過程:

建立一個對象
將函數的做用域賦給新對象(所以this指向這個新對象,如:person1)
執行構造函數的代碼
返回該對象
上述由Person構造函數生成的兩個對象person1與person2都是Person的實例,所以可使用instanceof判斷,而且由於全部對象都繼承Object,所以person1 instanceof Object也返回真:

alert(person1 instanceof Person);//true;
alert(person2 instanceof Person);//true;
alert(person1 instanceof Object);//true;
alert(person1.constructor === person2.constructor);//ture;

雖然構造函數方式比較不錯,但也存在缺點,那就是在建立對象時,特別針對對象的屬性指向函數時,會重複的建立函數實例,以上述代碼爲基礎,能夠改寫爲:

function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.getName = new Function () {//改寫後效果與原代碼相同,不過是爲了方便理解
return this.name;
}
}

上述代碼,建立多個實例時,會重複調用new Function();建立多個函數實例,這些函數實例還不是一個做用域中,固然這通常不會有錯,但這會形成內存浪費。固然,能夠在函數中定義一個getName = getName的引用,而getName函數在Person外定義,這樣能夠解決重複建立函數實例問題,但在效果上並無起到封裝的效果,以下所示:

function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.getName = getName;
}
function getName() {//處處是代碼,看着亂!!
return this.name;
}

三、原型模式

JS每一個函數都有一個prototype(原型)屬性,這個屬性是一個指針,指向一個對象,它是全部經過new操做符使用函數建立的實例的原型對象。原型對象最大特色是,全部對象實例共享它所包含的屬性和方法,也就是說,全部在原型對象中建立的屬性或方法都直接被全部對象實例共享。

function Person(){
}
Person.prototype.name = 'Jack';//使用原型來添加屬性
Person.prototype.age = 29;
Person.prototype.getName = function(){
return this.name;
}
var person1 = new Person();
alert(person1.getName());//Jack
var person2 = new Person();
alert(person1.getName === person2.getName);//true;共享一個原型對象的方法

原型是指向原型對象的,這個原型對象與構造函數沒有太大關係,惟一的關係是函數的prototype是指向這個原型對象!而基於構造函數建立的對象實例也包含一個內部指針爲:[[prototype]]指向原型對象。

實例屬性或方法的訪問過程是一次搜索過程:

首先從對象實例自己開始,若是找到屬性就直接返回該屬性值;
若是實例自己不存在要查找屬性,就繼續搜索指針指向的原型對象,在其中查找給定名字的屬性,若是有就返回;
基於以上分析,原型模式建立的對象實例,其屬性是共享原型對象的;但也能夠本身實例中再進行定義,在查找時,就不從原型對象獲取,而是根據搜索原則,獲得本實例的返回;簡單來講,就是實例中屬性會屏蔽原型對象中的屬性;

原型與in操做符

一句話:不管原型中屬性,仍是對象實例的屬性,均可以使用in操做符訪問到;要想判斷是不是實例自己的屬性可使用object.hasOwnProperty(‘attr’)來判斷;

原生對象中原型

原生對象中原型與普通對象的原型同樣,能夠添加/修改屬性或方法,如如下代碼爲全部字符串對象添加去左右空白原型方法:

String.prototype.trim = function(){
return this.replace(/^\s+/,'').replace(/\s+$/,'');
}
var str = ' word space ';
alert('!'+str.trim()+'!');//!word space!

原型模式的缺點,它省略了爲構造函數傳遞初始化參數,這在必定程序帶來不便;另外,最主要是當對象的屬性是引用類型時,它的值是不變的,老是引用同一個外部對象,全部實例對該對象的操做都會其它實例:

function Person() {
}
Person.prototype.name = 'Jack';
Person.prototype.lessons = ['Math','Physics'];
var person1 = new Person();
person1.lessons.push('Biology');
var person2 = new Person();
alert(person2.lessons);//Math,Physics,Biology,person1修改影響了person2

四、組合構造函數及原型模式

目前最爲經常使用的定義類型方式,是組合構造函數模式與原型模式。構造函數模式用於定義實例的屬性,而原型模式用於定義方法和共享的屬性。結果,每一個實例都會有本身的一份實例屬性的副本,但同時又共享着對方方法的引用,最大限度的節約內存。此外,組合模式還支持向構造函數傳遞參數,可謂是集兩家之所長。

function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.lessons = ['Math', 'Physics'];
}
Person.prototype = {
constructor: Person,//原型字面量方式會將對象的constructor變爲Object,此外強制指回Person
getName: function () {
return this.name;
}
}
var person1 = new Person('Jack', 19, 'SoftWare Engneer');
person1.lessons.push('Biology');
var person2 = new Person('Lily', 39, 'Mechanical Engneer');
alert(person1.lessons);//Math,Physics,Biology
alert(person2.lessons);//Math,Physics
alert(person1.getName === person2.getName);//true,//共享原型中定義方法

在所接觸的JS庫中,jQuery類型的封裝就是使用組合模式來實例的!!!


五、動態原型模式

組合模式中實例屬性與共享方法(由原型定義)是分離的,這與純面嚮對象語言不太一致;動態原型模式將全部構造信息都封裝在構造函數中,又保持了組合的優勢。其原理就是經過判斷構造函數的原型中是否已經定義了共享的方法或屬性,若是沒有則定義,不然再也不執行定義過程。該方式只原型上方法或屬性只定義一次,且將全部構造過程都封裝在構造函數中,對原型所作的修改能當即體現全部實例中:

function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.lessons = ['Math', 'Physics'];
}
if (typeof this.getName != 'function') {//經過判斷實例封裝
  Person.prototype = {
    constructor: Person,//原型字面量方式會將對象的constructor變爲Object,此外強制指回Person
    getName: function () {
      return this.name;
    }
  }
}
var person1 = new Person('Jack', 19, 'SoftWare Engneer');
person1.lessons.push('Biology');
var person2 = new Person('Lily', 39, 'Mechanical Engneer');
alert(person1.lessons);//Math,Physics,Biology
alert(person2.lessons);//Math,Physics
alert(person1.getName === person2.getName);//true,//共享原型中定義方法

注:以上內容參考《JavaScript 高級程序設計》第3版

相關文章
相關標籤/搜索