摘要:定義函數表達式的方式有兩種函數聲明。不過,這并不是匿名函數唯一的用途。可以使用命名函數表達式來達成相同的結果閉包匿名函數和閉包是兩個概念,容易混淆。匿名函數的執行環境具有全局性,因此其對象通常指向通過改變函數的執行環境的情況除外。
定義函數表達式的方式有兩種:
函數聲明。它的重要特征就是 函數聲明提升(function declaration hoisting) 即在執行代碼之前會先讀取函數聲明。這就意味著可以把函數聲明放在調用它的語句后面。
函數表達式。
// 函數聲明 function functionName(params) { ... }
// 函數表達式有幾種不同的方式,下面是最常見的一種 var functionName = function(params) { ... }
上面這種形式看起來好像是常規的變量賦值語句。而右邊這種形式創建的函數叫做 匿名函數 (anonymous function)(有時候也叫 拉姆達函數 lambda),因為function關鍵字后面沒有標識符。
函數表達式與其他表達式一樣,在使用前必須先賦值,否則會導致出錯。
sayHi(); // 錯誤,函數還不存在 var sayHi = function () { console.log("Hi!"); };
表面上看,下面的代碼沒有問題,condition為true時,使用一個定義,否則使用另一個定義。實際上,在ECMAScript中屬于無效語法,JavaScript引擎會嘗試修正錯誤,將其轉換為合理狀態。但問題是瀏覽器嘗試修正錯誤的做法并不一致。大多數瀏覽器會返回第二個聲明,忽略condition的值;Firefox會在condition為true的時候返回第一個聲明。因此這種做法很危險,不應該出現在你的代碼中。
// 不要這樣做! if (condition) { function sayHi() { console.log("Hi!"); } } else { function sayHi() { console.log("Yo!"); } }
上述代碼改為函數表達式就沒有問題
// 可以這樣做 var sayHi; if (condition) { sayHi = function() { console.log("Hi!"); } } else { sayHi = function() { console.log("Yo!"); } }
能夠創建函數再賦值給變量,也就能把函數作為其他函數的返回值。createComparisonFunction() 就返回了一個匿名函數。
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; } } }
在把函數當成值來使用的情況下,都可以使用匿名函數。不過,這并不是匿名函數唯一的用途。
遞歸遞歸函數是在一個函數通過名字調用自身的情況下構成的
// 經典的遞歸階乘函數 function factorial (num) { if (num <= -1) { return 1; } else { return num * factorial(num - 1); } }
雖然遞歸階乘函數表面看沒有什么問題,但下面的代碼卻可能導致它出錯
// 把factorial() 函數保存在變量anotherFactorial中 var anotherFactorial = factorial; // 將factorial設置為null // 現在指向原始函數的引用只剩下anotherFactorial factorial = null; // 原始函數必須執行factorial() // 但factorial不再是函數,所以導致出錯 anotherFactorial(4); // throw error!
可以使用 arguments.callee (指向正在執行函數的指針)實現函數的遞歸調用
// 非嚴格模式 function factorial (num) { if (num <= -1) { return 1; } else { return num * arguments.callee(num - 1); } }
但在嚴格模式下不能通過腳本訪問 arguments.callee,會導致出錯。可以使用命名函數表達式來達成相同的結果
var factorial = (function f(num) { if (num <= -1) { return 1; } else { return num * f(num - 1); } })閉包
匿名函數 和 閉包 是兩個概念,容易混淆。 閉包 是指有權訪問另一個函數作用域中的變量的函數。
創建閉包的常見方式,就是在一個函數內部創建另一個函數,仍以前面的 createComparisonFunction() 函數為例
function createComparisonFunction (propertyName) { return function (object1, object2) { // 下面兩行代碼訪問了外部函數中的變量propertyName // 即使這個內部函數被返回了,而且是在其他地方被調用了 // 它仍然可以訪問變量 propertyName var value1 = object1[propertyName]; var value2 = object2[propertyName]; if (value1 < value2) { return -1; } else if (value1 > value2) { return 1; } else { return 0; } } }
上述例子,即使內部函數被返回了,在其他地方調用,它仍然可以訪問propertName。因為這個內部函數的作用域鏈中包含 createComparisonFunction() 的作用域。要搞清楚其中細節,必須從理解函數被調用的時候都會發生什么入手。
第4章介紹過 作用域鏈。當某個函數被 調用 時會發生下列事情:
創建一個 執行環境(execution context) 及相應的 作用域鏈
使用 arguments 和其他命名參數的值來初始化函數的 活動對象(activation object)
形成作用域鏈。外部函數的活動對象始終處于第二位,外部函數的外部函數的活動對象處于第三位...直至作為作用域終點的全局執行環境
函數執行過程中,為讀寫變量的值,就需要在作用域鏈中查找變量。
function compare(value1, value2) { if (value1 < value2) { return -1; } else if (value1 > value2) { return 1; } else { return 0; } } var result = compare(5, 10);
后臺的每個執行環境都有一個表示變量的對象——變量對象。全局環境的變量對象始終存在,而像compare()函數這樣的局部環境的變量對象,則只在函數執行的過程中存在。
在創建 compare() 函數時,會創建一個預先包含全局變量對象的作用域鏈,這個作用域鏈被保存在內部的[[scope]]屬性中。
當調用 compare() 函數時,會為函數創建一個執行環境,然后通過復制函數的[[scope]]屬性中的對象構建起執行環境的作用域鏈。
此后,又有一個活動對象(在此作為變量對象使用)被創建并被推入執行環境作用域鏈的前端。
對于本例, compare() 函數的執行環境而言,其作用域鏈中包含兩個變量對象:
本地活動對象
全局活動對象
作用域鏈本質上是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。
無論什么時候在函數中訪問一個變量時,就會從作用域鏈中搜索具有相應名字的變量。一般而言,當函數執行完畢后,局部活動對象就會被銷毀,內存中近保存全局作用域(全局執行環境的變量對象)。
但是閉包的情況有所不同
function createComparisonFunction (propertyName) { return function (object1, object2) { // 下面兩行代碼訪問了外部函數中的變量propertyName // 即使這個內部函數被返回了,而且是在其他地方被調用了 // 它仍然可以訪問變量 propertyName // 即為 createComparisonFunction 的活動對象 var value1 = object1[propertyName]; var value2 = object2[propertyName]; if (value1 < value2) { return -1; } else if (value1 > value2) { return 1; } else { return 0; } } } // 創建比較函數 // 調用了 createComparisonFunction() 方法 // 創建了 createComparisonFunction 的活動對象 // 返回內部的匿名函數 保存在 compareNames // createComparisonFunction 執行完畢 // 但它的活動對象仍被 內部匿名函數引用,所以活動對象仍然存在,不會銷毀 var compareNames = createComparisonFunction("name"); // 此時result調用了 保存在 compareNames 的匿名函數 // 該匿名函數保持了對 createComparisonFunction 活動對象的引用 var result = compareNames({ name: "Nicholas" }, { name: "Greg" }); // 即使 compareNames 執行完畢,createComparisonFunction 活動對象依然存在 // 需要手動解除對匿名函數的引用(以便釋放內存) compareNames = null;
首先,創建的比較函數被保存在變量compareNames中,而通過將其設置為null解除引用,就等于通知垃圾回收例程將其消除。隨著匿名函數的作用域鏈被銷毀,其他作用域鏈(除了全局作用域)也都可以安全地的銷毀了。圖7-2展示了調用compareNames()的過程中產生的作用域鏈之間的關系
由于閉包會攜帶包含它的函數的作用域,因此會比其他函數占用更多的內存。過度使用閉包可能導致內存占用過多,我們建議讀者只在絕對必要時再考慮使用閉包。雖然像V8等優化后的JavaScript引擎會嘗試回收被閉包占用的內存,但請還是要謹慎使用。
閉包與變量作用域鏈的這種配置機制,引出了一個值得注意的副作用,即閉包只能取得包含函數中任何變量的最后一值。別忘了閉包所保存的是整個變量對象,而不是某個特殊的變量。
function createFunctions() { var result = new Array(); for (var i=0; i < 10; i++) { // 賦值給數組元素的是匿名函數本身,不是具體的值 // 所以在 createFunctions() 執行完畢后,調用數組內的函數,返回的是變量i的值 // 而變量i在執行完畢后,等于 10 result[i] = function() { // 返回指向變量 i 的指針 return i; }; } return result; }
這個函數會返回一個 函數數組。表面上看result里的每一項函數都應該返回自己的索引值。但實際上每一個函數返回的都是10。
因為每個函數的作用域中都保存著createFunctions()函數的活動對象,所以它們引用的都是同一個變量i。當createFunctions()函數返回后,變量i的值是10,此時每個函數都引用著保存變量i的同一個變量對象,所以在每個函數內部i的值都是10.
可以通過創建另一個匿名函數強制讓閉包的行為符合預期
function createFunctions() { var result = new Array(); for (var i=0; i < 10; i++) { // 此時返回的里層匿名函數調用了外層匿名函數的 num // 里層匿名函數創建并返回了一個訪問 num 的閉包 // 如此一來 result 數組中的每個函數都有自己的num變量副本 result[i] = function(num) { // 返回創建的另一個匿名函數 return function() { return num; }; }(i); } return result; }關于this對象
在閉包中使用this對象也可能會導致一些問題。this對象是在運行時基于函數的執行環境綁定的:
在全局函數中,this等于window
當函數被作為某個對象的方法調用時,this等于那個對象。
匿名函數的執行環境具有全局性,因此其this對象通常指向window(通過call() apply()改變函數的執行環境的情況除外)。但有時候由于變成寫閉包的方式不同,這一點可能不會那么明顯
var name = "The Window"; var object = { name: "My Object", getNameFunc: function() { return function() { return this.name; }; } }; // 在非嚴格模式下 object.getNameFunc()(); // "The Window"
每個函數在被調用時都會自動取得兩個特殊變量: this和arguments。內部函數在搜索這兩個變量時,只會搜索到其活動對象為止,因此永遠不可能直接訪問外部函數中的這兩個變量(這一點通過圖7-2可以看的清楚)。
不過,把外部作用域中的this對象保存在一個閉包能夠訪問到的變量里,就可以讓閉包訪問該對象了。
var name = "The Window"; var object = { name: "My Object", getNameFunc: function() { var that = this; return function() { return that.name; }; } }; object.getNameFunc()(); // "My Object"
在幾種特殊情況下,this的值可能會意外的改變。
var name = "The Window"; var object = { name: "My Object", getName: function() { return this.name; } }; // this 指向 object object.getName(); // "My Object" // 加上了括號,看似在引用一個函數 // 但 (object.getName) 和 object.getName 的定義是相同的 // 所以這行代碼與上面的代碼無異 (object.getName)(); // "My Object" // 非嚴格模式 // 賦值語句會返回 object.getName 的匿名函數 // 相當于將匿名函數在全局環境下運行 (object.getName = object.getName)(); // "The Window"
第三行代碼先執行了一條賦值語句,然后再調用賦值后的結果。因為這個賦值表達式的值是函數本身,所以this的值不能得到維持,結果就返回了 "The Window" 。
內存泄漏由于IE9之前的版本對JScript對象和COM對象使用不同的垃圾收集例程(第4章介紹過),因此閉包在IE的這些版本中會導致一些特殊的問題。具體來說,如果閉包的作用域鏈中保存著一個HTML元素,那么就意味著該元素將無法被銷毀
function assignHandler() { var element = document.getElementById("someElement"); element.onclick = function() { console.log(element.id); }; }
以上代碼創建了一個作為element元素處理程序的閉包,而這個閉包則又創建了一個循環引用(事件將在第13章討論)。由于匿名函數保存了一個對assignHandler()的活動對象的引用,因此就會導致無法減少element的引用數。只要匿名函數存在,element的引用數至少是1。
// 以下修改可以避免這個問題 function assignHandler() { var element = document.getElementById("someElement"); var id = element.id; element.onclick = function() { console.log(id); }; element = null; }
閉包中引用包含函數的整個活動對象,而其中包含著element。即使閉包不直接引用element,包含函數的活動對象也仍然會保存一個引用。因此有必要把element變量設置為null
模仿塊級作用域JavaScript沒有塊級作用域。這意味著在塊語句中定義的變量,實際上是在包含函數中而非語句中創建的。
function outputNumerbs(cout) { for (var i=0; i < cout; i++) { console.log(i); } console.log(i); // 計數 }
在Java, C++等語言中,變量i只會在for循環的語句塊中有定義,循環一旦結束,變量i就會被銷毀。可是在JavaScript中,變量i是定義在outputNumbers()的活動對象中的,因此從它有定義開始,就可以在函數內部隨處訪問它。即使像下面這樣錯誤的重新聲明變量也不會改變值。
function outputNumerbs(cout) { for (var i=0; i < cout; i++) { console.log(i); } var i; // 重新聲明變量 console.log(i); // 計數 }
JavaScript從來不會告訴你是否多次聲明了同一個變量,遇到這種情況,它只會對后續的聲明視而不見(不過,它會執行后續聲明中的變量初始化)。
匿名函數可以用來模仿塊級作用域并避免這個問題。用作塊級作用域(通常稱為 私有作用域 )的匿名函數的語法如下:
(function() { // 這里是塊級作用域 ... })()
// 常見的代碼片段 // 定義了一個函數,然后立即調用它 var someFunction = function() { // 這里是塊級作用域 ... }; someFunction();
那這里如果將函數名也去掉呢?答案是不行,會導致出錯。因為JavaScript將function關鍵字當做一個函數聲明的開始,而函數聲明后面不能跟圓括號。(函數表達式可以)
function() { // 這里是塊級作用域 ... }() // 出錯!
要將函數聲明轉換成函數表達式,只要外面包裹圓括號即可
(function() { // 這里是塊級作用域 ... }())
無論在什么地方,只要臨時需要一些變量,就可以使用私有作用域
function outputNumbers(cout) { // 這里是一個閉包,匿名函數可以訪問 cout (function () { for (var i=0; i < cout; i++) { console.log(i); } })(); // 在這里調用變量 i 會報錯 console.log(i); // throw error }
這種技術經常在全局作用域中被用在函數外部,從而限制向全局作用域中添加過多的變量和函數。
(function() { var now = new Date(); if (now.getMonth() == 0 && now.gettDate() == 1) { console.log("Haapy new year!"); } })();
這種做法可以減少閉包占用的內存,因為沒有指向匿名函數的引用。只要函數執行完畢,就可以立即銷毀其作用域鏈了。
私有變量嚴格來講,JavaScript中沒有私有成員的概念;所有對象屬性都是共有的。不過倒是有一個私有變量的概念。
任何在函數中定義的變量,都可以認為是私有變量,因為不能在函數的外部訪問這些變量。
如果在函數內部創建一個閉包,那么閉包通過自己的作用域鏈可以訪問這些私有變量。利用這一點,我們就可以創建用于訪問私有變量的公有方法。
有權訪問私有變量和私有函數的公有方法稱為 特權方法(privileged method) 。有兩種創建特權方法的方式:
在構造函數中定義特權方法(靜態私有變量)
模塊模式
構造函數中定義,基本的模式如下
// 構造函數Person // 入參 name 是它的私有變量 function Person(name) { this.getName = function() { return name; }; this.setName = function(value) { name = value; }; } var person = new Person("Nicholas"); console.log(person.getName()); // "Nicholas" person.setName("Greg"); console.log(person.getName()); // "Greg"
這種模式有一個缺點,那就是必須使用構造函數來達到目的,而第6章討論過,構造函數模式的確定是針對每一個實例都會創建出同樣的一組新方法
而使用靜態私有變量來實現特權方法就可以避免這個問題。
靜態私有變量(function() { // 私有變量 var privateVariable = 10; // 私有函數 function privateFunction() { return false; } // 構造函數 // 這里沒有使用var操作符,自動創建全局變量 // 嚴格模式下不能使用 MyObject = function() {}; // 公有/特權方法 MyObject.prototype.publicMethod = function() { privateVariable++; return privateFunction(); }; })();
公有方法是在原型上定義的,避免了重復創建方法的情況。
需要注意的是,這個模式在定義構造函數時沒有使用函數聲明,而是使用了函數表達式。函數聲明只能創建局部函數,但那并不是我們想要的。
這個特權方法,作為一個閉包,總是保存著對包含作用域的引用。
(function() { var name = ""; Person = function(value) { name = value; }; Person.prototype.getName = function() { return name; }; Person.prototype.setName = function (value) { name = value; }; })(); var person1 = new Person("Nicholas"); console.log(person1.getName()); // "Nicholas" person1.setName("Greg"); console.log(person1.getName()); // "Greg" var person2 = new Person("Michael"); console.log(person1.getName()); // "Michael" console.log(person2.getName()); // "Michael"
這個例子中的Person構造函數與getName() setName() 方法一樣,都有權訪問私有變量name。
name變成了一個靜態的、由所有實例共享的屬性。
以這種方式創建靜態私有變量會因為使用原型而增進代碼復用,但每個實例都沒有自己的私有變量。
多查找作用域鏈中的一個層次,就會在一定程度上影響查找速度。而這正是使用閉包和私有變量的一個明顯的不足之處。
模塊模式模塊模式通過為單例添加私有變量和特權方法使其得到增強
這種模式在需要對單例進行某些初始化,同時又需要維護其私有變量時非常有用
var application = function() { // 私有變量和函數 var components = new Array(); // 初始化 components.push(new BaseComponent()); // 公共 return { getComponentCuont: function() { return components.length; }, registerComponent: function(component) { if (typeof component == "object") { components.push(component) } } }; }();
在WEB應用程序中,經常需要使用一個單例來管理應用程序級的信息。這個簡單的例子創建了一個用于管理組件的application對象。
簡而言之,如果必須創建一個對象并以某些數據對其進行初始化,同時還要公開一些能夠訪問這些私有數據的方法,那么就可以使用模塊模式。
以模塊模式創建的每個單例都是Object的實例,因為最終要通過一個對象字面量來表示他。
增強的模塊模式增強的模塊模式適合那些單例必須是某種類型的實例,同時還必須添加某些屬性和(或)方法對其加以增強的情況。
如果前述例子中的application對象必須是BaseComponent的實例,可以如下代碼
var application = function() { // 私有變量和函數 var components = new Array(); // 初始化 components.push(new BaseComponent()); // 創建 application 的一個局部副本 var app = new BaseComponent(); // 公共接口 app.getComponentCuont = function() { return components.length; }; app.registerComponent = function(component) { if (typeof component == "object") { components.push(component); } }; // 返回這個副本 return app; }();
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/109501.html
摘要:具體說就是執行流進入下列任何一個語句時,作用域鏈就會得到加長語句的塊。如果局部環境中存在著同名的標識符,就不會使用位于父環境中的標識符訪問局部變量要比訪問全局變量更快,因為不用向上搜索作用域鏈。 基本類型和引用類型的值 ECMAscript變量包含 基本類型值和引用類型值 基本類型值值的是基本數據類型:Undefined, Null, Boolean, Number, String ...
摘要:表示應該立即下載腳本,但不應妨礙頁面中的其他操作可選。表示通過屬性指定的代碼的字符集。表示腳本可以延遲到文檔完全被解析和顯示之后再執行。實際上,服務器在傳送文件時使用的類型通常是,但在中設置這個值卻可能導致腳本被忽略。 第1章 JavaScript 簡介 雖然JavaScript和ECMAScript通常被人們用來表達相同的含義,但JavaScript的含義比ECMA-262要多得多...
摘要:用戶代理檢測用戶代理檢測是爭議最大的客戶端檢測技術。第二個要檢測是。由于實際的版本號可能會包含數字小數點和字母,所以捕獲組中使用了表示非空格的特殊字符。版本號不在后面,而是在后面。除了知道設備,最好還能知道的版本號。 檢測Web客戶端的手段很多,各有利弊,但不到萬不得已就不要使用客戶端檢測。只要能找到更通用的方法,就應該優先采用更通用的方法。一言蔽之,先設計最通用的方案,然后再使用特定...
摘要:本質上是由一組無序名值對組成的。浮點數值的最高精度是位小數,但在進行計算時其精度遠遠不如證書。例如這是使用基于數值的浮點計算的通病,并非獨此一家數值范圍。 函數名不能使用關鍵字(typeof不行但typeOf可以,區分大小寫) 標識符就是指變量、函數、屬性的名字,或者函數的參數。 第一個字符必須是一個字母、下劃線(_)或者一個美元符號($) 其他字符可以是字母、下劃線、美元符號或...
摘要:引用類型的值對象是引用類型的一個實例。引用類型是一種數據結構,用于將數據和功能組織在一起。對數組中的每一項運行給定函數,如果該函數對任一項返回,則返回。組零始終代表整個表達式。所以,使用非捕獲組較使用捕獲組更節省內存。 引用類型的值(對象)是引用類型的一個實例。 引用類型是一種數據結構,用于將數據和功能組織在一起。它同行被稱為類,但這種稱呼并不妥當,盡管ECMAScript從技術上講...
閱讀 3207·2021-11-08 13:18
閱讀 1363·2021-10-09 09:57
閱讀 1193·2021-09-22 15:33
閱讀 3985·2021-08-17 10:12
閱讀 5073·2021-08-16 11:02
閱讀 2688·2019-08-30 10:56
閱讀 971·2019-08-29 18:31
閱讀 3259·2019-08-29 16:30