js 函數深入解析

作者:F. Permadi 譯者:Sheneyan(子烏) 英文原文: INTRODUCTION TO JavaScript Functions 中文譯文(包括示例):javascript的函數子烏注:一篇相當不錯的function入門文章,個人感覺相當經典。 詞語翻譯列表: function:函數(Function未翻譯) declare:定義 assign:指派,分配 functionbody:函數體(就是函數的內容) object:對象 property:屬性 unnamed:匿名(在這裏沒翻譯成未命名) object oriented programming:面相對相編程 class:類(比如後面的class data type我翻譯成類數據類型) pointer:指針 reassign:重新分配 nest:嵌套 feature:功能,特性 local/global:局部/全局 blueprint:藍圖(?) user defined:用戶自定義 instance:實例 prototype:原型(除了標題都不翻譯) internal:內部 constructor:構造器 duplication: 函數:定義 有以下這些方法可以定義一個函數。所有這些都是有效的,但是它們在後臺如何實現的則有一些差別。 常用的寫法 一般大家都用這個寫法來定義一個函數: functionName([parameters]){functionBody}; Example D1: CODE: function add(a, b) { return a+b; } alert(add(1,2)); // 結果 3 當我們這麼定義函數的時候,函數內容會被編譯(但不會立即執行,除非我們去調用它)。而且,也許你不知道,當這個函數創建的時候有一個同名的對象也被創建。就我們的例子來說,我們現在有一個對象叫做“add”(要更深入瞭解,看底下函數:對象節。) 匿名函數 我們也可以通過指派一個變量名給匿名函數的方式來定義它。 Example D2 var add=function(a, b) { return a+b; } alert(add(1,2)); // 結果 3 這個代碼和前一個例子做了同樣的事情。也許語法看起來比較奇怪,但它應該更能讓你感覺到函數是一個對象,而且我們只是爲這個對指派了一個名稱。可以把它看做和 var myVar=[1,2,3]一樣的語句。以這種方式聲明的函數內容也一樣會被編譯。 當我們指派一個這樣的函數的時候,我們並不一定要求必須是匿名函數。在這裏,我作了和ExampleD2一樣的事情,但我加了函數名“theAdd”,而且我可以通過調用函數名或者是那個變量來引用函數。 Example D2A var add=function theAdd(a, b) { return a+b; } alert(add(1,2)); // 結果 3 alert(theAdd(1,2)); // 結果也是 3 使用這種方式來定義函數在面向對象編程中是很有用的,因爲我們能像底下這樣使一個函數成爲一個對象的屬性。 var myObject=new Object(); myObject.add=function(a,b){return a+b}; // myObject 現在有一個叫做“add”的屬性(或方法) // 而且我能夠象下面這樣使用它 myObject.add(1, 2); 我們也能夠通過使用運算符new來定義一個函數。這是一個最少見的定義函數的方式並且並不推薦使用這種方式除非有特殊的理由(可能的理由見下)。語法如下: varName=new Function([param1Name, param2Name,...paramNName], functionBody); Example D3: var add=new Function("a", "b", "return a+b;"); alert(add(3,4)); // 結果 7 我在這裏有兩個參數叫做a和b,而函數體返回a和b的和。請注意new Function(...)使用了大寫F,而不是小寫f。 這就告訴javascript,我們將要創建一個類型是Function的對象。 還要注意到,參數名和函數體都是作爲字符串而被傳遞。我們可以隨心所欲的增加參數,javascript知道函數體會是右括號前的最後一個字符串(如果沒有參數,你能夠只寫函數體)。你沒必要將所有東西都寫在一行裏(使用/或者使用字符串連接符+來分隔長代碼)。/標記告訴JavaScript在下一行查找字符串的其餘部分。例子如下: Example D4 var add=new Function("a", "b", "alert" + // 注意 "+" "('adding '+a+' and ' +b);/ // 和 "/"的不同用法 return a+b;"); alert(add(3,4)); // 結果 7 採用這種方式定義函數會導致函數並沒被編譯,而且它有可能會比用其它方式定義的函數要慢。至於爲什麼,看一下這個代碼: Example D5 function createMyFunction(myOperator) { return new Function("a", "b", "return a" + myOperator + "b;"); } var add=createMyFunction("+"); // 創建函數 "add" var subtract=createMyFunction("-"); // 創建函數 "subtract" var multiply=createMyFunction("*"); // 創建函數 "multiply" // test the functions alert("加的結果="+add(10,2)); // 結果是 12 alert("減的結果="+subtract(10,2)); // 結果是 8 alert("乘的結果="+multiply(10,2)); // 結果是 20 alert(add); 這個有趣的例子創建了三個不同的function,通過實時傳遞不同的參數來創建一個新Function。因爲編譯器沒法知道最終代碼會是什麼樣子的,所以new Function(...)的內容不會被編譯。那這有什麼好處呢?嗯,舉個例子,如果你需要用戶能夠創建他們自己的函數的時候這個功能也許很有用,比如在遊戲裏。我們也許需要允許用戶添加“行爲”給一個“player”。但是,再說一次,一般情況下,我們應該避免使用這種形式,除非有一個特殊的目的。 函數:對象 函數是javascript中的一種特殊形式的對象。它是第一個[b〕類數據類型(class data type)。這意味着我們能夠給它增加屬性。這裏有一些需要注意的有趣觀點: 對象的創建 就像剛纔提及的,當我們定義一個函數時,javascript實際上在後臺爲你創建了一個對象。這個對象的名稱就是函數名本身。這個對象的類型是function。在下面的例子,我們也許不會意識到這一點,但我們實際上已經創建了一個對象:它叫做Ball。 Example 1 function Ball() // 也許看起來有點奇怪,但是這個聲明 { // 創建了一個叫做Ball的對象 i=1; } alert(typeof Ball); // 結果 "function" 我們甚至能將這個對象的內容打印出來而且它會輸出這個函數的實際代碼,Example2: 點擊 alert(Ball);來看看Ball的內容。 屬性的添加 我們能夠添加給Object添加屬性,包括對象function。因爲定義一個函數的實質是創建一個對象。我們能夠“暗地裏”給函數添加屬性。比如,我們這裏定義了函數Ball,並添加屬性callsign。 function Ball() // 也許看起來有點奇怪,但是這個聲明 { // 創建了一個叫做Ball的對象,而且你能夠 } // 引用它或者象下面那樣給它增加屬性 Ball.callsign="The Ball"; // 給Ball增加屬性 alert(Ball.callsign); // 輸出 "The Ball" 指針 因爲function是一個對象,我們能夠爲一個function分配一個指針。如下例,變量ptr指向了對象myFunction。 function myFunction(message) { alert(message); } var ptr=myFunction; // ptr指向了myFunction ptr("hello"); // 這句會執行myFunction:輸出"hello" 我們能夠運行這個函數,就好像這個函數名已經被指針名代替了一樣。所以在上面,這行ptr("hello"); 和myFunction("hello");的意義是一樣的。 指向函數的指針在面向對象編程中相當有用。例如:當我們有多個對象指向同一個函數的時候(如下): Example 4A function sayName(name) { alert(name); } var object1=new Object(); // 創建三個對象 var object2=new Object(); var object3=new Object(); object1.sayMyName=sayName; // 將這個函數指派給所有對象 object2.sayMyName=sayName; object3.sayMyName=sayName; object1.sayMyName("object1"); // 輸出 "object1" object2.sayMyName("object2"); // 輸出 "object2" object3.sayMyName("object3"); // 輸出 "object3" 因爲只有指針被保存(而不是函數本身),當我們改變函數對象自身的時候,所有指向那個函數的指針都會發生變化。我們能夠在底下看到: Example 5: function myFunction() { alert(myFunction.message); } myFunction.message="old"; var ptr1=myFunction; // ptr1 指向 myFunction var ptr2=myFunction; // ptr2 也指向 myFunction ptr1(); // 輸出 "old" ptr2(); // 輸出 "old" myFunction.message="new"; ptr1(); // 輸出 "new" ptr2(); // 輸出 "new" 指針的指向 我們能夠在一個函數創建之後重新分配它,但是我們需要指向函數對象本身,而不是指向它的指針。在下例中,我將改變myfunction()的內容。 Example 6: function myFunction() { alert("Old"); } myFunction(); // 輸出 "Old" myFunction=function() { alert("New"); }; myFunction(); // 輸出 "New" 舊函數哪裏去了??被拋棄了。 如果我們需要保留它,我們可以在改變它之前給它分配一個指針。 Example 6A: function myFunction() { alert("Old"); } var savedFuncion=myFunction; myFunction=function() { alert("New"); }; myFunction(); // 輸出 "New" savedFuncion(); // 輸出 "Old" 不過要小心,象下面這樣的例子並不會有作用,因爲是創建了另一個叫做myFunctionPtr的函數而不是修改它。 Example 6B: function myFunction() { alert("Old"); } var savedFunc=myFunction; savedFunc=function() { alert("New"); }; myFunction(); // 輸出 "Old" savedFunc(); // 輸出 "New" 內嵌函數 我們還能夠在一個函數中嵌套一個函數。下例,我有一個叫做getHalfOf的函數,而在它裏面,我有另一個叫做calculate的函數。 Example 7 function getHalfOf(num1, num2, num3) { function calculate(number) { return number/2; } var result=""; result+=calculate(num1)+" "; result+=calculate(num2)+" "; result+=calculate(num3); } var resultString=getHalfOf(10,20,30); alert(resultString); // 輸出 "5 10 15" 你只能在內部調用嵌套的函數。就是說,你不能這麼調用:getHalfOf.calculate(10),因爲calculate只有當外部函數(getHalfOf())在運行的時候纔會存在。這和我們前面的討論一致(函數會被編譯,但只有當你去調用它的時候纔會執行)。 調用哪個函數? 你也許正在想命名衝突的問題。比如,下面哪一個叫做calculate的函數會被調用? Example 8 function calculate(number) { return number/3; } function getHalfOf(num1, num2, num3) { function calculate(number) { return number/2; } var result=""; result+=calculate(num1)+" "; result+=calculate(num2)+" "; result+=calculate(num3); } var resultString=getHalfOf(10,20,30); alert(resultString); // 輸出 "5 10 15" 在這個例子中,編譯器會首先搜索局部內存地址,所以它會使用內嵌的calculate函數。如果我們刪除了這個內嵌(局部)的calculate函數,這個代碼會使用全局的calculate函數。 函數:數據類型及構造函數 讓我們來看看函數的另一個特殊功能--這讓它和其它對象類型截然不同。一個函數能夠用來作爲一個數據類型的藍圖。這個特性通常被用在面向對象編程中來模擬用戶自定義數據類型(user defined data type)。使用用戶自定義數據類型創建的對象通常被成爲用戶自定義對象(user defined object)。 數據類型 在定義了一個函數之後,我們也同時創建了一個新的數據類型。這個數據類型能夠用來創建一個新對象。下例,我創建了一個叫做Ball的新數據類型。 Example DT1 function Ball() { } var ball0=new Ball(); // ball0 現在指向一個新對象 alert(ball0); // 輸出 "Object",因爲 ball0 現在是一個對象 這樣看來,ball0=new Ball()作了什麼?new關鍵字創建了一個類型是Object的新對象(叫做ball0)。然後它會執行Ball(),並將這個引用傳給ball0(用於調用對象)。下面,你會看到這條消息:“creating new Ball”,如果Ball()實際上被運行的話。 Example DT2 function Ball(message) { alert(message); } var ball0=new Ball("creating new Ball"); // 創建對象並輸出消息 ball0.name="ball-0"; // ball0現在有一個屬性:name alert(ball0.name); // 輸出 "ball-0" 我們可以把上面這段代碼的第6行看做是底下的代碼6-8行的一個簡寫: function Ball(message) { alert(message); } var ball0=new Object(); ball0.construct=Ball; ball0.construct("creating new ball"); // 執行 ball0.Ball("creating.."); ball0.name="ball-0"; alert(ball0.name); 這行代碼ball0.construct=Ball和Example 4中的ptr=myFunction語法一致。 如果你還是不明白這行的含義那就回過頭再複習一下Example 4。注意:你也許考慮直接運行ball0.Ball("..."),但是它不會起作用的,因爲ball0並沒有一個叫做Ball("...")的屬性,並且它也不知道你究竟想作些什麼。 添加屬性 當我們象上面那樣使用關鍵字new創建一個對象的時候,一個新的Object被創建了。我們可以在創建之後給這個對象添加屬性(就好像我在上面那樣添加屬性name。而接下來的問題就是如果我們創建了這個對象的另外一個實例,我們得象下面那樣再次給這個新對象添加這個屬性。) Example DT3 (creates 3 ball objects) function Ball() { } var ball0=new Ball(); // ball0 現在指向了類型Ball的一個新實例 ball0.name="ball-0"; // ball0 現在有一個屬性"name" var ball1=new Ball(); ball1.name="ball-1"; var ball2=new Ball(); alert(ball0.name); // 輸出 "ball-0" alert(ball1.name); // 輸出 "ball-1" alert(ball2.name); // 哦,我忘記給ball2添加“name”了! 我忘記給ball2添加屬性name了,如果在正式的程序中這也許會引發問題。有什麼好辦法可以自動增加屬性呢?嗯,有一個:使用this關鍵字。this這個詞在function中有特別的意義。它指向了調用函數的那個對象。讓我們看看下面的另一個示例,這時候我們在構造函數中添加上這些屬性: Example DT4 function Ball(message, specifiedName) { alert(message); this.name=specifiedName; } var ball0=new Ball("creating new Ball", "Soccer Ball"); alert(ball0.name); // prints "Soccer Ball" 請記住:是new關鍵字最終使得構造函數被執行。在這個例子中,它將會運行Ball("creating new Ball", "Soccer Ball");而關鍵字this將指向ball0。因此,這行:this.name=specifiedName變成了ball0.name="Soccer Ball"。它主要是說:給ball0添加屬性name,屬性值是Soccer Ball。 我們現在只是添加了一個name屬性給ball0,看起來和上一個例子中所做的很象,但卻是一個更好更具擴展性的方法。現在,我們可以隨心所欲的創建許多帶有屬性的ball而無需我們手動添加它們。而且,人們也希望創建的Ball對象能夠清晰的看懂它的構造函數並且能夠輕鬆找出Ball的所有屬性。讓我們添加更多屬性到Ball裏。 Example DT5 function Ball(color, specifiedName, owner, weight) { this.name=specifiedName; this.color=color; this.owner=owner; this.weight=weigth; } var ball0=new Ball("black/white", "Soccer Ball", "John", 20); var ball1=new Ball("gray", "Bowling Ball", "John", 30); var ball2=new Ball("yellow", "Golf Ball", "John", 55); var balloon=new Ball("red", "Balloon", "Pete", 10); alert(ball0.name); // 輸出 "Soccer Ball" alert(balloon.name); // 輸出 "Balloon" alert(ball2.weight); // 輸出 "55" 嘿!使用面向對象術語,你能夠說Ball是一個擁有如下屬性的對象類型:name, color, owner, weight。 將對象賦給屬性 我們並沒被限制只能添加形如字符串或者數字之類的簡單數據類型作爲屬性。我們也能夠將對象賦給屬性。下面,supervisor是Employee的一個屬性. Example DT6 function Employee(name, salary, mySupervisor) { this.name=name; this.salary=salary; this.supervisor=mySupervisor; } var boss=new Employee("John", 200); var manager=new Employee("Joan", 50, boss); var teamLeader=new Employee("Rose", 50, boss); alert(manager.supervisor.name+" is the supervisor of "+manager.name); alert(manager.name+"/'s supervisor is "+manager.supervisor.name); 會輸出什麼呢? 就像你在上面這個例子中看到的那樣,manager和teamLeader都有一個supervisor屬性,而這個屬性是類型Employee的一個對象。 將函數作爲屬性 任何類型的對象都可以作爲一個屬性,回憶一下前面的Example 4(不是Example DT4),函數也是一個對象。所以你可以讓一個函數作爲一個對象的一個屬性。下面,我將添加兩個函數getSalary和addSalary。 Example DT7 function Employee(name, salary) { this.name=name; this.salary=salary; this.addSalary=addSalaryFunction; this.getSalary=function() { return this.salary; }; } function addSalaryFunction(addition) { this.salary=this.salary+addition; } var boss=new Employee("John", 200000); boss.addSalary(10000); // boss 長了 10K 工資……爲什麼老闆工資可以長這麼多:'( alert(boss.getSalary()); // 輸出 210K……爲什麼默認工資也那麼高……:'( addSalary和getSalary演示了幾種將函數賦給屬性的不同方法。如果你記得我們最開始的討論;我討論了三種聲明函數的不同方式。所有那些在這裏都是適用的,但是上面展示的兩個最常用。 讓我們看看有什麼不同。下面,注意一下9-12行的代碼。當這部分代碼執行的時候,函數getSalary被聲明。如前面數次提到的,一個函數聲明的結果是一個對象被創建。所以這時候boss被創建(接下來的第19行),而boss裏有一個getSalary屬性。 function Employee(name, salary) { this.name=name; this.salary=salary; this.addSalary=addSalaryFunction; this.getSalary=function() { return this.salary; }; } function addSalaryFunction(addition) { this.salary=this.salary+addition; } var boss=new Employee("John", 200000); var boss2=new Employee("Joan", 200000); var boss3=new Employee("Kim", 200000); 當你創建這個對象的更多實例時(boss2和boss3),每一個實例都有一份getSalary代碼的單獨拷貝;而與此相反,addSalary則指向了同一個地方(即addSalaryFunction)。 看看下面的代碼來理解一下上面所描述的內容。 Example DT8 function Employee(name, salary) { this.name=name; this.salary=salary; this.addSalary=addSalaryFunction; this.getSalary=function() { return this.salary; }; } function addSalaryFunction(addition) { this.salary=this.salary+addition; } var boss1=new Employee("John", 200000); var boss2=new Employee("Joan", 200000); // 給getSalary函數對象添加屬性 boss1.getSalary.owner="boss1"; boss2.getSalary.owner="boss2"; alert(boss1.getSalary.owner); // 輸出 "boss1" alert(boss2.getSalary.owner); // 輸出 "boss2" // 如果兩個對象指向同一個函數對象,那麼 // 上面兩個輸出都應該是“boss2”。 // 給addSalary函數對象添加屬性 boss1.addSalary.owner="boss1"; boss1.addSalary.owner="boss2"; alert(boss1.addSalary.owner); // 輸出 "boss2" alert(boss2.addSalary.owner); // 輸出 "boss2" // 因爲兩個對象都指向同一個函數,(子烏注:原文寫are not pointing to the same function,疑爲筆誤) // 當修改其中一個的時候,會影響所有的實例(所以兩個都輸出“boss2”). 也許不是重要的事情,但這裏有一些關於運行類似上面的getSalary的內嵌函數的結論: 1) 需要更多的存儲空間來存儲對象(因爲每一個對象實例都會有它自己的getSalary代碼拷貝);2) javascript需要更多時間來構造這個對象。 讓我們重新寫這個示例來讓它更有效率些。 Example DT9 function Employee(name, salary) { this.name=name; this.salary=salary; this.addSalary=addSalaryFunction; this.getSalary=getSalaryFunction; } function getSalaryFunction() { return this.salary; } function addSalaryFunction(addition) { this.salary=this.salary+addition; } 看這兒,兩個函數都指向同一個地方,這將會節約空間和縮短構造時間(特別是當你有一大堆內嵌函數在一個構造函數的時候)。這裏有另外一個函數的功能能夠來提升這個設計,它叫做prototype,而我們將在下一節討論它。 函數:原型 每一個構造函數都有一個屬性叫做原型(prototype,下面都不再翻譯,使用其原文)。這個屬性非常有用:爲一個特定類聲明通用的變量或者函數。 prototype的定義 你不需要顯式地聲明一個prototype屬性,因爲在每一個構造函數中都有它的存在。你可以看看下面的例子: Example PT1 function Test() { } alert(Test.prototype); // 輸出 "Object" 給prototype添加屬性 就如你在上面所看到的,prototype是一個對象,因此,你能夠給它添加屬性。你添加給prototype的屬性將會成爲使用這個構造函數創建的對象的通用屬性。 例如,我下面有一個數據類型Fish,我想讓所有的魚都有這些屬性:livesIn="water"和price=20;爲了實現這個,我可以給構造函數Fish的prototype添加那些屬性。 Example PT2 function Fish(name, color) { this.name=name; this.color=color; } Fish.prototype.livesIn="water"; Fish.prototype.price=20; 接下來讓我們作幾條魚: var fish1=new Fish("mackarel", "gray"); var fish2=new Fish("goldfish", "orange"); var fish3=new Fish("salmon", "white"); 再來看看魚都有哪些屬性: for (int i=1; i<=3; i++) { var fish=eval("fish"+i); // 我只是取得指向這條魚的指針 alert(fish.name+","+fish.color+","+fish.livesIn+","+fish.price); } 輸出應該是: "mackarel, gray, water, 20" "goldfish, orange, water, 20" "salmon, white water, 20" 你看到所有的魚都有屬性livesIn和price,我們甚至都沒有爲每一條不同的魚特別聲明這些屬性。這時因爲當一個對象被創建時,這個構造函數將會把它的屬性prototype賦給新對象的內部屬性__proto__。這個__proto__被這個對象用來查找它的屬性。 你也可以通過prototype來給所有對象添加共用的函數。這有一個好處:你不需要每次在構造一個對象的時候創建並初始化這個函數。爲了解釋這一點,讓我們重新來看Example DT9並使用prototype來重寫它: 用prototype給對象添加函數 Example PT3 function Employee(name, salary) { this.name=name; this.salary=salary; } Employee.prototype.getSalary=function getSalaryFunction() { return this.salary; } Employee.prototype.addSalary=function addSalaryFunction(addition) { this.salary=this.salary+addition; } 我們可以象通常那樣創建對象: var boss1=new Employee("Joan", 200000); var boss2=new Employee("Kim", 100000); var boss3=new Employee("Sam", 150000); 並驗證它: alert(boss1.getSalary()); // 輸出 200000 alert(boss2.getSalary()); // 輸出 100000 alert(boss3.getSalary()); // 輸出 150000 這裏有一個圖示來說明prototype是如何工作的。這個對象的每一個實例(boss1, boss2, boss3)都有一個內部屬性叫做__proto__,這個屬性指向了它的構造器(Employee)的屬性prototype。當你執行getSalary或者addSalary的時候,這個對象會在它的__proto__找到並執行這個代碼。注意這點:這裏並沒有代碼的複製(和Example DT8的圖表作一下對比)。
發佈了31 篇原創文章 · 獲贊 1 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章