一、自定義函數function 函數就是功能、方法的封裝。函數能夠幫我們封裝一段程式代碼,這一段代碼會具備某一項功能,函數在執行時,封裝的這一段代碼都會執行一次,實現某種功能。而且,函數可以多次調用。 1.1函數的定義和調用 語法: 定義:把需要實現的功能預先做好 執行:需要的時候執行這個功能,而且 ...
一、自定義函數function
函數就是功能、方法的封裝。函數能夠幫我們封裝一段程式代碼,這一段代碼會具備某一項功能,函數在執行時,封裝的這一段代碼都會執行一次,實現某種功能。而且,函數可以多次調用。
1.1函數的定義和調用
語法:
定義:把需要實現的功能預先做好 執行:需要的時候執行這個功能,而且還可以執行多次 |
定義:function myName(){} 執行:myName() |
【語法解釋】:
function 定義函數的關鍵字 myName 函數名稱 () 參數集 {} 函數體,執行的代碼都放在{}裡面 |
多條語句,組成一個“語句軍團”,集體作戰。
//定義一個函數,函數就是一組語句的集合 function haha(){ console.log(1); console.log(2); console.log(3); console.log(4); } haha();//調用haha函數 haha();//調用haha函數 haha();//調用haha函數
函數必須先定義,然後才能調用。
定義一個函數,用關鍵字function來定義,function就是英語“功能”的意思。表示這裡面定義的語句,完成了一些功能。function後面有一個空格,後面就是函數名字,函數的名字也是關鍵字,命名規範和變數命名是一樣的。名字後面有一對兒圓括弧,裡面放置參數。然後就是大括弧,大括弧裡面是函數的語句。
function 函數名稱(){
} |
函數如果不調用,裡面的語句一輩子都不執行,等於白寫。
調用函數的方法,就是函數名稱加(),()是一個運算符,表示執行一個函數。
函數名稱() |
一旦調用函數,函數內部的代碼不管對錯,都會執行。
能感覺到,函數是一些語句的集合,讓語句稱為一個軍團,集體作戰。要不出動都不出動,要出動就全動。
函數的意義1:在出現大量程式代碼相同時候,可以為它門封裝成一個function,這樣只調用一次,就能執行很多語句。
1.2函數的參數
定義在函數內部的語句,都是相同的,但是實際上可以通過“參數”這個東西,來讓語句有差別。
定義函數時,內部語句可能有一些懸而未決的量,就是變數,這些變數,要求在定義時都羅列在圓括弧中:
function fun(a){ console.log("我第"+a+"次說你好!"); } fun(100); fun(1); fun(2); fun(3);
調用的時候,把這個變數真實的值,一起寫在括弧里,這樣隨著函數的調用,這個值也傳給了a變數參數。
羅列在function圓括弧中的參數,叫做形式參數;調用時傳遞的數值,叫做實際參數。
參數可以有多個,用逗號隔開。
function sum(a,b){ console.log(a + b); } sum(3,5); //8 sum(8,11); //19 sum("5",12); //512 sum(10); //NaN,因為a的值是10,b沒有被賦值是undefined,10+undefined=NaN sum(10,20,30,40,88); //30,後面的參數沒有變數接收
函數的意義2:在調用函數時,不用關心函數內部的實現細節,甚至這個函數是你網上抄的,可以運行。所以這個東西,給我們團隊開髮帶來了好處。
定義函數的時候,參數是什麼類型,不需要指定類型:
調用的時候,傳進去什麼類型,a、b變數就是什麼類型
sum("5",12); //512,做的是連字元串運算 |
另外,定義和調用的時候參數個數可以不一樣多,不報錯。
sum(10); |
因為只給a變數賦值,b沒有被賦值,b被隱式的var。b的值是undefined,10+undefined=NaN
sum(10,20,30,40,88); //30 |
只有前面兩個參數被形參變數接收了,後面的參數沒有變數接收,就被忽略了。
//封裝一個函數,計算m+....+n的和 //比如m是4,n是15,4+5+6+7...+15 function sum(m,n){ var s = 0; //累加器,累加和 for(var i = m;i <= n;i++){ s+=i //s = s + i; } console.log(s); } sum(1,100); //計算1到100的和 sum(10,13); //計算10+11+12+13的和 sum(13,10); //輸出0,所以你就知道函數順序關機,定義順序是什麼,傳遞順序就是什麼。
1.3函數的返回值
函數可以通過參數來接收東西,還可以通過return關鍵字來返回值,“吐出”東西。
function sum(a,b){ return a+b; //現在這個函數的返回值就是a+b的和 } //sum沒有輸出功能,就要用console.log輸出 console.log(sum(3,8)); //計算sum(3,8);實際上稱為表達式,需要計算,計算後是11 console.log(sum(3,sum(4,5)));//輸出12,實際上兩次執行了sum函數,先執行內層的,計算出9,然後sum(3,9)就是12
函數有一個return的值,那麼現在這個函數,實際上是一個表達式,換句話說這個函數就是一個值。
所以這個函數,可以當做其他的函數參數。
sum(3,sum(4,5)); |
程式從內層執行到外層,sum(3,9)
函數可以接收很多值,但是返回一個值。
函數的意義3:模塊化編程,讓複雜的邏輯變得更簡單。
函數只能有唯一的return,有if語句除外。
程式遇見return,會做兩件事:
1、立即返回結果,返回到調用它的地方
2、不執行return後面的代碼。
function fun(){ console.log(1); console.log(2); //return; //返回一個空值,undefined return "你好啊!"; console.log(3); //這行語句不執行,因為函數已經return,所以會終止執行後面的代碼。 } console.log(fun();
1.4函數模塊化編程
實現前提:函數有返回值,可以作為其他函數執行時傳的實參。
習慣將複雜工作,進行一步步的分工,將一部分工作的結果作為下一步工作的條件。
將程式中某個單獨的功能製作成單獨函數,這就是造輪子的過程。
業務邏輯上:將所有的輪子進行拼裝。
將程式分成有層次的模塊,製作過程中一部分函數要有返回值,執行結果作為另一些模塊的參數、條件。
現在做一個程式,輸出2~100的所有質數,所謂的質數,就是只有1和原數本身兩個約數,沒有其他約數。
把一個複雜的問題,拆分成一個個小問題,每個都是一個單獨的函數:
邏輯思維:約數個數函數 → 判斷質數函數 → 高層業務
編程需要逆向思維編程:製作約數個數函數 → 製作判斷質數函數 → 高層業務
函數思維找質數:
//約數個數函數:能夠傳入一個數字,返回它的約數個數 function yueshugeshu(a){ //計算這個數字的約數個數 var count = 0; //累加約數個數 for(var i = 1;i <= a;i++){ if(a % i == 0){ //判斷是否為約數 count++; } } return count; } //判斷是否是質數,如果一個函數名字取名為is,就暗示了將返回布爾值 //要麼返回true,要麼返回false。 //接收一個參數m,返回是否為質數(true或false) function isZhishu(m){ if(yueshugeshu(m) == 2){ return true; }else{ return false; } } //尋找1~100的質數 for(var i = 1;i <= 100; i++){ if(isZhishu(i)){ //可以省略==true的判斷 //isZhishu()給我們返回了true和false console.log(i); } }
利用函數驗證哥德巴赫猜想:用戶輸入偶數拆分兩個質數和:
哥德巴赫猜想:任何一個偶數,都可以拆分為兩個質數的和。
現在要求,用戶輸入一個偶數,你把所有的質數拆分可能,寫出來。
比如:
4 = 2 + 2
6 = 3 + 3
8 = 3 + 5
48 = 5 + 43
代碼見案例:
約數個數函數,裡面的細節不需要關心,它足夠的魯棒,就能返回約數個數。
上層的函數,可以使用下層的API:
//約數個數函數:能夠傳入一個數字,返回它的約數個數 function yueshugeshu(a){ //計算這個數字的約數個數 var count = 0; //累加約數個數 for(var i = 1;i <= a;i++){ if(a % i == 0){ //判斷是否為約數 count++; } } return count; } //判斷是否是質數,如果一個函數名字取名為is,就暗示了將返回布爾值 //要麼返回true,要麼返回false。 //接收一個參數m,返回是否為質數(true或false) function isZhishu(m){ if(yueshugeshu(m) == 2){ return true; }else{ return false; } } //哥德巴赫猜想,用戶輸入一個數字 //驗證偶數是否能被拆分兩個質數的和,拆分的思想就是窮舉法 //比如用戶輸入48,那麼就: //看看1、47是不是都質數 //看看2、46是不是都質數 //看看3、45是不是都質數 //... var even = parseInt(prompt("請輸入一個偶數")); for(var i = 4;i < even;i++){ if(isZhishu(i) && isZhishu(even - i)){ console.log(even + "可以拆分為" + i + "與" + (even - i) + "的和"); } }
利用函數驗證哥德巴赫猜想-一百萬以內的偶數拆分:優化
function yueshugeshu(a){ ... } function isZhishu(m){ ... } //註意驗證,驗證偶數能否被拆成兩個質數 waiceng:for(var i = 4 ; i <= 1000000 ; i+=2){ for(var j = 2 ; j < i ; j++){ if(isZhishu(j) && isZhishu(i - j)){ console.log(i +"可以拆分為"+ j +"與"+ (i - j) + "的和"); continue waiceng; } } }
1.5函數遞歸
函數可以自己調用自己,就是遞歸。
function haha(){ console.log("哈哈"); haha();//調用自己 } haha(); function sum(a){ if(a == 1){ return 1; }else{ return a + sum(a-1); //10 + 9 + 8 + sum(7) } } console.log(sum(10));
斐波那契數列就是經典的遞歸演算法:
1 1、1、2、3、5、8、13、21、34、55、89、144、233... |
輸出斐波那契數列
只需要一個函數,就可以搞定全部問題。
fib(n); 就能得到第n位的數字
fib(2) = 1
fib(3) = 2
fib(4) = 3
fib(5) = 5
...
fib(10) = 55
function fib(n){ if(n == 1 || n == 2){ return 1; }else{ return fib(n - 1) + fib(n - 2); } } // console.log(fib(10)); for(var i = 1;i <= 50;i++){ console.log(fib(i)); }
1.6函數表達式
定義函數除了使用function之外,還有一種方法,就是函數表達式。就是函數沒有名字,稱為“匿名函數”,為了今後能夠調用它,我們把這個匿名函數,直接賦值給一個變數。
var haha = function(){ console.log("哈哈"); } // console.log(haha); haha(); //以後要調用這個函數,就可以直接使用haha變數調用。
等價於:
function haha(){ console.log("哈哈"); } haha();
如果這個函數表達式的function不是匿名,而是有名字的:
var haha = function xixi(){ console.log("哈哈"); } xixi(); //這是錯誤的 haha(); //這的對的
那麼JS表現非常奇怪,在外部只能用haha()調用,xixi()會引發錯誤。
也就是說,JS這個奇怪的特性,給我們提了個醒,定義函數,只能用以下兩種方法,不能雜糅:
function haha(){} |
var haha = function(){} |
錯誤的:
var haha = function xixi(){} |
1.7函數聲明的提升(預解析)
//先調用,可以輸出,因為有函數聲明提升的特性 fun(); fun(); fun(); //後定義 function fun(){ console.log("我是函數!"); }
不會報錯。
JS在執行前,會有一個預解析的過程,把所有的函數聲明和變數的聲明,都提升到了最開頭,然後再執行第一行代碼。所以function定義在哪裡,都不重要,程式總能找到這個函數。
函數聲明頭可以提升,JS程式執行前,都會有一個函數預解釋階段,預解釋階段是自動進行的
函數優先:函數聲明和變數聲明都會被提升,但是面試常考的一個細節是:函數會被首先提升,然後才是變數。
函數提升是沒節操的,無視if等語句的判斷,強制提升
在JavaScript世界中,函數是一等公民。
函數聲明會被提升,但是函數表達式卻不會被提升:
fun(); var fun = function(){ //因為它是函數表達式,而不是function定義法 alert("我是函數!"); }
又給我們提了個醒,沒有特殊的理由,都要用function haha(){}來定義函數。
函數優先:
aaa(); //現在這個aa到底是函數,還是變數5? console.log(aaa);//函數優先,遇見同名的標識符,預解析階段一定把這個標識符給函數 var aaa = 5; //定義一個變數,是5 function aaa(){ alert("我是aaa函數!") }
面試題:
函數優先,現在foo這個標識符衝突了,一個函數叫foo,一個變數也叫foo。預解析階段,如果遇見標識符衝突,這個標識符給函數。
1.8函數是一個引用類型
基本類型:Number、String、Boolean、undefined、null
引用類型:Object、function、array、RegExp、Math、Date。
function fun(){} var haha = function (){} console.log(typeof fun); //引用類型中的function類型 console.log(typeof haha);//引用類型中的function類型
函數也是一種類型,這個類型叫function,是引用類型的其中一種。
基本類型:保存值
引用類型:保存地址
現在變數a = 1,那麼這個a變數裡面存儲1這個數字
//基本類型的賦值 var a = 1; var b = a; //b得到的值是a的副本,a把自己複製了一份,給了b b = 3; //改變了b的值,a不受影響 console.log(a); //1 console.log(b); //3
//引用類型的賦值: //定義了一變數a,引用了一個function //這個a變數存儲的是這個匿名函數的記憶體地址 var a = function(){ alert("我是一根函數"); } var b = a; //就是把匿名函數的地址也給了b。 b.xixi = 1; //給b添加一個屬性 console.log(a.xixi); //輸出a的xixi屬性,a也有這個屬性了 //b的xixi屬性和a的變數都改變了,因為都是指向同一個對象(同一個記憶體地址) b.xixi++; b.xixi++; b.xixi++; console.log(a.xixi); console.log(b.xixi);
總結:
預解釋:在js中,代碼從上到下執行之前,(瀏覽器預設)首先會把所有帶var和function關鍵字的進行提前聲明或者定義
var num=88;
聲明(declare):相當於種樹時"挖坑" var num; 只聲明沒有定義時,num的預設值是undefined
定義(defined):相當於種樹時"栽樹" num=88;(給變數賦值)
在預解釋的時候,帶var和帶function的還不一樣:
var:只是提前的聲明(定義賦值的部分是在代碼執行的時候完成的)
function:提前的聲明+定義
在瀏覽器載入HTML頁面時,首先會開闢一個供js代碼執行的環境-->"全局作用域"(window/global)
棧記憶體(作用域):存儲基本數據類型的值;提供js代碼執行的環境;
堆記憶體:在js中,對於引用數據類型來說,首先會開闢一個新的記憶體空間,然後把代碼存儲到這個空間中,最後把空間的地址給相關的變數--->我們把新開闢的這個記憶體空間稱為"堆記憶體"。
堆記憶體的作用:存儲引用數據類型值
二、作用域
2.1函數能封閉住作業域
變數的作用域無非就兩種:全局變數和局部變數。
2.1.1全局變數(全局作用域)
全局變數:在最外層函數定義的變數擁有全局作用域,即對任何內部函數來說,都是可以訪問的。
言外之意:如果變數沒有定義在任何的function中,那麼它將在程式中任意範圍內都有定義:
var a = 100; //定義在全局的變數,在程式任何一個角落都有定義 function fn(){ console.log("我是函數裡面的語句,我認識全局變數a值為:" + a); } fn(); console.log("我是函數外面的語句,我認識全局變數a值為:" + a);
2.1.2局部變數(局部作用域)
局部變數:和全局作用域相反,局部作用域一般只在固定的代碼片段內可訪問,而對於函數外部是無法訪問的。
例如:變數定義在function裡面,這個變數就是局部變數,只在當前這個function函數內部能使用。在函數外部不能使用這個變數,出了這個function,就如同沒有定義過一樣。
function fn(){ var a = 100; //定義在函數的變數,局部變數 console.log("我是函數裡面的語句,我認識變數a值為:" + a); } fn(); console.log("我是函數外面的語句,我認識變數a值為:" + a);
a被var在了function裡面,所以現在這個a變數只能在紅框範圍內有定義:
在ES5語法中,JavaScript變數作用域非常簡單,能關住作用域的只有一個,就是:函數。
【總結】:
● 定義在function裡面的變數,叫做局部變數,只在function裡面有定義,出了function沒有定義的。
● 定義在全局範圍內的,沒寫在任何function裡面的,叫做全局變數,都認識。
【原理】:
全局變數在定義時,就會直接生成一個新的變數,在任何位置查找變數都有定義。
局部變數定義在函數內部,函數如果不執行,相當於內部的代碼沒寫,局部變數等於從未定義過,在函數執行時,會在函數作用域內部立即定義了一個變數,使用完之後,變數立即被銷毀。所以在外部永遠找不到局部變數定義。
2.2作用域鏈
作用域鏈:根據在內部函數可以訪問外部函數變數的這種機制,用鏈式查找決定哪些數據能被內部函數訪問。
當遇見變數時,JS引擎會從其所在的作用域依次向外層查找,查找會在找到第一個匹配的標識符時停止。
在私有作用域中出現了變數,首先看是否為私有的,如果是私有變數,那麼就用私有的即可。如果不是私有變數,則往當前作用域的上級作用域查找,如果上級作用域也沒有,則繼續往上查找....一直找到window為止。
//變數的作用域,就是它var的時候最內層的function function outer(){ var a = 1; //a的作用域是outer inner(); function inner(){ var b = 2; //b的作用域是inner console.log(a); //能輸出1,a在本層沒有定義,就往上找 console.log(b); //能輸出2 } } outer(); console.log(a); //報錯,因為a的作用域是outer
多層嵌套:如果有同名的變數,那麼就會發生“遮蔽效應”:
var a = 10; //全局變數 function fn(){ console.log(a); //undefined,提升聲明瞭局部變數 var a = 13; //把外層的a變數遮蔽了,這函數內部看不見外層的a console.log(a); //輸出13,變數在當前作用域尋找,找到a定義為13 } fn(); fn(); fn(); console.log(a); //10,變數在當前作用域尋找,找到全局a
一個變數在使用的時候得幾?就會在當前作用域去尋找它的定義,找不到,去上一層找,直到找到全局(window),如果全局也沒有,就報錯。這就是作用域鏈。
題目:
var a = 1; //全局變數 var b = 2; //全局變數 function outer(){ var a = 3; //遮蔽了外層的a,a局部變數 function inner(){ var b = 4; //遮蔽了外層的b,b局部變數 console.log(a); //① 輸出3,a現在在當前層找不到定義的,所以就上一層尋找 console.log(b); //② 輸出4 } inner(); //調用函數 console.log(a); //③ 輸出3 console.log(b); //④ 輸出2 b現在在當前層找不到定義的,所以就上一層尋找 } outer(); //執行函數,控制權交給了outer console.log(a); // ⑤ 輸出1 console.log(b); // ⑥ 輸出2
2.3不寫var就自動成為全局變數了
需要註意,函數內部聲明的時候,一定要用var命令,如果不用,實際上聲明瞭一個全局變數。
function fn(){ a = 100;//這個a第一次賦值時,沒有var,所以就自動在全局作用域var了一次 } fn(); console.log(a);//100 |
這是JS的機理,如果遇見一個標識符,從來沒有var過,並賦值了:
a = 100; |
那麼就會自動在全局作用域定義var a;
2.4函數的形參變數,會預設定義為這個函數的局部變數
var a = 0; var b = 0; function fn(a,b){ a = 3; b = 4; console.log(a,b); } fn(); console.log(a); console.log(b);
a,b就是fn內部的局部變數,只能在當前function函數內部使用,出了fn就沒有定義。
2.5全局變數的作用
在函數內部使用自己的變數,儘量定義為局部。
全局變數有自己獨特的用途:累加、傳遞。
累加:函數沒執行一次,都要求變數在原來基礎上發生變化。
功能1:通信,共同操作傳遞同一個變數
兩個函數同時操作一個變數,一個增加,一個減少,函數和函數通信。
var num = 0; //全局變數 function add(){ num++; } function remove(){ num--; } add(); add(); add(); add(); remove(); remove(); add(); add(); console.log(num); //4
功能2:累加,重覆調用函數的時候,不會重置
var num = 0; function baoshu(){ num++; console.log(num); } baoshu();//1 baoshu();//2 baoshu();//3
如果num定義在baoshu裡面,每次執行完函數,作用域就被銷毀,所以裡面變數都是全新的。
function baoshu(){ var num = 0; num++; console.log(num); } baoshu(); //1 baoshu(); //1 baoshu(); //1
2.6函數定義也有作用域
function outer(){ var a = 10; function inner(){ //局部函數 console.log("哈哈"); } } outer(); inner(); //報錯,因為全局作用域下,沒有inner函數的定義 console.log(a);//報錯
公式:
function 大(){ function 小(){ } 小(); 可以運行 } 小(); //不能運行,因為小函數定義在大函數裡面,離開大函數就沒有作用域。
三、閉包
閉包有兩個作用:
1、可以讀取自身函數外部的變數(沿著作用域鏈尋找)
2、可以讓這些外部變數始終保存在記憶體中
3.1閉包
推導過程:之前已經學習過,inner這個函數不能在outer外面調用,因為outer外面沒有inner定義。
當函數執行的時候,會形成一個新的私有作用域,來保護裡面的私有變數不受外界干擾,我們把函數的這種保護機制--->"閉包"。
function fn(){ } fn(); function outer(){ var a = 100; function inner(){ console.log(a); } } outer(); inner(); //在全局作用域調用inner,全局沒有inner的定義,所以報錯
但是我們就想在全局作用域下,運行outer內部的inner,此時我們必須想一些奇奇怪怪的方法。
有一個簡單可行的方法,就是讓outer自己return掉inner。
非常經典的閉包案例,任何培訓機構、書、講閉包,一定是下麵的案例:
function outer(){ var a = 888; function inner(){ console.log(a); //888 } return inner; //outer返回了inner的引用 } var inn = outer();//inn就是inner函數了 inn(); //執行inn,全局作用域下沒有a的定義,但是函數閉包,能夠把定義函數時的作用域一起記憶住,輸出888
一個函數可以把自己內部的語句,和自己聲明時,所處的作用域一起封裝成了一個密閉的環境,就叫“閉包”。
每個函數都是閉包,每個函數天生都能夠記憶自己定義時所處的作用域環境。但是,我們必須將這個函數,挪到別的作用域,才能更好的觀察閉包。這樣才能實驗它有沒有把作用域給“記住”。
我們發現,把一個函數從它定義的那個作用域,挪走,運行。嘿,這個函數居然能夠記憶住定義時的那個作用域。不管函數走到哪裡,定義時的作用域就帶到了哪裡。這就是閉包。
閉包在工作中是一個用來防止產生隱患的事情,而不是加以利用