淺談原型對象和原型鏈(源於學習整理筆記)

Javascript中,萬物皆對象,但對象也有區別,大體能夠分爲兩類,即:普通對象(Object)和函數對象(Function)。html

通常而言,經過new Function產生的對象是函數對象,其餘對象都是普通對象。瀏覽器

 

 

Javascript中建立對象有兩種方式:對象字面量和使用new表達式、app

 

JS中,每當建立一個函數對象f1 時,該對象中都會內置一些屬性,其中包括prototype__proto__,  prototype即原型對象,它記錄着f1的一些屬性和方法。 less

須要注意的是,prototype f1是不可見的,也就是說,f1不會查找prototype中的屬性和方法。 ide

 

function f(){} 函數

f.prototype.foo = "abc";this

console.log(f.foo); //undefinedspa

那麼,prototype有什麼用呢? 其實prototype的主要做用就是繼承。 通俗一點講,prototype中定義的屬性和方法都是留給本身的後代用的,所以,子類徹底能夠訪問prototype中的屬性和方法。prototype

想要知道f1是如何把prototype留給後代,咱們須要瞭解一下JS中的原型鏈,此時,JS中的 __proto__ 入場了,這哥們長的很奇特,隱藏的也很深,以至於你常常見不到它,但它在普通對象和函數對象中都存在, 它的做用就是保存父類的prototype對象,JS在經過new 表達式建立一個對象的時候,一般會把父類的prototype賦值給新對象的__proto__屬性,這樣,就造成了一代代傳承...設計

 

 

1.    原型鏈的造成真正是靠__proto__ 而非prototype,JS引擎執行對象的方法時,先查找對象自己是否存在該方法,若是不存在,會在原型鏈上查找,但不會查找自身的prototype

 

2.    一個對象的__proto__記錄着本身的原型鏈,決定了自身的數據類型,改變__proto__就等於改變對象的數據類型。

3.    函數的prototype不屬於自身的原型鏈,它是子類建立的核心,決定了子類的數據類型,是鏈接子類原型鏈的橋樑。

4.    在原型對象上定義方法和屬性的目的是爲了被子類繼承和使用。

 

 

JavaScript 中,函數也是對象,實際上, JavaScript 裏一切都是對象。函數對象和其它對象同樣,擁有能夠經過代碼訪問的屬性和一系列僅供 JavaScript 引擎訪問的內部屬性。其中一個內部屬性是 [[Scope]] ,由 ECMA-262 標準第三版定義,該內部屬性包含了函數被建立的做用域中對象的集合,這個集合被稱爲函數的做用域鏈,它決定了哪些數據能被函數訪問。    

 

 

 

JS面向對象的程序設計

 

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

 1、理解對象:

         第一種:基於Object對象

var person = new Object();

person.name = 'My Name';

person.age = 18;

person.getName = function(){

    return this.name;

}

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

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-2625版定義了JS對象屬性中特徵(用於JS引擎,外部沒法直接訪問)。ECMAScript中有兩種屬性:數據屬性和訪問器屬性

      1、數據屬性:

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

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

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

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

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

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

      以下:(瀏覽器支持:IE9+Firefox 4+ChromeSafari5+

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;

      2、訪問器屬性:

      它主要包括一對gettersetter函數,在讀取訪問器屬性時,會調用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?'成年':'未成年');//成年

      從上面可知,定義訪問器屬性時gettersetter函數不是必須的,而且,在定義gettersetter時不能指定屬性的configurablewritable特性;

      此外,ECMA-2625)還提供了一個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()方法同時定義了_ageisAudlt兩個屬性的特性

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

var descriptor = Object.getOwnPropertyDescriptor(person,'_age');

alert(descriptor.value);//19

   對於數據屬性,能夠取得:configurable,enumberable,writablevalue

   對於訪問器屬性,能夠取得:configurable,enumberable,getset

3、建立對象

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

     1、工廠模式

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的原生構造函數來完成的。

     2、構造函數模式

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個過程:

1.   建立一個對象

2.   將函數的做用域賦給新對象(所以this指向這個新對象,如:person1

3.   執行構造函數的代碼

4.   返回該對象

     上述由Person構造函數生成的兩個對象person1person2都是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;

}

     3、原型模式

     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,Biologyperson1修改影響了person2

     4組合構造函數及原型模式

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

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類型的封裝就是使用組合模式來實例的!!!

 

    5、動態原型模式

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

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

相關文章
相關標籤/搜索