javascript
JavaScript中的原型(prototype)与继承
在JavaScript中,原型是用來模仿其他「類」語言繼承機制的基礎。原型并不復雜,原型只是一個對象。
一、原型對象
1.1 什么是原型對象
每當我們創建了一個函數后,這個函數就擁有了一個prototype屬性,這個屬性指向一個對象——原型對象。原型對象一開始就有一個constructor屬性,指向我們創建的函數。而當我們對這個函數進行構造函數調用創建了一個實例對象,這個對象將會有一個特殊的內置屬性[[prototype]],這個屬性就是對函數原型對象的引用。構造函數、實例對象和原型對象的關系請看下圖:
Person為「構造函數」,person1與person2為實例對象、Person Prototype為函數的原型對象。通過同一個函數構造的對象內部的[[prototype]]屬性都指向了同一個原型對象,這是一種共有的關聯關系,原型中的所有屬性和方法都被這些實例共享。從圖上也看出了,實例與構造函數之間不是直接關聯的,而是通過原型的constructor間接關聯的。
無論是通過構造函數實例化的對象還是通過對象字面量創建的對象,他們都有對應的原型,對于用對象字面量創建的對象來說,其默認的原型為Object.prototype。實際上,幾乎所有對象都有原型。
var Person = function(name){this.name = name; }var person1 = new Person('person1'); var obj1 = { name: 'obj1' };console.log(Object.getPrototypeOf(obj1) === Object.prototype);//true console.log(Object.getPrototypeOf(person1) === Person.prototype);//true復制代碼1.2 原型鏈
既然我們說了所有對象都有原型,那么就意味著原型也有原型,即原型對象的[[prototype]]屬性也指向另一個對象。如此一來就形成了一個原型鏈,而這個鏈的盡頭是Object.prototype。
通過上圖的[[prototype]]屬性的指向可以清晰地看到原型對象之間的連接。 ## 1.2 原型對象的作用 原型對象有什么用呢?大家一定還記得引擎是如何在作用域鏈中查找一個變量的,作用域鏈用于查找聲明的變量,而對象的屬性則在對象及其原型鏈中查找。當讀取對象的屬性時,首先在對象中查找,當查找不到時開始從對象的原型中查找。根據之前提到的原型也是一個對象,所以如果在對象的原型中找不到,那么將會到原型的原型中找,直到找到或者到了原型鏈的尾端返回undefined為止。 var Person = function(name){this.name = name; }Person.prototype = {constructor:Person,age:23 } console.log(person1.age);//23--原型中的屬性 復制代碼1.2.1 屬性屏蔽與設置
假設我們查找myObject.foo屬性,而原型鏈上有多個name屬性,會發生什么呢?與作用域鏈類似,會發生「屏蔽」。根據查找的機制,總會返回第一個找到的同名屬性而忽略后續的同名屬性。
那么給對象設置屬性的時候呢?這時候情況就有點復雜了:
簡單地說分為四種情況:①有且可寫 ②有但只讀 ③有且是setter ④沒有。只有①和④會在myObject上創建屬性。當原型鏈上有同名只讀屬性時將會阻止同名屬性的新增,如果是setter將會按照setter的邏輯操作。
var Person = function(name){this.name = name; } //修改原型 //Person.prototype.name = 'modifined';//重新連接(替換)原型而不是修改原型 Person.prototype = {constructor:Person,//恢復constructor屬性,但變成了可枚舉name:'constructor'//默認是可寫的 } Object.defineProperty(Person.prototype,'age',{value:23,writable:false//設置為只讀 }) var person1 = new Person('person1'); console.log(person1.age);//23person1.name = 'tom'; person1.age = 20;//嚴格模式下將會報錯console.log(person1.age);//23——修改被忽略 console.log(person1.name);//tom 復制代碼在上面的代碼中我們替換了Person的原型,注意這個操作將會導致實例失去與原原型的連接,轉而關聯替換后的原型。也就是說,實例再也無法訪問原原型中的屬性和方法。替換原型用于模仿繼承機制,接下來將會講到實際上并不是繼承。
二、類和構造函數
2.1 沒有「類」
JavaScript中并沒有類似于Java的那種類,關于「類」的一切都是圍繞著函數和原型來展開的。雖然ES6中增加了class特性,但是底層上它還是在操作函數和原型,也算不上是真正的類。我們看看Java的繼承和JavaScript的繼承:
//java public class Foo{String name = "foo";public String getName(){System.out.println(this.name);} } public class Bar extends Foo{} Bar bar1 = new Bar(); Bar bar2 = new Bar(); Foo foo = new Foo();foo.name = 'modifined'; bar1.name = "bar1"; bar2.getName();//foo--絲毫不受影響 復制代碼//js var Foo = function Foo(){} Foo.prototype = {name:'foo',getName:function getName(){console.log(this.name);} }var Bar = function(){} Bar.prototype = Foo.prototype;//"繼承"Foo Bar.prototype.sayHi = function sayHi(){//拓展Fooconsole.log('hi!'); } var bar1 = new Bar(); var bar2 = new Bar(); Foo.prototype.name = 'modifined';bar1.getName();//'modifined' bar2.getName();//'modifined' 復制代碼從這兩段代碼可以看到,在Java中繼承意味著子類實例對屬性進行了私有化,無論是父類對象還是其他子類對象都無法影響到繼承的屬性。而在JavaScript中,"子類實例"只是關聯了共同的對象,只要那個對象更改了就能馬上反映到每一個"子類對象"。所以我覺得不應該用「繼承」來形容這種機制了,用「委托」更形象,它們本質上是對象之間的關聯。
2.2 沒有「構造函數」
JavaScript中也沒有構造函數。所有的函數都是一樣的,都可以通過函數名+()調用,所有的函數都可以new實例化得到一個對象。如果你認為可以new的就是構造函數,那么JavaScript中所有的函數都是構造函數了。
var foo = function foo(){console.log('foo'); } var obj = new foo();//'foo' console.log(obj.constructor === foo);//truefoo.prototype.constructor = Object; console.log(obj.constructor === Object);//true 復制代碼我們可以看到,一個普通的foo函數也可以進行new調用實例化一個對象,可能你還想通過constructor屬性去證明他是obj的構造函數。但是可以看到我們隨后修改了原型中的constructor屬性,obj的"構造函數"也發生改變了。這說明什么?constructor根本就沒那么「權威」,它只是原型中的一個屬性,默認指向了被構造調用的函數,我們可以自行修改它。
2.3 還是想要「類」
可能有時候寫「類」語言寫多了一下子還沒轉過來,還是想去模仿類,怎么辦?沒關系有辦法的——方法可以共用,屬性要私有嘛,請看代碼:
var father = function father(name,age){this.name = name;this.age = age; } father.prototype = {constructor:father,//不讓這個屬性丟失sayName:function sayName(){console.log(this.name);},sayAge:function sayAge(){console.log(this.age);} } var son = function(name,age){father.call(this,name,age);//構造函數借用,綁定son的this } son.prototype = Object.create(father.prototype);//新建對象,與原father.prototype隔離 //不要這樣:son.prototype = father.prototype; //否則后續對son.prototype的拓展都是在修改father.prototypevar son1 = new son('tom',12); var son2 = new son('jack',15); father.prototype.name = 'mike';son1.sayName();//tom son1.name = 'kobee';//屏蔽 son2.sayName();//jack 復制代碼這里主要有兩點:一是構造函數借調、二是隔離原父函數的prototype。借調使得我們可以"借用"父函數的代碼,當然我們要綁定this才能實現對新對象賦值;接著我們要隔離父函數的原型,以免我們拓展子函數原型時影響到父函數原型。通過Object.create(...)創建一個空的新對象,這個對象的[[prototype]]指向我們傳遞的對象,這里是father.prototype。我們對son.prototype的修改都是在修改這個新對象,而不是father.prototype。
好了,類模仿完畢了,不過我覺得如果用類的思維去理解這段代碼效率會比較低,如果用函數+原型的概念去理解會更好,你覺得呢?
2.4 尋找原型(委托)對象
那么怎么尋找一個對象的原型呢。如果還是用類的思維去判斷的話,要判斷一個對象是否是一個"類"的實例就會這么做:
var foo = function foo(){}; var obj = new foo();obj instanceof foo;//true 復制代碼instanceof操作符的左操作數是一個對象,右操作數是一個函數,實際上它在問:在obj的[[prototype]]鏈中有沒有foo.prototype?在這里明顯有~所以我們知道了obj的原型(委托)對象為foo.prototype
但是如果想判斷兩個對象是否關通過[[prototype]]關聯呢?instanceof肯定不行,它的右操作數是函數,而我們只有兩個對象。如果你已經理解了JavaScript中對象關聯(委托)的這個思想,可以這么做:
var foo = function foo(){}; var a = new foo(); var b = Object.create(o1);function isRelatedTo(o1, o2) {function F(){}F.prototype = o2;return o1 instanceof F; }console.log(isRelatedTo(b,a));//true 復制代碼說實話這樣做沒有錯,o2被關聯到F函數的原型上,然后用instanceof操作符就可判斷o2是否在o1的[[prototype]]鏈上。這里對象a的確在對象b的原型鏈上。
但是我們有必要繞一個大圈嗎?或者是說沒有直接判斷的工具嗎?有的:
var foo = function foo(){}; var a = new foo();console.log(foo.prototype.isPrototypeOf(a));//true復制代碼a.isPrototypeOf(b)在問:a是否出現在b的[[prototype]]鏈上?這里與上面不同的是,我們用兩個對象進行判斷,即進行了原型與對象之間的關聯判斷。而不是"類"與對象之間關聯的判斷,
總結
以上是生活随笔為你收集整理的JavaScript中的原型(prototype)与继承的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: VueJs相关命令
- 下一篇: Zookeeper源码分析(二) ---