在之前的《JavaScript對象基礎》中,我們大概瞭解了對象的創建和使用,知道對象可以使用構造函數和字面量方式創建。那麼今天,我們就一起來深入瞭解一下JavaScript中的構造函數以及對象的原型及原型鏈。 一 構造函數 1,什麼是構造函數 Javascript中使用構造函數的目的是批量創建擁有相 ...
在之前的《JavaScript對象基礎》中,我們大概瞭解了對象的創建和使用,知道對象可以使用構造函數和字面量方式創建。那麼今天,我們就一起來深入瞭解一下JavaScript中的構造函數以及對象的原型及原型鏈。
一 構造函數
1,什麼是構造函數
Javascript中使用構造函數的目的是批量創建擁有相同屬性的不同對象。
實際上構造函數和普通函數並沒有本質上的區別,唯一不同的地方在於:構造函數一般使用new關鍵字調用。
為了容易區別哪些是普通函數,哪些是構造函數,業界的共識是:構造函數使用大駝峰式命名規則(所有單詞首字母大寫)。普通函數和變數採用小駝峰式命名規則。
1 function myFunction(){ 2 //這是普通函數 3 } 4 function MyFunction(){ 5 //這是構造函數 6 }
2,構造函數的特點
構造函數最大的特點就是通過關鍵字this來給即將創建的對象添加屬性和方法。
1 function Person(){ 2 this.name = 'ren'; 3 this.age = 12; 4 } 5 var perseon = new Person(); 6 console.log(person);//{name:'ren',age:12}
3,構造函數的執行過程
首先隱式的創建一個空對象,賦值給this。
然後通過this添加屬性和方法。
最後隱式的返回this對象。
1 //執行過程,以上面的Person為例 2 var this = {}; 3 this.name = 'ren'; 4 this.age = 12; 5 return this;
4,構造函數的返回值
上面提到,構造函數執行到最後將隱式的的返回一個對象,但相信大家也沒有忘記,普通函數可以使用return關鍵字指定返回值。那麼,如果我們手動的在構造函數最後添加了return關鍵字,那麼它究竟會返回什麼呢?
預設返回this。
如果手動添加原始值,還是返回this。
1 function Person(){ 2 this.name = 'ren'; 3 this.age = 12; 4 return 50; 5 } 6 var person = new Person(); 7 console.log(person);//{name:'ren',age:12}
手動添加引用值,最終返回這個引用值。
1 function Person(){ 2 this.name = 'ren'; 3 this.age = 12; 4 return {name:'ru',age:22}; 5 } 6 var person = new Person(); 7 console.log(person);//{name:'ru',age:22}
請註意,以上情況都是基於把它當做構造函數,使用new關鍵字調用的結果。如果把它當做普通函數執行,那麼無論return後面添加什麼值,都將原樣返回,如果沒有return,則只會返回undefined。並且,這時函數內部將不會創建一個空對象,而且this也將不再引用這個空對象了,而是指向window對象。
1 function Person(){ 2 this.name = 'ren'; 3 return {name:'person'}; 4 } 5 function Animal(){ 6 this.name = 'ren'; 7 return 'dog'; 8 } 9 var person = Person(); 10 var animal = Animal(); 11 console.log(person);//{name:'person'} 12 console.log(animal);//'dog'
二 函數的原型
一般我們在討論原型的時候通常是指構造函數的原型,因為使用普通函數的原型沒有實際意義。所以下麵提到的”原型”或“函數的原型”均指構造函數的原型。
要理解函數的原型其實很簡單。只需弄清楚3個屬性:
fn.prototype
obj.__proto__
obj.constructor
1, 函數的prototype屬性
prototype是函數才具有的屬性,它指向一個對象,該對象具有的屬性和方法將被構造函數創建的對象(實例)繼承。
1 function test(){} 2 console.log(typeof test.prototype);//'object' 3 console.log(test.prototype);//{...}
這裡說繼承其實並不准確。比如a繼承了b的屬性和方法。字面上的意思是:a擁有了和b完全相同的屬性和方法。但構造函數創建的對象(實例)並沒有直接擁有原型上的屬性和方法,它只是拿到了使用那些屬性和方法的許可權而已。
1 function Person(){} 2 Person.prototype.name = 'ren'; 3 var person = new Person(); 4 console.log(person.hasOwnProperty('name'));//false,hasOwnProperty()方法用於檢測對象真實具有某屬性,而非繼承
5 console.log(person.name);//'ren',但是可以訪問name屬性
2, 對象的__proto__屬性
大部分對象(不管什麼方式創建的)都有__proto__屬性。這個屬性將指向它自己的原型。那麼它自己的原型是個什麼東西呢?
前面提到,構造函數的目的是批量創建擁有相同屬性的不同對象,既然要創建大量擁有相同屬性的對象,那麼肯定需要一個統一的模板,這個統一的模板就是對象的原型。實際上它就是構造函數的prototype屬性指向的那個對象。
1 function Person(){} 2 var person = new Person(); 3 console.log(person.__proto__ === Person.prototype);//true
3, 對象的constructor屬性
constructor屬性並不是所有對象都有的,只有原型對象才具有該屬性。該屬性指向與之相關聯的構造函數。
1 function Person(){} 2 var person = new Person(); 3 console.log(person.hasOwnProperty('constructor'));//false 4 console.log(Person.prototype.constructor === Person);//true 5 console.log(person.__proto__.constructor === Person);//true
註意,雖然constructor屬性並不是所有對象都有的,但是實例依然可以訪問該屬性,並最終得到相應的構造函數。那是因為當讀取實例的屬性時,如果找不到,就會查找與對象關聯的原型中的屬性。
4, 實例、構造函數和原型的關係
三 原型鏈
1,原型鏈
前面講到,大部分對象都有__proto__屬性,指向它自己的原型對象。那麼原型對象自身呢?原型對象自身作為對象,當然也具有__proto__屬性,並且指向原型的原型。
同樣的,原型的原型也是一個對象,那麼它也就有一個constructor屬性指向一個關聯的構造函數。依次類推,原型對象最終將指向Object對象的原型,與之相關聯的構造函數則是Object。並且Object對象的原型就沒有原型對象了,如果訪問Object.prototype.__proto__將返回null。
1 function Father(){} 2 Father.prototype.name = 'ren'; 3 var father = new Father(); 4 5 function Son(){} 6 Son.prototype = father; 7 Son.prototype.age = 12; 8 var son = new Son(); 9 10 function GrandSon(){} 11 GrandSon.prototype = son; 12 GrandSon.prototype.address = 'cd'; 13 var grandson = new GrandSon(); 14 15 Object.prototype.mail = '@'; 16 17 console.log(grandson.name);//'ren' 18 console.log(grandson.age);//12 19 console.log(grandson.address);//'cd' 20 console.log(grandson.mail);//'@'
從上面的例子可以看出,訪問對象的屬性和方法,其實是通過__proto__屬性在對象的原型鏈上查找,這一點和在函數內訪問變數有一點類似。
其實上面的例子有一個小小的bug,不知道你們發現了沒有。
1 console.log(Son.prototype.constructor);//Father (){} 2 console.log(GrandSon.prototype.constructor);//Father (){} 3 //列印的都是構造函數Father,這是為什麼呢?
Son構造函數的原型被我們手動指定為了father(Father構造函數的一個實例),father作為一個實例,它並不真實擁有constructor屬性,所以當我們訪問Son.prototype.constructor屬性時,實際在訪問father.__proto__.constructor,即Father構造函數。但Son作為一個構造函數,被它構造出來的對象在訪問constructor時理應指向Son本身才符合邏輯,所以我們應該在修改Son的原型後手動為father實例添加constructor屬性,並引用Son。相似的,son實例也應添加一個constructor屬性,並引用GrandSon(使用father或Son.prototype都可以達到目的,因為他們指向同一個對象。同理,son或者GrandSon.prototype也一樣)。
1 Son.prototype.constructor = Son; 2 GrandSon.prototype.constructor = GrandSon();
這一段可能理解起來有點繞,但是請務必多嘗試,並真正理解它。原型鏈是JS中一個相當重要的概念。最後附一張原型鏈的圖解。
2,Object.create()
方法接受一個對象或者null作為參數,返回一個對象。如果傳遞了一個對象,那麼該對象將成為返回對象的原型。
1 var myProto = {name:“ren”}; 2 var obj = Object.create(myProto); 3 obj.__proto__ === myProto;//true
如果傳遞了null作為參數,那麼它將沒有原型,所以也沒有__proto__屬性了。這就是在第2.2節說大部分對象都有__proto__屬性的原因了。還有一種說法是:並不是所有對象最終都繼承自Object。當然null也不繼承自Object。
1 var obj = Object.create(null); 2 console.log(obj.__proto__);//undefined