Object 構造函數或對象字面量均可以用來建立單個對象。但這個方法的缺點很是明顯:同一個接口建立很可耐多對象會產生大量的重複代碼。爲了解決這個問題,人們開始使用工廠模式的一種變體。html
這個模式沒有解決對象識別的問題(即怎樣知道一個對象的類型)。如:數組
具體的建立單個對象:函數
var person = {}; person.name = "Oliver"; person.age = 18; person.sayName = function(){ return this.Name; };
改變成工廠模式:this
function createPerson(name,age){ var obj = {}; obj.name = name; obj.age = age; obj.sayName = function(){ return this.name }; return obj; //注意這裏要返回obj 對象,這樣才能把obj 對象傳給createPerson 變量。 } var newPerson = createPerson("Oliver",18);
構造函數能夠建立特定類型的對象。因此,能夠建立自定義的構造函數,從而定義自定義對象類型的屬性和方法。如:prototype
function Person(name,age){ //注意大小寫,構造函數應該把第一個字幕大寫化 this.name = name; this.age = age; this.sayName = function (){ return this.name; }; } var newPerson = new Person("Oliver",18); document.write(newPerson.name); //Oliver document.write(newPerson.age); //18 document.write(newPerson.sayName()); //Oliver
確實至關方便設計
這裏必定要記住,構造函數都應該以一個大寫字母開頭,用來區分其餘的函數指針
又如:code
function Person(name,age){ //注意大小寫,構造函數應該把第一個字幕大寫化 this.name = name; this.age = age; this.sayName = function (){ return this.name; }; } var person1 = new Person("Oliver",18); var person2 = new Person("Troy",24); document.write(person1.constructor == Person); //true document.write(person2.constructor == Person); //true
這裏的person1 和person2 分別保存着Person 的一個不一樣的實例。兩個對象都有一個constructor(構造函數)屬性,該屬性指向Person。htm
在上面這個例子中,person1 和person2 便是Object 的實例,同時也是Person 的實例。能夠經過instanceof 操做符來驗證:對象
console.log((person1 instanceof Object) && (person2 instanceof Person)); //true
以這種方式建立構造函數是定義在Global 中的(window 對象)
任何函數,只要經過new 操做符來調用,那它就能夠座位構造函數;而任何函數,若是不經過new 操做符來調用,那它就跟普通函數沒區別。以下面這個構造函數:
function Car(name,color,sound){ this.name = name; this.color = color; this.sound = function(){ return sound; }; console.log(this.name + " " + this.color + " " + this.sound()); }
若是當作構造函數來使用:
var benz = new Car("C200","White","Boom Boom"); //C200 White Boom Boom
若是做爲普通函數來調用:
Car("Benz","White","Boom!"); //Benz White Boom! console.log(window.name + window.color + window.sound()); //BenzWhiteBoom!
若是在另外一個對象的做用域中調用:
var cars = {}; Car.call(cars,"Benz","White","Boom Boom!"); document.write(cars.sound()); //Boom Boom!
問題是每一個方法都要在每一個實例是從新建立一遍。可用經過把內部的函數轉移到外部來解決這些問題。如:
function Car(name,color){ this.name = name; this.color = color; this.show = show; } function show(){ console.log(this.name + this.color); } var benz = new Car("Benz","white"); benz.show(); //Benzwhite
但這個問題是徹底沒有了封裝性可言。不過能夠經過原型模式來解決。
function Person(){}; Person.prototype.name = "Oliver"; Person.prototype.age = 18; Person.prototype.sayName = function(){ console.log(this.name); }; var person1 = new Person(); person1.sayName(); //Oliver var person2 = new Person(); person2.sayName(); //Oliver; console.log(person1.sayName == person2.sayName); //true
與構造函數不一樣的是,新對象的這些屬性和方法是由全部實例共享的。這裏兩個新的person 訪問的都是同一組屬性和同一個sayName() 函數。
以上面的Person 爲例,Person 構造函數裏面存在一個prototype 屬性,這個屬性指向原型對象Person Prototype,該Person Prototype 裏面包含了constructor 屬性,該屬性又指向構造函數Person。構造函數的實例包含了一個[[Prototype]]的內部屬性,該內部屬性則指向Person Prototype。如:
function Person(){}; Person.prototype.name = "Oliver"; Person.prototype.age = 18; Person.prototype.sayName = function(){ console.log(this.name); }; var person1 = new Person(); person1.sayName(); //Oliver var person2 = new Person(); person2.sayName(); //Oliver; console.log(Person.prototype); /* age: 18 constructor: function Person() {} name: "Oliver" sayName: function () { __proto__: Object */ console.log(Person.prototype.constructor); //function Person() {} console.log(Object.getPrototypeOf(person1)); /* age: 18 constructor: function Person() {} name: "Oliver" sayName: function () { __proto__: Object */
對於構造函數、原型屬性以及實例之間的關係,參見《js高級程序設計》一書中第6.2.3 章節。
兩個方法:isPrototypeOf()
和Object.getProtytypeOf()
(ECMAScript 5)。前者是用來肯定[[Prototype]];後者是用來返回[[Prototype]]值。如:
console.log(Person.prototype.isPrototypeOf(person1)); //true console.log(Object.getPrototypeOf(person1).name); //Oliver console.log(Object.getPrototypeOf(person1)); /* age: 18 constructor: function Person() {} name: "Oliver" sayName: function () { __proto__: Object */
爲對象添加一個屬性時,這個屬性會屏蔽原型對象中的同名屬性,可是並不會修改那個屬性。若是使用delete 刪除這個屬性,就能夠從新訪問原型中的屬性。如:
function Person(){}; Person.prototype.name = "Oliver"; Person.prototype.age = 18; Person.prototype.sayName = function(){ console.log(this.name); }; var person1 = new Person(); person1.sayName(); //Oliver 原型中的Name person1.name = "Troy"; person1.sayName(); //Troy 實例中的Name delete person1.name; person1.sayName(); //Oliver 原型中的Name
每次讀取某個對象的某個屬性,都會從實例自己開始搜索,若是沒有找到給定名字的屬性,則會在原型對象中再次搜索。
方法hasOwnProperty()
檢測屬性若是在對象實例中時,返回true。如:
console.log(person1.hasOwnProperty("age")); //false age屬性來自於原型 console.log(person1.hasOwnProperty("name")); //true name屬性來自於實例
兩種方法使用in 操做符:單獨使用和for-in 循環中使用。
單獨使用時,in 返回true 說明該屬性存在於實例或原型中。如:
function Person(){}; Person.prototype.name = "Oliver"; Person.prototype.age = 18; Person.prototype.sayName = function(){ console.log(this.name); }; var person1 = new Person(); person1.name = "Troy"; person1.sayName(); //Troy 實例中的Name console.log("name" in person1); //true name屬性在實例或原型中 console.log(person1.hasOwnProperty("name")); //true name屬性在實例中 //上面兩個就說明name屬性必定在實例中
又如:
function Person(){}; Person.prototype.name = "Oliver"; Person.prototype.age = 18; Person.prototype.sayName = function(){ console.log(this.name); }; var person1 = new Person(); person1.name = "Troy"; person1.sayName(); //Troy 實例中的Name var person2 = new Person(); console.log("name" in person1); //true name屬性在實例或原型中 console.log(person1.hasOwnProperty("name")); //true name屬性在實例中 //上面兩個就說明name屬性必定在實例中 console.log("name" in person2); //true console.log(person2.hasOwnProperty("name")); //false //上面兩個說明name屬性必定在原型中
自定義一個函數hasPrototypeProperty(object,name)
;即同時使用上面兩個方法來肯定屬性究竟是不是存在於實例中。如:
function Person(){}; Person.prototype.name = "Oliver"; Person.prototype.age = 18; Person.prototype.sayName = function(){ console.log(this.name); }; var person1 = new Person(); person1.name = "Troy"; person1.sayName(); //Troy 實例中的Name var person2 = new Person(); function hasPrototypeProperty(object,name){ console.log((name in object) && !(object.hasOwnProperty(name))) } hasPrototypeProperty(person2,"name"); //true name屬性是在原型中 hasPrototypeProperty(person1,"name"); //false name屬性是在實例中
用Object.defineProperty()
方法定義的屬性:
function Person(){}; Person.prototype.name = "Oliver"; Person.prototype.sayName = function(){ console.log(this.name); }; var person1 = new Person(); Object.defineProperty(person1, "age", { value: 18 }) console.log(person1.hasOwnProperty("age")); //true age屬性是實例屬性
關於for-in、[[enumerable]]、defineProperty、hasOwnProperty 的例子:
var person1 = { age: 18 }; Object.defineProperty(person1, "name", { value: "Oliver", enumerable: true }) for(var x in person1){ console.log(x); } console.log(person1.hasOwnProperty("name")); //true
又如:
function Person(age){ this.age = age; } var person1 = new Person(18); Object.defineProperty(person1, "name", { value: "Oliver", enumerable: false }) for(var x in person1){ console.log(x); //用defineProperty 定義的name 屬性是實例屬性,這裏不會枚舉到 } console.log(person1.hasOwnProperty("name")); //true
又如:
function Person(){}; Person.prototype.age = 18; var person1 = new Person(); Object.defineProperty(person1, "name", { value: "Oliver", enumerable: false }) for(x in person1){ console.log(x); //這裏仍然不回枚舉到自定義的name 實例屬性 }
可是:
function Person(){}; Person.prototype.age = 18; Person.prototype.name = "Oliver"; var person1 = new Person(); Object.defineProperty(person1, "name", { enumerable: false }) for(x in person1){ console.log(x); //這裏則返回枚舉到自定義的name 原型屬性 }
原型屬性的[[enumerable]]設置爲false,調用for-in 仍然能夠被枚舉到。
另外,Object.keys()
方法能夠返回全部可枚舉屬性的字符串數組:
function Person(){}; Person.prototype.age = 18; Person.prototype.name = "Oliver"; var person1 = new Person(); Object.defineProperty(person1, "sound", { value: "miao~", enumerable: true //可枚舉 }); Object.defineProperty(person1, "sound2", { value: "wang~", enumerable: false //不可枚舉 }); console.log(Object.keys(Person.prototype)); //["age", "name"] console.log(Object.keys(person1)); //["sound"]
Object.getOwnPropertyName()
方法,則能夠返回不管能否枚舉的全部實例屬性:
function Person(){}; Person.prototype.age = 18; Person.prototype.name = "Oliver"; var person1 = new Person(); Object.defineProperty(person1, "sound", { value: "miao~", enumerable: true //可枚舉 }); Object.defineProperty(person1, "sound2", { value: "wang~", enumerable: false //不可枚舉 }); console.log(Object.keys(Person.prototype)); //["age", "name"] console.log(Object.keys(person1)); //["sound"] console.log(Object.getOwnPropertyNames(Person.prototype)); //["constructor", "age", "name"] console.log(Object.getOwnPropertyNames(person1)); //["sound","sound2"]
即字面量方法:
function Person(){}; Person.prototype = { name: "Oliver", age: 18, sayName: function(){ console.log(this.name); } }; var person1 = new Person(); console.log(Person.prototype.constructor); //再也不指向Person()構造函數 function People(){}; People.prototype.name = "Troy"; People.prototype.age = 26; People.prototype.sayName = function(){ console.log(this.name); }; var people1 = new People(); console.log(People.prototype.constructor); //這裏則指向People()構造函數
上面第一種就是字面量方法。可是由此帶來的問題是,他的原型對象中的constructor 屬性將再也不指向上個例子中的Person() 構造函數了。(其實咱們本質上是重寫了prototype對象)
若是constructor 值真的很是重要,則只須要把它設置回適當的值就能夠了:
function Person(){}; Person.prototype = { constructor: Person, name: "Oliver", age: 18, sayName: function(){ console.log(this.name); } }; var person1 = new Person(); console.log(Person.prototype.constructor); //從新指向Person()構造函數 function People(){}; People.prototype.name = "Troy"; People.prototype.age = 26; People.prototype.sayName = function(){ console.log(this.name); }; var people1 = new People(); console.log(People.prototype.constructor); //這裏則指向People()構造函數
然而用字面量的方法致使的問題仍然沒有結束,以上面這種方式重設constructor 屬性會致使[[Enumerable]]特性被設置爲true。所以在支持ECMAScript 5 的js 引擎中能夠用Object.defineProperty()
方法把它修改成false:
function Person(){}; Person.prototype = { constructor: Person, name: "Oliver", age: 18, sayName: function(){ console.log(this.name); } }; var person1 = new Person(); console.log(Person.prototype.constructor); for (var x in person1){ console.log(x); //這裏會出現constructor,可是咱們實際上不該該讓他可以被枚舉出 } Object.defineProperty(Person.prototype, "constructor", { enumerable: false }); for (var x in person1){ console.log(x); //這裏就不會出現constructor 了,可是這種方法只支持ECMAScript 5的js 引擎 } /* [Log] function Person() {} (repetition.html, line 130) [Log] constructor (repetition.html, line 132) [Log] name (repetition.html, line 132) [Log] age (repetition.html, line 132) [Log] sayName (repetition.html, line 132) [Log] name (repetition.html, line 140) [Log] age (repetition.html, line 140) [Log] sayName (repetition.html, line 140) */
咱們對原型對象所作的任何修改都能當即從實例上反應出來。由於實例與原型之間的連接只不過是一個指針而不是副本:
function Person(){}; var person = new Person(); //person在Person()構造函數修改以前建立的 Person.prototype.name = "Oliver"; console.log(person.name); //仍然會出現實時的變化
可是若是重寫了prototype 則就不一樣了,由於實例的[[Prototype]]會指向原型對象,若是修改了原來的原型對象,則就是切斷了構造函數與最初原型之間的聯繫:
function Person(){}; var person = new Person(); Person.prototype = { //這裏重寫了Person.prototype,屬於新的Person.prototype constructor: Person, name: "Oliver" } console.log(person.name); //原型對象被修改了,指針仍然指向舊的Person.prototype
從這裏就能夠很明顯看出,Person.prototype={},實際上字面量方法就是重寫了原型對象。若是是Person.prototype.name="Oliver",則並非重寫而是修改,不會建立「新的原型對象。」
《js高級程序設計》一書中6.2.3 中的圖6-3 很清楚的描述了該原理
全部原生的引用類型(Object、Array、String等等)都在其構造函數的原型上定義了方法。同時,咱們也能夠給原生對象自定義方法:
var array = new Array(); Array.prototype.name = function(){ console.log("Array") }; array.push("hello ","there"); console.log(array); array.name();
也能夠修改:
var array = new Array(); Array.prototype.toString = function(){ return("Array") }; array.push("hello ","there"); console.log(array.toString()); //這樣就抹去了toString()方法
強烈不推薦修改和重寫原生對象的原型
就是包含引用類型值的屬性來講,問題比較嚴重。具體體如今原型中的屬性被實例共享:
function Person(){}; Person.prototype = { constructor: Person, name: "Oliver", age: 18, friends: ["Troy","Alice"] } var person1 = new Person(); var person2 = new Person(); person1.friends.push("Ellen"); console.log(person1.friends); //["Troy", "Alice", "Ellen"] console.log(person2.friends); //["Troy", "Alice", "Ellen"] //二者徹底相同,由於原型中的該屬性被實例所共享,push()方法只是修改了person1.friends,沒有重寫 person1.friends = ["Troy", "Alice"]; console.log(person1.friends); //["Troy", "Alice"] console.log(person2.friends); //["Troy", "Alice", "Ellen"] //雖然能夠經過重寫和覆蓋來解決該問題,可是仍然很是麻煩 console.log(person1.hasOwnProperty("friends")); //true; console.log(person2.hasOwnProperty("friends")); //false; //這裏就能夠看到,重寫能解決問題是由於重寫致使它建立了實例屬性"friends"
這裏能夠看出,若是咱們的初衷像這樣只是想建立一個共享的數組,那麼固然不會有什麼問題;可是實例通常都會有本身的屬性,因此不該該單獨使用原型模式。而是組合使用構造函數模式和原型模式。
這是一種用來定義引用類型的一種默認模式:
function Person(name,age){ this.name = name; this.age = age; this.friends = []; } //獨享的部分 Person.prototype = { constructor: Person, sayName: function(){ return this.name; } } //共享的部分 var person1 = new Person("Oliver",18); var person2 = new Person("Troy",24); person1.friends.push("Alice","Mark"); person2.friends.push("Mac"); console.log(person1.friends.toString()); console.log(person2.friends.toString()); /* [Log] Alice,Mark (repetition.html, line 228) [Log] Mac (repetition.html, line 229) */
能夠經過檢查某個應該存在的方法是否有效,來決定是否須要初始化原型:
function Person(name,age){ this.name = name; this.age = age; if (typeof this.sayName != "function"){ Person.prototype.sayName = function(){ return (this.name); }; } } var person = new Person("Oliver",18); console.log(person.sayName()); //Oliver
實際上就是把下面代碼封裝在了構造函數中:
function Person(name,age){ this.name = name; this.age = age; } Person.prototype.sayName = function(){ return(this.name); }; var person = new Person("Troy",24); console.log(person.sayName()); //Troy
世紀撒好難過跟工廠模式同樣。建議在可使用其餘模式的狀況下,不要使用該模式。
穩妥對象,指的是沒有公共屬性,且其方法也不引用this 的對象如:
function Person(name,age){ var obj = new Object(); obj.sayName = function(){ console.log(name); }; return obj; } var person1 = Person("Oliver",18); person1.sayName(); //Oliver