DIV CSS 佈局教程網

 DIV+CSS佈局教程網 >> 網頁腳本 >> JavaScript入門知識 >> JavaScript基礎知識 >> JavaScript高級程序設計(第3版)學習筆記9 js函數(下)
JavaScript高級程序設計(第3版)學習筆記9 js函數(下)
編輯:JavaScript基礎知識     
再接著看函數——具有魔幻色彩的對象。

9、作為值的函數

  在一般的編程語言中,如果要將函數作為值來使用,需要使用類似函數指針或者代理的方式來實現,但是在ECMAScript中,函數是一種對象,擁有一般對象具有的所有特征,除了函數可以有自己的屬性和方法外,還可以做為一個引用類型的值去使用,實際上我們前面的例子中已經有過將函數作為一個對象屬性的值,又比如函數也可以作為另一個函數的參數或者返回值,異步處理中的回調函數就是一個典型的用法。

復制代碼 代碼如下:
var name = 'linjisong';
var person = {name:'oulinhai'};
function getName(){
return this.name;
}
function sum(){
var total = 0,
l = arguments.length;
for(; l; l--)
{
total += arguments[l-1];
}
return total;
}

// 定義調用函數的函數,使用函數作為形式參數
function callFn(fn,arguments,scope){
arguments = arguments || [];
scope = scope || window;
return fn.apply(scope, arguments);
}
// 調用callFn,使用函數作為實際參數
console.info(callFn(getName));//linjisong
console.info(callFn(getName,'',person));//oulinhai
console.info(callFn(sum,[1,2,3,4]));//10

再看一個使用函數作為返回值的典型例子,這個例子出自於原書第5章:
復制代碼 代碼如下:
function createComparisonFunction(propertyName) {
return function(object1, object2){
var value1 = object1[propertyName];
var value2 = object2[propertyName];

if (value1 < value2){
return -1;
} else if (value1 > value2){
return 1;
} else {
return 0;
}
};
}

var data = [{name: "Zachary", age: 28}, {name: "Nicholas", age: 29}];

data.sort(createComparisonFunction("name"));
console.info(data[0].name); //Nicholas

data.sort(createComparisonFunction("age"));
console.info(data[0].name); //Zachary


10、閉包(Closure)

  閉包是指有權訪問另一個函數作用域中的變量的函數。對象是帶函數的數據,而閉包是帶數據的函數。

  首先閉包是一個函數,然後閉包是一個帶有數據的函數,那麼,帶有的是什麼數據呢?我們往上看看函數作為返回值的例子,返回的是一個匿名函數,而隨著這個匿名函數被返回,外層的createComparisonFunction()函數代碼也就執行完成,按照前面的結論,外層函數的執行環境會被彈出棧並銷毀,但是接下來的排序中可以看到在返回的匿名函數中依舊可以訪問處於createComparisonFunction()作用域中的propertyName,這說明盡管createComparisonFunction()對應的執行環境已經被銷毀,但是這個執行環境相對應的活動對象並沒有被銷毀,而是作為返回的匿名函數的作用域鏈中的一個對象了,換句話說,返回的匿名函數構成的閉包帶有的數據就是:外層函數相應的活動對象。由於活動對象的屬性(也就是外層函數中定義的變量、函數和形式參數)會隨著外層函數的代碼執行而變化,因此最終返回的匿名函數構成的閉包帶有的數據是外層函數代碼執行完成之後的活動對象,也就是最終狀態。

  希望好好理解一下上面這段話,反復理解一下。雖然我已經盡我所能描述的更易於理解一些,但是閉包的概念還是有些抽象,下面看一個例子,這個例子來自原書第7章:
復制代碼 代碼如下:
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
return i;
};
}
return result;
}

var funcs = createFunctions();
for (var i=0,l=funcs.length; i < l; i++){
console.info(funcs[i]());//每一個函數都輸出10
}

這裡由於閉包帶有的數據是createFunctions相應的活動對象的最終狀態,而在createFunctions()代碼執行完成之後,活動對象的屬性i已經變成10,因此在下面的調用中每一個返回的函數都輸出10了,要處理這種問題,可以采用匿名函數作用域來保存狀態:
復制代碼 代碼如下:
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = (function(num){
return function(){
return num;
};
})(i);
}
return result;
}

將每一個狀態都使用一個立即調用的匿名函數來保存(保存在匿名函數相應的活動對象中),然後在最終返回的函數被調用時,就可以通過閉包帶有的數據(相應的匿名函數活動對象中的數據)來正確訪問了,輸出結果變成0,1,...9。當然,這樣做,就創建了10個閉包,在性能上會有較大影響,因此建議不要濫用閉包,另外,由於閉包會保存其它執行環境的活動對象作為自身作用域鏈中的一環,這也可能會造成內存洩露。盡管閉包存在效率和內存的隱患,但是閉包的功能是在太強大,下面就來看看閉包的應用——首先讓我們回到昨天所說的函數綁定方法bind()。

(1)函數綁定與柯裡化(currying)

A、再看this,先看一個例子(原書第22章):
復制代碼 代碼如下:
<button id='my-btn' title='Button'>Hello</button>
<script type="text/javascript">
var handler = {
title:'Event',
handleClick:function(event){
console.info(this.title);
}
};
var btn = document.getElementById('my-btn');//獲取頁面按鈕
btn.onclick = handler.handleClick;//給頁面按鈕添加事件處理函數
</script>

如果你去點擊“Hello”按鈕,控制台打印的是什麼呢?竟然是Button,而不是期望中的Event,原因就是這裡在點擊按鈕的時候,處理函數內部屬性this指向了按鈕對象。可以使用閉包來解決這個問題:
復制代碼 代碼如下:
btn.onclick = function(event){
handler.handleClick(event);//形成一個閉包,調用函數的就是對象handler了,函數內部屬性this指向handler對象,因此會輸出Event}

B、上面的解決方案並不優雅,在ES5中新增了函數綁定方法bind(),我們使用這個方法來改寫一下:
復制代碼 代碼如下:
if(!Function.prototype.bind){//bind為ES5中新增,為了保證運行正常,在不支持的浏覽器上添加這個方法
Function.prototype.bind = function(scope){
var that = this;//調用bind()方法的函數對象
return function(){
that.apply(scope, arguments);//使用apply方法,指定that函數對象的內部屬性this
};
};
}
btn.onclick = handler.handleClick.bind(handler);//使用bind()方法時只需要使用一條語句即可

這裡添加的bind()方法中,主要技術也是創建一個閉包,保存綁定時的參數作為函數實際調用時的內部屬性this。如果你不確定是浏覽器本身就支持bind()還是我們這裡的bind()起了作用,你可以把特性檢測的條件判斷去掉,然後換個方法名稱試試。
C、上面對函數使用bind()方法時,只使用了第一個參數,如果調用bind()時傳入多個參數並且將第2個參數開始作為函數實際調用時的參數,那我們就可以給函數綁定默認參數了。
復制代碼 代碼如下:
if(!Function.prototype.bind){
Function.prototype.bind = function(scope){
var that = this;//調用bind()方法的函數對象
var args = Array.prototype.slice.call(arguments,1);//從第2個參數開始組成的參數數組
return function(){
var innerArgs = Array.prototype.slice.apply(arguments);
that.apply(scope, args.concat(innerArgs));//使用apply方法,指定that函數對象的內部屬性this,並且填充綁定時傳入的參數
};
};
}

D、柯裡化:在上面綁定時,第一個參數都是用來設置函數調用時的內部屬性this,如果把所有綁定時的參數都作為預填的參數,則稱之為函數柯裡化。
復制代碼 代碼如下:
if(!Function.prototype.curry){
Function.prototype.curry = function(){
var that = this;//調用curry()方法的函數對象
var args = Array.prototype.slice.call(arguments);//預填參數數組
return function(){
var innerArgs = Array.prototype.slice.apply(arguments);//實際調用時參數數組
that.apply(this, args.concat(innerArgs));//使用apply方法,並且加入預填的參數
};
};
}

(2)利用閉包緩存

  還記得前面使用遞歸實現斐波那契數列的函數嗎?使用閉包緩存來改寫一下:
復制代碼 代碼如下:
var fibonacci = (function(){//使用閉包緩存,遞歸
var cache = [];
function f(n){
if(1 == n || 2 == n){
return 1;
}else{
cache[n] = cache[n] || (f(n-1) + f(n-2));
return cache[n];
}
}
return f;
})();

var f2 = function(n){//不使用閉包緩存,直接遞歸
if(1 == n || 2 == n){
return 1;
}else{
return f2(n-1) + f2(n-2);
}
};

下面是測試代碼以及我機器上的運行結果:
復制代碼 代碼如下:
var test = function(n){
var start = new Date().getTime();
console.info(fibonacci(n));
console.info(new Date().getTime() - start);

start = new Date().getTime();
console.info(f2(n));
console.info(new Date().getTime() - start);
};
test(10);//55,2,55,2
test(20);//6765,1,6765,7
test(30);//832040,2,832040,643

可以看到,n值越大,使用緩存計算的優勢越明顯。作為練習,你可以嘗試自己修改一下計算階乘的函數。

(3)模仿塊級作用域

  在ECMAScript中,有語句塊,但是卻沒有相應的塊級作用域,但我們可以使用閉包來模仿塊級作用域,一般格式為:
復制代碼 代碼如下:
(function(){
//這裡是塊語句
})();

上面這種模式也稱為立即調用的函數表達式,這種模式已經非常流行了,特別是由於jQuery源碼使用這種方式而大規模普及起來。
  閉包還有很多有趣的應用,比如模仿私有變量和私有函數、模塊模式等,這裡先不討論了,在深入理解對象之後再看這些內容。

  關於函數,就先說這些,在網上也有很多非常棒的文章,有興趣的可以自己搜索一下閱讀。這裡推薦一篇文章,《JavaScript高級程序設計(第3版)》譯者的一篇譯文:命名函數表達式探秘。
XML學習教程| jQuery入門知識| AJAX入門| Dreamweaver教程| Fireworks入門知識| SEO技巧| SEO優化集錦|
Copyright © DIV+CSS佈局教程網 All Rights Reserved