建立自定義對象最簡單的方法就是建立一個Object 的實例,而後再爲它添加屬性和方法:編程
var person = new Object(); person.name = "Lilei"; person.age = 15; persion.sayName = function(){ console.log(this.name); }
或者使用對象字面量的形式:數組
var person = { name: "Lilei", age: 15, sayName: function(){ console.log(this.name); } }
建立自定義對象最簡單的方法就是建立一個Object 的實例,而後再爲它添加屬性和方法:app
var person = new Object(); person.name = "Lilei"; person.age = 15; persion.sayName = function(){ console.log(this.name); }
或者使用對象字面量的形式:函數
var person = { name: "Lilei", age: 15, sayName: function(){ console.log(this.name); } }
1.屬性類型this
ECMAScript 5 在定義只有內部採用的特性時,描述了屬性的各類特性,這些特性都是爲了實現JavaScript引擎用的,所以在JavaScript中不能直接訪問。爲了表示特性是內部值,該規範把他們放在了兩對方括號中。spa
要修改屬性默認的特性,必須使用ECMAScript 5 的Object.defineProperty()方法。這個方法接受三個參數:屬性所在的對象、屬性名字和一個描述符對象。其中描述符對象的屬性必須是:configurable、enumerable、writable 和 value。設置其中的一個或多個值,能夠修改對應的特性值。prototype
var person = {}; Object.defineProperty(person, "name", { writable: false, value: "Lilei" }); console.log(person.name); //Lilei person.name = "HanMeimei"; console.log(person.name); //Lilei
須要注意的是,若是把 configurable 特性改成false 後,就不能夠再對 除 writable以外的特性進行修改了,也就是說configurable 改爲false 後就改不回來了。指針
2.訪問器屬性code
訪問器屬性包括一對setter getter 函數,在讀取訪問器屬性時,會調用getter 函數,這個函數負責返回有效的值;在寫入訪問器屬性時,會調用setter函數並傳入新值,這個函數負責決定如何處理數據。訪問器屬性有以下4個特性。對象
訪問器屬性不能直接定義,必須用Object.defineProperty() 來定義。
var person = { name: "Lilei", age: 18, _year: 2015 }; Object.defineProperty(person, "year", { get: function(){ console.log("getFunction"); return this._year; }, set: function(newValue){ console.log("setFunction"); this._year = newValue; this.age += this.year - 2015; } }) person.year = 2016; console.log(person.age + " " + person._year); /** setFunction getFunction 19 2016 */
_year前面的下劃線是一種經常使用幾號,用於表示只能經過對象方法訪問的屬性(可是從例子中能夠看出,外部仍是能夠訪問的,只是一種規範約束)。
ECMAScript 5 提供了一個能夠一次性定義多個屬性的方法 Object.defineProperties()。接受兩個參數,第一個參數是要添加和修改其屬性的對象,第二個參數的屬性與第一個對象中要添加或修改的屬性一一對應。
var person = {}; Object.defineProperties(person, { _year:{ value: 2015 }, age:{ value: 18 }, year:{ get:function(){ console.log("getFunction"); return this._year; }, set: function(newValue){ console.log("setFunction"); this._year = newValue; this.age += newValue - 2015; } } }); person.year=2016; console.log(person.age + " " + person._year); //18 2015 var descriptor = Object.getOwnPropertyDescriptor(person, "_year"); console.log(descriptor); /** { value: 2015, writable: false, enumerable: false, configurable: false } */
須要注意的是,使用這種方法定義屬性,須要顯示定義屬性的特性,若是不指定,則默認爲false。有ECMA-262規則不一致。
var person = {}; Object.defineProperties(person, { _year:{ value: 2015, writable: true, enumerable: true, configurable: true }, age:{ value: 18, writable: true, enumerable: true, configurable: true }, year:{ get:function(){ console.log("getFunction"); return this._year; }, set: function(newValue){ console.log("setFunction"); this._year = newValue; this.age += newValue - 2015; } } }); person.year=2016; console.log(person.age + " " + person._year); //19 2016 var descriptor = Object.getOwnPropertyDescriptor(person, "_year"); console.log(descriptor); /** { value: 2016, writable: true, enumerable: true, configurable: true } */
上例中同時也用到了讀取屬性特性的函數:Object.getOwnPropertyDescriptor()方法。該函數接受兩個參數,屬性所在的對象和要讀取其描述符的屬性名稱。
前文涉及的兩種建立對象的方式有個明顯的缺點:使用同一個接口建立不少的對象,會產生大量的重複代碼。爲了解決這個問題,人們開始使用工廠模式的一種變體。
1.工廠模式
實際上就是使用一種函數來封裝以特定接口建立對象的細節。
function createPerson(name, age){ var o = new Object(); o.name = name; o.age = age; o.sayName = function(){ console.log(this.name); }; return o; } var person1 = createPerson("Leilei", 18); var person2 = createPerson("Hanmeimei", 17);
該方法有一個問題就是,沒有解決對象識別的問題,也就是person1 或 person2 看起來都是Object類型,不是Person類型。
因而人們繼續探索,隨着JavaScript的發展,又一個新的模式出現了。
2.構造函數模式
像Object Array這種的原生構造函數,在運行時會自動出如今執行環境中。此外,也能夠建立自定義的構造函數。
function Person(name, age){ this.name = name; this.age = age; this.sayName = function(){ console.log(this.name); } } var person1 = new Person("Lilei", 18); var person2 = new Person("Hanmeimei", 17); console.log(person1 instanceof Person); //true
這樣Person 就和Array這種的類型及其類似了。使用new操做符調用函數後,會發生如下事情:
經過該方法構造的對象既是Object對象又是Person對象(由於Object是全部類的基礎),如上,使用instanceof 操做符能夠獲得驗證。
構造函數和其餘函數的惟一區別就在於調用他們的方式不一樣。其實任何函數只要經過new操做符來調用,那它就能夠做爲構造函數;任何函數不經過new 操做符來調用,那它跟普通函數也不會有什麼兩樣。
function Person(name, age){ this.name = name; this.age = age; this.sayName = function(){ console.log(this.name); } } //做爲構造函數 var person1 = new Person("Lilei", 18); person1.sayName(); //Lilei //做爲普通函數 Person("HanMeimei", 17); sayName(); //HanMeimei //在另外一個對象的做用域中調用 var o = new Object(); Person.call(o, "Lily", 16); o.sayName(); //Lily
構造函數的問題是:每一個方法都要在每一個實例上從新建立一遍。例如前例中的sayName()函數,在person1和 person2 中都包含各自的Function 實例(Function 在Js中也是對象)。
所以person1 和 person2 的sayName()實例不不想等的。
console.log(person1.sayName == person2.sayName); //false
可一個作一個改進以下:
function Person(name, age){ this.name = name; this.age = age; this.sayName = sayName; } function sayName(){ console.log(this.name); } var person1 = new Person("Lilei", 18); var person2 = new Person("HanMeimei", 17); console.log(person1.sayName == person2.sayName); //true
但這樣破壞了對象的封裝性,sayName能夠在全局中調用,加上做用域之後能夠在任何對象上調用。
因而人們又創造出了原型模式。
3.原型模式
JavaScript中,每個函數都有一個prototype (原型)屬性,這個屬性是個指針,指向一個對象,而這個對象的用途是包含能夠由特定類型的全部實例共享的屬性和方法。
或者說,prototype 就是經過調用構造函數而建立的哪一個對象實例的原型對象。而那個原型對象中的屬性和方法是全部對象實例共享的。
只要咱們把屬性和方法放到原型對象中,就可讓全部的對象實例共享這些屬性和方法了。
function Person(){ } Person.prototype.name = "Lilei"; Person.prototype.age = 18; Person.prototype.sayName = function(){ console.log(this.name); } var person1 = new Person(); person1.sayName(); //Lilei var person2 = new Person(); person2.sayName(); //Lilei console.log(person1.sayName == person2.sayName); //true
每當讀取某個對象的某個屬性時,都會執行一次搜索,目標是具備給定名字的屬性。
搜索首先從對象實例自己開始,若是在實例中找到了具備給定名字的屬性,則返回該屬性的值,若是沒有找到,則繼續搜索指針指向的原型對象,在原型對象中查找具備給定名字的屬性,若是在原型對象中找到了該屬性,則返回。
上例中咱們執行person1.sayName()時,先查找實例person1 有 sayName 屬性嗎,沒有,則繼續查找prototype 中有sayName屬性嗎?有,則返回。
因此,若是person1 中存在name屬性,則不會訪問到原型的name 屬性。
使用delete 操做符,能夠徹底刪除實例屬性,可是不會刪除原型屬性,刪除後,就能夠訪問到原型中的屬性了。
function Person(){ } Person.prototype.name = "Lilei"; Person.prototype.age = 18; Person.prototype.sayName = function(){ console.log(this.name); } var person1 = new Person(); person1.name = "HanMeimei"; person1.sayName(); //HanMeimei var person2 = new Person(); person2.sayName(); //Lilei delete person1.name; person1.sayName(); //Lilei
使用hasOwnProperty() 方法能夠檢測一個屬性是存在於實例中,仍是存在與原型中。當給定屬性存在與對象實例中時返回true,不然返回false。
function Person(){ } Person.prototype.name = "Lilei"; Person.prototype.age = 18; var person1 = new Person(); console.log(person1.hasOwnProperty("name")); //false person1.name = "HanMeimei"; console.log(person1.hasOwnProperty("name")); //true delete person1.name; console.log(person1.hasOwnProperty("name")); //false
in 操做符有兩種用法:單獨使用 和 在for-in 循環中使用。單獨使用時,in 會在對象可以訪問屬性時,返回true,不然返回false。
也就是說,無論屬性是在對象實例中仍是原型中,只有屬性存在,in 就能返回true。
function hasPrototypeProperty(object, name){ return !object.hasOwnProperty(name) && (name in object); }
上面函數能夠判斷屬性是否只在原型中。
要取得對象上全部能夠枚舉的實例屬性,可使用ECMAScript 5 的 Object.keys() 方法,
使用Object.getOwnPropertyName() 方法能夠返回全部的實例屬性(包括不可枚舉的屬性)。
function Person(){ } Person.prototype.name = "Lilei"; Person.prototype.age = 18; Person.prototype.sayName = function(){ console.log(this.name); } var person1 = new Person(); person1.weight = 60; var keys = Object.keys(person1); //[ 'weight' ] console.log(keys); var keys2 = Object.getOwnPropertyNames(Person.prototype); console.log(keys2); //[ 'constructor', 'name', 'age', 'sayName' ]
也能夠直接把prototype 指針指向一個對象,這樣就不須要每添加一個屬性或方法都 要敲一遍 Person.prototype了。
可是隻是把prototype指向一個對象,那麼原型中的constructor就指向Object 了,而不會指向Person 函數了,因此能夠顯示制定constructor 屬性。
function Person(){ console.log("hello"); } Person.prototype = { constructor: Person, name: "Lilei", age:18, sayName: function(){ console.log(this.name); } } var person1 = new Person(); //hello console.log(person1 instanceof Person); //true console.log(person1.constructor()); //hello
但這樣設置constructor 屬性會致使它的[[Enumerable]] 特性被設置爲true。默認狀況下 constructor 屬性是不可枚舉的,所以能夠用Object.defineProperty()來設置constructor 屬性。
function Person(){ } Person.prototype = { name: "Lilei", age:18, sayName: function(){ console.log(this.name); } } Object.defineProperty(Person.prototype, "constructor", { enumerable: false, value: Person });
原型模式的問題是:全部的實例在默認狀況下都將取得相同的屬性值,對於那些包含基本值的屬性倒還好,畢竟經過在實例上添加一個同名屬性,能夠隱藏原型中的對象屬性。然而對於包含引用類型的屬性來講,一個實例上的修改會影響其餘實例的屬性。
function Person(){ } Person.prototype = { constructor: Person, name: "Lilei", age: 18, friends: ["Polly", "Tom"], sayName: function(){ console.log(this.name); } }; var person1 = new Person(); var person2 = new Person(); person1.friends.push("Jim"); console.log(person1.friends); //[ 'Polly', 'Tom', 'Jim' ] console.log(person2.friends); //[ 'Polly', 'Tom', 'Jim' ]
4.組合使對於包含引用類型的屬性來講,一個實例上的修改會影響其餘實例的屬性用構造函數和原型模式
建立自定義類型最多見的方式,就是組合使用構造函數模式與原型模式,構造函數模式用於構造實例屬性,而原型模式用於定義方法和共享的屬性。
結果是,每一個實例都有本身的一份實例屬性的副本,但同事有共享着對方法的引用,最大限度地節省了內存。
function Person(name, age){ this.name = name; this.age = age; this.friends = ["Polly", "Tom"]; } Person.prototype = { constructor: Person, sayName : function(){ console.log(this.name); } } var person1 = new Person("Lilei", 18); var person2 = new Person("HanMeimei", 17); person1.friends.push("Jim"); console.log(person1.friends); //[ 'Polly', 'Tom', 'Jim' ] console.log(person2.friends); //[ 'Polly', 'Tom' ]
這種構造函數與原型混成的模式,是目前ECMAScript 中使用最普遍、認同度最高的一種建立自定義類型的方法。能夠說,這是用來定義引用類型的一種默認模式。
5.動態原型模式
該模式把全部信息都封裝到構造函數內部,而在構造函數中初始化原型(在必要的狀況下),又保持了同事使用構造函數和原型的優勢。
function Person(name, age){ this.name = name; this.age = age; this.friends = ["Polly", "Tom"]; if(typeof this.sayName != "function"){ Person.prototype.sayName = function(){ console.log(this.name); } } } var person1 = new Person("Lilei", 18); var person2 = new Person("HanMeimei", 17); person1.friends.push("Jim"); console.log(person1.friends); //[ 'Polly', 'Tom', 'Jim' ] console.log(person2.friends); //[ 'Polly', 'Tom' ]
上例中 if語句只須要檢測一個函數的類型,而沒必要挨個檢查。if 語句內能夠定義多個原型函數。
6.寄生構造函數模式
有點相似與裝飾者模式,該模式的基本思想是建立一個函數,該函數的做用僅僅是封裝建立對象的代碼,而後再返回新建立的對象。從表面上看,這個函數又很像是典型的構造函數。
function Person(name, age){ var o = new Object(); o.name = name; o.age = age; o.sayName = function(){ console.log(this.name); } return o; } var person1 = new Person("Lilei", 18); person1.sayName();
是否是和工廠模式很像,可是這裏通常用的時候是新建一個複雜的相似的對象,並非Object對象,好比咱們要想建立一個擁有額外方法的特殊數組。因爲不能直接修改Array 構造函數,那麼就可使用這種模式。
function SpecialArray(){ var values = new Array(); values.push.apply(values, arguments); values.toPipedString = function(){ return this.join("|"); } return values; } var colors = new SpecialArray("red", "blue", "green"); console.log(colors.toPipedString()); //red|blue|green
console.log(colors instanceof SpecialArray); //false
實際上,返回的對象與構造函數或者與構造函數的原型屬性之間沒有關係;也就是說構造函數返回的對象與在構造函數外部建立的對象沒有什麼不一樣,因此同工廠模式同樣,返回的對象不能用instanceof來肯定對象類型,因此這種方法不推薦使用。
通常OO語言都支持兩種繼承方式:接口繼承和實現繼承。接口繼承只繼承方法簽名,而實現繼承則繼承實際的方法。
因爲JavaScript中沒有函數簽名,因此沒法實現接口繼承。JavaScript 只支持實現繼承,並且其實現繼承主要依賴原型鏈來實現。
1.原型鏈
ECMAScript中描述了原型鏈的概念,並將原型鏈做爲實現繼承的主要方法。其基本思想是利用原型讓一個引用類型繼承另外一個引用類型的屬性和方法。
每一個對象都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針。
加入咱們讓原型對象等於另外一個類型的實例,結果該實例的原型對象就將包含yi額指向另外一個原型的指針,相應的,另外一個原型中也包含着一個指向另外一個構造函數的指針。
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; }; function SubType(){ this.subproperty = false; } // SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function(){ return this.subproperty; }; var instance = new SubType(); console.log(instance.getSuperValue()); //true
在代碼中,沒有使用SubType默認的原型,而是給他換了一個新原型,就是SuperType 實例。因而新原型不只具備做爲一個SuperType 的實例所擁有的所有屬性和方法,並且其內部還有一個指針,指向了SuperType 的原型。
最終結果是這樣:instance 指向SubType的原型, SubType的原型有指向 SuperType 的原型。SubType.prototype.getSubValue 至關於在SuperType實例上增長方法。而getSuperValue 方法在SuperType 實例的原型對象中。此外,instance.constuctor 如今指向的是SuperType,這是由於原來 SubType.prototype 中的 constructor 被重寫了的緣故。
經過原型鏈,本質上擴展了前面介紹的原型搜索機制。
能夠用兩種方法來判斷實例的類型。
console.log(instance instanceof Object); //true console.log(instance instanceof SuperType); //true console.log(instance instanceof SubType); //true console.log(Object.prototype.isPrototypeOf(instance)); //true console.log(SuperType.prototype.isPrototypeOf(instance)); //true console.log(SubType.prototype.isPrototypeOf(instance)); //true
使用原型鏈方法須要注意,給子類型添加原型方法或者重寫原型方法必須在替換原型語句以後進行,並且不能使用家對象字面量建立原型的方法實現。不然原型鏈將會被切斷。
若是父類包含引用類型的屬性,那麼它成爲子類對象的原型後,引用類型的屬性將被全部子類對象共享。
function SuperType(){ this.colors = ["red", "blue", "green"]; } function SubType(){ } // SubType.prototype = new SuperType(); var sub1 = new SubType(); var sub2 = new SubType(); sub1.colors.push("yellow"); console.log(sub1.colors); //[ 'red', 'blue', 'green', 'yellow' ] console.log(sub2.colors); //[ 'red', 'blue', 'green', 'yellow' ]
2借用構造函數
這種技術的基本思想是在子類構造函數的內部調用超類構造函數。(別忘了,函數只是在特定環境中執行代碼的對象,所以經過使用 apply() 和 call() 方法也能夠在未來新建立的對象上執行構造函數)
function SuperType(){ this.colors = ["red", "blue", "green"]; } function SubType(){ SuperType.call(this); }var sub1 = new SubType(); sub1.colors.push("yellow"); console.log(sub1.colors); //[ 'red', 'blue', 'green', 'yellow' ] var sub2 = new SubType(); console.log(sub2.colors); //[ 'red', 'blue', 'green' ]
經過借調父類的構造函數,咱們其實是在新建立的SubType 實例中調用了SuperType 構造函數,結果SubType 的每一個實例都會具備本身的 colors 屬性的副本了。
借用構造函數的方法存在一個問題:就是方法都在構造函數中定義,所以函數服用就無從談起了。並且在超類型的原型中定義的方法,對子類型那個而言也是不可見的。
3.組合繼承
也叫作僞經典繼承,值得是將原型鏈和借用構造函數的技術組合到一塊兒,從而發揮兩者之長的一種繼承模式。
其思路是 使用原型鏈實現對原型屬性和方法的繼承,而經過借用構造函數來實現對實例屬性的繼承。這樣,即經過在原型上定義方法實現了函數複用,又可以保證每一個實例都有它本身的屬性。
function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function(){ console.log(this.name); }; function SubType(name, age){ SuperType.call(this, name); this.age = age; } SubType.prototype = new SuperType(); SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function(){ console.log(this.age); }; var sub1 = new SubType("Lilei", 18); sub1.colors.push("yellow"); console.log(sub1.colors); //[ 'red', 'blue', 'green', 'yellow' ] sub1.sayName(); //Lilei sub1.sayAge(); //18 var sub2 = new SubType("HanMeimei", 17); console.log(sub2.colors); //[ 'red', 'blue', 'green' ] sub2.sayName(); //HanMeimei sub2.sayAge(); //17
組合繼承避免了原型鏈和借用構造函數的缺陷,融合了他們的優勢,成爲JavaScript中最經常使用的繼承模式。並且,instanceof 和 isProtorypeOf() 也可以用於識別基於組合繼承建立的對象。
4.原型式繼承
原型式繼承要求你必須有一個對象能夠做爲另外一個對象的基礎。而後在根據具體需求對獲得的對象加以修改便可。
function object(o){ function F(){}; F.prototype = o; return new F(); } var person = { name: "Lilei", friends: ["Polly", "Tom"] }; var anotherPerson = object(person); anotherPerson.name = "Lei Li"; anotherPerson.friends.push("Jim"); var yetAnotherPerson = object(person); console.log(yetAnotherPerson.friends); //[ 'Polly', 'Tom', 'Jim' ] yetAnotherPerson.name = "HanMeimei"; yetAnotherPerson.friends.push("Lily"); console.log(person.friends); //[ 'Polly', 'Tom', 'Jim', 'Lily' ]
ECMAScript 5 經過新增Object.create()方法 規範化了原型式繼承。這個方法接收兩個參數,一個用做新對象原型的對象和(可選的)一個爲新對象定義額外屬性的對象。當傳入一個參數是,和上例中object()函數功能是同樣的。下例演示傳入兩個參數的方法。
var person = { name: "Lilei", friends: ["Polly", "Tom"] } var anotherPerson = Object.create(person, { name: { value: "HanMeimei" } }); console.log(anotherPerson.name); //HanMeimei console.log(person.name); //Lilei
在沒有必要興師動衆地建立構造函數,而只是想讓一個對象與另外一個對象保持相似的狀況下,原型式繼承是徹底能夠勝任的。包含引用類型的屬性始終都會共享相應的值,就像使用原型模式同樣。
5.寄生式繼承
寄生式繼承是與原型模式機密相關的一種思路,即建立一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來加強對象,最後再返回對象。
function createAnother(original){ var clone = Object.create(original); clone.sayHi = function(){ console.log("Hi"); }; return clone; } var person = { name: "Lilei", friends: ["Polly", "Tom"] }; var anotherPerson = createAnother(person); anotherPerson.sayHi(); //Hi
上例中Object.create()函數能夠用任何返回對象的函數代替。
使用寄生式繼承來爲對象添加函數,會因爲不能作到函數複用而下降效率;這一點與構造函數模式相似。
6.寄生組合式繼承
組合式繼承是Javascript中最經常使用的繼承模式,可是他也有本身的不足。組合繼承最大的問題就是不管什麼狀況下,都會調用兩次超類型的構造函數:一次是在建立子類型原型的時候,另外一次是在子類型構造函數的內部。
寄生組合式繼承,經過借用構造函數來繼承屬性,經過原型鏈的混成形式來繼承方法。其思路是:沒必要爲了制定子類型的原型而調用超類型的構造函數,咱們所須要的無非就是超類型的一個副本而已。
function inheritPrototype(subType, superType){ var prototype = Object(superType.prototype); prototype.constructor = subType; subType.prototype = prototype; } function SuperType(name){ this.name = name; this.friends = ["Polly", "Tom"]; } SuperType.prototype.sayName = function(){ console.log(this.name); }; function SubType(name, age){ SuperType.call(this, name); this.age = age; } inheritPrototype(SubType, SuperType); SubType.prototype.sayAge = function(){ console.log(this.age); };
這個例子的高效率體如今它只調用了一次 SuperType 構造函數,而且所以避免了在 SubType.prototype 上面建立沒必要要的、多餘的屬性。於此同時,原型鏈還能保持不變。所以還能正常shiyo個instanceof 和 isPrototypeOf()。寄生組合式繼承是引用類型最理想的繼承範式。
ECMAScript 支持面向對象編程,但不使用類或者接口。對象能夠在代碼執行過程當中,建立和加強,所以具備動態性而非嚴格定義的實體。能夠採用下面的犯法建立對象。
JavaScript 主要經過原型鏈實現繼承。原型鏈的構建是經過將一個類型的實例賦值給另外一個構造函數的原型實現的。這樣子類型就可以訪問超類型的全部屬性和方法,這一點與基於類的繼承很類似。原型鏈的問題是對象實例共享全部繼承的屬性和方法,所以不適合單獨使用。解決方法是借用構造函數,即在子類型構造函數的內部調用父類型的構造函數。這樣就能夠作到每一個實例都具備本身的屬性,同事還能保證是使用構造函數模式來定義類型。使用最多的繼承模式是組合繼承,這種模式使用原型鏈繼承共享的屬性和方法,而經過借用構造函數繼承是實例屬性。
另外,還存在下列可供選擇的繼承模式: