先來了解一些基礎概念,才能更好的理解知識。chrome
對象的定義數組
ECMA-262把對象定義爲:「無序屬性的集合,其屬性能夠包含基本值、對象或者函數。
4個特性瀏覽器
以上屬性通常經過Object.defineProperty()方法來修改,接受三個參數:屬性所在的對象、屬性的名字和一個描述符對象。其中描述符就是上面四個特性。app
4個特性函數
訪問器屬性不能直接定義,必須使用 Object.defineProperty()來定義。學習
Object.defineProperties()方法優化
在調用 Object.defineProperty()方法時,若是不指定,configurable、enumerable 和
writable 特性的默認值都是 false。
Object.getOwnPropertyDescriptor()方法this
接受兩個參數:屬性所在的對象和要讀取其描述符的屬性名稱。
var book = {}; Object.defineProperties(book, { _year: { value: 2004 }, edition: { value: 1 }, year: { get: function(){ return this._year; }, set: function(newValue){ if (newValue > 2004) { this._year = newValue; this.edition += newValue - 2004; } } } }); var descriptor = Object.getOwnPropertyDescriptor(book, "_year"); alert(descriptor.value); //2004 alert(descriptor.configurable); //false alert(typeof descriptor.get); //"undefined" var descriptor = Object.getOwnPropertyDescriptor(book, "year"); alert(descriptor.value); //undefined alert(descriptor.enumerable); //false alert(typeof descriptor.get); //"function"
用函數來封裝以特定接口建立對象的細節spa
function createPerson(name, age, job) { var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function () { alert(this.name); }; return o; } var person1 = createPerson("Nicholas", 29, "Software Engineer"); var person2 = createPerson("Greg", 27, "Doctor");
做用:解決了建立多個類似對象的問題
缺陷:沒有解決對象識別的問題(即怎麼知道一個對象的類型)prototype
new 一個構造函數一般會經歷一下四個步驟
因爲構造函數內部定義的屬性和方法,經過實例化後都有本身的做用域鏈和辨識符解析。爲了讓屬性和函數可以更好的公用,這時候原型模式就要登場了。
isPrototypeOf()的做用
當調用構造函數建立一個新實例後,該實例的內部包含一個指針(內部屬性),指向構造函數的原型對象。ECMA-262中管這個指針叫[[Prototype]]。在腳本中沒有標準的方式訪問[[Prototype]],但瀏覽器(Firefox,Safari,chrome)在每一個對象上都支持一個屬性/__proto/__;咱們要知道的是,這個屬性對腳本則是徹底不可見的。能夠經過isPrototypeOf()方法來肯定對象之間是否存在這種關係。從本質上講,若是[[Prototype]]指向調用isPrototypeOf()方法的對象(Person.prototype),那麼這個方法就返回true
function Person(){ } Person.prototype.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){ alert(this.name); }; var person1 = new Person(); person1.sayName(); //"Nicholas" Person.prototype.isPrototypeOf(person1)//true
ECMAScript 5 增長了一個新方法 Object.getPrototypeOf(),在全部支持的實現中,這個方法返回[[Prototype]]的值。例如
Object.getPrototypeOF(person1) === Person.prototype // true
hasOwnProperty()的使用場景
當咱們從一個構造函數實例一個對象後,若是在這個實例上建立一個屬性或者方法,正好與這個實例的原型上的屬性或者方法同名,那麼訪問時就會 屏蔽原型上的屬性或者方法。固然這樣作不會修改原型上的屬性或者方法。若是要恢復指向原型,那麼使用delete操做符則能夠徹底刪除實例屬性。 使用 hasOwnProperty()方法能夠檢測一個屬性是存在於實例中,仍是存在於原型中。 不要忘記這個方法是從Object繼承來的,因此只在給定屬性存在於對象實例中時,纔會返回true.
in 操做符的做用
in 操做符只要經過對象可以訪問到屬性就會返回true. ,hasOwnProperty()只在屬性存在於實例中時才返回 true,所以只要 in 操做符返回 true 而hasOwnProperty()返回 false,就能夠肯定屬性是原型中的屬性。
function hasPrototypeProperty(object, name){ return !object.hasOwnProperty(name) && (name in object); } function Person(){ } Person.prototype.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){ alert(this.name); }; var person = new Person(); alert(hasPrototypeProperty(person, "name")); //true person.name = "Greg"; alert(hasPrototypeProperty(person, "name")); //false
for-in的特殊
在使用 for-in 循環時,返回的是全部可以經過對象訪問的、可枚舉的(enumerated)屬性,其中既包括存在於實例中的屬性,也包括存在於原型中的屬性。屏蔽了原型中不可枚舉屬性(即將[[Enumerable]]標記爲 false 的屬性)的實例屬性也會在 for-in 循環中返回,由於根據規定,全部開發人員定義的屬性都是可枚舉的——只有在 IE8 及更早版本中例外。
Object.keys()
獲取對象上全部 可枚舉的實例屬性,返回一個字符串數組。
Object.getOwnPropertyNames()
若是是想獲得全部的實例屬性,包括不可枚舉的,就能使用Object.getOwnPropertyNames() 可以獲取到不可枚舉的constructor屬性。
constructor屬性什麼狀況下會指向Object
function Person(){ } Person.prototype = { name : "Nicholas", age : 29, job: "Software Engineer", sayName : function () { alert(this.name); } };
爲了減小重複代碼,咱們常常用一個包含全部屬性和方法的對象字面量來重寫整個原型對象。代碼如上所示。這裏會產生一個狀況,就是constructor屬性再也不指向Person。爲何會這樣呢?咱們知道,每建立一個函數,就會同時建立它的prototype對象,這個對象也會自動得到constructor屬性。而咱們上面這樣的寫法,本質上徹底重寫了默認的prototype對象,所以constructor屬性也就變成了新對象的constructor屬性(指向Object構造函數),再也不指向Person函數。因此爲何咱們用instanceof並不能準確的判別實例的構造函數。以下代碼
var p = new Person() console.log(p instanceof Object)//true console.log(p instanceof Person)//true console.log(p.constructor === Person)//false console.log(p.constructor === Object)//true
有下面兩種方法將constructor設置回來
function Person(){ } Person.prototype = { constructor : Person, name : "Nicholas", age : 29, job: "Software Engineer", sayName : function () { alert(this.name); } };
以上這種重設constructor屬性會致使它的[[Ennumerable]]特性設置爲true,因此咱們能夠用下面這種方法;
//重設構造函數,只適用於 ECMAScript 5 兼容的瀏覽器 Object.defineProperty(Person.prototype, "constructor", { enumerable: false, value: Person });
原型模式的缺點就是在共享時屬性和方法後,要是修改了內容值,那麼同一個構造函數出來的實例就會都受到影響。例以下面代碼
function Person(){ } Person.prototype = { constructor: Person, friends : ["Shelby", "Court"] }; var person1 = new Person(); var person2 = new Person(); person1.friends.push("Van"); alert(person1.friends); //"Shelby,Court,Van" alert(person2.friends); //"Shelby,Court,Van" alert(person1.friends === person2.friends); //true
因此組合使用構造函數模式與原型模式就是你們廣泛用到的方法,咱們想要定義本身的屬性和方法,那麼就在構造函數中定義便可。例以下面代碼:
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.friends = ["Shelby", "Court"]; } Person.prototype = { constructor : Person, sayName : function(){ alert(this.name); } } var person1 = new Person("Nicholas", 29, "Software Engineer"); var person2 = new Person("Greg", 27, "Doctor"); person1.friends.push("Van"); alert(person1.friends); //"Shelby,Count,Van" alert(person2.friends); //"Shelby,Count" alert(person1.friends === person2.friends); //false alert(person1.sayName === person2.sayName); //true
意思就在初始化實例的時候就檢測判斷是否原型中存在你想要的屬性或者方法,要是沒有,就能夠在原型上定義你想要的屬性或方法。如一下代碼:
function Person(name, age, job){ //屬性 this.name = name; this.age = age; this.job = job; if (typeof this.sayName != "function"){ Person.prototype.sayName = function(){ alert(this.name); }; } } var friend = new Person("Nicholas", 29, "Software Engineer"); friend.sayName()
這個模式跟工廠模式幾乎是同樣的。經過在一個構造函數內部建立一個新的對象(數組、字符串),而後你能夠隨意在這個定義的新對象上添加屬性和方法,最後return這個新的對象。例如一下代碼:
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"); alert(colors.toPipedString()); //"red|blue|green"
這樣作的優勢是咱們能夠很輕鬆在原生對象上建立一個本身想要的方法和屬性。可是返回的對象與構造函數或者與構造函數的原型屬性之間沒有關係。這樣咱們不能經過instanceof操做符肯定對象類型。
所謂穩妥對象,指的是沒有公共屬性,並且其方法也不引用this對象。一是新建立對象的實例方法不引用this;二是不使用new操做符調用構造函數。
咱們先來弄懂一句話: 每一個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針。這就是至關於構建一個原型鏈的基石,整個原型鏈就是由這樣一個基石累計起來。
主要有下面兩個緣由
好比下面的例子中咱們實現簡單的繼承
function Father(){ this.colors = ["red","blue","green"]; } function Son(){} Son.prototype = new Father(); var instance1 = new Son(); instance1.colors.push('black'); console.log(instance1.colors);// red blue,green,black var instance2 = new Son(); console.log(instance2.colors);// red blue,green,black
上面例子咱們能夠看見兩個實例共享了一個方法,這是咱們不肯意看到的地方,那麼有什麼方法可以解決這個問題呢?
什麼是 借用構造函數? 就是在子類型構造函數的內部調用超類型的構造函數。以下面代碼:
function Father(){ this.colors = ['red','blue','green']; } function Son(){ //繼承 或者使用apply Father.call(this) } var instance1 = new Son(); instance1.colors.push('black'); console.log(instance1.colors);//red blue,green,black var instance2 = new Son(); console.log(instance2.colors);// red blue,green
怎麼傳遞參數? 就是在調用父類構造函數時,傳遞參數。具體見下面代碼
function Father(name) { this.name = name; } function Son(){ Father.call(this,'pan'); this.age = 28; } var instance = new Son() console.log(instance.name)//pan console.log(instance.age)//28
可是借用構造函數也用的很少,爲何呢?
上面兩種繼承都有本身的缺點和優勢,那麼咱們是否能夠取長補短,把兩家結合在一塊兒?組合繼承就是用來解決這個問題的。思路就是使用原型鏈實現對原型屬性和方法的繼承,而經過借用構造函數來實現對實例屬性的繼承。這樣,即經過在原型上定義方法實現了函數複用,又可以保證每一個實例都有它本身的屬性。
function Father(name) { this.name = name; this.colors = ['red','blue','green']; } Father.prototype.sayName = function(){ console.log(this.name) } function Son(name,age){ Father.call(this,name); this.age = age; } //繼承的實現 Son.prototype = new Father(); Son.prototype.constructor = Son; Son.prototype.sayAge = function(){ console.log(this.age); } var instance1 = new Son('pan',28); instance1.colors.push('black'); console.log(instance1.colors);// red ,blue,green,black instance1.sayName();//pan instance1.sayAge();//28 var instance2 = new Son('lin',26); console.log(instance2.colors);// red ,blue,green instance2.sayName();//lin instance2.sayAge();//26
上面的例子咱們看到取長補短的優點,便可以定義本身的屬性和方法,又可使用相同的方法。這是咱們經常使用的繼承模式,而且instanceof和isPrototypeOf也可以用於識別基於組合繼承建立的對象。
咱們先來看一個Object.create()的前身的寫法,藉助原型能夠基於已有的對象建立新對象,同時還沒必要所以建立自定義類型。
function object(o) { function F(){} F.prototype = o; return new F(); }
在一個function 內部建立一個臨時性的構造函數,而後重寫這個構造函數的原型,而後在返回這個構造函數的新的實例。不知道你又沒有看出什麼很差的地方沒?從本質上講,object()對傳入其中的對象執行一次淺複製。
var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = object(person); anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob"); var yetAnotherPerson = object(person); yetAnotherPerson.name = "Linda"; yetAnotherPerson.friends.push("Barbie"); console.log(person.friends); //"Shelby,Court,Van,Rob,Barbie"
咱們用新的方法Object.create()來從新規範上門的例子。這個方法接收兩個參數:一個做用新對象原型的對象和一個爲新對象定義額外屬性的對象。
var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = Object.create(person); anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob"); var yetAnotherPerson = Object.create(person); yetAnotherPerson.name = "Linda"; yetAnotherPerson.friends.push("Barbie"); console.log(person.friends); //"Shelby,Court,Van,Rob,Barbie"
Object.create()方法的第二個參數與Object.defineProperties()方法的第二個參數格式相同:每一個屬性都是經過本身的描述符定義的。以這種方式指定的任何屬性都會覆蓋原型對象上的同名屬性。例如:
var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = Object.create(person, { name: { value: "Greg" } }); console.log(anotherPerson.name); //"Greg"
若是隻是簡單的繼承,那麼原型模式就能勝任,可是想要建立構造函數,那麼引用類型值的會共享這個坑是不能避免的。
都是寄生,天然和構造函數的寄生相似,建立一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來增前對象,而後再返回這個對象。
function createAnother(original){ var clone = object(original); clone.sayHi = function(){ console.log('hi') } return clone; }
接下里咱們運用上面這個函數
var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = createAnother(person); anotherPerson.sayHi(); //"hi"
新對象anotherPerson不只具備person的全部屬性和方法,並且還有本身的sayHi()方法。可是此寄生式繼承不能複用函數,因此效率不是很高。
咱們前面說的組合繼承是經常使用的模式,可是也有一個缺點:那就是會調用兩次超類型構造函數。什麼意思?咱們來看代碼;
function Father(name) { this.name = name; this.colors = ['red','blue','green']; } Father.prototype.sayName = function(){ console.log(this.name) } function Son(name,age){ Father.call(this,name);//第二次調用Father() this.age = age; } //繼承的實現 Son.prototype = new Father();//第一次調用Father() Son.prototype.constructor = Son; Son.prototype.sayAge = function(){ console.log(this.age); } var instance1 = new Son('pan',28); instance1.colors.push('black'); console.log(instance1.colors);// red ,blue,green,black instance1.sayName();//pan instance1.sayAge();//28
上面代碼中兩個註釋很好的反應了這兩次構造函數。那麼寄生組合式繼承就可以很好的優化這個問題。經過Object.create()來很好的實現,只須要修改一處便可。
將Son.prototype = new Father(); 替換成 Son.prototype = Object.create(Father.prototype)
高效率的體現就在只調用了一次Father構造函數,能夠避免在Son.prototype上面建立沒必要要的、多餘的屬性。與此同時,原型鏈還能保持不變。而且還能正常使用instanceof和isPrototypeOf()。
上面的寄生組合式繼承只是一個 單繼承,那 多繼承又該如何實現?
咱們能夠用混入的方式來實現多個對象的繼承
//第一個構造函數 function Father(name) { this.name = name; this.colors = ['red','blue','green']; } Father.prototype.sayName = function(){ console.log(this.name) } //第二個構造函數 function FatherBrother(hobby){ this.hobby = hobby } function Son(name,age,hobby){ Father.call(this,name); FatherBrother.call(this,hobby) this.age = age; } //繼承的實現 Son.prototype = Object.create(Father.prototype); //混合 Object.assign(Son.prototype,FatherBrother.prototype) //從新指定constructor Son.prototype.constructor = Son; Son.prototype.sayAge = function(){ console.log(this.age); } var instance1 = new Son('pan',28,'play'); instance1.colors.push('black'); console.log(instance1.colors);// red ,blue,green,black instance1.sayName();//pan instance1.sayAge();//28 instance1.hobby;//play
這裏關鍵點是:Object.assign 會把 FatherBrother原型上的函數拷貝到 Son原型上,使 Son 的全部實例均可用 FatherBrother 的方法。
工廠模式(簡單的添加屬性和方法) -->構造函數模式(優勢:能夠建立內置對象實例。缺點:成員沒法複用。)-->原型模式(結合構造函數模式和原型模式兩方的優勢,使用構造函數定義實例屬性,而使用原型定義共享的屬性和方法。)-->原型式繼承(優勢:沒必要預先定義。缺點:會存在重寫原型)-->寄生式繼承(優勢:效率高,沒必要屢次調用超類型構造函數。缺點:複用率不高)-->寄生組合式繼承(集寄生式繼承和組合繼承的優勢與一身。)
若是大神您想繼續探討或者學習更多知識,歡迎加入QQ一塊兒探討:854280588![]()
參考文章