悟透JavaScript(理解JS面向對象的好文章)(二)
原型擴展
想必君的悟性極高,可能你會這樣想:如果在JavaScript內置的那些如Object和Function等函數的prototype上添加些新的方法和屬性,是不是就能擴展JavaScript的功能呢?
那麼,恭喜你,你得到了!
在AJAX技術迅猛發展的今天,許多成功的AJAX項目的JavaScript運行庫都大量擴展了內置函數的prototype功能。比如微軟的 ASP.NET AJAX,就給這些內置函數及其prototype添加了大量的新特性,從而增強了JavaScript的功能。
我們來看一段摘自MicrosoftAjax.debug.js中的代碼:
String.prototype.trim = function String$trim() {
if (arguments.length !== 0) throw Error.parameterCount();
return this.replace(/^s+|s+$/g, "");
}
這段代碼就是給內置String函數的prototype擴展了一個trim方法,於是所有的String類對象都有了trim方法了。有了這個 擴展,今後要去除字元串兩段的空白,就不用再分別處理了,因為任何字元串都有了這個擴展功能,只要調用即可,真的很方便。
當然,幾乎很少有人去給Object的prototype添加方法,因為那會影響到所有的對象,除非在你的架構中這種方法的確是所有對象都需要的。
前兩年,微軟在設計AJAX類庫的初期,用了一種被稱為「閉包」(closure)的技術來模擬「類」。其大致模型如下:
function Person(firstName, lastName, age)
{
//私有變數:
var _firstName = firstName;
var _lastName = lastName;
//公共變數:
this.age = age;
//方法:
this.getName = function()
{
return(firstName + " " + lastName);
};
this.SayHello = function()
{
alert("Hello, I"m " + firstName + " " + lastName);
};
};
var BillGates = new Person("Bill", "Gates", 53);
var SteveJobs = new Person("Steve", "Jobs", 53);
BillGates.SayHello();
SteveJobs.SayHello();
alert(BillGates.getName() + " " + BillGates.age);
alert(BillGates.firstName); //這裡不能訪問到私有變數
很顯然,這種模型的類描述特別象C#語言的描述形式,在一個構造函數里依次定義了私有成員、公共屬性和可用的方法,顯得非常優雅嘛。特別是「閉包」機制可以模擬對私有成員的保護機制,做得非常漂亮。
所謂的「閉包」,就是在構造函數體內定義另外的函數作為目標對象的方法函數,而這個對象的方法函數反過來引用外層外層函數體中的臨時變數。這使得 只要目標對象在生存期內始終能保持其方法,就能間接保持原構造函數體當時用到的臨時變數值。儘管最開始的構造函數調用已經結束,臨時變數的名稱也都消失 了,但在目標對象的方法內卻始終能引用到該變數的值,而且該值只能通這種方法來訪問。即使再次調用相同的構造函數,但只會生成新對象和方法,新的臨時變數 只是對應新的值,和上次那次調用的是各自獨立的。的確很巧妙!
但是前面我們說過,給每一個對象設置一份方法是一種很大的浪費。還有,「閉包」這種間接保持變數值的機制,往往會給JavaSript的垃圾回收 器製造難題。特別是遇到對象間複雜的循環引用時,垃圾回收的判斷邏輯非常複雜。無獨有偶,IE瀏覽器早期版本確實存在JavaSript垃圾回收方面的內 存泄漏問題。再加上「閉包」模型在性能測試方面的表現不佳,微軟最終放棄了「閉包」模型,而改用「原型」模型。正所謂「有得必有失」嘛。
原型模型需要一個構造函數來定義對象的成員,而方法卻依附在該構造函數的原型上。大致寫法如下:
//定義構造函數
function Person(name)
{
this.name = name; //在構造函數中定義成員
};
//方法定義到構造函數的prototype上
Person.prototype.SayHello = function()
{
alert("Hello, I"m " + this.name);
};
//子類構造函數
function Employee(name, salary)
{
Person.call(this, name); //調用上層構造函數
this.salary = salary; //擴展的成員
};
//子類構造函數首先需要用上層構造函數來建立prototype對象,實現繼承的概念
Employee.prototype = new Person() //只需要其prototype的方法,此對象的成員沒有任何意義!
//子類方法也定義到構造函數之上
Employee.prototype.ShowMeTheMoney = function()
{
alert(this.name + " $" + this.salary);
};
var BillGates = new Person("Bill Gates");
BillGates.SayHello();
var SteveJobs = new Employee("Steve Jobs", 1234);
SteveJobs.SayHello();
SteveJobs.ShowMeTheMoney();
原型類模型雖然不能模擬真正的私有變數,而且也要分兩部分來定義類,顯得不怎麼「優雅」。不過,對象間的方法是共享的,不會遇到垃圾回收問題,而且性能優於「閉包」模型。正所謂「有失必有得」嘛。
在原型模型中,為了實現類繼承,必須首先將子類構造函數的prototype設置為一個父類的對象實例。創建這個父類對象實例的目的就是為 了構成原型鏈,以起到共享上層原型方法作用。但創建這個實例對象時,上層構造函數也會給它設置對象成員,這些對象成員對於繼承來說是沒有意義的。雖然,我 們也沒有給構造函數傳遞參數,但確實創建了若干沒有用的成員,儘管其值是undefined,這也是一種浪費啊。
唉!世界上沒有完美的事情啊!
原型真諦
正當我們感概萬分時,天空中一道紅光閃過,祥雲中出現了觀音菩薩。只見她手持玉凈瓶,輕拂翠柳枝,灑下幾滴甘露,頓時讓JavaScript又添新的靈氣。
觀音灑下的甘露在JavaScript的世界裡凝結成塊,成為了一種稱為「語法甘露」的東西。這種語法甘露可以讓我們編寫的代碼看起來更象對象語言。
要想知道這「語法甘露」為何物,就請君側耳細聽。
在理解這些語法甘露之前,我們需要重新再回顧一下JavaScript構造對象的過程。
我們已經知道,用 var anObject = new aFunction() 形式創建對象的過程實際上可以分為三步:第一步是建立一個新對象;第二步將該對象內置的原型對象設置為構造函數prototype引用的那個原型對象;第 三步就是將該對象作為this參數調用構造函數,完成成員設置等初始化工作。對象建立之後,對象上的任何訪問和操作都只與對象自身及其原型鏈上的那串對象 有關,與構造函數再扯不上關係了。換句話說,構造函數只是在創建對象時起到介紹原型對象和初始化對象兩個作用。
那麼,我們能否自己定義一個對象來當作原型,並在這個原型上描述類,然後將這個原型設置給新創建的對象,將其當作對象的類呢?我們又能否將這個原型中的一個方法當作構造函數,去初始化新建的對象呢?例如,我們定義這樣一個原型對象:
var Person = //定義一個對象來作為原型類
{
Create: function(name, age) //這個當構造函數
{
this.name = name;
this.age = age;
},
SayHello: function() //定義方法
{
alert("Hello, I"m " + this.name);
},
HowOld: function() //定義方法
{
alert(this.name + " is " + this.age + " years old.");
}
};
這個JSON形式的寫法多麼象一個C#的類啊!既有構造函數,又有各種方法。如果可以用某種形式來創建對象,並將對象的內置的原型設置為上面這個「類」對象,不就相當於創建該類的對象了嗎?
但遺憾的是,我們幾乎不能訪問到對象內置的原型屬性!儘管有些瀏覽器可以訪問到對象的內置原型,但這樣做的話就只能限定了用戶必須使用那種瀏覽器。這也幾乎不可行。
那麼,我們可不可以通過一個函數對象來做媒介,利用該函數對象的prototype屬性來中轉這個原型,並用new操作符傳遞給新建的對象呢?
其實,象這樣的代碼就可以實現這一目標:
function anyfunc(){}; //定義一個函數軀殼
anyfunc.prototype = Person; //將原型對象放到中轉站prototype
var BillGates = new anyfunc(); //新建對象的內置原型將是我們期望的原型對象
不過,這個anyfunc函數只是一個軀殼,在使用過這個軀殼之後它就成了多餘的東西了,而且這和直接使用構造函數來創建對象也沒啥不同,有點不爽。
可是,如果我們將這些代碼寫成一個通用函數,而那個函數軀殼也就成了函數內的函數,這個內部函數不就可以在外層函數退出作用域後自動消亡嗎?而且,我們可以將原型對象作為通用函數的參數,讓通用函數返回創建的對象。我們需要的就是下面這個形式:
function New(aClass, aParams) //通用創建函數
{
function new_() //定義臨時的中轉函數殼
{
aClass.Create.apply(this, aParams); //調用原型中定義的的構造函數,中轉構造邏輯及構造參數
};
new_.prototype = aClass; //準備中轉原型對象
return new new_(); //返回建立最終建立的對象
};
var Person = //定義的類
{
Create: function(name, age)
{
this.name = name;
this.age = age;
},
SayHello: function()
{
alert("Hello, I"m " + this.name);
},
HowOld: function()
{
alert(this.name + " is " + this.age + " years old.");
}
};
var BillGates = New(Person, ["Bill Gates", 53]); //調用通用函數創建對象,並以數組形式傳遞構造參數
BillGates.SayHello();
BillGates.HowOld();
alert(BillGates.constructor == Object); //輸出:true
這裡的通用函數New()就是一個「語法甘露」!這個語法甘露不但中轉了原型對象,還中轉了構造函數邏輯及構造參數。
有趣的是,每次創建完對象退出New函數作用域時,臨時的new_函數對象會被自動釋放。由於new_的prototype屬性被設置為新的原型 對象,其原來的原型對象和new_之間就已解開了引用鏈,臨時函數及其原來的原型對象都會被正確回收了。上面代碼的最後一句證明,新創建的對象的 constructor屬性返回的是Object函數。其實新建的對象自己及其原型里沒有constructor屬性,那返回的只是最頂層原型對象的構造 函數,即Object。
有了New這個語法甘露,類的定義就很像C#那些靜態對象語言的形式了,這樣的代碼顯得多麼文靜而優雅啊!
當然,這個代碼僅僅展示了「語法甘露」的概念。我們還需要多一些的語法甘露,才能實現用簡潔而優雅的代碼書寫類層次及其繼承關係。好了,我們再來看一個更豐富的示例吧:
//語法甘露:
var object = //定義小寫的object基本類,用於實現最基礎的方法等
{
isA: function(aType) //一個判斷類與類之間以及對象與類之間關係的基礎方法
{
var self = this;
while(self)
{
if (self == aType)
return true;
self = self.Type;
};
return false;
}
};
function Class(aBaseClass, aClassDefine) //創建類的函數,用於聲明類及繼承關係
{
function class_() //創建類的臨時函數殼
{
this.Type = aBaseClass; //我們給每一個類約定一個Type屬性,引用其繼承的類
for(var member in aClassDefine)
this[member] = aClassDefine[member]; //複製類的全部定義到當前創建的類
};
class_.prototype = aBaseClass;
return new class_();
};
function New(aClass, aParams) //創建對象的函數,用於任意類的對象創建
{
function new_() //創建對象的臨時函數殼
{
this.Type = aClass; //我們也給每一個對象約定一個Type屬性,據此可以訪問到對象所屬的類
if (aClass.Create)
aClass.Create.apply(this, aParams); //我們約定所有類的構造函數都叫Create,這和DELPHI比較相似
};
new_.prototype = aClass;
return new new_();
};
//語法甘露的應用效果:
var Person = Class(object, //派生至object基本類
{
Create: function(name, age)
{
this.name = name;
this.age = age;
},
SayHello: function()
{
alert("Hello, I"m " + this.name + ", " + this.age + " years old.");
}
});
var Employee = Class(Person, //派生至Person類,是不是和一般對象語言很相似?
{
Create: function(name, age, salary)
{
Person.Create.call(this, name, age); //調用基類的構造函數
this.salary = salary;
},
ShowMeTheMoney: function()
{
alert(this.name + " $" + this.salary);
}
});
var BillGates = New(Person, ["Bill Gates", 53]);
var SteveJobs = New(Employee, ["Steve Jobs", 53, 1234]);
BillGates.SayHello();
SteveJobs.SayHello();
SteveJobs.ShowMeTheMoney();
var LittleBill = New(BillGates.Type, ["Little Bill", 6]); //根據BillGate的類型創建LittleBill
LittleBill.SayHello();
alert(BillGates.isA(Person)); //true
alert(BillGates.isA(Employee)); //false
alert(SteveJobs.isA(Person)); //true
alert(Person.isA(Employee)); //false
alert(Employee.isA(Person)); //true
「語法甘露」不用太多,只要那麼一點點,就能改觀整個代碼的易讀性和流暢性,從而讓代碼顯得更優雅。有了這些語法甘露,JavaScript就很像一般對象語言了,寫起代碼了感覺也就爽多了!
令人高興的是,受這些甘露滋養的JavaScript程序效率會更高。因為其原型對象里既沒有了毫無用處的那些對象級的成員,而且還不存在 constructor屬性體,少了與構造函數間的牽連,但依舊保持了方法的共享性。這讓JavaScript在追溯原型鏈和搜索屬性及方法時,少費許多 工夫啊。
我們就把這種形式稱為「甘露模型」吧!其實,這種「甘露模型」的原型用法才是符合prototype概念的本意,才是的JavaScript原型的真諦!
想必微軟那些設計AJAX架構的工程師看到這個甘露模型時,肯定後悔沒有早點把AJAX部門從美國搬到咱中國的觀音廟來,錯過了觀音菩薩的點化。 當然,我們也只能是在代碼的示例中,把Bill Gates當作對象玩玩,真要讓他放棄上帝轉而皈依我佛肯定是不容易的,機緣未到啊!如果哪天你在微軟新出的AJAX類庫中看到這種甘露模型,那才是真正 的緣分!
編程的快樂
在軟體工業迅猛發展的今天,各式各樣的編程語言層出不窮,新語言的誕生,舊語言的演化,似乎已經讓我們眼花繚亂。為了適應面向對象編程的潮 流,JavaScript語言也在向完全面向對象的方向發展,新的JavaScript標準已經從語義上擴展了許多面向對象的新元素。與此相反的是,許多 靜態的對象語言也在向JavaScript的那種簡潔而幽雅的方向發展。例如,新版本的C#語言就吸收了JSON那樣的簡潔表示法,以及一些其他形式的 JavaScript特性。
我們應該看到,隨著RIA(強互聯應用)的發展和普及,AJAX技術也將逐漸淡出江湖,JavaScript也將最終消失或演化成其他形式的語 言。但不管編程語言如何發展和演化,編程世界永遠都會在「數據」與「代碼」這千絲萬縷的糾纏中保持著無限的生機。只要我們能看透這一點,我們就能很容易地 學習和理解軟體世界的各種新事物。不管是已熟悉的過程式編程,還是正在發展的函數式編程,以及未來量子糾纏態的大規模並行式編程,我們都有足夠的法力來化 解一切複雜的難題。
佛最後淡淡地說:只要我們放下那些表面的「類」,放下那些對象的「自我」,就能達到一種「對象本無根,類型亦無形」的境界,從而將自我融入到整個 宇宙的生命輪循環中。我們將沒有自我,也沒有自私的慾望,你就是我,我就是你,你中有我,我中有你。這時,我們再看這生機勃勃的編程世界時,我們的內心將 自然生起無限的慈愛之心,這種慈愛之心不是虛偽而是真誠的。關愛他人就是關愛自己,就是關愛這世界中的一切。那麼,我們的心是永遠快樂的,我們的程序是永 遠快樂的,我們的類是永遠快樂的,我們的對象也是永遠快樂的。這就是編程的極樂!
說到這裡,在座的比丘都猶如醍醐灌頂,心中豁然開朗。看看左邊這位早已喜不自禁,再看看右邊那位也是心花怒放。
驀然回首時,唯見君拈花微笑...
悟透JavaScript(理解JS面向對象的好文章)(一)
原著:李戰(leadzen).深圳
via:http://www.cnblogs.com/zhangshiwen/p/3627085.html
※拿工資不僅僅是讓你寫代碼的
※互聯網協議入門
※TCP 協議簡介
TAG:全棧開發者中心 |
※思科聯合Pure Storage發布面向AI的FlashStack融合系統
※TensorFlow官方最新tf.keras指南:面向對象構建深度網路
※Silicon Labs Giant Gecko系列1 MCU在貿澤開售 面向性能密集型物聯網應用
※微軟Chromium版Edge瀏覽器Dev版面向Windows 7/8.1推出
※Google發布面向Linux和Mac的VR180 Creator
※Facebook Reality Labs正式成立面向VR/AR開發
※Google只發布面向Linux和Mac的VR180 Creator
※[圖]不亞於WannaCry:微軟面向Windows XP發布緊急修復補丁
※華為面向全球發布AI-Native資料庫GaussDB
※Connect() 2018:微軟發布面向Visual Studio開發者的AI生產力工具
※三星或正在研發第二款Bixby智能音箱,對標Google Home Mini面向低端市場
※鍊金術士系列新作「Atelier Raiza」面向PS4公布
※Dell宣布與Google合作:面向商務領域推出Chromebook
※E3:《煮糊了2》面向PS4/Xbox One/Switch/PC公布
※Surface Phone:ARW處理器和觸屏筆,將面向高端市場開放
※AppleCare+ for Mac正式面向中國市場推出
※面向未來的工作?——《Unnatural death》告訴你
※Bantam Tools 桌面 PCB 銑床通過 Digi-Key 面向全球即時供貨
※蘋果AppleCare+for Mac正式面向中國市場推出
※Semtech的LoRa技術被集成到群登科技(AcSiP)面向物聯網應用的模塊之中