1、定義:java
1.函數聲明 function func () {} 數組
2.函數表達式 var func = function () {} 瀏覽器
注意點:緩存
var func = function test () {} func(); // ok test(); // 報錯, test isn't defined
匿名函數表達式 和 命名函數表達式 區別閉包
(1)命名函數表達式app
function test () {} console.log(test.name); // test
(2)匿名函數表達式函數
var test = function func () {} console.log(func.name); // func is not defined console.log(test.name); // func
2、return做用this
1.返回通過函數一系列處理的結果值spa
2.終止函數的運行code
3、實參傳遞的數目和設定的形參數目相比,可多,可少,都不算錯
爲何? 由於函數的形式上下文(一個對象)中有個名爲arguments屬性,其值爲一個類數組,儲存着全部傳遞過來的實參,因此調用函數傳實參時直接將全部實參按形參名做爲屬性名存入argumengs這個類數組中,而不會去在乎實參的數目和形參設定的數目是否同樣
可經過funcName.length 查看形參數目, 經過arguments.length 查看實參數目
4、做用域
一、定義:變量(變量做用域又稱爲上下文)和函數生效(能被訪問)的區域
2.做用於相關定義:
(1)執行期上下文:當函數執行時,會建立一個稱爲執行期上下文的內部對象。一個執行期上下文定義了一個函數執行時的環境,函數每次執行時對應的執行上下文都是獨一無二的,因此屢次調用一個函數會致使建立多個執行上下文,當函數執行完畢,本身的執行上下文被銷燬(注意僅僅銷燬本身自己的執行期上下文)。
(2)查找變量:從做用域鏈(scope chain的頂端依次向下查找。
三、經過函數來簡單理解的做用域
以下js代碼:
function a() { var aa = 'aa'; function b() { var bb = 'bb'; console.log(aa);
console.log(b); } return b; } var demo = a(); demo(); // aa function b(){...}
首先,在a函數定義時會生成一個將全局執行期上下文存入scope chain(做用域鏈),但不會產生本身自己的執行期上下文。
而後,在a函數執行後,會再次造成一個新的屬於本身的scope chain(做用域鏈),同時將全局執行期上下文(執行期上下文是一個對象,爲引用值)存入剛造成的scope chain頂部,而後再將本身自己的執行期上下文存入剛造成的scope chain頂部,也可這樣理解,scope chain是一個數組,如今0序號位置存的是a函數本身自己的執行期上下文引用,1序號位置存的是全局執行器上下文引用(從數組頭部插入)
(若在a函數在內部訪問一個變量,則去scope chain中尋找該變量,先從0位置開始尋找,若0位置存儲的執行期上下文沒有該變量,則再看1序號位置存儲的,直到找到該變量爲止,最壞的狀況,scope chain 中沒有改變量的信息,則確定是報錯了唄,嘿嘿),
咱們須要注意一個狀況,在a函數執行的過程當中在a函數內定義了一個函數b,因此b函數會和a函數同樣造成scope chain,再將全局執行期上下文的引用、a的執行器上下文的引用按順序存入scope chain中
(只有a函數執行,纔會產生a函數本身自己的執行器上下文,纔會b函數被定義, 也正由於如此,b函數定義時才能將a函數的執行期上下文存入本身的scope chain中, 因此最終在b函數內部才能打印出b函數,這是串行操做,中間一個環節斷了都不行,這是個人我的理解,僅限參考,嘿嘿)
(最後最重要的,a函數執行的最終結果是將定義好的b函數返回給demo,此過程是將b函數的引用賦值給demo(因此demo函數執行打印的是b函數),須要注意的是,此賦值過程同時是變量demo將b函數定義時產生的scope chain也一樣完整的繼承了過去,即demo的scope chain中1和0序號位置分別存儲了全局執行期上下文的引用的函數a的執行期上下文的引用,因此demo函數才能打印出a函數內定義的變量aa和a函數內定義的函數b)。
( 注意這裏的存入,值得是scope chain該索引位置存的是執行期上下文對象的引用,能夠形象的想象爲scope chain該索引位置用箭頭指向了執行期上下文)
最後一點,其實其中所說的執行期上下文就是預編譯中所建立的AO對象(Activation Object),不懂預編譯的能夠去了解一下,將做用域和預編譯一塊兒理解感受會好一些
5、閉包
一、定義:當內部函數(在函數內定義的函數)被保存到外部時,將會生成閉包。這是最多見的一種 狀況而已,廣義來講,就是一個做用域引用或保存了另外一個做用域的值,致使另外一個做用域 本該銷燬被沒法銷燬,這就是閉包。
二、閉包是這樣的:
仍然經過上邊的例子解釋:
function a() { var aa = 'aa'; function b() { var bb = 'bb'; console.log(aa); console.log(b); } return b; } var demo = a(); demo(); // aa function b(){...}
首先,問一個問題?大家是否發如今a函數中完畢後,按定義說a的執行器上下文應該被銷燬啊,那爲何demo執行後仍然能打印出aa和b函數呢?
緣由就是造成了閉包,致使a函數執行結束後a的執行期上下文沒有被銷燬,全部demo執行仍能正常打印,也就是因爲函數b被return出來給了demo,使demo和a的執行期上下文有聯繫,也就是demo佔用了a的執行期上下文,因此a執行完畢後a的執行期上下文不會被銷燬,最重要的是demo執行結束後,demo自己的執行期上下文被銷燬,但其鏈接的a的執行期上下文中存儲了b函數,即b函數的引用(至關於demo),因此a的執行期上下文永遠不會被銷燬(能夠經過賦值demo爲null,表示demo不佔用a的執行期上下文,那麼a的執行期上下文就能夠被銷燬了),這就造成了閉包,這就是閉包的基本原理。
3.缺點:閉包會致使原有做用域鏈不釋放,形成內存泄露。
解釋: 上面原本a函數執行完畢後其執行期上下文應該被銷燬的,但由於閉包緣由沒有被銷燬,因此致使可以使用的總內存量減小,即內存泄漏。
4.簡單應用:
(1)實現公有變量:
eg: 函數累加器
// 閉包造成的累加器,變量num就相似java中說的共有變量 function bibao() { var num = 0; return function () { console.log(num++); } } var add1 = bibao(); add1(); //0 add1(); //1 add1(); //2 add1(); //3 add1(); //4
有於閉包緣由,致使bibao函數的執行期上下文不會被銷燬,因此num一直能夠在以前基礎上累加
(2)能夠作緩存:
// 兩個函數公用eater函數的做用域鏈 function eater() { var food; var obj = { eat: function () { if (food) { console.log('I eat ' + food); food = null; }else { console.log('There is nothing'); } }, push: function (myFood) { food = myFood; } } return obj; } var oEater = eater(); oEater.eat(); //There is nothing oEater.push('apple'); oEater.eat(); //I eat apple
(3)、實現封裝,屬性私有化
又是咱們須要這樣一些需求,咱們但願函數內部的某些屬性沒法被外部寫,僅僅只能被外部讀而已等等。
最初,你們是共同制定了一個約定,那就是在函數內部不但願被外部寫的屬性錢加一個下劃線,標示着該屬性不能被其餘人更改,可是該約定約束性不強。
後來,你們想到了一個方法,那就是用閉包來實現屬性的私有化,此方法具備很強的約束性。以下demo舉例:
function demo () { var num = 1, name = 'lyl'; // 和返回一個函數造成的閉包是同樣的,只不過此處是返回多個函數,且多個函數用一個對象包裝了。 // 該對象中的多個函數都佔用了demo執行時產生的執行期上下文,致使demo執行時產生的執行期上下文不會被銷燬,從而產生閉包 return { sayName: function () { console.log(name); }, sayNum: function () { console.log(num); } } } var obj = demo(); obj.sayName(); // lyl console.log(obj.num); //undefined
5.閉包的防範:閉包會致使多個執行函數共用一個公有變量,若是不是特殊須要,應儘可能防止這種狀況發生。
以下例子:arr數組中多個值公用一個test函數執行期上下文的變量 i,致使打印結果超出意料
function test() { var arr = []; for(var i = 0; i < 10; i++ ) { //arr中全部值對應一個執行期上下文,其中的i是隨循環不斷累加變化的,最終i固定爲10不變,因此打印出的都爲10 arr[i] = function () { console.log(i); } } return arr; } var retArr = test(); for(var i = 0, len = retArr.length; i < len; i++ ) { retArr[i](); } // 打印結果是10個10
解決方法: 利用當即執行函數(下面會講),讓arr數組中每一個值都對應一個獨立的執行期上下文,避免多個值公用一個執行期上下文的變量
function test() { var arr = []; for(var i = 0; i < 10; i++) { (function(j) { // 每次都將對應的i轉換爲j,因此每次arr[j]對應一個本身獨有的執行期上下文,其中的j是固定不變的,因此能從0打印到9 // 即arr數組中有10個數,則分別對應10個獨立的執行期上下文 arr[j] = function () { console.log(j); } } (i) ); } return arr; } var retArr = test(); for(var i = 0, len = retArr.length; i < len; i++) { retArr[i](); } // 打印結果爲0 1 2 3 4 5 6 7 8 9
6、當即執行函數
1.定義:此類函數沒有聲明,在一次執行事後即釋放。適合作初始化工做。(即此函數不需調用,直接執行)
二、使用形式以下:
// 當即執行函數基本形式 // 第一種,推薦 (function (形參) { //handle code } (實參) ); // 第二種 (function (形參) { // handle code } )(實參); // demo var ret = (function (a, b) { return a + b; } (1, 2) ); console.log(ret); //3
三、當即執行函數的原理:
爲何這樣寫,就能夠執行了?大概原理以下:
其實重點在於小括號上,小括號()表明了執行,而()前放什麼才能夠執行呢?答案是表達式。 那表達式是什麼呢?權威指南上關於表達式有這樣一句話‘表達式是JavaScript的一個短語,JavaScript解釋器會將其計算出一個結果’,簡單來講只要是某一個類型的值,就能夠看做表達式,固然此處關於表達式的理解是不全面的,但對於當即執行函數的當即來講是足夠的了。因此函數執行就能夠解釋了,函數名是該函數的引用,爲一個函數表達式,因此加上()後也就能夠執行了。下面根據此理論來解釋當即執行函數:
凡是()圈起來的都是表達式,因此(function (){})爲一個表達式,而後再加上一個()則能夠執行,因此實參放入後面那個括號中,形參放於function後的括號中也就合理了;而(function() {} ())此種形式能夠算做當即執行函數的一個標準寫法,省去了function (){}轉換爲表達式的時間,執行能更快一些。
除了經常使用的一些正規寫法外,利用其原理,還有一些其餘寫法,一樣能實現當即執行函數的效果。只要()前是表達式便可。以下:
// 成功的demo var demo1 = function func() {console.log('demo1')}(); //demo1, demo1被賦值爲實名函數表達式 var demo2 = function () {console.log('demo2')}; // demo2, demo2被複製爲匿名函數表達式 +function (){console.log('demo3')}; // demo3, 經過+運算符的隱士類型轉換將+後面的轉換爲number原始表達式,因此也就能夠執行了 console.log(typeof +function (){}); //number -function (){console.log('demo4')}; // demo4, 經過-運算符的隱士類型轉換將-後面的轉換爲number原始表達式,因此也就能夠執行了 console.log(typeof -function (){}); //number !function (){console.log('demo5')}; // demo5, 經過+運算符的隱士類型轉換將+後面的轉換爲boolean原始表達式,因此也就能夠執行了 console.log(typeof !function (){}); //boolean 1 && function (){console.log('demo6')}(); // demo6, 經過&&運算符的隱士類型轉換將&&後面的轉換爲boolean原始表達式,因此也就能夠執行了,但此處須要注意一點,
&&運算符僅僅判斷是將值隱士轉換爲boolean類型的,但返回的是原值,由於都斷定爲true因此返回&&後面的原始值,不然返回&&前面的原始值 0 || function () {console.log('demo7')}(); // demo7, ||隱士類型轉換爲boolean原始表達式,因此能夠執行,0 斷定失敗, 看||後面的,||後面的斷定爲true,返回該函數因此執行 // 失敗的須要注意的demo *function (){console.log('demo1')}(); // 報錯,覺得*沒法隱士類型轉換 1 || function (){console.log('demo2')}(); // 報錯,由於1斷定爲true,因此不看||後面的,直接返回1,因此沒法執行 0 && function (){console.log('demo3')}(); // 報錯,由於0斷定爲false,因此不看&&後面的,直接返回0,因此沒法執行
7、預編譯:
一、js運行三部曲:
(1)、語法分析(通篇分析查找是否存在低級語法錯誤,低級語法錯誤是直接能夠看出的,不用執行就能夠找出的)
*低級語法錯誤:
console.log('aa'); console.log('a'; //低級語法錯誤,不用執行就能夠直接看出的 |* 影響上面代碼的執行,也影響下面代碼的執行 // 執行結果: missing ) after argument list
*邏輯語法錯誤:
console.log('aa'); console.log(a); //邏輯語法錯誤,沒法直接看出,須要執行瀏覽器執行才能找出 |*不影響上面代碼的執行,但影響下面代碼的執行 //執行結果: //aa //a is not defined(…)
(2)、預編譯
*預編譯前奏:
i、imply global 暗示全局變量:即任何變量,若是變量未經聲明就賦值,此變量就爲全局對象全部
function test() { var a = b = 10; } test(); console.log(b); // 10 console.log(a); // a is not defined(…) // 此demo存在一個坑, 就是連續賦值 var a = b = 10; // 該賦值能夠分解爲兩條語句,分別爲b = 10; var a = b; // 因此b歸全局對象全部,而a歸test執行時的執行期上下文全部, //外部全局對象window訪問不了test內部的a變量,而且test執行結束後a會隨着test的執行期上下文銷燬而消失
ii、一切聲明的全局變量,全是window的屬性。
var a = 10; console.log(window.a); // 10
(題外話:關於此處有關於對象的一個知識點,關聯一下, delete操做符能夠刪除對象的屬性,因此未經聲明就賦值的變量爲window對象的屬性,能夠用delete操做符刪除,但在全局環境中用var聲明的變量雖然能夠用window.name訪問,但沒法用delete操做符刪除)(能夠這樣理解,不用var聲明直接爲變量賦值的行爲是爲全局對象的屬性賦值的操做,而用var聲明的變量是真正的咱們所說的變量,而不是某個對象的屬性,固然該變量也是當前環境下this的屬性)
*預編譯四部曲:
i、建立AO(Activation Object)對象
ii、找形參和變量聲明,將變量和形參名做爲AO屬性名,值爲undefined
iii、將實參值和形參統一
iiii、在函數體裏面找函數聲明,值賦予函數體
var a = 20; function test () { console.log(a); // 此處一個小坑 if(false) { var a = 10; } } test(); // 20 or undefined? answer is undefined // 預編譯時與if判斷的正誤沒有關係,只有在解釋執行時纔會在意if判斷的正誤
(3)、解釋執行
------------------------------end