在JS中,Function(函數)類型實際上是對象;每個函數都是Function類型的實例,而且都與其他引用類型一樣具有屬性和方法。由於函數是對象,因此函數名實際上也是一個指向函數對象的指針。
一 函數的聲明方式
//1.函數聲明方式 function add(num1,num2){ return num1+num2; } //2.函數表達式定義函數 var add= function(num1,num2){ // 通過變量box即可引用函數; return num1+num2; }; // 注意函數末尾有一個分號,就像聲明其他變量時一樣; var another = add; // 使用不帶圓括號的函數名是訪問函數指針;而非調用函數; console.log(another(10,10)); //3.使用Function構造函數 var add = new Function('num1','num2','return num1+num2'); // 第三種方式不推薦,這種語法會導致解析兩次代碼(第一次解析常規JS代碼,第二次解析傳入構造函數中的字符串),從而影響性能; // 可以通過這種語法來理解"函數是對象,函數名是指針"的概念;
通常來說,在全局作用域內聲明一個對象,只不過是對一個屬性賦值而已,比如上例中的add函數,事實上只是為全局對象添加了一個屬性,屬性名為add,而屬性的值是一個對象,即function(x, y){return x+y;},理解這一點很重要,這條語句在語法上跟:
var str = "This is a string";
並沒有什么區別。都是給全局對象動態的增加一個新的屬性,如此而已。
二 作為值的函數
// JS中的函數名本身就是變量,所以函數也可以作為值來使用; // 也就是說,不僅可以像傳參數一樣把一個函數傳遞給另一個函數,而且可以將一個函數作為另一個函數的結果返回; function box(sumFunction,num){ // 無論第一個參數傳遞進來的是什么函數, return sumFunction(num); // 它都會返回執行第一參數后的結果; } function sum(num){ return num+10; } // 傳遞函數到另一個函數里; // 要訪問函數的指針不執行函數的話,須去掉函數名后的圓括號; var result = box(sum,10); // =>20;
三、函數的內部屬性
// 函數內部有兩個特殊的對象:arguments和this; // 1.arguments:包含着傳入函數中的所有參數,arguments並不是一個數組,只是與數組相似。除了擁有length屬性,數組的所有屬性和方法都不具備。
// arguments這個對象還有一個名叫callee的屬性,該屬性是一個指針,指向擁有這個arguments對象的函數; function box(num){ if(num<=1){ return 1; }else{ return num*arguments.callee(num-1); // 使用arguments.callee來執行box本身; } } // 2.this:引用的是函數據以操作的對象,或者說函數調用語句所處的作用域; // 當在全局作用域調用函數時,this對象引用的就是window; window.color = "red"; alert(this.color); // 打印全局的color;=>red; var box = { color:'blue', sayColor:function(){ alert(this.color); // 打印局部的color;=>blue; } };
四 函數屬性和方法
// JS中的函數是對象,因此函數也有屬性和方法;包含length和prototype; // length屬性:表示函數希望接收到命名參數的個數; function box(name,age){ alert(name+age); } alert(box.length); // 2s // prototype屬性:它是保存所有實例方法的真正所在,也就是原型; // prototype包含兩個方法:apply()和call(),每個函數都包含這兩個非繼承而來的方法; // 這兩個方法的用途都在特定的作用域中調用函數,實際上等於設置函數體內this對象的值; var color = 'red'; var box = { color = 'blue'; } function sayColor({ alert(this.color); }); sayColor(); // 作用域在window; sayColor.call(this); // 作用域在window; sayColor.call(window); // 作用域在window; sayColor.call(box); // 作用域在box,對象冒充;=>red; // 使用call(box)方法的時候,sayColor()方法的運行環境已經變成了box對象里了; // 使用call()或apply()來擴充作用域的最大好處,就是對象不需要與方法發生任何耦合關系; // 耦合:相互關聯的意思,擴展和維護會發生連鎖反應; // 也就是說,box對象和sayColor()方法之間不會有多余的關聯操作,比如:box.sayColor = sayColor; function Animal(){ this.name = "Animal"; this.showName = function(){ alert(this.name); } } function Cat(){ this.name = "Cat"; } var animal = new Animal(); var cat = new Cat(); //通過call或apply方法,將原本屬於Animal對象的showName()方法交給對象cat來使用。 //輸入結果為"Cat" animal.showName.call(cat,","); //animal.showName.apply(cat,[]);
函數返回值(return)
當一個函數被調用,通常會從函數的{開始執行到}結束。如果想提前結束該函數的執行可以使用return語句,此時,return語句后面的所有語句將永遠不會執行。如:
function test(){ alert("first"); return; alert("second"); // 該語句永遠被不會執行 } test(); // 一個函數總是會返回值,如果沒有使用return返回值,默認返回undefined。如: function test(){ alert("first"); } alert(test()); // 輸出:undefined // 如果函數前使用new方式調用,且返回值不是一個對象,則返回this(新對象)。如: function test(){ alert("first"); } var t = new test(); alert(typeof t); // 輸出:‘object' alert(t instanceof test); // 輸出:true
異常(exception)
異常是干擾程序正常流程的非正常事故(可能人為有意的)。當檢查出這樣的事故,應當拋出異常。如:
function add(a, b){ // 定義一個加法函數 // 如果傳遞的參數不是數字類型,則拋出一個異常信息 if(typeof a != 'number' || typeof b != 'number'){ throw { 'name' : "typeError", // 屬性是自定義的,名字可以任意取 'message': "add方法必須使用數字作為參數" }; } return a + b; } (function(){ // 捕獲add方法可能產生的異常 try{ add(10, ""); } catch(e){ // 一個try語句只有一個catch語句,如果要處理多個異常,則通過異常的name屬性來區別 // 判斷異常的類型 if(e.name === "typeError"){ alert(e.message); } } })();
給類型添加方法
javascript中允許給基本類型添加方法。如:boolean、string、Number
實例:在Function中添加一個method函數,該函數為Function添加其他自定義的函數(避免使用prototype),然后利用method函數想Function中添加一個add函數,最后測試add函數在Function中確實存在。該方法將func函數添加到Function中,以name命名。然后,返回Function的對象
Function.prototype.method = function(name, func){ // 避免覆蓋已有的方法 if(!this.prototype[name]){ this.prototype[name] = func; } return this; }; // 通過Function.method方法添加一個加法函數到Function,該函數的名稱為“add” Function.method("add", function(a, b){ if(typeof a != 'number' || typeof b != 'number'){ throw { 'name' : "typeError", 'message' : "add方法必須傳入數字" }; } return a + b; }); // 調用Function的add方法是否存在 (function(){ try{ alert(Function.add(1, 3)); // 輸出:4 } catch(e){ if(e.name === 'typeError'){ alert(e.message); } } })(); // 去除字符串兩端的空白 String.method("trim", function(){ return this.replace(/^\s+|\s+$/g, ''); }); alert('|' + " hello world ".trim() + '|'); // 輸出: '|hello world|' // 添加數字的取整函數 Number.method("integer", function(){ // 可以通過此種方式調用函數,如:Math.random() == Math['random']() == Math["random"]() return Math[this < 0 ? 'ceil' : 'floor'](this); }); alert((-10 / 3).integer()); // 輸出:-3
遞歸調用(arguments.callee)
遞歸調用就是自己調用自己。調用分為:直接調用和間接調用下面展示使用遞歸調用來計算指定值的斐波那契數列。
// 求i的階乘 function factorial(i){ if(i < 2){ return 1; } return i*factorial(i-1); // 遞歸調用 } alert(factorial(5)); // 求5的階乘 // 以上方式存在一個問題?如下: var factorial = function(i){ if(i < 2){ return 1; } return i*factorial(i-1); // factorial還能被調用嗎?不能 } var test = factorial; factorial = null; alert(test(2)); // 解決方案: var factorial = function(i){ if(i < 2){ return 1; } return i*arguments.callee(i-1); // arguments.callee返回正被執行的 Function 對象,也就是所指定的 Function 對象的正文 } var test = factorial; factorial = null; alert(test(5));
函數的作用域
作用域的概念在幾乎所有的主流語言中都有體現,在JavaScript中,則有其特殊性:JavaScript中的變量作用域為函數體內有效,而無塊作用域,我們在Java語言中,可以這樣定義for循環塊中的下標變量:
public void method(){ for(int i = 0; i < obj1.length; i++){ //do something here; } //此時的i為未定義 for(int i = 0; i < obj2.length; i++){ //do something else; } }
而在JavaScript中:
function func(){ for(var i = 0; i < array.length; i++){ //do something here. } //此時i仍然有值,及I == array.length print(i);//i == array.length; }
再看一個例子
// 在程序中,作用域控制着變量的可見性和生命周期。 var name = "default"; // 全局作用域 function getName(){ var name = "getName"; // getName作用域下 for(var i=0; i<2; i++){ var inName = "inName"; } alert(i + "," + inName); // 2,inName 注意:在js中沒有塊級作用域,及if、for、while中聲明的變量是放在塊所在的作用域下 return name; } alert(getName()); // getName 注意:js存在函數作用域,所以在函數內部定義的變量在外部是不可見的 alert(name); // default
注意:在現代的很多語言中,推薦將變量盡可能的延遲聲明。如:java而在js中,卻不推薦這樣做,因為js不支持塊級作用域。推薦在函數的開始就將所有用到的變量進行聲明。
JavaScript的函數是在局部作用域內運行的,在局部作用域內運行的函數體可以訪問其外層的(可能是全局作用域)的變量和函數。JavaScript的作用域為詞法作用域,所謂詞法作用域是說,其作用域為在定義時(詞法分析時)就確定下來的,而並非在執行時確定,如下例:
var str = "global"; function scopeTest(){ print(str); var str = "local"; print(str); } scopeTest();
運行結果是什么呢?初學者很可能得出這樣的答案:
global
local
而正確的結果應該是:
undefined
local
因為在函數scopeTest的定義中,預先訪問了未聲明的變量str,然后才對str變量進行初始化,所以第一個print(str)會返回undifined錯誤。那為什么函數這個時候不去訪問外部的str變量呢?這是因為,在詞法分析結束后,構造作用域鏈的時候,會將函數內定義的var變量放入該鏈,因此str在整個函數scopeTest內都是可見的(從函數體的第一行到最后一行),由於str變量本身是未定義的,程序順序執行,到第一行就會返回未定義,第二行為str賦值,所以第三行的print(str)將返回”local”。
函數上下文
在Java或者C/C++等語言中,方法(函數)只能依附於對象而存在,不是獨立的。而在JavaScript中,函數也是一種對象,並非其他任何對象的一部分,理解這一點尤為重要,特別是對理解函數式的JavaScript非常有用,在函數式編程語言中,函數被認為是一等的。
函數的上下文是可以變化的,因此,函數內的this也是可以變化的,函數可以作為一個對象的方法,也可以同時作為另一個對象的方法,總之,函數本身是獨立的。可以通過Function對象上的call或者apply函數來修改函數的上下文:
call和apply
call和apply通常用來修改函數的上下文,函數中的this指針將被替換為call或者apply的第一個參數,我們不妨來看看JavaScript入門之對象與JSON中的例子:
//定義一個人,名字為jack var jack = { name : "jack", age : 26 } //定義另一個人,名字為abruzzi var abruzzi = { name : "abruzzi", age : 26 } //定義一個全局的函數對象 function printName(){ return this.name; } //設置printName的上下文為jack, 此時的this為jack print(printName.call(jack)); //設置printName的上下文為abruzzi,此時的this為abruzzi print(printName.call(abruzzi)); print(printName.apply(jack)); print(printName.apply(abruzzi));
apply和call的區別
// 定一個對象,包含一個add方法,返回a、b的和 var Person = { 'add' : function(a, b){ return a + b; } }; // 顯示a、b的和 function showInfo(a, b){ alert(this.add(a, b)); } // 通過apply方法改變showInfo方法的this指向 //showInfo(1, 3); // 對象不支持次對象 showInfo.apply(Person, [1, 3]); showInfo.call(Person, 1, 3); // 從上面可以看出,apply和call的區別是apply接受一個數組作為被調函數的參數, // 而call是通過將被調函數的所有參數以逗號分隔的形式展開
匿名函數和嵌套函數
在JavaScript可以聲明一個沒有名稱的函數,稱為匿名函數(Anonymouse Function)。同時JavaScript還允許在函數內部聲明函數,稱為嵌套函數(Nested Function),嵌套函數的作用域為整個父函數。
在前面函數聲明的部分就看到了匿名函數和嵌套函數的一種用法,由於匿名函數沒有名稱,不會引入新的變量污染上下文環境,而且會帶來新的變量作用域,因此匿名函數常被用來防止全局環境污染。
JavaScript運行時中有一個特殊的全局環境(global object),這個對象上面存放全局的函數和變量,實際開發中經常會使用若干第三方的庫或多個js文件,若不小心在全局對象引入重復的變量或函數聲明,則會造成代碼執行混亂。例如先后引入兩個js文件,分別定義了自己的函數log作為內部使用,則第二引入的函數會覆蓋第一個的定義且不會拋出任何錯誤,在后續的執行中調用log函數可能會造成錯誤。這時候使用一個匿名函數將整個js內的邏輯包裝起來,就可以避免這種錯誤,這種方法已經被絕大多數開源js庫使用。
(function() { // 匿名函數 function log(msg) { console.log(msg); } // 其他代碼 }()); // 立即執行
以上代碼就是一個簡單的示例,log函數的作用域被限制在這個匿名函數之內,而匿名函數則因為被外面一對小括號()包括起來,形成一個函數表達式,表達式的值是一個函數,緊接着一對小括號表示立即執行這個函數,讓原有的代碼正常執行一次。不過,這種方式聲明的函數、通過var聲明的變量等等都是內部的,不能被任何匿名函數以外的代碼訪問到。如果你需要對外暴露一些函數作為接口的話有如下幾種方法:
var mylib = (function(global) { function log(msg) { console.log(msg); } log1 = log; // 法一:利用沒有var的變量聲明的默認行為,在log1成為全局變量(不推薦) global.log2 = log; // 法二:直接在全局對象上添加log2屬性,賦值為log函數(推薦) return { // 法三:通過匿名函數返回值得到一系列接口函數集合對象,賦值給全局變量mylib(推薦) log: log }; }(window));
高階函數(High-order Function)
如果函數作為參數或返回值使用時,就稱為高階函數,JavaScript中的函數都可以作為高階函數來使用,這也是第一類函數的特征。下面我們就分別分析一下這兩種使用方法。
function negative(n) { return -n; // 取n的相反值 } function square(n) { return n*n; // n的平方 } function process(nums, callback) { var result = []; for(var i = 0, length = nums.length; i < length; i++) { result[i] = callback(nums[i]); // 對數組nums中的所有元素傳遞給callback進行處理,將返回值作為結果保存 } return result; } var nums = [-3, -2, -1, 0, 1, 2, 3, 4]; var n_neg = process(nums, negative); // n_neg = [3, 2, 1, 0, -1, -2, -3, -4]; var n_square = process(nums, square); // n_square = [9, 4, 1, 0, 1, 4, 9, 16];
以上代碼展示了把函數作為參數傳入另一個函數process調用的示例,在process函數的實現中,把callback作為一個黑盒子看待,負責把參數傳給它,然后獲取返回值,在調用之前並不清楚callback的具體實現。只有當執行到20行和22行時,callback才被分別代表negative或square,分別對每個元素進行取相反值或平方值的操作。
function generator() { var i = 0; return function() { return i++; }; } var gen1 = generator(); // 得到一個自然數生成器 var gen2 = generator(); // 得到另一個自然數生成器 var r1 = gen1(); // r1 = 0 var r2 = gen1(); // r2 = 1 var r3 = gen2(); // r3 = 0 var r4 = gen2(); // r4 = 1
類構造函數
JavaScript的函數同時作為類的構造函數,因此只要聲明一個函數就可以使用new關鍵字創建類的實例。
在一些面向對象的語言,如Java、C++、PHP中,構造函數是很常見的。在Javascript中構造函數首先是一個普通的函數,它可以使用new 操作符來調用,並生成一個特殊類型的對象。
function Benjamin(username, sex) { this.username = username; this.sex = sex; } var benjamin = new Benjamin("zuojj", "male"); //Outputs: Benjamin{sex: "male",username: "zuojj"} console.log(benjamin);
正如我們所看到的,“Benjamin”構造函數僅僅是接收傳遞過來的參數,並把它們賦值給this對象。這是因為當構造函數被new操作符調用時,構造函數的this對象賦值為new操作返回的對象。
這意味着上面的代碼等同於:
benjamin = { "username": "zuojj", "sex": "male" }
function Person(name) { this.name = name; this.toString = function() { return 'Hello, ' + this.name + '!'; }; } var p = new Person('Ghostheaven'); alert(p); // Hello, Ghostheaven! 在以上實例中Person函數作為類的構造函數使用,此時this指向新創建的實例對象,可以為實例增加屬性和方法,關於詳細的面向對象的JavaScript編程可以參考這篇文章。這里我想要說的是,JavaScript函數作為類構造函數使用時的返回值問題。
function MyClass(name) { this.name = name; return name; // 構造函數的返回值? } var obj1 = new MyClass('foo'); var obj2 = MyClass('foo'); var obj3 = new MyClass({}); var obj4 = MyClass({});
上面的構造函數比較特別,有返回語句,那么obj1~obj4分別指向什么對象呢?實際結果是這樣的:
obj1 = MyClass對象 obj2 = 'foo' obj3 = {} obj4 = {}
具體原因這篇文章有解釋,本文不再贅述,由於帶返回值的構造函數會產生奇怪的結果,因此不要在構造函數中調用有返回值的返回語句(空return可以)。
為什么使用構造函數,有以下幾個方面的原因:
1.使用構造函數,意味着所有的這些對象,都可以使用相同的基本結構創建
2.使用構造函數,意味着“benjamin”對象被明確的標記為“Benjamin”函數的實例
function Benjamin(username, sex) { this.username = username; this.sex = sex; } var benjamin = new Benjamin("zuojj", "male"); var ben = { "username": "zuojj", "sex": "male" } //Outputs: true console.log(benjamin instanceof Benjamin); //Outputs: false console.log(ben instanceof Benjamin);
3.使用構造函數,意味着我們可以在原型上定義公共方法,供多個實例共享
function Benjamin(username, sex) { this.username = username; this.sex = sex; } Benjamin.prototype.getName = function() { return this.username; } var benjamin = new Benjamin("zuojj", "male"); var ben = new Benjamin("lemon", "female"); //Outputs: zuojj console.log(benjamin.getName()); //Outputs: lemon console.log(ben.getName());
1.new 關鍵字
在實例化構造函數的時候一定不要忘了使用new關鍵字,是否使用new關鍵字,對this對象的影響很大,不用new關鍵字的情況下,this對象會指向全局對象(window in browser and global in node)。因此定義構造函數時,建議函數名稱首字母大寫。
2.如果被調用的函數沒有顯式的 return 表達式,則隱式的會返回 this 對象 – 也就是新創建的對象,否則將會影響返回的結果,但僅限於返回的是一個對象
function Bar() { return 2; } var bar = new Bar(); //返回新創建的對象 //Outputs: Bar {} console.log(bar); function Test() { this.value = 2; return { foo: 1 }; } var test = new Test(); //返回的對象 //Outputs: Object {foo: 1} console.log(test);
我們需要注意的是:
a) new Bar() 返回的是新創建的對象,而不是數字的字面值 2。 因此 new Bar().constructor === Bar,但是如果返回的是數字對象,結果就不同了;
b) 這里得到的 new Test()是函數返回的對象,而不是通過new關鍵字新創建的對象,如下所示:
function Bar() { return 2; } var bar = new Bar(); function BarN() { return new Number(2); } var barn = new BarN(); //Outputs: true console.log(bar.constructor === Bar); //Outputs: Number {} console.log(barn); //Ouputs: false console.log(barn.constructor === BarN); //Outputs: true console.log(barn.constructor === Number); /* -------------------------------------- */ function Test() { this.value = 2; return { foo: 1 }; } var test = new Test(); //Outputs: undefined console.log(test.value); //Ouputs: 1 console.log(test.foo);
閉包
var myObject = { value : 0, increment : function(inc){ this.value = typeof inc === 'number' ? inc : 1; }, getValue : function(){ return this.value; } }; myObject.increment(10); alert(myObject.value); alert(myObject.getValue()); // 上面使用字面常量方式定義了一個myObject對象。但是value變量可以被外部對象訪問 var myObject = function(){ var value = 0; return { increment: function(inc){ value += typeof inc === 'number' ? inc : 1; }, getValue : function(){ return value; } }; }(); myObject.increment(10); alert(myObject.value); // 不能被外部對象訪問 alert(myObject.getValue()); // 10 // 漸變body的背景色(黃色到白色) var fade = function(node){ var level = 1; var step = function(){ var hex = level.toString(16); node.style.backgroundColor = '#FFFF' + hex + hex; if(level < 15){ level += 1; setTimeout(step, 500); // 如果level小於15,則內部函數自我調用 } }; setTimeout(step, 1); // 調用內部函數 }; fade(document.body); // 下面是一個很糟糕的例子 <a href="#" name="test">點擊我...</a><br> // 點擊時顯示3 <a href="#" name="test">點擊我...</a><br> // 點擊時顯示3 <a href="#" name="test">點擊我...</a><br> // 點擊時顯示3 var add_the_handlers = function(nodes){ var i; for(i = 0; i < nodes.length; i += 1) { nodes[i].onclick = function(e){ // 函數構造時的:i alert(i); }; } }; var objs = document.getElementsByName("test"); add_the_handlers(objs); // 造成上面的原因是:a標簽的事件函數綁定了變量i,則不是函數在構造時的i值。 // 解決方案如下: var add_the_handlers = function(nodes){ var i; for(i = 0; i < nodes.length; i += 1) { nodes[i].onclick = function(i){ return function(e){ alert(i); // 輸出的i是構造函數傳遞進來的i,不是事件處理綁定的i。 }; }(i); } }; var objs = document.getElementsByName("test"); add_the_handlers(objs);
回調
// data表示參數,而call_function則表示回調函數 function sendRequest(data, call_function){ // setTimeout來模仿客戶端請求服務端中傳輸數據的時間。 // 當3秒鍾后就調用回調函數(有客戶端實現回調函數) setTimeout(function(){ call_function(data); // 調用回調函數 }, 3000); } // 測試sendRequest函數 sendRequest("參數", function(context){ alert("context=" + context); });
模塊
模塊是一個提供接口而隱藏狀態和實現的函數或對象。
一般形式:一個定義了私有變量和函數的函數;利用閉包創建可以訪問私有變量和函數的特權函數;最后返回這個特權函數,或者把他們保存到一個可以被訪問到的地方。
Function.prototype.method = function(name,func){ this.prototype[name] = func; return this; }; String.method("deentityify",function(){ var entity = { quot : '"', lt : '<', gt : '>' }; return function(){ return this.replace(/&([^&;]+);/g, function(a, b){ // 怎樣知道a、b的值,了解正則表達式 var r = entity[b]; return typeof r === "string" ? r : a; }); }; }()); alert("<">".deentityify()); // 測試:<">
注:模塊模式通常結合單例模式使用,JavaScript的單例模式就是用對象字面量方式創建的對象,對象的屬性值可以是數值或函數,並且屬性值在該對象的生命周期中不會發生變化。
級聯(鏈式操作)
對於一些不返回值的方法,我們返回this,而不是undefined,那么我們就可以啟動以級聯(鏈式)去操作該對象。如下:
var $ = function(id){ var obj = document.getElementById(id); obj.setColor = function(color){ this.style.color = color; return this; }; obj.setBgColor = function(color){ this.style.backgroundColor = color; return this; // 返回this對象,啟動級聯 }; obj.setFontSize = function(size){ this.style.fontSize = size; return this; }; return obj; }; $("test").setColor("red") .setFontSize("30px") .setBgColor("blue"); // 改進后的代碼: (function(id){ var _$ = function(id){ this.element = document.getElementById(id); }; _$.prototype = { setColor : function(color){ this.element.style.color = color; return this; }, setBgColor : function(color){ this.element.style.backgroundColor = color; return this; }, setFontSize : function(size){ this.element.style.fontSize = size; return this; } }; // 添加到window原型鏈中 window.$ = function(id){ return new _$(id); }; })(); $("test").setColor("red") .setFontSize("30px") .setBgColor("blue");
套用
所謂套用就是將函數與傳遞給它的參數相結合,產生一個新的函數。如:下面代碼中定義一個add()函數,該函數能夠返回一個新的函數,並把參數值傳遞給這個新函數,從而實現連加操作。
// 第一種方式: var add = function(a){ return function(b){ return a + b; } }; alert(add(1)(2)); // 3 // 第二種方式:用arguments實現 var add = function(){ var arg = arguments; return function(){ var sum = 0; for(var i=0; i<arg.length; i++){ sum += arg[i]; } for(i=0; i<arguments.length; i++){ sum += arguments[i]; } return sum; } }; alert(add(1,2,3)(4,5,6)); // 21 // 第三種方式:通過一個套用方法(curry)實現 var add = function(){ var sum = 0; for(var i=0; i<arguments.length; i++){ sum += arguments[i]; } return sum; }; // 添加方法到Function的原型鏈上 Function.prototype.method = function(name, func){ this.prototype[name] = func; return this; }; // 套用方法 Function.method('curry', function(){ // 通過數組Array的slice方法,使得arguments也具有concat方法 var slice = Array.prototype.slice, args = slice.apply(arguments), that = this; return function(){ return that.apply(null, args.concat(slice.apply(arguments))); }; }); alert(add.curry(1,2)(3,4)); // 10
記憶
函數可以用對象去記住先前操作的結果,從而能避免無謂的運算。這種優化被稱為記憶。
var fibonacci = function(){ var mome = [0,1]; // 存放計算后的數據 var fib = function(n){ var result = mome[n]; // 如果不存在被計算過的數據,則直接計算。然后在將計算結果緩存 if(typeof result !== 'number'){ result = fib(n-1) + fib(n-2); mome[n] = result; } return result; }; return fib; }(); for(var i=0; i<=10; i++){ document.writeln("// " + i + ": " + fibonacci(i) + "<br/>"); } //========================== // 創建一個具有記憶的函數 //========================== var memoizer = function(memo, fundamental){ var shell = function(n){ var result = memo[n]; if(typeof result !== "number"){ result = fundamental(shell, n); memo[n] = result; } return result; }; return shell; }; // 通過記憶函數memoizer完成斐波那契數列 var fibonacci = memoizer([0,1], function(shell, n){ return shell(n-1) + shell(n-2); }); // 通過記憶函數memoizer完成階乘 var factorial = memoizer([1,1], function(shell, n){ return n * shell(n-1); }); for(var i=0; i<=15; i++){ document.writeln("// " + i + ": " + factorial(i) + "<br/>"); }
自更新函數(Self-update Function)
在很多語言中,函數一旦聲明過就不能再次聲明同名函數,否則會產生語法錯誤,而在JavaScript中的函數不僅可以重復聲明,而且還可以自己更新自己。自己吃自己的妖怪來了!
function selfUpdate() { window.selfUpdate = function() { alert('second run!'); }; alert('first run!'); } selfUpdate(); // first run! selfUpdate(); // second run! 這種函數可以用於只運行一次的邏輯,在第一次運行之后就整個替換成一段新的邏輯。
使用函數
前面已經提到,在JavaScript中,函數可以
◆ 被賦值給一個變量
//聲明一個函數,接受兩個參數,返回其和 function add(x, y){ return x + y; } var a = 0; a = add;//將函數賦值給一個變量 var b = a(2, 3);//調用這個新的函數a print(b);
這段代碼會打印”5”,因為賦值之后,變量a引用函數add,也就是說,a的值是一個函數對象(一個可執行代碼塊),因此可以使用a(2, 3)這樣的語句來進行求和操作。
◆ 被賦值為對象的屬性
var obj = { id : "obj1" } obj.func = add;//賦值為obj對象的屬性 obj.func(2, 3);//返回5
事實上,這個例子與上個例子的本質上是一樣的,第一個例子中的a變量,事實上是全局對象(如果在客戶端環境中,表示為window對象)的一個屬性。而第二個例子則為obj對象,由於我們很少直接的引用全局對象,就分開來描述。
◆ 作為參數被傳入別的函數
//高級打印函數的第二個版本 function adPrint2(str, handler){ print(handler(str)); } //將字符串轉換為大寫形式,並返回 function up(str){ return str.toUpperCase(); } //將字符串轉換為小寫形式,並返回 function low(str){ return str.toLowerCase(); } adPrint2("Hello, world", up); adPrint2("Hello, world", low);
運行此片段,可以得到這樣的結果:
HELLO, WORLD
hello, world
應該注意到,函數adPrint2的第二個參數,事實上是一個函數,將這個處理函數作為參數傳入,在adPrint2的內部,仍然可以調用這個函數,這個特點在很多地方都是有用的,特別是,當我們想要處理一些對象,但是又不確定以何種形式來處理,則完全可以將“處理方式”作為一個抽象的粒度來進行包裝(即函數)。
◆ 作為函數的結果被返回
function currying(){ return function(){ print("curring"); } }
函數currying返回一個匿名函數,這個匿名函數會打印”curring”,簡單的調用currying()會得到下面的結果:
function (){print("curring");}
如果要調用currying返回的這個匿名函數,需要這樣:
currying()();
第一個括號操作,表示調用currying本身,此時返回值為函數,第二個括號操作符調用這個返回值,則會得到這樣的結果:
currying
2、函數的四種調用模式及this的初始化
第一種:方法調用模式
以下事例證明通過方法調用模式調用時,this綁定到擁有該方法的對象。如:
var person = { name: "defaultName", setName : function(name){ this.name = name; } }; person.setName("zhangsan"); alert(person.name);
第二種:函數調用模式
以下事例證明通過函數調用模式調用時,this綁定到全局對象上。如:
var test = add(value1, value2); var name = "defaultName"; var person = { name: "zhangsan", // person中定義的name getName : function(){ // 通過此方法可以將test函數的this改變為person的this對象 var that = this; // 解決方案 // getName中定義的name var name = "lisi"; var test = function(){ // 通過that來訪問person中的對象 // this指向Global對象 // this.name = defaultName // that.name = zhangsan alert([this.name, that.name]); }; test(); // 函數調用模式 } } person.getName();
第三種:構造器調用模式
// 定義一個Person的構造器,在調用時一定要用new調用 var Person = function(name){ this.name = name; } // 添加一個方法到Person Person.prototype.getName = function(){ return this.name; }; // 構造一個Person對象 var person = new Person("zhangsan"); alert(person.getName()); // 調用getName獲取person對象中name屬性的值
<script type="text/javascript"> // 定一個累加方法。如sum(1,2,3,4...) // 該方法位於window執行環境中。 var displayName = function(){ alert("sum的執行環境: " + typeof(this)); alert("Name: " + this.name); // 取出當前執行環境中name屬性 } // 定一個Person對象 var Person = { name: "zhangsan" }; displayName.apply(Person); </script>