JavaScript之函式
在JavaScript中,函式也是物件,所以它們可以像其他變數一樣被使用。函式物件都會連線到原型物件Function.prototype,而Function.prototype又會連線到原型物件Object.prototype。
1、函式的宣告定義:
①常規方式:使用function 方法名(形式引數){}定義函式。例如:
function test(a,b){
return a+b;
}
②匿名方式:將一個匿名函式賦值給一個函式字面量。例如:
var test=function(a,b){ return a+b; };
③ES6標準新增了一種箭頭函式:使用=>:=>之前為引數,=>之後為方法體,例如:
var test = x => x * x;
//相當於
var test = function(x){
return x * x;
};
箭頭函式相當於匿名函式,如果沒有入參或有多個,需用()括住,如果方法體中包含多條語句,{}t 則不能省略,例如:
//無引數 () => 3.14 //兩個引數: (x, y) => x * x + y * y //可變引數,方法體有多行 (x, y, ...rest) => { var i, sum = x + y; for (i=0; i<rest.length; i++) { sum += rest[i]; } return sum; }
④generator(生成器):是ES6標準引入的新的資料型別,看上去像一個函式,但和函式不同的是,generator由function*(注意多出的*
號),並且除了return語句,還可以用yield返回多次。定義方式如下:
function* foo(x) {
yield x + 1;
yield x + 2;
return x + 3;
}
可以用如下方式使用:
var test = foo(5); test.next(); //{value: 6, done: false} test.next(); //{value: 7, done: false} test.next(); //{value: 8, done: true} test.next(); //{value: undefined, done: true}
foo()只是建立了generator物件,還沒有去執行它。next()方法會執行generator的程式碼,然後每次遇到yield x;就返回一個物件{value: x, done: true/false},然後“暫停”。返回的value就是yield的返回值,done表示這個generator是否已經執行結束了。如果done為true,則value就是return的返回值。當執行到done為true時,這個generator物件就已經全部執行完畢,不要再繼續呼叫next()了,否則返回的物件中value的值為undefined。
也可以使用for...of迴圈迭代generator物件,這種方式不需要我們自己判斷done,會迴圈出所有的yield的值,不包含return的值。
var test = foo(5);
for (var x of test) {
console.log(x); //依次輸出6,7
}
2、函式的呼叫:使用:函式名(實際引數)的格式進行函式呼叫,函式被呼叫時會隱形接收兩個附加引數:this和arguments。arguments會將值傳給形式引數,由於JavaScript是弱型別的,所以不進行型別檢查,任何型別的引數都可以傳。也不會進行引數數量檢查。如果實際引數比形式引數的數量多,多出的引數忽略;如果實際引數比形式引數的數量少,少的引數用undefined代替。this的值取決於呼叫模式,JavaScript中共有4種呼叫模式,分別是:
①方法呼叫模式:當函式被儲存為物件的一個屬性時,稱這個函式為方法。因為此方法作為物件的屬性,所以可以使用.屬性名或['屬性名']呼叫該方法。此時的this是包含該方法的物件,在該方法內部通過this可以訪問呼叫本方法的物件的屬性值。
var people={id:1,name:'Tom',age:30,sex:'male',speak:function(){alert("我是"+this.name+",是一個程式猿");}};
people.speak();
people['speak']();
②函式呼叫模式:當函式並非一個物件的屬性時,它就被當作函式來呼叫。此時的this是全域性物件window,這是語言設計上的一個錯誤。
function speak(){
alert("我是"+this+",是一個程式猿");
}
speak();
解決方案是在呼叫該方法時傳入正確的this的值,而該值是由外部的函式使用方法呼叫模式獲取到的正確的物件。
var people={id:1,name:'Tom',age:30,sex:'male',mouth:function(){var that=this;speak(that);}};
function speak(that){
alert("我是"+that.name+",是一個程式猿");
}
people.mouth();
③構造器呼叫模式:當在函式呼叫時在函式名之前加new關鍵字時就成為構造器呼叫模式。通常被使用構造器呼叫模式呼叫的函式名首字母大寫,此時的this是新建立的物件。
var Entity = function(status){
this.status=status;
}
var entity = new Entity("status0");
alert(entity.status);
④Apply/Call呼叫模式:
apply方式呼叫採用:方法名.apply(this要指向的物件,實際引數陣列物件)的方式呼叫。apply的兩個引數都必須是物件,第一個引數指定this的值,第二個引數指定呼叫該方法時的實際引數的陣列。
var people={id:1,name:'Tom',age:30,sex:'male'};
var array=['程式猿','碼農'];
function speak(job1,job2){
alert("我是"+this.name+",是一個"+job1+",也叫"+job2);
}
speak.apply(people,array);
call方式呼叫採用:方法名.call(this要指向的物件,實際引數)的方式呼叫。第一個引數指定this的值,第一個引數後的其它引數是呼叫該方法時的實際引數。
var people={id:1,name:'Tom',age:30,sex:'male'};
function speak(job1,job2){
alert("我是"+this.name+",是一個"+job1+",也叫"+job2);
}
speak.call(people,'程式猿','碼農');
3、函式的特性
①引數arguments:只在函式內有效,函式可以通過變數arguments獲取到呼叫函式時傳遞過來的實際引數值,包括超出形式引數數量的多餘的引數值。
②引數reset:ES6標準引入,以便很方便獲取多餘的引數:
function foo(a, b, ...rest) {
console.log('a = ' + a);
console.log('b = ' + b);
console.log(rest);
}
foo(1, 2, 3, 4, 5);
//結果:
//a = 1
//b = 2
//Array [ 3, 4, 5 ]
③作用域:JavaScript不支援塊級作用域,即{}之外的程式也可以訪問{}之內的變數(這裡的{}不包括函式的{}),但支援函式級作用域,即函式的{}外的程式不能訪問函式{}之內的var宣告的變數(未加var宣告的變數視為全域性變數,所以是可以訪問的)。所以像for迴圈條件中定義的var變數仍能被訪問,為了解決這個問題,ES6引入了新的關鍵字let和const,用let替代var可以宣告一個塊級作用域的變數,const用於定義常量,lef和const都具有塊級作用域。
function foo(){
var sum=0;
for(let i=0;i<100;i++) {
sum+=i;
}
i+=1; //報錯
}
④回撥:回撥函式是一個作為變數傳遞給另外一個函式的函式,它在主體函式執行完之後執行。例如:
function test(a,b,callback){
var c = a+b;
callback(c,a);
}
function test1(a,b){
var d = a*b;
alert(d);
}
test(2,4,test1); //12
或者:
function test(a,b,callback){
var c = a+b;
callback(c,a);
}
test(2,4,function(a,b){
var d = a*b;
alert(d);
});
⑤級聯:當函式返回值為this時,則可以在單獨一條語句中依次呼叫同一個物件的多個方法,例如a,b,c,d分別是obj的4個方法,則可以:
obj.a().b().c().d();
⑥閉包:
因為JavaScript中函式外部無法訪問函式內部的變數,所以可以利用如下結構從函式外部訪問函式內部的區域性變數。
function outer(){
var innerValue = [1,2,3];
var inner = function(){
return innerValue;
}
return inner;
}
var v = outer();
alert(innerValue); //顯示undefined,無法從函式外部獲取函式內部區域性變數的值
alert(v); //顯示ret對應的匿名函式
alert(v()); //顯示outer函式內區域性變數innerValue的值
像上面這樣的程式結構:在一個外部函式outer中有一內部函式inner,這個內部函式inner會引用外部函式outer中的區域性變數innerValue,並且這個內部函式inner是外部函式outer的返回值,即外部函式outer最終返回的不是值而是返回一個函式,這樣的程式結構就是閉包。當呼叫外部函式outer並將返回值賦給另一變數v時,v並不是具體的值而是一個函式,所以要想得到值必須呼叫v這個函式才能訪問具體的值,這是閉包的作用之一。
閉包另一個作用就是讓函式內部的這些變數保持在記憶體中而不被回收,如上述示例中當呼叫外部函式outer並將返回值賦給另一變數v時,由於返回的是內部函式inner對應的匿名函式,而該匿名函式又引用外部函式outer的區域性變數innerValue,所以變數innerValue會持續存在於記憶體中而不會被回收直到變數v被回收。這樣就達到了讓區域性變數保持在記憶體中的目的。所以在使用閉包時一定要小心,記錄最後一定要釋放變數v,否則會造成記憶體洩露。
有一種情況一定要特別注意,例如:
function pows() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push(function () {
return i * i;
});
}
return arr;
}
var results = pows();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
當分別執行f1()、f2()和f3()時得到的結果均是16,並非期望中的1、4、9。原因就在於返回的每個函式都引用了變數i
,但函式並沒有立刻執行,而是等迴圈結束之後才會執行,當迴圈結束後i已經變為4,所以結果都是16。所以使用閉包時一定要注意:返回函式不要引用任何迴圈變數,或者後續會發生變化的變數。
⑦柯里化:通過利用閉包的特性(讓區域性變數存在於閉包中而使其持續存在於記憶體中),將含多個引數的函式逐級分解,最終分解為逐級巢狀返回的只含一個引數的函式,目的是對函式細節實現模組化管理,便於更靈活使用該函式的某部分。例如:
原函式為:
var add=function(a,b,c,d){
return a+b+c+d;
}
var result = add(1,2,3,4);
alert(result);
如果a,b,c引數值不變,d引數值變化,那麼呼叫該方法時,都必須呼叫含4個引數的add方法,並且每次都要將a,b,c引數重新傳值,例如:
var result1=add(1,2,3,4);
var result2=add(1,2,3,5);
var result3=add(1,2,3,6);
柯里化之後的函式為:
var add=function(a){
return function(b){
return function(c){
return function(d){
return a+b+c+d;
};
};
};
}
var result=add(1)(2)(3)(4);
alert(result);
如果a,b,c引數不變,d引數變化,那麼呼叫該方法時,則不需要每次都將a,b,c引數傳值。只需向a,b,c傳一次值即可,例如:
var add1 = add(1)(2)(3);
var result1=add1(4);
var result1=add1(5);
var result1=add1(6);
因此,柯里化也常稱為“區域性套用”。
參考:Douglas Crockford《JavaScript語言精粹》
http://www.cnblogs.com/sanshi/archive/2009/07/08/1519036.html
https://www.liaoxuefeng.com/wiki/001434446689867b27157e896e74d51a89c25cc8b43bdb3000