JavaScript入門易,可深究起來,竟搞得我如此混亂,這恐怕就是弱類型語言的特點吧?寫慣了C++,還真是不適應。
近日在google上搜來搜去,學習了半天function、this和prototype,這就總結一下,但願能把它們理清楚。
這是第一篇,關於JavaScript中的function。
參考了一些文章,我認為JavaScript中的function可以有以下兩種用法:
一是做“普通邏輯代碼容器”,也就是我們通常意義上的函數、方法,和我們C/C++里的函數沒什么大分別,只是寫法稍有不同、用法更加靈活;
二是做對象,有的地方叫它函數對象,其用法和作用有點類似C++里的class(類)。
下面來詳細說說這兩種用法。
一、 function用作普通函數
function用作普通函數的定義方法如下:
function functionName([argument1] [, argument2] [..., argumentN]){
[statements]
}
具體寫法有以下兩種:
1. 定義式:
如:
- function multiply(x, y){
- return x*y;
- }
它的使用方法如下:
- var product = multiply(128,128); // product = 16384
2. 聲明式:
如:
- var product = function multiply(x, y){
- return x*y;
- }
需要說明的是:
1. 用作普通函數時,function幾乎可以在腳本的任何地方定義,但推薦在一個HTML文檔的<head></head>區域里定義,這樣可以保證如果另一個腳本需要立即使用這里聲明的函數時,就可以立即使用它。
2. 上述兩種具體寫法在重復定義的時候也有一些差別,如下
若做如下函數定義:
- var example = function(){
- return 1;
- }
- example();
- var example = function(){
- return 2;
- }
- example();
得到結果是
- 1
- 2
若做如下函數定義:
- function example(){
- return 1;
- }
- example();
- function example(){
- return 2;
- }
- example();
那么會得到另一種結果:
- 2
- 2
在采用定義式創建同名函數時,后創建的函數會覆蓋先創建的函數。這種差別是由於JavaScript解釋引擎的工作機制所導致的。由於注冊函數時,后定義的函數重寫了先定義的函數,因此無論調用語句位於何處,執行的都是后定義的函數。相反,對於聲明式創建的函數,JavaScript解釋引擎會像對待任何聲明的變量一樣,等到執行調用該變量的代碼時才會對變量求值。因此當執行第一個example()調用時,example函數的代碼就是首先定義代碼;而當執行第二個example()調用時,example函數的代碼又變成了后來定義的代碼。
當然,好的習慣是不要這樣寫,也不要試圖利用“聲明式”的這種機制來投機取巧。不過,函數重載除外,但是,javascript里好像並沒有函數重載這種寫法吧?
二、函數對象
1.基本概念
在JavaScript中,function還可以被用做對象(或者竊以為該叫做類更合適)。這也許聽起來很怪異也很難理解,但考慮到JavaScript既然是一種面向對象的語言,那么它里面總得可以實現類和對象吧?看看下面的用法就知道了——用function來實現類和對象倒也真無可厚非。
首先要說明一下的是與function有密切關系的this這個東西。“JavaScript在解析代碼時,會為聲明或定義的函數指定調用對象。所謂調用對象,就是函數的執行環境。”也就是說,在函數體中,可以以this關鍵字來使用它的調用對象(關於this的具體用法,另作討論,詳見下篇)。“如果函數體內有以關鍵字this聲明的變量,則this引用的就是調用對象。”
下面就來看看作為函數對象的function通常是怎么寫的:
- function Animal(sort, character){
- this.sort = sort;
- this.character = character;
- }
上面的代碼就定義了一個函數對象,其意義與C++中的class相似,它的構造函數就是這個函數Animal。其實看起來跟上面的普通函數沒什么分別,換句話說,按照上面介紹的普通函數定義方法寫,結果就會得到一個函數對象,竊以為JavaScript中其實只存在函數對象,不存在我們傳統意義上的“函數”,只是它的使用方法靈活多樣,可以按照我們傳統的使用方法functionName(…)直接調用,也可以按下面的方法作為對象使用:
- var dog = new Animal(”mammal”,”four legs”); //創建一個函數對象實例
2.函數對象創建過程
函數怎么又成了對象了呢?它是怎么構造的呢?先來了解一下JavaScript里的函數對象都有什么吧~簡單地說,JavaScript里的函數對象最初包含一個默認的構造函數,函數名是Object,同時,還有個成員(屬性)——__proto__,與大名鼎鼎的prototype屬性相關(用於實現JavaScript里的繼承),關於prototype的用法,另作討論,詳見后文。
了解了這些,再看看上面這個dog對象的構造過程吧~
“創建dog的對象的過程如下:首先,new運算符創建一個空對象({}),然后以這個空對象為調用對象調用函數Animal”(也就是跟傳統意義上的對象構造過程相同,調用它的構造函數進行初始化)“,為這個空對象添加兩個屬性sort和character,接着,再將這個空對象的默認constructor屬性修改為構造函數的名稱(即Animal;空對象創建時默認的constructor屬性值是Object),並且將空對象的__proto__屬性設置為指向Animal.prototype——這就是所謂的對象初始化。最后,返回初始化完畢的對象。這里將返回的新對象賦值給了變量dog。”
3.直接實例化的寫法
函數對象的定義、實例化過程也可以簡化如下:
- var dog = {};
- dog.name = “heibao”;
- dog.age = “3 months”;
- dog.shout = function(){
- return “Hello, My name is “+ this.name + ” and I am ” + this.age + ” old!”;
- }
- dog.shout(); // “Hello, My name is heibao and I am 3 months old!”
上面的代碼中,dog是個對象,它有name、age兩個屬性,還有個成員函數(也是個對象,就是我們的函數對象)shout。這里的shout的定義方法就是做了簡化——直接被function賦值。
對象也可以借用其他對象的方法:
- var cat = {};
- cat.name = “xiaohua”;
- cat.age = “2 years”;
- cat.greet = dog.shout;
- cat.greet(); // “Hello, My name is xiaohua and I am 2 years old!”
這里需要強調的是,每個函數對象都有兩個特殊的方法——call和apply,用它們可以動態指定函數或方法的調用對象:
- dog.shout.call(cat); // “Hello, My name is xiaohua and I am 2 years old!”
- //或者
- dog.shout.apply(cat); // “Hello, My name is xiaohua and I am 2 years old!”
從這里想到,是不是我們可以用call或apple函數來替代上面的方法進行函數對象的實例化呢?答案是否定的。讓我們來從這個角度進一步分析一下函數對象的構造過程,以便加深理解:
如果我們企圖這么寫來達到函數對象實例化的效果:
- var dog = {};
- Animal.call(dog, “mammal”,”four legs”);
那么,“表面上看,這兩行代碼與var dog = new Animal(”mammal”,”four legs”);是等價的,其實卻不是。雖然通過指定函數的執行環境能夠部分達到初始化對象的目的,例如空對象dog確實獲得了sort和character這兩個屬性“:
- dog.sort; // mammal
- dog.character; // four legs
- dog.constructor; // Object —— 注意,沒有修改dog對象默認的constructor屬性
然而,“最關鍵的是新創建的dog對象失去了通過Animal.prototype屬性繼承其他對象的能力。只要與前面采用new運算符調用構造函數創建對象的過程對比一下,就會發現,new運算符在初始化新對象期間,除了為新對象添加顯式聲明的屬性外,還會對新對象進行了一番“暗箱操作”—— 即將新對象的constructor屬性重寫為Animal,將新對象的__proto__屬性設置為指向Animal.prototype。雖然手工“初始化對象”也可以將dog.constructor重寫為Animal,但根據ECMA262規范,對象的__proto__屬性對開發人員是只讀的,對它的設置只能在通過new運算符創建對象時由JavaScript解釋引擎替我們完成。”
看看這樣做的后果:如果不能正確設置對象的__proto__屬性,那么就意味着默認的繼承機制會失效:
- Animal.prototype.greet = “Hi, good lucky!”;
- dog.greet; // undefined
事實上,雖然在Firefox中,__proto__屬性也是可寫的:
- Animal.prototype.greet = “Hi, good lucky!”;
- dog.__proto__ = Animal.prototype;
- dog.greet; // Hi, good lucky!
但這樣做只能在Firefox中行得通。考慮到在兼容多瀏覽器,必須依賴於new運算符,才能實現基於原型的繼承。
參考文獻:
1.http://www.okajax.com/a/200812/1225S312008.html
2.《JavaScript技術大全》,(美)R.Allen Wyke 等著,聞道工作室 譯,機械工業出版社