摘要:通過這種操作,就有了構造函數的原型對象里的方法。你也看到了,就是一個普通對象,所以這種寄生式繼承適合于根據已有對象創建一個加強版的對象,在主要考慮通過已有對象來繼承而不是構造函數的情況下,這種方式的確很方便。
原文地址在我的博客, 轉載請注明出處,謝謝!
標簽: [es5對象、原型, 原型鏈, 繼承]
什么是JavaScript對象注意(這篇文章特別長)
這篇文章僅僅是我個人對于JavaScript對象的理解,并不是教程。這篇文章寫于我剛了解js對象之后。文章肯定有錯誤之處,還望讀者費心指出,在下方評論即可^-^
var person = { //person就是對象,對象都有各種屬性,每個屬性又都對應著自己的值 //鍵值對形式 name: "Mofan",//可以包含字符串 age: 20,//數字 parents: [ //數組 "Daddy", "Mami", ] sayName: function(){ //函數 console.log(this.name); }, features: { //甚至是對象(很少用,我是沒見過) height: "178cm", weight: "60kg", } }
js里除了基本類型外所有事物都是對象:
函數是對象:function sayName(){} ——sayName是函數對象
數組是對象:var arr = new Array() ——arr是數組對象
為什么JavaScript要這么設計呢?我覺得首先這樣一來,統一了數據結構,使JavaScript成為一門編程風格非常自由化的腳本語言:無論定義什么變量,統統var;其次,JavaScript對象都有屬性和方法,函數數組都是對象,調用引用就會非常靈活方便;再者,為了構建原型鏈?
創建對象的幾種方式Object()模式
使用對象字面量:var obj={...}就像上面那樣,或者使用原生構造函數Object():
var person = new Object(); person.name = "Mofan"; person.sayName = function(){ console.log(this.name); }; console.log(person.name);//Mofan obj.sayName();//Mofan
利用函數作用域使用自定義構造函數模式模仿類(構造器模式):
function Person(name,age){ this.name = name; this.age = age; this.print = function(){ console.log(this.name + this.age) }; } var person = new Person("Mofan",19); console.log(person.name+person.age);//Mofan19 person.print();//Mofan19
原型模式:
function Person(){} //可以這樣寫 /*Person.prototype.name = "Mofan"; Person.prototype.age = 19; Person.prototype.print = function(){ console.log(this.name+this.age); }*/ //推薦下面這樣寫,但兩種方式不能混用!因為下面這種方式實際上重寫了 //Person原型對象,如果兩者混用,后面賦值方式會覆蓋前面賦值方式 Person.prototype = { name:"Mofan", age:19, print:function(){ console.log(this.name+this.age); } } var person = new Person(); console.log(person.name+person.age);//Mofan19 person.print();//Mofan19
組合構造函數模式和原型模式:
function Person(name,age){ //這里面初始化屬性 this.name = name; this.age = age; ... } Person.prototype = { //這里面定義公有方法 print:function(){ console.log(this.name+this.age); }, ... } var person = new Person("Mofan",19); console.log(person.name+person.age);//Mofan19 person.print();//Mofan19
動態創建原型模式:
function Person(name,age){ //初始化屬性 this.name = name; this.age = age; //在創建第一個對象(第一次被調用)時定義所有公有方法,以后不再調用 if(typeof this.print !="function"){ Person.prototype.print =function(){ console.log(this.name+this.age); }; Person.prototype.introduction=function(){ console.log("Hi!I"m "+this.name+",I"m "+this.age); }; //如果采用對象字面量對原型添加方法的話,第一次創建的對象將不會有這些方法 }; } var person = new Person("Mofan",19); person.print();//Mofan19 person.introduction();//Hi!I"m Mofan,I"m 19
還有一些模式用的場景比較少
這些模式的應用場景怎么會有這么多的創建模式?其實是因為js語言太靈活了,因此前輩們總結出這幾種創建方式以應對不同的場景,它們各有利弊。
第一種方式,使用字面量或者使用構造函數Object()常用于創建普通對象存儲數據等。它們的原型都是Object,彼此之間沒有什么關聯。事實上,下面創建方式都是一樣的:
var o1 = {};//字面量的表現形式 var o2 = new Object; var o3 = new Object(); var o4 = new Object(null); var o5 = new Object(undefined); var o6 = Object.create(Object.prototype);//等價于 var o = {};//即以 Object.prototype 對象為一個原型模板,新建一個以這個原型模板為原型的對象
第二種方式,利用函數作用域模仿類,這樣就可以在創建對象時傳參了,可以創建不同屬性值得對象,實現對象定制。不過print方法也定義在了構造函數里面,如果要把它當做公有方法的話,這樣每new一個對象,都會有這個方法,太浪費內存了。可以這樣修改一下構造器模式:
//構造器方法2 function print(){ //定義一個全局的 Function 對象,把要公有的方法拿出來 console.log(this.name + this.age); } function Person(name,age){ this.name = name; this.age = age; this.print = print.bind(this);//每個 Person 對象共享同一個print 方法版本(方法有自己的作用域,不用擔心變量被共享) } var person = new Person("Mofan",19); console.log(person.name+person.age);//Mofan19 person.print();//Mofan19
然而這樣看起來很亂,也談不上類的封裝性。還是使用原型吧
第三種方式,純原型模式,不管是屬性還是方法都添加到原型里面去了,這樣做好處是很省內存,但是應用范圍就少了,更多的對象 內部的屬性是需要定制的,而且一旦更改原型,所有這個原型實例都會跟著改變。因此可以結合構造函數方式來實現對對象的定制,于是就有了第四種方式——組合構造函數模式與原型模式,可以定制的放在構造器里,共有的放在原型里,這也符合構造器和原型的特性。
“這是es5中使用最廣泛、認同度最高的創建自定義類型的方法”---《JavaScript高級程序設計》第三版
第五種方式,動態原型模式,出現這種方式是因為有些面向對象開發人員習慣了類構造函數,于是對這種獨立出來的構造函數和原型感到困惑和不習慣。于是,就出現了把定義原型也寫進構造函數里的動態原型模式。
上面在動態原型模式程序里面講“如果采用對象字面量對原型添加方法的話,第一次創建的對象將不會有這些方法”這是因為在if語句執行以前,第一個對象已經被創建了,然后執行if里面的語句,如果采用對象字面量給原型賦值,就會導致原型在實例創建之后被重寫,創建的第一個實例就會失去與原型的鏈接,也就沒有原型里的方法了。不過以后創建的對象就可以使用原型里的方法了,因為它們都是原型被修改后創建的。
在JavaScript中,原型就是一個對象,沒必要把原型和其他對象區別對待,只是通過它可以實現對象之間屬性的繼承。任何一個對象也可以成為原型。之所以經常說對象的原型,實際上就是想找對象繼承的上一級對象。對象與原型的稱呼是相對的,也就是說,一個對象,它稱呼繼承的上一級對象為原型,它自己也可以稱作原型鏈下一級對象的原型。
一個對象內部的[[Prototype]]屬性生來就被創建,它指向繼承的上一級對象,稱為原型。函數對象內部的prototype屬性也是生來就被創建(只有函數對象有prototype屬性),它指向函數的原型對象(不是函數的原型!)。
當使用var instance = new Class();這樣每new一個函數(函數被當做構造函數來使用)創建實例時,JavaScript就會把這個原型的引用賦值給實例的原型屬性,于是實例內部的[[Prototype]]屬性就指向了函數的原型對象,也就是prototype屬性。
原型真正意義上指的是一個對象內部的[[Prototype]]屬性,而不是函數對象內部的prototype屬性,這兩者之間沒有關系!對于一個對象內部的[[Prototype]]屬性,不同瀏覽器有不同的實現:
var a = {}; //Firefox 3.6+ and Chrome 5+ Object.getPrototypeOf(a); //[object Object] //Firefox 3.6+, Chrome 5+ and Safari 4+ a.__proto__; //[object Object] //all browsers a.constructor.prototype; //[object Object]
之所以函數對象內部存在prototype屬性,并且可以用這個屬性創建一個原型,是因為這樣以來,每new一個這樣的函數(函數被當做構造函數來使用)創建實例,JavaScript就會把這個原型的引用賦值給實例的原型屬性,這樣以來,在原型中定義的方法等都會被所有實例共用,而且,一旦原型中的某個屬性被定義,就會被所有實例所繼承(就像上面的例子)。這種操作在性能和維護方面其意義是不言自明的。這也正是構造函數存在的意義(JavaScript并沒有定義構造函數,更沒有區分構造函數和普通函數,是開發人員約定俗成)。下面是一些例子:
var a = {} //一個普通的對象 function fun(){} //一個普通的函數 //普通對象沒有prototype屬性 console.log(a.prototype);//undefined console.log(a.__proto__===Object.prototype);//true //只有函數對象有prototype屬性 console.log(fun.prototype);//Object console.log(fun.__proto__===Function.prototype);//true console.log(fun.prototype.__proto__===Object.prototype);//true console.log(fun.__proto__.__proto__===Object.prototype);//true console.log(Function.prototype.__proto__===Object.prototype);//true console.log(Object.prototype.__proto__);//null
當執行console.log(fun.prototype);輸出為
可以看到,每創建一個函數,就會創建prototype屬性,這個屬性指向函數的原型對象(不是函數的原型),并且這個原型對象會自動獲得constructor屬性,這個屬性是指向prototype屬性所在函數的指針。而__proto__屬性是每個對象都有的。
接著上面再看:
function Person(){}//構造函數,約定首字母大寫 var person1 = new Person();//person1為Person的實例 console.log(person1.prototype);//undefined console.log(person1.__proto__===Person.prototype);//true console.log(person1.__proto__.__proto__===Object.prototype);//true console.log(person1.constructor);//function Person(){} //函數Person是Function構造函數的實例 console.log(Person.__proto__===Function.prototype);//true //Person的原型對象是構造函數Object的實例 console.log(Person.prototype.__proto__===Object.prototype);//true
person1和上面那個普通的對象a有區別,它是構造函數Person的實例。前面講過:
當使用var instance = new Class();這樣每new一個函數(函數被當做構造函數來使用)創建實例時,JavaScript就會把這個原型的引用賦值給實例的原型屬性,于是實例內部的[[Prototype]]屬性就指向了函數的原型對象,也就是prototype屬性。
因此person1內部的[[Prototype]]屬性就指向了Person的原型對象,然后Person的原型對象內部的[[Prototype]]屬性再指向Object.prototype,相當于在原型鏈中加了一個對象。通過這種操作,person1就有了構造函數的原型對象里的方法。
另外,上面代碼console.log(person1.constructor);//function Person(){}中,person1內部并沒有constructor屬性,它只是順著原型鏈往上找,在person1.__proto__里面找到的。
可以用下面這張圖理清原型、構造函數、實例之間的關系:
JavaScript并沒有繼承這一現有的機制,但可以利用函數、原型、原型鏈模仿。
下面是三種繼承方式:
//父類 function SuperClass(){ this.superValue = "super"; } SuperClass.prototype.getSuperValue = function(){ return this.superValue; }; //子類 function SubClass(){ this.subValue = "sub"; } //類式繼承,將父類實例賦值給子類原型,子類原型和子類實例可以訪問到父類原型上以及從父類構造函數中復制的屬性和方法 SubClass.prototype = new SuperClass(); //為子類添加方法 SubClass.prototype.getSubValue = function(){ return this.subValue; } //使用 var instance = new SubClass(); console.log(instance.getSuperValue);//super console.log(instance.getSubValue);//sub
這種繼承方式有很明顯的兩個缺點:
實例化子類時無法向父類構造函數傳參
如果父類中的共有屬性有引用類型,就會在子類中被所有實例所共用,那么任何一個子類的實例更改這個引用類型就會影響其他子類實例,可以使用構造函數繼承方式解決這一問題
構造函數繼承//父類 function SuperClass(id){ this.superValue = ["big","large"];//引用類型 this.id = id; } SuperClass.prototype.getSuperValue = function(){ return this.superValue; }; //子類 function SubClass(id){ SuperClass.call(this,id);//調用父類構造函數并傳參 this.subValue = "sub"; } var instance1 = new SubClass(10);//可以向父類傳參 var instance2 = new SubClass(11); instance1.superValue.push("super"); console.log(instance1.superValue);//["big", "large", "super"] console.log(instance1.id);//10 console.log(instance2.superValue);["big", "large"] console.log(instance2.id);//11 console.log(instance1.getSuperValue());//error
這種方式是解決了類式繼承的缺點,不過在代碼的最后一行你也看到了,沒有涉及父類原型,因此違背了代碼復用的原則。所以組合它們:
組合繼承function SuperClass(id){ this.superValue = ["big","large"];//引用類型 this.id = id; } SuperClass.prototype.getSuperValue = function(){ return this.superValue; }; //子類 function SubClass(id,subValue){ SuperClass.call(this,id);//調用父類構造函數并傳參 this.subValue = subValue; } SubClass.prototype = new SuperClass(); SubClass.prototype.getSubValue = function(){ return this.subValue; } var instance1 = new SubClass(10,"sub");//可以向父類傳參 var instance2 = new SubClass(11,"sub-sub"); instance1.superValue.push("super"); console.log(instance1.superValue);//["big", "large", "super"] console.log(instance1.id);//10 console.log(instance2.superValue);["big", "large"] console.log(instance2.id);//11 console.log(instance1.getSuperValue());["big", "large", "super"] console.log(instance1.getSubValue());//sub console.log(instance2.getSuperValue());//["big", "large"] console.log(instance2.getSubValue());//sub-sub
嗯,比較完美了,但是有一點,父類構造函數被調用了兩次,這就導致第二次調用也就是創建實例時重寫了原型屬性,原型和實例都有這些屬性,顯然性能并不好。先來看看克羅克福德的寄生式繼承:
function object(o){ function F(){}; F.prototype = o; return new F(); } function createAnnther(original){ var clone = object(original); clone.sayName = function(){ console.log(this.name); } return clone; } var person = { name:"Mofan", friends:["xiaoM","Alice","Neo"], }; var anotherPerson = createAnnther(person); anotherPerson.sayName();//"Mofan" }
就是讓一個已有對象變成新對象的原型,然后再在createAnother函數里加強。你也看到了,person就是一個普通對象,所以這種寄生式繼承適合于根據已有對象創建一個加強版的對象,在主要考慮通過已有對象來繼承而不是構造函數的情況下,這種方式的確很方便。但缺點也是明顯的,createAnother函數不能復用,我如果想給另外一個新創建的對象定義其他方法,還得再寫一個函數。仔細觀察一下,其實寄生模式就是把原型給了新對象,對象再加強。
等等,寫到這個地方,我腦子有點亂,讓我們回到原點:繼承的目的是什么?應該繼承父類哪些東西?我覺得取決于我們想要父類的什么,我想要父類全部的共有屬性(原型里)并且可以自定義繼承的父類私有屬性(構造函數里)!前面那么多模式它們的缺點主要是因為這個:
SubClass.prototype = new SuperClass();
那為什么要寫這一句呢?是只想要繼承父類的原型嗎?如果是為什么不這么寫:
SubClass.prototype = SuperClass.prototype;
這樣寫是可以繼承父類原型,但是風險極大:SuperClass.prototype屬性它是一個指針,指向SuperClass的原型,如果把這個指針賦給子類prototype屬性,那么子類prototype也會指向父類原型。對SubClass.prototype任何更改,就是對父類原型的更改,這顯然是不行的。
寄生組合式繼承但出發點沒錯,可以換種繼承方式,看看上面的寄生式繼承里的object()函數,如果把父類原型作為參數,它返回的對象實現了對父類原型的繼承,沒有調用父類構造函數,也不會對父類原型產生影響,堪稱完美。
function object(o){ function F(){}; F.prototype = o; return new F(); } function inheritPrototype(subType,superType){ var proto = object(superType.prototype); proto.constructor = subType;//矯正一下construcor屬性 subType.prototype = proto; } function SuperClass(id){ this.superValue = ["big","large"];//引用類型 this.id = id; } SuperClass.prototype.getSuperValue = function(){ return this.superValue; }; //子類 function SubClass(id,subValue){ SuperClass.call(this,id);//調用父類構造函數并傳參 this.subValue = subValue; } inheritPrototype(SubClass,SuperClass);//繼承父類原型 SubClass.prototype.getSubValue = function(){ return this.subValue; } var instance1 = new SubClass(10,"sub");//可以向父類傳參 var instance2 = new SubClass(11,"sub-sub"); instance1.superValue.push("super"); console.log(instance1.superValue);//["big", "large", "super"] console.log(instance1.id);//10 console.log(instance2.superValue);//["big", "large"] console.log(instance2.id);//11 console.log(instance1.getSuperValue());//["big", "large", "super"] console.log(instance1.getSubValue());//sub console.log(instance2.getSuperValue());//["big", "large"] console.log(instance2.getSubValue());//sub-sub
解決了組合繼承的問題,只調用了一次父類構造函數,而且還能保持原型鏈不變,為什么這么說,看對寄生組合的測試:
console.log(SubClass.prototype.__proto__===SuperClass.prototype);//ture console.log(SubClass.prototype.hasOwnProperty("getSuperValue"));//false
因此,這是引用類型最理想的繼承方式。
總結創建用于繼承的對象最理想的方式是組合構造函數模式和原型模式(或者動態原型模式),就是讓可定義的私有屬性放在構造函數里,共有的放在原型里;繼承最理想的方式是寄生式組合,就是讓子類的原型的[[prototype]]屬性指向父類原型,然后在子類構造函數里調用父類構造函數實現自定義繼承的父類屬性。
JavaScript對象總有一些讓我困惑的地方,不過我還會繼續探索。我在此先把我了解的記錄下來,與各位共勉。錯誤的地方請費心指出,我將感謝您的批評指正。
本文為作者原創,轉載請注明本文鏈接,作者保留權利。
參考文獻:
[1] http://www.cnblogs.com/chuaWe...
[2] http://www.cnblogs.com/xjser/...
[3] https://javascriptweblog.word...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/88985.html
摘要:匿名函數的執行環境具有全局性,因此它的對象通常指向。如果對此有疑惑,可以看知乎上的答案知乎匿名函數的指向為什么是作為對象方法的調用,指向該對象當函數作為某個對象的方法調用時,就指這個函數所在的對象。 因為日常工作中經常使用到this,而且在JavaScript中this的指向問題也很容易讓人混淆一部分知識。 這段時間翻閱了一些書籍也查閱了網上一些資料然后結合自己的經驗,為了能讓自...
摘要:因為我們用這個函數來構造對象,所以我們也把稱作構造函數。所以通過定義構造函數,就相當于定義了一個類,通過關鍵字,即可生成一個實例化的對象。 一、序言 ??和其他面向對象的語言(如Java)不同,Javascript語言對類的實現和繼承的實現沒有標準的定義,而是將這些交給了程序員,讓程序員更加靈活地(當然剛開始也更加頭疼)去定義類,實現繼承。(以下不討論ES6中利用class、exten...
摘要:這兩個函數接受定時器的例如我們上面提到的兩個函數產生的定時器,并停止對定時器中指定函數的調用。注意,定時器雖然觸發了,但是并不會立即執行,它只是把需要延遲執行的函數加入了執行隊列,在線程的某一個可用的時間點,這個函數就能夠得到執行。 擼了今年阿里、頭條和美團的面試,我有一個重要發現....... javascript定時器工作原理是一個重要的基礎知識點。因為定時器在單線程中工作,它們表...
摘要:本課程之所以叫做闖關記,是因為部分章節精心設計了挑戰關卡,通過提供更多的實戰機會,讓大家可以循序漸進地有目的地有挑戰地開展學習。課程結構及目錄以下目錄只是初步構想,課程結構及內容會根據實際情況隨時進行調整。 為何寫作此課程 stone 主要負責基于 Web 的企業內部管理系統的開發,雖然能夠熟練地使用 JavaScript,但隨著對 JavaScript 的理解越來越深,才發現自己尚...
摘要:函數式編程一開始我并不理解。漸漸地,我熟練掌握了使用函數式的方法去編程。但是自從學習了函數式編程,我將循環都改成了使用和來實現。只有數據和函數,而且因為函數沒有和對象綁定,更加容易復用。在函數式的中,這些問題不復存在。 譯者按: 當從業20的JavaScript老司機學會函數式編程時,他扔掉了90%的特性,也不用面向對象了,最后發現了真愛啊!!! 原文: How I rediscov...
閱讀 1721·2021-11-11 10:58
閱讀 4200·2021-09-09 09:33
閱讀 1264·2021-08-18 10:23
閱讀 1552·2019-08-30 15:52
閱讀 1630·2019-08-30 11:06
閱讀 1875·2019-08-29 14:03
閱讀 1513·2019-08-26 14:06
閱讀 2955·2019-08-26 10:39