為什么有JavaScript有原型
私有變量和函數
在函數內部定義的變量和函數,如果不對外提供接口,外部是無法訪問到的。
function School(){ var name = "GDUT"; var fn = function(){} } var obj = new School(); console.log(obj.name); //undefined console.log(obj.fn); //undefined
靜態變量和函數
當定義一個函數后通過點號 “.”為其添加的屬性和函數,通過對象本身仍然可以訪問得到,但是其實例卻訪問不到,這樣的變量和函數分別被稱為靜態變量和靜態函數。
<script type="text/javascript"> function Obj(){}; Obj.num = 72;//靜態變量 Obj.fn = function(){} //靜態函數 alert(Obj.num);//72 alert(typeof Obj.fn)//function var t = new Obj(); alert(t.name);//undefined alert(typeof t.fn);//undefined </script>
實例變量和函數
在面向對象編程中除了一些庫函數我們還是希望在對象定義的時候同時定義一些屬性和方法,實例化后可以訪問,js也能做到這樣
<script type="text/javascript"> function Box(){ this.a=[]; //實例變量 this.fn=function(){} //實例方法 } console.log(typeof Box.a); //undefined console.log(typeof Box.fn); //undefined var box=new Box(); console.log(typeof box.a); //object console.log(typeof box.fn); //function </script>
為實例變量和方法添加新的方法和屬性
<script type="text/javascript"> function Box(){ this.a=[]; //實例變量 this.fn=function(){} //實例方法 } var box1=new Box(); box1.a.push(1); box1.fn={}; console.log(box1.a); //[1] console.log(typeof box1.fn); //object var box2=new Box(); console.log(box2.a); //[] console.log(typeof box2.fn); //function </script>
在box1中修改了a和fn,而在box2中沒有改變,由於數組和函數都是對象,是引用類型,這就說明box1中的屬性和方法與box2中的屬性與方法雖然同名但卻不是一個引用,而是對Box對象定義的屬性和方法的一個復制。
這個對屬性來說沒有什么問題,但是對於方法來說問題就很大了,因為方法都是在做完全一樣的功能,但是卻又兩份復制,如果一個函數對象有上千和實例方法,那么它的每個實例都要保持一份上千個方法的復制,這顯然是不科學的,這可腫么辦呢,prototype應運而生。
基本概念
我們創建的每個函數都有一個prototype屬性,這個屬性是一個指針,指向一個對象,這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法。那么,prototype就是通過調用構造函數而創建的那個對象實例的原型對象。
使用原型的好處是可以讓對象實例共享它所包含的屬性和方法。也就是說,不必在構造函數中添加定義對象信息,而是可以直接將這些信息添加到原型中。使用構造函數的主要問題就是每個方法都要在每個實例中創建一遍。
在JavaScript中,一共有兩種類型的值,原始值和對象值。每個對象都有一個內部屬性 prototype ,我們通常稱之為原型。原型的值可以是一個對象,也可以是null。如果它的值是一個對象,則這個對象也一定有自己的原型。這樣就形成了一條線性的鏈,我們稱之為原型鏈。
含義
函數可以用來作為構造函數來使用。另外只有函數才有prototype屬性並且可以訪問到,但是對象實例不具有該屬性,只有一個內部的不可訪問的__proto__屬性。__proto__是對象中一個指向相關原型的神秘鏈接。按照標准,__proto__是不對外公開的,也就是說是個私有屬性,但是Firefox的引擎將他暴露了出來成為了一個共有的屬性,我們可以對外訪問和設置。
<script type="text/javascript"> var Browser = function(){}; Browser.prototype.run = function(){ alert("I'm Gecko,a kernel of firefox"); } var Bro = new Browser(); Bro.run(); </script>
當我們調用Bro.run()方法時,由於Bro中沒有這個方法,所以,他就會去他的__proto__中去找,也就是Browser.prototype,所以最終執行了該run()方法。(在這里,函數首字母大寫的都代表構造函數,以用來區分普通函數)
當調用構造函數創建一個實例的時候,實例內部將包含一個內部指針(__proto__)指向構造函數的prototype,這個連接存在於實例和構造函數的prototype之間,而不是實例與構造函數之間。
《JavaScript高級程序設計》P148也有詳細解釋
<script type="text/javascript"> function Person(name){ //構造函數 this.name=name; } Person.prototype.printName=function() //原型對象 { alert(this.name); } var person1=new Person('Byron');//實例化對象 console.log(person1.__proto__);//Person console.log(person1.constructor);//指向Person的構造函數 console.log(Person.prototype);//指向原型對象Person var person2=new Person('Frank'); </script>
Person的實例person1中包含了name屬性,同時自動生成一個__proto__屬性,該屬性指向Person的prototype,可以訪問到prototype內定義的printName方法
每個JavaScript函數都有prototype屬性,這個屬性引用了一個對象,這個對象就是原型對象。原型對象初始化的時候是空的,我們可以在里面自定義任何屬性和方法,這些方法和屬性都將被該構造函數所創建的對象繼承。
構造函數、實例和原型對象三者之間有什么關系呢?
實例就是通過構造函數創建的。實例一創造出來就具有constructor屬性和__proto__屬性,其中constructor屬性指向它的構造函數,__proto__屬性指向原型對象。
構造函數中有一個prototype屬性,這個屬性是一個指針,指向它的原型對象。
原型對象內部也有一個指針(constructor屬性)指向構造函數:Person.prototype.constructor = Person;
實例可以訪問原型對象上定義的屬性和方法。
原型鏈
原型鏈:當從一個對象那里調取屬性或方法時,如果該對象自身不存在這樣的屬性或方法,就會去自己關聯的prototype對象那里尋找,如果prototype沒有,就會去prototype關聯的前輩prototype那里尋找,如果再沒有則繼續查找Prototype.Prototype引用的對象,依次類推,直到Prototype.….Prototype為undefined(Object的Prototype就是undefined)從而形成了所謂的“原型鏈”。
《JavaScript高級程序設計》P162
var a = { x: 10, calculate: function (z) { return this.x + this.y + z } };var b = { y: 20, __proto__: a };var c = { y: 30, __proto__: a }; // call the inherited method b.calculate(30); // 60 c.calculate(40); // 80
我們看到b和c訪問到了在對象a中定義的calculate方法。這是通過原型鏈實現的。
規則很簡單:如果一個屬性或者一個方法在對象自身中無法找到(也就是對象自身沒有一個那樣的屬性),然后它會嘗試在原型鏈中尋找這個屬性/方法。如果這個屬性在原型中沒有查找到,那么將會查找這個原型的原型,以此類推,遍歷整個原型鏈(當然這在類繼承中也是一樣的,當解析一個繼承的方法的時候-我們遍歷class鏈( class chain))。第一個被查找到的同名屬性/方法會被使用。因此,一個被查找到的屬性叫作繼承屬性。如果在遍歷了整個原型鏈之后還是沒有查找到這個屬性的話,返回undefined值。
在總結一下,當要查找對象的屬性或者方法的時候,會先在對象的實例中查找,沒找到的話再到構造函數中查找,構造函數中找不到就到原型對象中查找,在找不到就到原型對象的__protoc__中查找,即上一級對象。這樣就構成了一條原型鏈出來。如下圖所示:
構造函數內的方法與構造函數prototype屬性上方法的對比
理解什么情況下把函數的方法寫在JavaScript的構造函數上,什么時候把方法寫在函數的prototype
屬性上;以及這樣做的好處。
為了閱讀方便,我們約定一下:把方法寫在構造函數內的情況我們簡稱為函數內方法,把方法寫在prototype
屬性上的情況我們簡稱為prototype上的方法。
首先我們先了解一下這篇文章的重點:
- 函數內的方法: 使用函數內的方法我們可以訪問到函數內部的私有變量,如果我們通過構造函數
new
出來的對象需要我們操作構造函數內部的私有變量的話, 我們這個時候就要考慮使用函數內的方法. - prototype上的方法: 當我們需要通過一個函數創建大量的對象,並且這些對象還都有許多的方法的時候;這時我們就要考慮在函數的
prototype
上添加這些方法. 這種情況下我們代碼的內存占用就比較小. - 在實際的應用中,這兩種方法往往是結合使用的;所以我們要首先了解我們需要的是什么,然后再去選擇如何使用.
我們還是根據下面的代碼來說明一下這些要點吧,下面是代碼部分:
// 構造函數A function A(name) { this.name = name || 'a'; this.sayHello = function() { console.log('Hello, my name is: ' + this.name); } } // 構造函數B function B(name) { this.name = name || 'b'; } B.prototype.sayHello = function() { console.log('Hello, my name is: ' + this.name); }; var a1 = new A('a1'); var a2 = new A('a2'); a1.sayHello(); a2.sayHello(); var b1 = new B('b1'); var b2 = new B('b2'); b1.sayHello(); b2.sayHello();
我們首先寫了兩個構造函數,第一個是A
,這個構造函數里面包含了一個方法sayHello
;第二個是構造函數B
, 我們把那個方法sayHello
寫在了構造函數B
的prototype
屬性上面.
需要指出的是,通過這兩個構造函數new
出來的對象具有一樣的屬性和方法,但是它們的區別我們可以通過下面的一個圖來說明:
我們通過使用構造函數A
創建了兩個對象,分別是a1
,a2
;通過構造函數B
創建了兩個對象b1
,b2
;我們可以發現b1
,b2
這兩個對象的那個sayHello
方法 都是指向了它們的構造函數的prototype
屬性的sayHello
方法.而a1
,a2
都是在自己內部定義了這個方法. 定義在構造函數內部的方法,會在它的每一個實例上都克隆這個方法;定義在構造函數的prototype
屬性上的方法會讓它的所有示例都共享這個方法,但是不會在每個實例的內部重新定義這個方法. 如果我們的應用需要創建很多新的對象,並且這些對象還有許多的方法,為了節省內存,我們建議把這些方法都定義在構造函數的prototype
屬性上。
當然,在某些情況下,我們需要將某些方法定義在構造函數中,這種情況一般是因為我們需要訪問構造函數內部的私有變量。
下面我們舉一個兩者結合的例子,代碼如下:
function Person(name, family) { this.name = name; this.family = family; var records = [{type: "in", amount: 0}]; this.addTransaction = function(trans) { if(trans.hasOwnProperty("type") && trans.hasOwnProperty("amount")) { records.push(trans); } } this.balance = function() { var total = 0; records.forEach(function(record) { if(record.type === "in") { total += record.amount; } else { total -= record.amount; } }); return total; }; }; Person.prototype.getFull = function() { return this.name + " " + this.family; }; Person.prototype.getProfile = function() { return this.getFull() + ", total balance: " + this.balance(); };
在上面的代碼中,我們定義了一個Person
構造函數;這個函數有一個內部的私有變量records
,這個變量我們是不希望通過函數內部以外的方法 去操作這個變量,所以我們把操作這個變量的方法都寫在了函數的內部.而把一些可以公開的方法寫在了Person
的prototype
屬性上,比如方法getFull
和getProfile
.
把方法寫在構造函數的內部,增加了通過構造函數初始化一個對象的成本,把方法寫在prototype
屬性上就有效的減少了這種成本. 你也許會覺得,調用對象上的方法要比調用它的原型鏈上的方法快得多,其實並不是這樣的,如果你的那個對象上面不是有很多的原型的話,它們的速度其實是差不多的
另外,需要注意的一些地方:
- 首先如果是在函數的
prototype
屬性上定義方法的話,要牢記一點,如果你改變某個方法,那么由這個構造函數產生的所有對象的那個方法都會被改變. - 還有一點就是變量提升的問題,我們可以稍微的看一下下面的代碼:
func1(); // 這里會報錯,因為在函數執行的時候,func1還沒有被賦值. error: func1 is not a function var func1 = function() { console.log('func1'); }; func2(); // 這個會被正確執行,因為函數的聲明會被提升. function func2() { console.log('func2'); }
- 關於對象序列化的問題.定義在函數的
prototype
上的屬性不會被序列化,可以看下面的代碼:function A(name) { this.name = name; } A.prototype.sayWhat = 'say what...'; var a = new A('dreamapple'); console.log(JSON.stringify(a));
{"name":"dreamapple"}
構造函數內的方法與構造函數prototype屬性上方法的對比