摘要:使用原型模式添加方法和屬性在前面的章節中,已經學習過了如何定義一個構建新對象時使用的構造函數。向構造函數的中添加方法和屬性是在對象被創建的時候為對象添加功能的另一種方式。讓我們繼續使用對象作為構造函數的原型屬性。
原型模式(Prototype)本文原文來源:《Object-Oriented JavaScript》By Stoyan Stefanov
本文翻譯來源:赤石俊哉 原創翻譯
版權申明: 如果您是原文的原作者并且不希望此文被公開,可以聯系作者刪除。本文翻譯由 赤石俊哉 翻譯整理,您可以用于學習交流的目的,但是禁止用于其他用途,因私自濫用引發的版權糾紛本人概不負責。
在這一章節中你將會學習使用“函數(function)”對象中的prototype屬性。在JavaScript的學習過程中,理解prototype的工作原理是很重要的一個部分。畢竟,JavaScript被分類為是一個基于原型模式對象模型的語言。其實原型模式并不難,但是它是一種新的觀念而且往往需要花些時間去理解。它是JavaScript中的一部分(閉包是另一部分),一旦你“get“了他們,他們就會變得很容易理解也是很有意義的。在本書的剩余部分中,強烈建議多打多試這些示例。那樣會更加容易地學習和記住這些觀念。
本章中將會討論以下話題:
每一個函數都有一個prototype屬性,而且它包含了一個對象。
向prototype中添加屬性。
使用向prototype中添加的屬性。
函數自身屬性以及原型屬性的區別。
每一個對象保存在prototype中的私密鏈接——__proto__。
方法:isPrototypeOf(),hasOwnProperty(),propertyIsEnumerable()。
如何加強內建對象,比如數組(array)和字符串(string)。
原型屬性JavaScript中的函數是對象,而且包含了方法和屬性。包括我們常見的一些的方法,像apply()、call()等,常見的屬性,像length、constructor等。還有一個屬性就是prototype。
當你定義了一個簡單的函數foo()之后,你可以像其他對象一樣,直接訪問這個函數的屬性:
>>> function foo(a, b){return a * b;} >>> foo.length 2 >>> foo.constructor Function()
prototype這個屬性在你定義函數的時候就創建好了。他的初始值是一個空對象。
>>> typeof foo.prototype "object"
你可以使用屬性和方法來擴充這個空對象。他們不會對foo()函數本身產生任何影響。他們只會在當你使用foo()作為構造函數的時候被使用。
使用原型模式添加方法和屬性在前面的章節中,已經學習過了如何定義一個構建新對象時使用的構造函數。最主要的思想是在函數中調用new時,訪問this變量,它包含了構建函數返回的對象。擴張(添加方法和屬性)this對象是在對象被創建時添加功能的一種方法。
讓我們來看個例子,Gadget()構造方法中使用this來添加兩個屬性和一個方法到它創建的對象里。
function Gadget(name, color){ this.name = name; this.color = color; this.whatAreYou = function(){ return "I am a " + this.color + " " + this.name; } }
向構造函數的prototype中添加方法和屬性是在對象被創建的時候為對象添加功能的另一種方式。接下來再添加兩個屬性price和rating和一個getInfo()方法。因為prototype包含一個對象,所以你可以像這樣添加:
Gadget.prototype.price = 100; Gadget.prototype.rating = 3; Gadget.prototype.getInfo = function(){ return "Rating: " + this.rating + ", Price: " + this.price; };
你也可以通過另一種方式達到同樣的目的,就是完全覆蓋掉原型屬性,將它換成你選擇的對象:
Gadget.prototype = { price: 100, rating: 3, getInfo: function() { return `Rating: ` + this.rating + ", Price:" + this.price; } };使用原型屬性的方法和屬性
你添加到構造函數的原型屬性中的所有方法和屬性你都是可以直接在使用這個構造函數構造新對象之后,直接使用的。比如,如果你使用Gadget()構建函數,創建了一個newtoy對象,你可以直接訪問已經定義的所有方法和屬性。
>>> var newtoy = new Gadget("webcam", "black"); >>> newtoy.name; "webcam" >>> newtoy.color; "black" >>> newtoy.whatAreYou(); "I am a black webcam" >>> newtoy.price; 100 >>> newtoy.rating; 3 >>> newtoy.getInfo(); "Rating:3, Price: 100"
有一點很重要的是,原型屬性是”活的“,在JavaScript中對象的傳遞是通過引用來進行的。為此,原型類型不是直接復制到新對象中的。這意味著什么呢?這意味著,我們可以在任何時間修改任何對象的原型屬性(甚至你都可以在新建對象之后進行修改),它們都是生效的。
讓我們繼續來看一個例子,添加下面的方法到原型屬性里面:
Gadget.prototype.get = function(what){ return this[what]; };
盡管我們在定義get()方法之前已經生成了newtoy對象,然而newtoy依舊可以訪問這個新的方法:
>>> newtoy.get("price"); 100 >>> newtoy.get("color"); "black"“函數自身屬性”與“原型屬性”的對比
在上面的getInfo()例子中,使用了this來從內部指向對象本身,使用Gadget.prototype也可以達到一樣的目的:
Gadget.prototype.getInfo = function(){ return "Rating: " + Gadget.prototype.rating + ", Price: " + Gadget.prototype.price; };
這有啥不一樣呢?在回答這個問題之前,我們先來測試一下看看原型屬性是怎么工作的吧。
讓我們再拿出我們的newtoy對象:
>>> var newtoy = new Gadget("webcam", "black");
當你嘗試訪問newtoy的一個屬性,使用表達式newtoy.name,JavaScript引擎將會瀏覽對象的所有屬性,尋找一個叫作name,如果找到它,它的值就會被返回。
>>> newtoy.name "webcam"
什么?你想嘗試著訪問rating屬性?JavaScript引擎會檢查newtoy中的所有屬性,然后沒有找到一個叫作rating的。然后腳本引擎就會鑒別出,構造函數中的原型屬性曾經嘗試著創建這個對象(就像你使用newtoy.constructor.prototype的時候一樣)。如果屬性在原型屬性中找到了這個屬性,就會使用原型屬性中的這個屬性。
>>> newtoy.rating 3
這和你直接訪問原型屬性一樣。每一個對象都有一個構造函數的屬性,它是對創建該對象使用的構造函數的引用。所以,在這個例子中:
>>> newtoy.constructor Gadget(name, color) >>> newtoy.constructor.prototype.rating 3
現在,讓我們再來看看第一步,每一個對象都有一個構造函數。原型屬性是一個對象,所以,它也應該也有一個構造函數。進而它的構造函數又有一個原型屬性……
>>> newtoy.constructor.prototype.constructor Gadget(name, color) >>> newtoy.constructor.prototype.constructor.prototype Object price=100 rating=3
這個循環將會持續下去,具體有多長取決于這個原型屬性鏈有多長。但是最后會終結于一個內建的Object()對象。它是最外層父類。在這個例子中,如果你嘗試著使用newtoy.toString(),而newtoy他沒有自己的toString()方法,而且它的原型屬性對象里也沒有,他就會一直往上找,最后會調用Object對象的toString()方法。
>>> newtoy.toString() "[object Object]"使用函數自身的屬性覆蓋原型屬性的屬性
如上面所演示的,如果你的對象沒有一個確切的自己的屬性,可以使用一個原型鏈上層的對象。如果對象和原型屬性里面有相同名字的屬性,自身的屬性會被優先使用。
接下來我們來模擬一個屬性同時存在于自身屬性和原型屬性中:
function Gadget(name){ this.name = name; } >>> Gadget.prototype.name = "foo"; "foo"
創建一個新對象,訪問它的name屬性,它會給你對象自身的name屬性。
>>> var toy = new Gadget("camera"); >>> toy.name; "camera"
如果你刪除這個屬性,那么原型屬性中使用相同名字的屬性就會“表現出來”:
>>> delete toy.name; true >>> toy.name; "foo"
當然,你可以重新創建它的自身屬性:
>>> toy.name = "camera"; >>> toy.name; "camera"遍歷屬性
如果你希望列出一個對象的所有屬性,你可以使用一個for-in循環。在第二章節中,學習了如何遍歷一個數組里面的所有元素:
var a = [1, 2, 3]; for (var i in a) { console.log(a[i]); }
數組是一個對象,所以可以推導出for-in遍歷對象的時候:
var o = {p1: 1, p2: 2}; for (var i in o) { console.log(i + "=" + o[i]); }
這將會產生:
p1=1 p2=2
需要知道的幾個細節:
不是所有的屬性都在for-in循環中顯示出來。比如,數組的length,以及constructor屬性就不會被顯示出來。被顯示出來的屬性叫做可枚舉的。你可以使用每個對象都能提供的propertyIsEnumerable()方法來檢查一個屬性是不是可枚舉的。
原型鏈中原型屬性如果是可枚舉的,也會被顯示出來。你可以使用hasOwnProperty()方法來檢查一個屬性是自身屬性還是原型屬性。
propertyIsEnumerable()將會對所有原型屬性中的屬性返回false,盡管他們會在for-in循環中顯示出來,也是可枚舉的。
為了看看這些函數的效果,我們使用一個簡化版本的Gadget():
function Gadget(name, color) { this.name = name; this.color = color; this.someMethod = function(){ return 1; } } Gadget.prototype.price = 100; Gadget.prototype.rating = 3;
創建一個新的對象:
var newtoy = new Gadget("webcam", "black");
如果你使用for-in循環,你可以看到對象的所有屬性,包括那些原型屬性的:
for (var prop in newtoy){ console.log(prop + " = " + newtoy[prop]; }
這個結果也包含對象的方法(那是因為方法是正好類型是函數的屬性):
name = webcam color = black someMethod = function(){ return 1;} price = 100 rating = 3
如果你想區分對象自身屬性和原型屬性的屬性,使用hasOwnProperty(),試試這個:
>>> newtoy.hasOwnProperty("name") true >>> newtoy.hasOwnProperty("price") false
讓我們再來循環一次,但是這次只顯示自身的屬性:
for (var prop in newtoy){ if (newtoy.hasOwnProperty(prop)){ console.log(prop + "=" + newtoy[prop]); } }
結果:
name=webcam color=black someMethod=function(){return 1;}
接下來讓我們試試propertyIsEnumerable()。如果自身屬性不是內置的屬性,這個函數就會返回true:
>>> newtoy.propertyIsEnumerable("name") true >>> newtoy.propertyIsEnumerable("constructor") false
任何從原型鏈上來的屬性都是不可枚舉的:
>>> newtoy.propertyIsEnumerable("price") false
注意,雖然如果你獲取了包含在原型屬性中對象,并且調用了它的propertyIsEnumerable(),這個屬性是可以枚舉的。
>>> newtoy.constructor.prototype.propertyIsEnumberable("price") trueisPrototypeOf()
每一個對象都有isPrototypeOf()方法。這個方法會告訴你指定的對象是誰的原型屬性。
我們先寫一個簡單的對象monkey:
var monkey = { hair: true, feeds: "bananas", breathes: "air" };
接下來,讓我們建立一個Human()構造函數,然后設定它的prototype屬性指向monkey。
function Human(name){ this.name = name; } Human.prototype = monkey;
如果你創建一個叫作george的Human對象,然后問它:“monkey是george的原型屬性嗎?”,你就會得到true。
>>> var george = new Human("George"); >>> monkey.isPrototypeOf(george) true秘密的__proto__鏈接
如你所知的,當你嘗試訪問一個不存在與當前對象的屬性時,它會查詢原型屬性的屬性。
讓我們繼續使用monkey對象作為Human()構造函數的原型屬性。
var monkey = { feeds: "bananas", breathes: "air" }; function Human() {} Human.prototype = monkey;
接下來創建一個developer對象,然后給他一些屬性:
var developer = new Human(); developer.feeds = "pizza"; developer.hacks = "JavaScript";
現在,我們來做些查詢吧。hacks是developer的屬性:
>>> developer.hacks "JavaScript"
feeds可以在對象中被找到:
>>> developer.feeds "pizza"
breathes不存在于developer對象中,由于有一個秘密的鏈接指向原型類型對象,所以轉而查找原型類型。
>>> developer.breathes "air"
可以從developer對象中得到原型屬性對象呢?當然,可以啦。使用constructor作為中間對象,就像developer.constructor.prototype指向monkey一樣。但是這并不是十分可靠的。因為constructor大多時候用于提供信息的用途,而且是可以隨時被覆蓋修改的。你甚至可以用一個不是對象的東西覆蓋掉它。這樣做絲毫不會影響到原型鏈的功能。
讓我們看一些字符串的構造屬性:
>>> developer.constructor = "junk" "junk"
看上去,prototype已經亂成一團了:
>>> typeof developer.constructor.prototype "undefined"
但是事實卻并非如此,因為開發者仍然呼吸著“空氣”(developer的breathes屬性仍然是air):
>>> developer.breathes "air"
這表示原型屬性的秘密鏈接仍然存在。在火狐瀏覽器中公開的這個秘密鏈接是__proto__屬性(proto前后各加兩個下劃線)。
>>> developer._proto__ Object feeds = bananas breathes=air
你可以在學習的過程中使用這個秘密鏈接,但是實際編碼中不推薦使用。因為它不存在于Internet Explorer中,所以你的代碼將會變得難以移植。打個比方,如果你使用monkey創建了一堆對象,而且你現在想在所有的對象中更改一些東西。你可以修改monkey,而且所有的實例都會繼承這些變化。
>>> monkey.test = 1 1 >>> developer.test 1
__proto__不是等效于prototype。__proto__是實例的一個屬性,盡管prototype是構造函數的一個屬性。
>>> typeof developer.__proto__ "object" >>> typeof developer.prototype "undefined"
再次強調,你可以在Debug或者是學習的時候使用__proto__,其他時候不要。
擴充內建對象內建的一些對象像構造函數Array,String,甚至是Object和Function()都可以通過他們的原型屬性來進行擴充。打個比方,你就可以向Array原型屬性中添加新方法,而且它們可以在所有的數組中被使用。讓我們來試試。
在PHP中,有一個函數叫做in_array(),它會告訴你如果數組中是否存在某個值。在JavaScript中,沒有inArray()這樣的函數,所以我們可以實現它,并添加到Array.prototype中。
Array.prototype.inArray = function(needle) { for (var i = 0, len = this.length; i < len; i++) { if (this[i] === needle) { return true; } } return false; }
現在,所有的數組就都有新的方法了。讓我試試:
>>> var a = ["red", "green", "blue"]; >>> a.inArray("red"); true >>> a.inArray("yellow"); false
真是簡單快捷!讓我再來做一個。想象一下你的程序可能經常需要反轉字符串吧,或許你會認為字符串對象應該有一個內建的reverse()方法,畢竟數組有reverse()方法。你可以輕松地添加reverse()方法給String的原型屬性。瀏覽Array.prototype.reverse()(這和第四章末尾的練習相似)。
String.prototype.reverse = function() { return Array.prototype.reverse.apply(this.split("")).join(""); }
這個代碼使用split()使用字符串生成了一個數組,然后調用了這個數組上的reverse()方法,生成了一個反轉的數組。然后再使用join()將反轉的數組變回了字符串。讓我們試試新的方法:
>>> "Stoyan".reverse(); "nayotS"擴充內建對象——討論
通過原型屬性來擴充內建對象是一項強力的技術,而且你可以用它來將JavaScript塑造成你想要的樣子。你在使用這種強有力的方法之前都要徹底地思考清楚你的想法。
看看一個叫做Prototype的JavaScript庫,它的作者太愛這個方法了,以至于連庫的名字都叫這個了。使用這個庫,你可以使用一些JavaScript方法,讓使用JavaScript如Ruby語言一樣靈活。
YUI(雅虎用戶界面)庫是另一個比較流行的JavaScript庫。它的作者則是明確地反對這個領域。他們不會以任何方式更改內建對象。不管你用的是什么庫,修改核心對象都只會迷惑庫的使用者,而且造成意料之外的錯誤。
事實是,JavaScript發生了變化,瀏覽器也帶來了支持更多功能的新版本?,F在你認為需要擴充到原型屬性的缺失的功能,也許在明天就變成了內建的方法。因此你的方法可能就不被需要了。但是如果你使用這種方法已經寫了很多代碼而且你的方法又有些不同于內建的新內建實現呢?
最起碼來說你能做的是,在實現一個方法之前先去檢查一下它是否存在。我們的上一個例子就應該像這樣:
if (!String.prototype.reverse) { String.prototype.reverse = function() { return Array.prototype.reverse.apply(this.split("")).join(""); } }一些原型屬性的陷阱
在處理原型屬性的時候,這兩個現象是需要考慮在內的:
prototype.constructor是不可靠的。
創建一個簡單的構建函數和兩個對象:
>>> function Dog(){ this.tail = true; } >>> var benji = new Dog(); >>> var rusty = new Dog();
甚至在創建了對象之后,你仍可以向原型屬性添加屬性,而且對象會使用新的屬性。讓我們插進方法say():
>>> Dog.prototype.say = function(){ return "Woof!";}
兩個對象都會使用新的方法:
>>> benji.say(); "Woof!" >>> rusty.say(); "Woof!"
到此為止,如果你詢問你的對象,用來創建他們的構建函數是什么,他們還會正確地匯報:
>>> benji.constructor; Dog(); >>> rusty.constructor; Dog();
一個有趣的現象是如果你問原型屬性的構造函數是什么,你仍然會得到Dog(),他不算太準確。原型屬性是Object()創建的一個普通對象而已。使用Dog()構造的不含任何屬性的對象。
>>> benji.constructor.prototype.constructor Dog() >>> typeof benji.constructor.prototype.tail "undefined"
現在我們用一個全新的對象完全覆蓋原型屬性對象:
>>> Dog.prototype = {paws: 4, hair: true};
這證明我們的舊對象不能訪問新原型屬性的屬性。他們仍保持著與舊原型屬性對象的秘密鏈接。
>>> typeof benji.paws "undefined" >>> benji.say() "Woof!" >>> typeof benji.__proto__.say "function" >>> typeof benji.__proto__.paws "undefined"
你再創建新的對象,將會使用更新后的原型屬性:
>>> var lucy = new Dog(); >>> lucy.say() TypeError: lucy.say is not a function >>> lucy.paws 4
指向新原型屬性的私密鏈接__proto__:
>>> typeof lucy.__proto__.say "undefined" >>> typeof lucy.__proto__.paws "number"
新對象的構建函數屬性不再被正確地匯報出來了。本來應該指向Dog(),但是卻指向了Object()。
>>> lucy.constructor Object() >>> benji.constructor Dog()
最難區分的部分是當你查找構造函數的原型屬性時:
>>> typeof lucy.constructor.prototype.paws "undefined" >>> typeof benji.constructor.prototype.paws "number"
下面的語句將會修復上面所有的意料之外的現象:
>>> Dog.prototype = {paws: 4, hair: true}; >>> Dog.prototype.constructor = Dog;
當你覆蓋原型屬性,推薦重置constructor屬性。
總結讓我們來總結一下這一章節中學習的幾個要點。
所有的函數都有一個叫作prototype的屬性,初始情況下,它包含一個空白的對象。
你可以向原型屬性中添加屬性和方法。你甚至可以將它完全替換成你選擇的對象。
當你使用構造函數穿件對象(使用new),這個對象會有一個秘密鏈接指向它的原型屬性,而且可以把原型屬性的屬性當成自己的來用。
相比原型屬性的屬性,同名自身的屬性擁有更高的優先級。
使用hasOwnProperty()方法來區分自身屬性和原型屬性的屬性。
存在一個原型鏈:如果你的對象foo沒有屬性bar,當你使用foo.bar的時候,JavaScript會從它的原型屬性中去尋找bar屬性。如果沒有找到,它會繼續在原型屬性的原型屬性中找,然后是原型屬性的原型屬性的原型屬性,而且一步一步向上,直到最高層父類Object。
你可以擴充內建構造函數。所有的對象都可以應用你的擴充。申明Array.prototype.flip,而后所有的數組都會馬上擁有一個flip()方法。[1,2,3].flip()。在擴充方法和屬性之前,檢查是否存在,為你的代碼添加未來的保證。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/81879.html
摘要:對象詳解對象深度剖析,深度理解對象這算是醞釀很久的一篇文章了。用空構造函數設置類名每個對象都共享相同屬性每個對象共享一個方法版本,省內存。 js對象詳解(JavaScript對象深度剖析,深度理解js對象) 這算是醞釀很久的一篇文章了。 JavaScript作為一個基于對象(沒有類的概念)的語言,從入門到精通到放棄一直會被對象這個問題圍繞。 平時發的文章基本都是開發中遇到的問題和對...
摘要:創建對象的方式有很多,通過構造函數或對象字面量的方式也可以創建單個對象,顯然這兩種方式會產生大量的重復代碼,并不適合量產。四組合使用構造函數模式和原型模式組合使用構造函數模式和原型模式是使用最為廣泛認同度最高的一種創建自定義類型的方法。 JavaScript創建對象的方式有很多,通過Object構造函數或對象字面量的方式也可以創建單個對象,顯然這兩種方式會產生大量的重復代碼,并不適合量...
摘要:對象重新認識面向對象面向對象從設計模式上看,對象是計算機抽象現實世界的一種方式。除了字面式聲明方式之外,允許通過構造器創建對象。每個構造器實際上是一個函數對象該函數對象含有一個屬性用于實現基于原型的繼承和共享屬性。 title: JS對象(1)重新認識面向對象 date: 2016-10-05 tags: JavaScript 0x00 面向對象 從設計模式上看,對象是...
摘要:在創建子類實例時,不能向超類型的構造函數中傳遞參數。構造函數繼承子類傳進的值是基本思想是在子類構造函數的內部調用超類或父類型構造函數。繼承保證構造函數指針指向如果想同時繼承多個,還可使用添加屬性的方式類繼承, OOP:Object Oriented Programming 面向對象編程。 題外話:面向對象的范圍實在太大,先把這些大的東西理解理解。 1.什么是對象? 根據高程和權威指南上...
摘要:雖然,也是面向疾苦的語言,但是,它和靜態類型語言的面向接口編程不一而足。對象對他自己的行為負責,其他對象不關心它的內部實現。 ‘工欲善其事,必先利其器’,在深入學習JavaScript之前,我認為我們很有必要了解以下,JavaScript這門面向對象的動態語言到底是一門什么樣的語言。 JavaScript vs 其他面向對象語言 它沒有使用像Java等傳統的面向對象語言的類式繼承,而...
閱讀 2902·2023-04-26 02:14
閱讀 3751·2019-08-30 15:55
閱讀 1843·2019-08-29 16:42
閱讀 2757·2019-08-26 11:55
閱讀 2846·2019-08-23 13:38
閱讀 480·2019-08-23 12:10
閱讀 1308·2019-08-23 11:44
閱讀 2791·2019-08-23 11:43