目錄javascript
定義函數的兩種方式:html
function func(arg0, arg1) { // 函數體 }
var func = function(arg0, arg1) { // 函數體 };
它們之間是有很大區別的:java
爲何?jquery
示例:編程
/** * 執行報異常:(intermediate value)(intermediate value)(...) is not a function(…) * 函數myMethod直接運行了,由於後面有一對括號,並且傳入的參數是一個方法。 * myMethod返回的42被當作函數名調用了,致使出錯了。 */ var myMethod = function() { console.log('myMethod run'); //執行 return 42; } // 這裏沒有分號 (function() { console.log('main run'); //未執行 })();
而函數聲明則不會,「是由於JavaScript將function關鍵字看作是一個函數聲明的開始,而函數聲明後面不容許跟圓括號。」 —— P185segmentfault
即在執行代碼以前會讀取函數聲明。數組
示例:瀏覽器
sayHi(); // 無誤,會出現函數聲明提高 function sayHi(){ console.log('Hi'); }
sayHi(); // TypeError: sayHi is not a function #此時函數還不存在 var sayHi = function(){ console.log('Hi'); }
這裏有一個經典的例子:閉包
/** * 表面上看是表示在condition爲true時,使用第一個定義,不然使用第二個定義 * 實際上,這在ECMAScript中屬於無效語法,Javascript引擎會嘗試修正錯誤,轉換到合理的狀態 * 不要這樣作,會出現函數聲明,某些瀏覽器會返回第二個聲明,不考慮condition的值 */ if(condition) { function func() { console.log('Hi.'); } } else { function func() { console.log('Hello.'); } }
下面是推薦的寫法,這樣就不會有什麼意外。編程語言
var func; if(condition) { func = function() { console.log('Hi.'); } } else { func = function() { console.log('Hello.'); } }
另外一個值得一提的是變量名提高:
var a = 2;
以上代碼其實會分爲兩個過程,一個是 var a;
一個是 a = 2;
其中var a;
是在編譯過程當中執行的,a = 2
是在執行過程當中執行的。
例:
console.log(a); // undefined var a = 2;
其執行效果其實是這樣的:
var a; console.log( a ); // undefined a = 2;
在編譯階段,編譯器會將函數裏全部的聲明都提早到函數體內的上部,而真正賦值的操做留在原來的位置上,這也就是上面的代碼打出undefined
的緣由。不然的話應該是報錯:Uncaught ReferenceError: a is not defined
下面根據經典的遞歸階乘函數的示例分析遞歸的使用。遞歸函數的使用能夠分爲如下幾個不一樣的境界:
function factorial(num) { if(num <= 1) { //每個遞歸都必須有一個終止條件 return 1; } return num * factorial(num-1); }
分析: 這個遞歸的調用正常使用沒什麼問題,可是當咱們將另外一個變量也指向這個函數,將原來的指向函數的引用變量賦爲null
會致使錯誤:
var anotherFactorial = factorial; factorial = null; anotherFactorial(10); //error, factorial已再也不是函數
function factorial(num) { if(num <= 1) { return 1; } return num * arguments.callee(num-1); }
分析: arguments.callee
是一個指向正在執行的函數的指針,比直接使用函數名保險。不過在嚴格模式下('use strict')
,訪問這個屬性會致使錯誤。
var factorial = (function f(num) { if(num <= 1) { return 1; } return num * f(num-1); });
分析: 通常函數表達式都是建立一個匿名函數,並將其賦值給變量——函數表達式(上述例子是不會進行函數聲明提高的)。
可是此處是建立了一個名爲f()
的命名函數表達式。即便賦值給了另外一個變量,函數的名字 f
仍然有效,因此遞歸不管是在嚴格模式仍是非嚴格模式下都照樣能正確完成。
console.log(factorial.name); // 'f'
閉包的定義:有權訪問另外一個函數的做用域中的變量的函數。也就是說,閉包是內部函數以及其做用域鏈組成的一個總體。
閉包主要有兩個概念:能夠訪問外部函數,維持函數做用域。
第一個概念並無什麼特別,大部分編程語言都有這個特性,內部函數能夠訪問其外部變量這種事情很常見。因此重點在於第二點。
建立閉包的常見方式:在一個函數內建立另外一個函數。
示例:
var globalValue; function outter() { var value = 1; function inner() { return value; } globalValue = inner; } outter(); globalValue(); // return 1;
先不考慮閉包地看一下這個問題:
outter
函數。outter
函數,調用函數的過程當中全局變量被賦值了一個函數。outter
函數調用結束以後,按照內存處理機制,它內部的全部變量應該都被釋放掉了,不過咱們把 inner
賦值給了全局變量,因此還能夠在外部調用它。outter
內部做用域已經被釋放了,因此應該找不到value
的值,返回的應該是undefined
。1
,即內部變量。本該已經消失了,只能存在於 out
函數內部的變量,走到了牆外。這就是閉包的強大之處。實際的執行流程:
outter
函數時,會建立一個預先包含全局變量對象的做用域鏈,保存在內部的 [[Scope]]
屬性中,以下圖。outter
函數時,會建立執行環境,而後經過複製函數的[[Scope]]
屬性中的對象構建執行環境的做用域鏈,並初始化函數的活動對象(activation object)。outter
函數執行完畢以後,其執行環境的做用域鏈被銷燬,但它的活動對象仍然會留在內存中。inner
函數的引用解除後,outter
函數的活動對象纔會被銷燬 (globalValue = null;)
。在某個構造函數中查看 [[Scope]]
屬性:
閉包會保存包含函數的活動對象:
這裏的例子除了書中的一個經典的例子外,在MDN上有一個更好的、更直觀的例子,參見 MDN 在循環中建立閉包:一個常見錯誤,示例以下
該瀏覽器不支持iframe
數組 helpText
中定義了三個有用的提示信息,每個都關聯於對應的文檔中的輸入域的 ID
。經過循環這三項定義,依次爲每個輸入域添加了一個 onfocus
事件處理函數,以便顯示幫助信息。
運行這段代碼後,您會發現它沒有達到想要的效果。不管焦點在哪一個輸入域上,顯示的都是關於年齡的消息。
該問題的緣由在於賦給 onfocus
是閉包(setupHelp)中的匿名函數而不是閉包對象;在閉包(setupHelp)中一共建立了三個匿名函數,可是它們都共享同一個環境(item)。在 onfocus
的回調被執行時,循環早已經完成,且此時item
變量(由全部三個閉包所共享)已經指向了 helpText
列表中的最後一項。
解決這個問題的一種方案是使onfocus
指向一個新的閉包對象。
該瀏覽器不支持iframe
這段代碼能夠如咱們所指望的那樣工做。全部的回調再也不共享同一個環境, makeHelpCallback
函數爲每個回調建立一個新的環境。在這些環境中,help
指向 helpText
數組中對應的字符串。
上面的代碼至關於將每次迭代的item.help
複製給參數argus
(由於函數參數都是按值傳遞的),這樣在匿名函數內部建立並返回的是一個訪問的這個argus
的閉包。
document.getElementById(item.id).onfocus = function(argus) { return function() { showHelp(argus); }; }(item.help);
尤爲是當在閉包中只使用包含函數的一部分變量,能夠釋放無用的變量。例如:
var foo = function(){ var elem = $('.demo'); return function(elem.length){ // 函數體 } }
改寫爲:
var foo = function(){ var elem = $('.demo'), len = elem.length; elem = null; // 解除對該對象的引用 return function(len){ // 函數體 } }
this
對象是在運行時基於函數的執行環境綁定的:
this
等於window
this
等於這個對象this
等於window
示例1:
var name = 'The Window'; var obj = { name: 'My Object', getNameFunc: function(){ return this.name; } } console.log(obj.getNameFunc()); // My Object
示例2:
var name = 'The Window'; var obj = { name: 'My Object', getNameFunc: function(){ var name = "shih"; return this.name; } } console.log(obj.getNameFunc()); // My Object
示例3:
var name = 'The Window'; var obj = { name: 'My Object', getNameFunc: function(){ return function(){ // 匿名函數的執行環境具備全局性 return this.name; } } } console.log(obj.getNameFunc()()); // The Window
還有一個例子:(obj.getNameFunc = obj.getNameFunc)(); // The Window
this永遠指向的是最後調用它的對象,匿名函數的執行環境具備全局性,匿名函數的調用者是window.
疑惑:匿名函數的this指向爲何是window —— 對於返回的閉包(匿名函數)與函數表達式建立的匿名函數?
下面的例子是一個測試,其中obj2定義這兩種匿名函數,執行結果在註釋中,this
對象都是指向 Window
。
var name = 'The Window'; var obj = { name: 'My Object', getNameFunc0: function(){ return this.name; // "My Object" }, obj2: { // obj2 對象中沒有定義 name getNameFunc1: function(){ var func = function(){ console.group('getNameFunc2 func Anonymous'); console.log(this); // Window console.groupEnd(); }; func(); console.group('getNameFunc'); console.log(this); // Object console.groupEnd(); return this.name; // undefined }, getNameFunc2: function(){ return function(){ console.group('getNameFunc2 Anonymous'); console.log(this); // Window console.groupEnd(); return this.name; // "The Window" } } } }; console.log(obj.getNameFunc0()); // "My Object" console.log(obj.obj2.getNameFunc1()); // undefiend console.log(obj.obj2.getNameFunc2()()); // "The Window"
在下面的例子中,在 C++、Java等編程語言中,變量 i
只會在for循環的語句塊中有定義,循環結束後就會被銷燬。可是在JavaScript中,變量 i
是定義在outputNumbers()
的活動對象中的,從它定義的地方開始,在函數內部均可以訪問它。
示例:
function outputNumbers(count){ for(var i=0; i<count; i++){ // 代碼塊 } console.log(i); // i = count }
從新聲明變量時,JavaScript會忽略後續的聲明。可是執行後續聲明的變量初始化。
function outputNumbers(count){ for(var i=0; i<count; i++){ // 代碼塊 } var i; //從新聲明變量,會被忽略 console.log(i); //i = count }
function outputNumners(count){ (function(){ //閉包 for(var i=0; i<count; i++){ // 代碼塊 } })(); console.log(i);//Error: i未定義 }
不管在什麼地方,只要臨時須要一些變量,就可使用這種私有做用域。由於沒有指向該匿名函數的引用,因此只要函數執行完畢,就能夠當即銷燬其做用域鏈。所以能夠減小閉包占用的內存問題。
以下面幾種狀況:
var obj = {a: 2, b: 3, c: 4}; with(obj) { // 均做用於obj上 a = 5; b = 5; }
let
是ES6新增的定義變量的方法,其定義的變量僅存在於最近的{}
以內
var foo = true; if (foo) { let bar = foo * 2; console.log(bar); // 2 } console.log(bar); // ReferenceError
與let
同樣,惟一不一樣的是const
定義的變量值不能修改。
var foo = true; if (foo) { var a = 2; const b = 3; // 僅存在於if的{}內 a = 3; b = 4; // 出錯,值不能修改 } console.log(a); // 3 console.log(b); // ReferenceError
嚴格來講,JavaScript中沒有私有成員的概念,全部的對象屬性都是公開的,可是有私有變量的概念。任何在函數中定義的變量均可以認爲是私有變量。
私有變量包括:函數的參數、局部變量、在函數內部定義的其餘函數。
由於函數外部不能訪問私有變量,而閉包可以經過做用域鏈能夠訪問這些變量。因此能夠建立用於訪問私有變量的公有方法 —— 特權方法(privileged method)
。
有幾種建立這種特權方法的方式:
function MyObject(){ // 私有變量和私有函數 var privateVariable = 10; function privateFunction(){ return false; } // 公有方法,能夠被實例所調用 this.publicMethod = function(){ ++privateVariable; return privateFunction(); }; }
這種模式的缺點是,針對每一個實例都會建立一組相同的方法。
//建立私有做用域,並在其中封裝一個構造函數和相應的方法 (function(){ //私有變量和私有函數 var privateVariable = 10; function privateFunction(){ return privateVariable; } //構造函數,使用的是函數表達式,由於函數聲明只能建立局部函數 MyMethod = function(){ }; //公有方法 MyMethod.prototype.publicMethod = function(){ ++privateVariable; return privateFunction(); }; })();
公有方法是在原型上定義的。這個模式在定義構造函數時並無使用函數聲明,而是使用了函數表達式,這是由於函數聲明只能建立局部函數,這不是咱們想要的。一樣,在聲明MyObject
時也沒有使用var關鍵字,由於直接初始化一個未經聲明的變量,總會建立一個全局變量。所以MyObject
就成了一個全局變量,可以在私有做用域以外被訪問到。但值得注意的是,在嚴格模式('use strict')
下,給未經聲明的變量賦值會致使錯誤。
這個公有方法做爲一個閉包,老是保存着對做用域的引用。與在構造函數中定義公有方法的區別是:由於公有方法是在原型上定義的,全部實例都使用同一個函數,私有變量和函數是由實例所共享的。但上面的代碼有個缺陷,當建立多個實例的時候,因爲變量也共享,因此在一個實例上調用publicMethod
會影響其餘實例。以這種方式建立的靜態私有變量會由於使用原型而增長代碼的複用,但每一個實例都沒有本身的私有變量。究竟是使用實例本身的變量,仍是上面這種靜態私有變量,須要視需求而定。
正是因爲上述緣由,咱們不多單獨使用原型模式,一般都是將構造函數模式結合原型模式一塊兒使用。
以上的模式都是給自定義類型建立私有變量和特權方法的。而這裏所說的模塊模式則是爲單例建立私有變量和特權方法,加強單例對象。
單例模式是指只有一個實例的對象。
JavaScript推薦使用對象字面量的方式建立單例對象:
var singleton = { name: 'value', method: function(){ // 代碼塊 } };
var singleton = function(){ // 私有變量和私有函數 var privateVariable = 10; function privateFunction(){ return false; } // 公有方法:返回對象字面量,是這個單例的公共接口。 return{ publicProperty: true, publicMethod: function(){ ++privateVariable; return privateFunction(); }; } }
「若是必須建立一個對象並以某些數據對其進行初始化,同時還要公開一些可以訪問這些私有數據的方法,就可使用模塊模式。」 —— P190
var singleton = function(){ // 私有變量和私有函數 var privateVariable = 10; function privateFunction(){ return false; } // 建立一個特定的對象實例 var object = new CustomType(); // 添加屬性和方法 object.publicProperty = true; object.publicMethod = function(){ ++privateVariable; return privateFunction(); }; return object; }
建立一個特定類型的實例,即適用於那些單例必須是某種特定類型的實例,同時還須要對它添加一些屬性和方法加以加強。
閉包:
var foo = function(){ // 聲明一些局部變量 return function(){ // 閉包 // 能夠引用這些局部變量 } } foo()(); // 能夠對foo函數內的局部變量進行操做,具體方法在閉包函數的定義中
即時函數:
(function(){ // 執行代碼 })();
相同點:它們都是函數的一種特殊形態,而且能夠共存。
不一樣點:即時函數是定義一個函數,並當即執行。它只能被使用一次,至關於「閱後即焚」。它是爲了造成塊級做用域,來彌補js函數級做用域的侷限,主要是爲了模塊化,不少庫都這麼來解決耦合,並且考慮到沒有加分號 ;
會致使錯誤的緣由,不少庫都會在開始處加上 ;
。
好比jquery.media.js
:
; (function($) { "use strict"; var mode = document.documentMode || 0; var msie = /MSIE/.test(navigator.userAgent); var lameIE = msie && (/MSIE (6|7|8)\.0/.test(navigator.userAgent) || mode < 9); // ... })(jQuery);
閉包是指一個函數與它捕獲的外部變量的合體。用來保存局部變量,造成私有屬性與方法,好比module模式。