文章同步到githubhtml
建立對象的幾種方式git
原型模式github
繼承segmentfault
我的擴展補充瀏覽器
直接上《JavaScript高級教程》的截圖app
補充:
js中說一切都是對象,是不徹底的,在js中6種數據類型(Undefined,Null,Number,Boolean,String,Object)中,前五種是基本數據類型,是原始值類型,這些值是在底層實現的,他們不是object,因此沒有原型,沒有構造函數,因此並非像建立對象那樣經過構造函數建立的實例。關於對象屬性類型的介紹就不介紹了,能夠看我上一篇文章Object.defineProperty()和defineProperties()ecmascript
var obj = new Object();
var obj = {};
若是使用構造函數和字面量建立不少對象,每一個對象自己又有不少相同的屬性和方法的話,就會產生大量重複代碼,每一個對象添加屬性都須要從新寫一次。如兩個對象都須要添加name、age屬性及showName方法:函數
var p1 = new Object(); p1.name = '張三' p1.age = '16', p1.showName = function() { return this.name } var p2 = new Object(); p2.name = '李四' p2.age = '18', p2.showName = function() { return this.name }
爲了解決這個問題,人們採用了工廠模式,抽象了建立對象的過程,採用函數封裝以特定接口(相同的屬性和方法)建立對象的過程。this
function createPerson(name, age) { var obj = new Object(); obj.name = name; obj.age = age; obj.showName = function () { return this.name; }; return obj; } var p1 = createPerson('張三', 16); var p2 = createPerson('李四', 18);
雖然工廠模式解決了建立多個對象的多個相同屬性問題,卻沒法斷定對象的具體類型,由於都是Object,沒法識別是Array、或是Function等類型,這個時候構造函數模式出現了。spa
js中提供了像Object,Array,Function等這樣的原生的構造函數,同時也能夠建立自定義的構造函數,構造函數是一個函數,用來建立並初始化新建立的對象。將工廠模式的例子用構造函數能夠重寫爲:
function Person(name, age) { this.name = name; this.age = age; this.showName = function() { console.log(this.name); } } var p1 = new Person('張三', '16'); var p2 = new Person('李四', '18');
用Person代替了工廠模式的createPerson函數,並且函數名首字母P大寫,這是由於按照慣例,構造函數首字母應該大寫,而做爲非構造函數的函數首字母小寫。另外能夠注意到構造函數內部的特色:
另外,還使用了new操做, 要建立一個實例,必須使用new操做符,使用new操做符調用構造函數,在調用構造函數的時候經歷了以下幾個階段:
用僞代碼來講明上述new Person()的過程以下:
// 使用new操做符時,會激活函數自己的內部屬性[[Construct]],負責分配內存 Person.[[Construct]](initialParameters): // 使用原生構造函數建立實例 var Obj = new NativeObject() //NativeObject爲原生構造函數,如Object、Array、Function等 // 給建立的實例添加[[Class]]內部屬性,字符串對象的一種表示, 如[Object Array] // Object.prototype.toString.call(obj)返回值指向的就是[[Class]]這個內部屬性 Obj.[[Class]] = Object/Array/Function; // 給建立的實例添加[[Prototype]]內部屬性,指向構造函數的prototype O.[[Prototype]] = Person.prototype; // 調用構造函數內部屬性[Call],將Person執行上下文中this設置爲內部建立的對象Obj, this = Obj; result = Person.[[Call]](initialParameters); // result是若是構造函數內部若是存在返回值的話,調用[[call]]時做爲返回值,通常爲Object類型 // 調用Person.[[call]]時,執行Person中的代碼,給this對象添加屬性和方法 this.name = name; this.age = age; this.showName = function() { console.log(this.name); }; //若是Person.[[call]]的返回值result爲Object類型 return result // 不然 return Obj;
補充,貼出ECMAScript 5.1版本標準中[[Construct]]的規範,我本人對[[Call]]的返回值問題理解的也很差,但願哪位大神能夠指點指點。
構造函數雖然解決了實例多個同名屬性重複添加的問題,可是也存在每一個實例的方法都須要從新建立一遍,由於每一個方法都是Function的不一樣實例,看下面這段代碼就明白了:
function Person(name, age) { this.name = name; this.age = age; this.showName = new Function("console.log(this.name);"); } var p1 = new Person('張三', '16'); var p2 = new Person('李四', '18'); console.log(p1.showName === p2.showName); //false
這個問題能夠用如下辦法來解決,把showName變成全局函數
function Person(name, age) { this.name = name; this.age = age; this.showName = showName; } function showName() { console.log(this.name) }
可是這樣若是對象須要添加不少方法就會產生不少全局函數,這些問題能夠經過原型模式來解決
當每個函數建立時,都會給函數設置一個prototype(原型)屬性,這個屬性是一個指針,指向一個對象,這個對象包含全部實例共享的屬性和方法,在默認狀況下,都會爲prototype對象添加一個constructor屬性,指向該函數。
Person.prototype.constructor = Person;
原型模式就是沒必要在構造函數中定義實例的屬性和方法,而是將屬性和方法都添加到原型對象中。建立自定義構造函數,其原型對象只會默認取得constructor屬性,其餘的屬性和方法都是從Object繼承來的。當使用構造函數建立一個實例以後,會給實例添加內部屬性[[prototype]],這個屬性是一個指針,指向構造函數的prototype(原型)對象,因爲是內部屬性,沒法經過腳本獲取,可是在一些Chrome、Firefox、Safari等瀏覽器中在每一個對象身上支持一個__proto__屬性,指向的就是構造函數的原型對象。另外能夠經過isProtoTypeOf()來判斷建立的實例是否有指向某構造函數的指針,若是存在,返回true,若是不存在,返回false。
function Person() { } Person.prototype.name = '張三'; Person.prototype.friends = ['張三', '李四']; Person.prototype.showName = function() { console.log(this.name); } var p1 = new Person(); var p2 = new Person() console.log(p1.__proto__ === Person.prototype) // true console.log(Person.prototype.isPrototypeOf(p1)) // true
在ECMA5中增長了一個方法Object.getPrototypeOf(params),返回值就是建立對象的原型對象
console.log(Object.getPrototypeOf(p1) === Person.prototype); // true console.log(Object.getPrototypeOf(p1).name); //張三
原型模式雖然解決了方法共享的問題,可是對於實例共享來講是個比較大的問題,由於每一個實例都須要有描述本身自己特性的專有屬性,仍是上面的代碼:
console.log(p1.name) // '張三' console.log(p2.name) // '張三'
另外對於屬性是引用類型的值來講缺點就更明顯了,若是執行下面這段代碼:
p1.friends.push('王五'); console.log(p1.priends); //['張三', '李四', '王五'] console.log(p2.priends); //['張三', '李四', '王五']
爲了解決原型模式的問題,人們採用了原型和構造組合模式,使用構造函數定義實例,使用原型模式共享方法。
直接上代碼:
function Person(name, age) { this.name = name; this.age = age; this.friends = ['張三', '李四']; // this.friends = new Array('張三', '李四') } Person.prototype.showName = function() { console.log(this.name); }; var p1 = new Person('John'); var p2 = new Person('Alice'); p1.friends.push('王五'); console.log(p1.friends); // ['張三', '李四', '王五']; console.log(p2.friends); // ['張三', '李四']; // 由於這時候每一個實例建立的時候的friends屬性的指針地址不一樣,因此操做p1的friends屬性並不會對p2的friends屬性有影響 console.log(p1.showName === p2.showName) // true 都指向了Person.prototype中的showName
這種構造函數模式和原型模式組合使用,基本上能夠說是js中面向對象開發的一種默認模式,介紹了以上這幾種經常使用建立對象的方式, 還有其餘不經常使用的模式就不介紹了,接下來想說的是js中比較重要的繼承。
ECMA中繼承的主要方法就是經過原型鏈,主要是一個原型對象等於另外一個類型的實例,因爲實例內部含有一個指向構造函數的指針,這時候至關於重寫了該原型對象,此時該原型對象就包含了一個指向另外一個原型的指針,假如另外一個原型又是另外一個類型的實例,這樣就造成了原型鏈的概念,原型鏈最底層爲Object.prototype.__proto__,爲null。
js中實例屬性的查找,是按照原型鏈進行查找,先找實例自己有沒有這個屬性,若是沒有就去查找查找實例的原型對象,也就是[[prototype]]屬性指向的原型對象,一直查到Object.prototype,若是仍是沒有該屬性,返回undefined。全部函數的默認原型都是Object實例。
function Parent() { this.surname = '張'; this.name = '張三'; this.like = ['apple', 'banana']; } var par = new Parent() function Child() { this.name = '張小三'; } Parent.prototype.showSurname = function() { return this.surname } // 繼承實現 Child.prototype = new Parent(); var chi = new Child(); console.log(chi.showSurname()) // 張
以上代碼證實,此時Child實例已經能夠訪問到showSurname方法,這就是經過原型鏈繼承Parent原型方法,剖析一下其過程:
Child.prototype = new Parent();
至關於重寫了Child.prototype,指向了父實例par,同時也包含了父實例的[[prototype]]屬性,此時
console.log(Child.prototype.__proto__ === par.__proto__); // true console.log(Child.prototype.__proto__ === Parent.prototype); // true
執行chi.showSurname()時,根據屬性查找機制:
console.log(chi.showSurname()) // 張
補充:
全部函數默認繼承Object:
function Person() { } console.log(Person.prototype.__proto__ === Object.prototype); // true
只經過原型來實現繼承,還存在必定問題,因此js中通常經過借用構造函數和原型組合的方式來實現繼承,也稱經典繼承,仍是繼承那段代碼,再貼過來把,方便閱讀
function Parent() { this.surname = '張'; this.name = '張三'; this.like = ['apple', 'banana']; } var par = new Parent() function Child() { this.name = '張小三'; } Parent.prototype.showSurname = function() { return this.surname } // 繼承實現 Child.prototype = new Parent(); var chi1 = new Child(); var chi2 = new Child(); console.log(chi.showSurname()) // 張 // 主要看繼承的屬性 console.log(chi.like) // ['apple', 'banana'] 這是由於Child.prototype指向父實例,當查找實例chi自己沒有like屬性,就去查找chi的原型對象Child.prototype,因此找到了
那麼還存在什麼問題呢,主要就是涉及到引用類型的屬性時,引用類型數據的原始屬性會被實例所共享,而實例自己的屬性應該有實例本身的特性,仍是以上代碼
chi.like.push('orange'); console.log(chi1.like); // ['apple', 'banana', 'orange'] console.log(chi2.like); // ['apple', 'banana', 'orange']
因此構造函數和原型組合的經典繼承出現了,也是本篇最重要的內容:
1.屬性繼承
在子構造函數內,使用apply()或call()方法調用父構造函數,並傳遞子構造函數的this
2.方法繼承
使用上文提到的原型鏈繼承,繼承父構造器的方法
上代碼:
function Parent(name) { this.name = name; this.like = ['apple', 'banana']; } Parent.prototype.showName = function() { console.log(this.name); }; function Child(name, age) { // 繼承屬性 Parent.call(this, name); // 添加本身的屬性 this.age = age; } Child.prototype = new Parent(); // 子構造函數添加本身的方法 Child.prototype.showAge = function() { console.log(this.age); }; var chi1 = new Child('張三', 16); var chi2 = new Child('李四', 18); chi1.showName(); //張三 chi1.showAge(); //16 chi1.like.push('orange'); console.log(chi1.like); // ['apple', 'banana', 'orange'] console.log(chi2.like); // ['apple', 'banana']
在子構造函數Child中是用call()調用Parent(),在new Child()建立實例的時候,執行Parent中的代碼,而此時的this已經被call()指向Child中的this,因此新建的子實例,就擁有了父實例的所有屬性,這就是繼承屬性的原理。對chi1和chi2的like屬性,是每一個實例本身的屬性,兩者間不存在引用依賴關係,因此操做chi.like並不會對chi.like形成影響。方法繼承,就是上文講的到的原型鏈機制繼承,另外能夠給子構造函數添加本身的屬性和方法。
這就是經典繼承,避免了可是使用構造函數或者單獨使用原型鏈的缺陷,成爲js中最經常使用的繼承方式。
用法: obj.hasOwnProperty(prop)
使用hasOwnProperty()方法能夠判斷訪問的屬性是原型屬性仍是實例屬性,若是是實例屬性返回true,不然返回false
function Person() { } Person.prototype.name = '張三' var p1 = new Person(); var p2 = new Person(); p1.name = '張三'; console.log(p1.hasOwnProperty('name')) //true console.log(p2.hasOwnProperty('name')) //false
在實際開發中,若是原型對象有不少方法,每每咱們可使用字面量的形式,重寫原型,可是須要手工指定constructor屬性
function Person(name, age) { this.name = name; this.age = age; } var p1 = new Person('張三', 16); Person.prototype.showName = function() { return this.name; } Person.prototype.showAge = function() { return this.age; }
若是構造函數的prototype方法不少,能夠採用字面量方式定義
Person.prototype = { constructor: Person, showName: function() { return this.name; }, showAge: function() { return this.age; } }
注意這裏面手動加了一個constructor屬性指向Person構造函數,這是由於使用字面量重寫原型對象,這個原型對象變成了一個Object的實例,原型對象自己已經不存在最初函數建立時初始化的constructor屬性,這是原型對象的[[prototype]]指針指向了Object.prototype
function Person() { } Person.prototype.a = 10; var p = new Person(); console.log(p.a) //10 Person.prototype = { constructor: Person, a: 20, b: 30 } console.log(p.a) // 10 console.log(p.b) // undefined var p2 = new Person(); console.log(p2.a) // 20 console.log(p2.b) // 30
所以,有的文章說「動態修改原型將影響全部的對象都會擁有新的原型」是錯誤的,新原型僅僅在原型修改之後的新建立對象上生效。
這裏的主要規則是:對象的原型是對象的建立的時候建立的,而且在此以後不能修改成新的對象,若是依然引用到同一個對象,能夠經過構造函數的顯式prototype引用,對象建立之後,只能對原型的屬性進行添加或修改。
以上就是我梳理出來的js中面向對象部分的相關概念和理解,依舊主要參考《JavaScript高教程》和《深刻理解JavaScript系列》文章,另外翻看了ECMAScript5.1中文版。本人對引用書中的概念和相關知識,爲保證文章不誤導你們,並非拿來主義,但願本文能對你們有幫助,也但願你們多多指教。