與其它編程語言不同的是,javascript的面向對象並不是依賴於抽象的類,而是經過原型鏈
,將一個個具體的對象實例進行鏈接,位於原型鏈下游的對象實例能夠讀取/使用位於上游的對象實例的屬性/方法。
下文由簡及深,試圖一步步理清javascript面向對象的本質。javascript
Object
javascript定義了最基礎的對象類型Object
,而且爲這一對象類型定義了許多成員方法。其它許多原生對象類型,實際上都是繼承自Object
,好比說Function
、Date
等。想要生成一個Object
對象類型的對象實例也不是一件什麼難事:java
var obj = {a: 2333}; //又或者是利用Object()這一構造函數 var obj = new Object();
實際上,更符合面向對象思想的應該是利用構造函數來生成對象實例。編程
new
,以及構造函數constructor
new
這一運算符來生成對象實例在其它編程語言中,new
每每也是用來生成對象實例的,用法通常是這樣:new ClassName[([arguments])]
,而生成出來的對象實例也被冠以「ClassName對象」這樣的稱謂;而javascript則大不相同,因爲原生沒有類這一律念,所以構造函數
便取而代之:new constructor[([arguments])]
,而稱謂也改成「constructor對象」或"constructor類型的對象",構造函數名直接就等同於數據類型了。瀏覽器
function A() {} //有一函數(如果用來生成對象,則稱爲構造函數)名A。 var obj = new A(); //使用構造函數A來生成實例對象obj console.dir(obj); //打印實例對象obj
從上圖可知,利用構造函數A生成的實例對象obj的數據類型爲A,另外,咱們留意到obj有且只有一個__proto__
屬性,這是什麼呢?先別急,下面就說到。編程語言
constructor
從上文可知,要想生成對象實例,必須使用構造函數(constructor
),即使是var arr = [];
或var obj = {}
這樣的形式,javascript也會在內部調用Function()
、Object()
這樣預設的構造函數(因爲是預設的構造函數/數據類型,所以不需顯式指定)。函數
繼續沿用上述例子,此次咱們把構造函數A打印出來看看長什麼樣兒:性能
function A() {} var obj = new A(); console.dir(A);
實際上,構造函數跟普通的函數並沒有二致,只是由於用途(用來生成對象實例),所以才冠以「構造函數」的大名。從上圖看,咱們略過一些與面向對象無關的屬性(arguments/caller/length/name),以及其函數做用域(<function scope>
),剩下的就是與面向對象息息相關的倆屬性了:prototype
和__proto__
。這__proto__
分外眼熟,赫然也出如今對象實例obj裏,不過這裏仍是先跳過,咱們先說這prototype
屬性。this
prototype
屬性特別註明是「構造函數中的prototype
屬性」,是由於,對於通常的函數來講,prototype
屬性沒什麼意義。prototype
屬性指定了使用該構造函數生成的對象實例繼承了哪一個對象實例。
如上述的function A()的prototype
屬性指向了一個默認的Object
類型的對象實例(在javascript中,變量只是對象實例的一個引用,所以此處用「指向」比較準確),那麼,用function A()做爲構造函數實例化的obj實際上就是繼承了那默認的Object類型的對象實例,雖然obj自己並無自定義的屬性/方法,可是能經過obj調用繼承回來的全部屬性/方法。spa
既然構造函數的prototype
屬性能指定繼承的對象實例,那麼只要咱們修改這prototype
屬性,使其指向其它對象實例,那麼就能夠達到實現繼承任意對象的效果了,看下面代碼:prototype
var car = { //一個普通的Object類型實例對象 status: 'stop' } function audi() {} //構造函數audi audi.prototype = car; //修改構造函數audi的prototype屬性,使其指向car console.dir(audi); var audiQ3 = new audi(); //利用構造函數audi生成的數據類型爲audi的實例對象 console.dir(audiQ3);
先來看看構造函數audi打印出來的結果:
從audi.prototype.status能夠看出,此時的audi.prototype的確是指向{status: 'stop'}
這個對象實例了。
那麼接下來看看利用修改後的構造函數audi所生成的對象實例audiQ3:
咱們能夠發現audiQ3這一實例對象的數據類型是audi
(與構造函數同名),另外,audiQ3其下只有__proto__
這惟一一個成員屬性,繼續查看__proto__
,赫然發現,裏面居然有status: "stop"
。沒錯,__proto__
屬性正是指向{status: 'stop'}
這個對象實例的一個引用。
__proto__
經過對象實例中的__proto__
屬性,繼承的對象實例得以與被繼承的對象實例連接起來,因而,一環扣一環,造成了一條由對象實例、指向被繼承對象實例的引用所構成的鏈條:原型鏈。
因爲__proto__
是由構造函數的prototype
屬性決定的(也能夠說是prototype
直接賦值給__proto__
),所以咱們能夠經過修改prototype
屬性來操縱這條原型鏈
。
構造函數,主要用來在建立對象時初始化對象, 即爲對象成員變量賦初始值,那麼,javascript裏的構造函數,是怎麼實現這樣的功能的呢?如下面的DEMO做爲示例說明:
function car() { //定義了一個名爲car的構造函數 this.status = 'stop'; //爲往後使用car這一構造函數來生成的對象實例添加個status成員變量,並賦初始值'stop' this.start = function() { //爲對象實例添加一個名爲start的成員方法 this.status = 'running'; } } var audiQ3 = new car(); //利用car生成一個對象實例,並將其賦給變量audiQ3 console.dir(audiQ3); audiQ3.start(); //調用audiQ3的start方法 console.dir(audiQ3);
首先來看構造函數car,咱們看到this.status = 'stop';
,這this
是指代car這一function嗎?不是的,這個this
其實是指向當前函數執行時的上下文環境,用在構造函數時,指的則是新生成的對象實例(在本DEMO中指的是audiQ3)。所以,只要利用this
,就能在構造函數中,爲將來利用此構造函數生成的對象實例,添加成員屬性和成員方法了。
Object.create
ECMAScript 5
定義了一種原生的原型繼承方式:Object.create
,咱們能夠經過這種方式更簡便地實現原型繼承。
Object.create(proto, [ propertiesObject ])
proto 一個對象,做爲新建立對象的原型。
propertiesObject 可選。該參數對象是一組屬性與值,該對象的屬性名稱將是新建立的對象的屬性名稱,值是屬性描述符(這些屬性描述符的結構與Object.defineProperties()的第二個參數同樣)。注意:該參數對象不能是undefined,另外只有該對象中自身擁有的可枚舉的屬性纔有效,也就是說該對象的原型鏈上屬性是無效的。
var car = { status: 'stop', start: function() { this.status = 'running' } } var audiQ3 = Object.create(car); console.dir(audiQ3);
怎麼樣,利用Object.create
這種方法是否是很簡單就實現了原型繼承呢?實際上,這是ECMAScript 5
給咱們作了一下封裝,至關於:
function (proto) { var constructor = function(){} constructor.prototype = proto; return new constructor(); }
考慮到ECMAScript 5
在IE上到IE10
才徹底支持,所以咱們有必要對低版本的IE瀏覽器進行兼容,實際上也很簡單,對上面的代碼稍做修改便可:
if(typeof Object.create !== 'function') { Object.create = function(proto) { var constructor = function(){} constructor.prototype = proto; return new constructor(); } }