四、Function對象
1. 匿名函數
- 什麼是:定義函數時,不指定函數名的函數
- 爲什麼:2大優點
- 節約內存
- 避免產生全局變量,造成全局污染
- 何時用:2種情況
- 幾乎所有回調函數都要定義爲匿名函數
- 匿名函數自調用
- 如何用:2種情況
- 回調函數
- 什麼是:自己定義了函數,但是不由自己調用,而是交給另一個函數,由另一個函數按需自動調用-----給別人用的函數
舉例1:想將一個數字內容的數組升序排列arr.sot(function(a,b){return a-b})
補充:
sort() 方法,有一個可選參數,必須是函數,供它調用。那麼就是個回調函數咯!回調函數的參數要有兩個:第一個參數的元素肯定在第二個參數的元素前面!!!這個方法的排序是看回調函數的返回值: 如果返回值大於 0,則位置互換。 如果返回值小於 0,則位置不變。
舉例2:想根據不同的敏感詞,動態選擇不同的新值替換str=str.replace(/正則表達式/,function(keyword){return 不同新值})
- 爲什麼回調函數都要定義爲匿名函數:爲了節約內存!
- 什麼是:自己定義了函數,但是不由自己調用,而是交給另一個函數,由另一個函數按需自動調用-----給別人用的函數
- 匿名函數自調:
- 什麼是:定義一個匿名函數後,立刻調用該函數執行,調用後立即釋放
- 爲什麼:避免產生全局變量,造成全局污染
- 何時用:今後一切js代碼都應該放在一個大的匿名函數自調內!儘量不要使用全局變量!
- 如何做:
- 標準寫法:
創建新函數,立刻調用執行var 變量名=(function(形參變量列表){ ...return 返回值 })(實參值列表);
因爲該函數沒有名字,所以調用後,立刻釋放 - 非主流寫法:
+function(){...}()
!function(){...}()
…
強調:結尾的(),必須要加,表示調用
- 標準寫法:
- 示例:使用匿名函數自調避免全局污染
輸出結果:<script> // 全局變量,記錄程序執行的總時間 var t=0; // 第一段程序:執行0.3s !function(){ t+=0.3; console.log(`任務一耗時0.3s`); }(); // 假設之後在此處添加了以下代碼,這裏出錯了,需要記錄出錯時間 (function(){ // 局部變量,碰巧和全局變量重名了,也不會影響到全局變量 var t=new Date(); console.log(`這裏出錯了,在${t.toLocaleString()}`) })(); // 第二段程序:執行0.8s +function(){ t+=0.8; console.log(`任務二耗時0.8s`); }(); console.log(`共耗時${t}s`); </script>
任務一耗時0.3s
這裏出錯了,在2020/5/4 下午8:21:04
任務二耗時0.8s
共耗時1.1s
- 回調函數
2. 作用域和作用域鏈
- 什麼是作用域(scope):
- 從作用是說:作用域就是一個變量的可用的範圍
- 從本質上說:作用域其實是保存變量的特殊對象
- 爲什麼用作用域:爲了避免不同範圍之間的變量互相干擾
- JS中包括2級作用域
-
全局作用域
- 什麼是:在程序任何位置都可以隨意訪問的,專門保存全局變量的存儲空間
- 在瀏覽器中,全局作用域由window對象擔當
- 當打開網頁的一剎那,瀏覽器就會自動提前創建window對象,等待着保存後續創建的所有全局變量
- 我們創建的所有全局變量都默認保存在window對象中
- 保存在window對象中的變量,可在程序的任何位置被訪問到
- 示例:驗證全局變量到底存在哪裏
輸出結果:<script> // window var a=10; var fun=function(){console.log(1)}; var arr=[1,2,3]; console.log(a); console.log(fun); console.log(arr); console.log(window); console.log(window.a); console.log(window.fun); console.log(window.arr); </script>
10
ƒ (){console.log(1)}
(3) [1, 2, 3]
Window {parent: Window, opener: null, top: Window, length: 0, frames: Window, …}
10
ƒ (){console.log(1)}
(3) [1, 2, 3]
執行過程:
-
函數作用域
-
什麼是:專門保存僅函數內可用的局部變量的存儲空間
-
JS中一個函數的作用域不調用是不存在的!只有調用函數時,才臨時創建該函數的作用域,臨時保存局部變量,函數調用後,函數作用域對象以及本次函數調用使用的局部變量一同釋放。
-
行數作用域對象創建和釋放的過程----4步
-
當定義一個函數時:
都會創建一個函數對象,在每個函數對象上都會保存一個作用域鏈(好友列表)
-
當調用函數時:
- 會臨時創建一個函數作用域對象
- 在函數作用域對象中添加函數中所需要的的所有局部變量:這裏的局部變量包含2種情況
a. 在函數內var出的變量
b. 函數的形參變量 - 將函數作用域對象的地址,臨時保存到函數作用域鏈(好友列表)中離函數近的格子中
-
函數執行過程中:
函數會按照先局部,在全局的順序,使用變量
如果局部有要用的變量,則優先使用局部的變量
除非函數作用域中沒有要用的局部變量,才被迫去下一級作用域鏈—即此處的window中找變量使用
-
調用函數後:
函數會釋放函數作用域對象,導致函數作用域對象中的所有局部變量一同釋放,所以,所有局部變量都不可重用
-
-
示例:判斷程序的輸出結果
<script> //例子1: // var a=10; // function fun(){ // var a=100; // a++; // console.log(a); // } // fun();//101 // console.log(a);//10 //例子2: // var a=10; // function fun(){ // a=100; // a++; // console.log(a); // } // fun();//101 // console.log(a);//101 //例子3: var a=10; // 此處a爲形參,函數調用完就釋放了 function fun(a){ a=100; a++; console.log(a); } fun();//101 console.log(a);//10 </script>
-
-
- 作用域鏈(scope chain)
-
什麼是:由多級作用域對象鏈接起來組成的數據結構,就叫作用域鏈----即上面的好友列表
-
保存了一個函數可用的所有變量
-
控制着變量的使用順序:先局部,後全局
-
3. 閉包
-
什麼是閉包:
- 從作用上來說:既重用一個變量,又保護變量不被污染的一種編程方式
- 從本質上來講:外層函數的作用域對象,被內層函數引用着而無法釋放,就形成了閉包對象
-
爲什麼要用閉包:因爲js中,全局變量和局部變量都有不可兼得的優缺點
- 全局變量:
優點:可重用;缺點:極易被污染和篡改----開發中幾乎禁止使用全局變量 - 局部變量:
優點:因爲只能在函數內使用,出了函數就用不了,絕對不會被篡改;缺點:不可重用
- 全局變量:
-
何時用:今後只要想重用一個變量,而且還想保護這個變量不會被別人篡改是,都要用閉包!
-
如何用:3步
- 用外層函數包裹要保護的變量和內層函數
- 外層函數將內層函數對象返回到外部
- 想使用內層函數的人,調外層函數,就可獲得返回出來的內層函數的對象,再保存在變量中。就可反覆使用內層函數對象了
-
結果:外層函數與內層函數之間的這個被保護的變量,既可以反覆使用,又不會被篡改
-
示例:需求定義函數,爲小孩保管壓歲錢
<script> // 需求:定義一個函數,爲小孩保管壓歲錢 // 小孩兒每花一筆錢,可以從總錢數中扣除花的錢,提示還剩xxx // 3步 // 1. 定義一個外層函數,包裹要保護的變量和內層函數 function parent(){ var total=1000; // 2. 外層函數將內層函數對象返回到函數外部,讓外部可用 // 步用給內層函數起名字 return function(money){ total-=money; console.log(`花了${money},還剩${total}`) } } // 3. 想用內層函數的人需要調用外層函數,才能獲得返回出來的內層函數,保存在變量中,反覆使用 var pay=parent(); // 試圖篡改total變量 total=0; console.log(total);//輸出0,但是不影響函數內的局部變量total pay(100);//花了100,還剩900 pay(100);//花了100,還剩800 pay(100);//花了100,還剩700 </script>
-
閉包原理:筆試題:一句話概括閉包是如何形成的
外層函數調用後,由於外層函數的作用域對象被內層函數對象引用着,無法釋放形成了閉包!
-
閉包的缺點:比一般的函數多佔用一塊內存空間----多佔外層函數的作用域對象(閉包對象)
-
解決方法:當閉包不在使用時,要儘早釋放閉包!
pay=null;
----內存函數對象被釋放,導致外層函數的作用域也被釋放` -
筆試題:如何判斷閉包問題的輸出:找2個東西
-
找處於外層函數和內層函數之間的變量----要保護的變量
-
找外層函數共向外跑出來幾個內層函數對象:3種情況
- 通過return返回一個內存函數
- 通過給全局變量賦值,將內層函數,賦值到外部的變量中
- 還是用return或賦值的方式。返回一個數組或一個對象,但是數組或對象中包含函數成員:
例如:var arr=[]; arr[0]=function(){...}; arr[1]=function(){...}; arr[2]=function(){...}; return arr;//多黃蛋
-
結論:包裹的多個內層函數共用一個受保護的變量----媽媽一次生的多個孩子共用同一個受保護的變量
-
示例:畫簡圖分析閉包程序的輸出結果:
<script> function fun(){ var n=999; // js語法規定:任何時候給一個未聲明過的變量賦值,不會報錯!而是自動在全局創建該變量 nAdd=function(){n++}; return function(){ console.log(n); } } var getN=fun(); getN();//999 nAdd(); getN();//1000 </script>
-
如果第二次調用外層函數:則會創建全新的一個閉包對象和變量,重新生成一組內層函數對象。與第一次調用外層函數生成的閉包,毫無關係。
-
總結: Function
- 創建函數: 3種:
- 聲明方式: 缺點: 會被聲明提前
function 函數名(形參變量列表){ 函數體; return 返回值 }
- 賦值方式----函數表達式: 優點: 不會被聲明提前
var 函數名=function (形參變量列表){ 函數體; return 返回值 }
- 用new: 幾乎不用
var 函數名=new Function("形參1", "形參2",...,"函數體; return 返回值")
- 聲明方式: 缺點: 會被聲明提前
- 重載:
- 何時: 一件事可根據傳入的實參值不同,可自動選擇執行不同的邏輯
- 如何:
function 一個函數( 不要形參 ){ // arguments{ , , , }.length // 0 1 2 ... //通過判斷arguments中的實參值個數或實參內容不同,來決定執行何種邏輯 }
- 匿名函數:
- 何時: 回調函數 以及 匿名函數自調
- 匿名函數自調:
- 今後幾乎所有js代碼都要放在匿名函數自調中。避免使用全局變量
- 如何:
a. 標準寫法:
b. 殺馬特寫法:var 返回值=(function(形參列表){ 函數體; return 返回值; })(實參值列表);
+function(){ ... }()
!function(){ ... }{}
... ...
- 作用域和作用域鏈:
- Js中只有兩種作用域:
- 全局作用域對象window:
1). 自動保存所有全局變量
2). 在程序的任何位置都可訪問 - 函數作用域對象:
- 只在調用函數時創建,調用後自動釋放
- 函數作用域對象中專門保存函數的局部變量: 2種:
i. 形參
ii. 函數內var出的變量
- js中和其它語言的差別之一: 沒有塊級作用域:
1). 什麼是塊作用域: 分支結構(if else if else)或循環結構(for while do while switch)的{},在其它編程語言中稱爲塊級作用域
2). 在其它編程語言中,分支結構或循環結構{}內聲明的變量,出了{},不能使用!
3). Js中,因爲沒有塊級作用域,所以,在分支結構或循環結構{}中聲明的變量,出了{},依然可以使用
4). 示例: 有沒有塊作用域情況下,程序不同的寫法<script> // js中 // 如果隨機生成一個數是偶數 if(parseInt(Math.random()*10)%2==0){ var result="偶數"; }else{ var result="奇數"; } console.log(result); // 在其他有塊級作用域的語言中 var result=""; if(parseInt(Math.random()*10)%2==0){ result="偶數"; }else{ result="奇數"; } console.log(result); // js種 // 求1~100的所有數字的和 for(var i=1,sum=0;i<=100;i++){ sum+=i; } console.log(sum); // 在其他有塊級作用域的語言中 var sum=0; for(var i=1;i<=100;i++){ sum+=i; } console.log(sum); </script>
- 全局作用域對象window:
- 作用域鏈:
- 每個函數對象上其實都保存着一個作用域鏈列表
- 普通函數作用域鏈中都有兩個格子:
- 離自己近的格子,不調用函數時,暫時爲空,當調用函數時,會引用臨時創建的函數作用域對象
- 離自己遠的格子始終保存window
- 作用域鏈保存着當前函數所有可用的變量
- 作用域鏈控制着變量的使用順序: 先局部,後全局
- Js中只有兩種作用域:
- 閉包:
- 什麼是閉包: 外層函數調用後,外層函數的作用域對象,被內層函數引用着無法釋放,形成了閉包對象.
- 爲什麼: 全局變量和局部變量都有不可兼得的優缺點:
- 全局變量: 優點: 可重用; 缺點: 極易被污染——今後極力避免使用全局變量
- 局部變量: 優點: 不會被外部篡改; 缺點: 不可重用
- 何時: 希望既可重用一個變量,又想保護該變量不會被隨意篡改時
其實是想給一個函數或一組函數保存一個專屬的變量 - 如何: 3步:
- 用外層函數包裹要保護的變量和要使用變量的內層函數
(注意: 如果內層函數中沒有用到外層函數的局部變量,則不會形成閉包)function outer(){ var i=0; function fun(){ console.log(a) } } )
- 外層函數將內層函數對象返回到外層函數外部
- 想使用內層函數的人應該調用外層函數獲得內層函數對象,並保存在變量中,可反覆使用
- 用外層函數包裹要保護的變量和要使用變量的內層函數
- 結果: 外層函數的局部變量,只歸內層函數專屬,其他人無法修改受保護的變量。
- 一句話概括閉包如何形成: 外層函數調用後,外層函數的作用域對象,被內層函數引用着,無法釋放,形成了閉包。
- 閉包缺點: 比一般的函數,多佔用一塊內存——外層函數的作用域對象
- 解決: 將保存內層函數的變量=null
結果: 釋放了內層函數,導致內層函數引用的外層函數作用域對象被釋放 - 筆試題: 畫簡圖:
- 找外層函數和內層函數之間的局部變量。——要保護的變量
- 找外層函數媽媽共向外生出了幾個內層函數孩子: 在外層函數內3種途徑
return function(){ ... }
全局變量=function(){ ... }
- return arr或obj,但是arr或obj中,可能包含函數成員
- 結果:
- 媽媽一次生出的多個孩子,共享同一個閉包變量,一個函數修改了閉包變量,則另一個函數也受影響----即外層函數內包裹的多個內層函數共用一個閉包變量
- 媽媽分兩次生的不同的孩子和閉包變量之間,互不影響,毫無關係。-----即多次調用外層函數創建的不同變量之間,閉包變量毫無關係,不共享