函數是一段能夠重複調用的代碼塊。接收相應的參數並能夠返回對應的值javascript
javascript 有三種聲明函數的方法。function 命令、函數表達式、Function構造器。java
(1)function 命令es6
function命令聲明的代碼區塊,就是一個函數。function命令後面是函數名,函數名後面是一對圓括號,裏面是傳入函數的參數。函數體放在大括號裏面。 數組
function add(x, y) { console.log(x + y) } add(3,4);//7
上面的代碼命名了一個add函數,輸出兩個數字相加的和,之後使用add()這種形式,就能夠調用相應的代碼。這叫作函數的聲明(Function Declaration)。安全
(2)函數表達式閉包
除了用function命令聲明函數,還能夠採用變量賦值的寫法。 函數
let add=function (x, y) { console.log(x + y) }; add(3,4);
這種寫法將一個匿名函數賦值給變量。這時,這個匿名函數又稱函數表達式(Function Expression),由於賦值語句的等號右側只能放表達式。性能
採用函數表達式聲明函數時,function命令後面不帶有函數名。若是加上函數名,該函數名只在函數體內部有效,在函數體外部無效。 優化
let add=function addAdd(x, y) { console.log(addAdd) }; console.log(addAdd) //報錯 未定義 add(3,4);// 輸出函數自己
上面代碼在函數表達式中,加入了函數名addAdd。這個addAdd只在函數體內部可用,指代函數表達式自己,其餘地方都不可用。這種寫法的用處有兩個,一是能夠在函數體內部調用自身,二是方便除錯(函數內部報錯時,函數名存在時報錯時錯誤信息中會包含函數名,函數名不存在時會指向引用的值的名稱)。所以,下面的形式聲明函數也很是常見。 spa
let add = function add(x, y) { console.log(x + y) }; add(3, 4);
須要注意的是,函數的表達式須要在語句的結尾加上分號,表示語句結束。而函數的聲明在結尾的大括號後面不用加分號。總的來講,這兩種聲明函數的方式,差異很細微,能夠近似認爲是等價的。
(3) Function 構造器
let add=new Function( 'x','y','console.log(x+y)' ) add(3,4);//7 let say=new Function( 'console.log("hello")' ) say();//hello
你能夠傳遞任意數量的參數給Function構造函數,只有最後一個參數會被當作函數體,若是隻有一個參數,該參數就是函數體。
Function構造函數能夠不使用new命令,返回結果徹底同樣。 總的來講,這種聲明函數的方式很是不直觀,幾乎無人使用。
若是同一個函數被屢次聲明,後面的聲明就會覆蓋前面的聲明
function add(x,y){ console.log(x+y); } add(3,4);//12 function add(x,y){ console.log(x*y) } add(3,4);//12
上面代碼中,後一次的函數聲明覆蓋了前面一次。並且,因爲函數名的提高(參見下文),前一次聲明在任什麼時候候都是無效的,這一點要特別注意。
調用函數時,要使用圓括號運算符。圓括號之中,能夠加入函數的參數。
function add(x,y){ return x+y
console.log('result')//不會執行 } let result=add(3,4); console.log(result)//7
上面代碼中,函數名後面緊跟一對圓括號,就會調用這個函數。 函數體內部的return語句,表示返回。JavaScript 引擎遇到return語句,就直接返回return後面的那個表達式的值,後面即便還有語句,也不會獲得執行。也就是說,return語句所帶的那個表達式,就是函數的返回值。return語句不是必需的,若是沒有的話,該函數就不返回任何值,或者說返回undefined。
函數能夠調用自身,這就是遞歸(recursion)。下面就是經過遞歸,計算數字的階乘
function fact(i) { if (i == 1) { return 1 } return i * fact(i - 1) } console.log(fact(3)) //6
上面的代碼就是
fact(3)==>3*fact(3-1)==>3*fact(2) fact(2)==>2*fact(2-1)==>3*fact(1) fact(1)==>1 fact(3)==>3*2*1==>6
一等公民:通常來講,若是某程序設計語言中的一個值能夠做爲參數傳遞,能夠從子程序中返回,能夠賦值給變量
二等公民:能夠做爲參數傳遞,可是不能從子程序中返回,也不能賦給變量
三等公民:它的值連做爲參數傳遞都不行
JavaScript 語言將函數看做一種值,與其它值(數值、字符串、布爾值等等)地位相同。凡是可使用值的地方,就能使用函數。好比,能夠把函數賦值給變量和對象的屬性,也能夠看成參數傳入其餘函數,或者做爲函數的結果返回。函數只是一個能夠執行的值,此外並沒有特殊之處。 因爲函數與其餘數據類型地位平等,因此在 JavaScript 語言中又稱函數爲第一等公民
function add(x, y) { console.log(x + y); } // 將函數賦值給一個變量 let operator = add; operator(3, 4); //7 // 將函數做爲參數和返回值 function addOperat(fn) { return fn; } addOperat(add)(3, 4) //7
JavaScript 引擎將函數名視同變量名,因此採用function命令聲明函數時,整個函數會像變量聲明同樣,被提高到代碼頭部、(另外兩種聲明是不會存在這種狀況的)因此,下面的代碼不會報錯。
let operator = add; operator(3, 4); //7 function add(x, y) { console.log(x + y); }
下面這兩種狀況都會報錯
// add 未定義 add(); let add=function(){ console.log(1) }
// add 不是一個函數 let add; add(); add=function(){ console.log(1) }
所以,若是同時採用function命令和賦值語句聲明同一個函數,最後老是採用賦值語句的定義。由於函數表達式聲明的函數不會存在函數名的變量提高
// 這裏不使用 let f var f=function(){ console.log(1) }; function f(){ console.log(2) } f();//1
獲取函數的原始名稱
function f(){} console.log(f.name);//f let f2=function (){}; console.log(f2.name);//f2 let f4=function f3(){}; console.log(f4.name);//f3
函數的length屬性返回函數預期傳入的參數個數,即函數定義之中的參數個數
function f(){} console.log(f.length);//0 let f2=function (a,b){}; console.log(f2.length);//2 let f4=function f3(a){}; console.log(f4.length);//1
輸出函數的內容(一切內容、包括註釋)
function add(x,y){ let a=x; let b=y; console.log(x+y) } console.log(add.toString())// 輸出函數本省 console.log(Math.ceil.toString()) //對於那些原生的函數,toString()方法返回function (){[native code]}
做用域(scope)指的是變量存在的範圍。在 ES5 的規範中,JavaScript 只有兩種做用域:一種是全局做用域,變量在整個程序中一直存在,全部地方均可以讀取;另外一種是函數做用域,變量只在函數內部存在。ES6 又新增了塊級做用域。
全局做用域,在任何地方均可以訪問
let age='18'; function getAge(){ console.log(age) } console.log(age);//18 getAge();// 18 age全局做用域 函數內部也可訪問
函數做用域,只在函數內部能夠訪問
function getAge(){ var age='qzy'; console.log(age) } getAge()//qzy console.log(age)// age未定義 函數內部定義的變量 只有函數內部能夠訪問
塊級做用域 es6 新增的 let 聲明變量
{ let age='25' } console.log(age) // age未定義
函數內部定義的變量,會在該做用域內覆蓋同名全局變量。
let age=18; function getAge(){ let age=25 console.log(age) } console.log(age);//18 getAge();//25
注意,對於var命令來講,局部變量只能在函數內部聲明,在其餘區塊中聲明,一概都是全局變量。
if(true){ var age=25; } console.log(age);//25
與全局做用域同樣,函數做用域內部也會產生「變量提高」現象。var命令聲明的變量,無論在什麼位置,變量聲明都會被提高到函數體的頭部。
function ageLevel(age){ if(age>100){ var level='高手' } console.log(level) } ageLevel(101);//高手 // 等價於 function ageLevelEqual(age){ var level; if(age>100){ level='高手' } console.log(level) } ageLevelEqual(101);//高手
函數自己也是一個值,也有本身的做用域。它的做用域與變量同樣,就是其聲明時所在的做用域,與其運行時所在的做用域無關
let age=25; function getAge(){ console.log(age) } function setAge(){ let age=26; getAge(); } setAge();//25
getAge函數是在最外層聲明的,因此它綁定的做用域是最外層,在函數內部變量age不會影響到其綁定的最外層的值
一樣的,函數體內部聲明的函數,做用域綁定函數體內部。
let age = 25; function setAge() { let age = 26; function getAge() { console.log(age) } // 將聲明的函數返回 return getAge } setAge()();// 26
正是這種機制,構成了下文要講解的「閉包」現象。
函數運行的時候,有時須要提供外部數據,不一樣的外部數據會獲得不一樣的結果,這種外部數據就叫參數。函數參數不是必需的,JavaScript 容許省略參數。
function square(x){ return x*x } console.log(square(2));//4 console.log(square(3));//9
函數的參數傳遞方式分爲兩種:傳值傳遞、傳址傳遞
傳值傳遞:函數參數若是是原始類型的值(數值、字符串、布爾值),則採用這種方式,此時在函數體內修改參數值,不會影響到函數外部
let age=25; function setAge(age){ age=26; } console.log(age);//25
傳址傳遞:若是函數參數是複合類型的值(數組、對象、其餘函數),則採用這種方式,傳入函數的原始值的地址,此時在函數體內修改參數值,會影響到函數外部(有特殊的)
let person={ age:25 }; function setPerson(person){ person.age=26; } setPerson(person); console.log(person.age);//26
person 爲一個對象 在函數背部修改其中的屬性,外面的變量中的值也作相應的改變
注意,若是函數內部修改的,不是參數對象的某個屬性,而是替換掉整個參數,這時不會影響到原始值。
let person={ age:25 }; function setPerson(person){ person={ age:26 }; } setPerson(person); console.log(person.age);//25
在函數f內部,參數對象person被整個替換成另外一個值。這時不會影響到原始值。這是由於,形式參數person的值原本是是參數person的地址,從新對person賦值致使person指向另外一個地址,保存在原地址上的值固然不受影響。
若是有同名的參數,則取最後出現的那個值。
function add(a,a){ console.log(a) } add(1);// undefined 形參爲兩個 同名取最後一個 可是未傳遞 因此未定義 add(1,2);// 2 add(1,2,3);// 2
因爲 JavaScript 容許函數有不定數目的參數,因此須要一種機制,能夠在函數體內部讀取全部參數。這就是arguments對象的由來。 arguments對象包含了函數運行時的全部參數,arguments[0]就是第一個參數,arguments[1]就是第二個參數,以此類推。這個對象只有在函數體內部,纔可使用。
function add(one){ let len=arguments.length;// 獲取傳入的參數個數 for(let i=0;i<len;i++){ console.log(arguments[i]) } } add(1,2,3,4,5)
注:arguments對象能夠在運行時修改,修改後對應的形參就是修改後的值;可是在開啓嚴格模式下能夠修改,可是不會影響到形參,
var f = function(a, b) { 'use strict'; // 開啓嚴格模式 arguments[0] = 3; arguments[1] = 2; return a + b; } console.log(f(1, 1)) // 2
與數組的關係,須要注意的是,雖然arguments很像數組,但它是一個對象。數組專有的方法(好比slice和forEach),不能在arguments對象上直接使用。 若是要讓arguments對象使用數組方法,真正的解決方法是將arguments轉爲真正的數組。下面是兩種經常使用的轉換方法:slice方法和逐一填入新數組。
function add(one){ let args = Array.prototype.slice.call(arguments); args.forEach(list=>{ console.log(list) }) } add(1,2,3,4,5)
function add(one) { let args = []; for (let i = 0; i < arguments.length; i++) { args.push(arguments[i]); } args.forEach((list)=>{ console.log(list) }) } add(1, 2, 3, 4, 5)
均可以使用forEach 輸出因此的參數值
arguments對象帶有一個callee屬性,返回它所對應的原函數,能夠經過arguments.callee,達到調用函數自身的目的。這個屬性在嚴格模式裏面是禁用的,所以不建議使用
function add() { console.log(arguments.callee==add) } add();//true
閉包(closure)是 JavaScript 語言的一個難點,也是它的特點,不少高級應用都要依靠閉包實現。 理解閉包,首先必須理解變量做用域。前面提到,JavaScript 有兩種做用域:全局做用域和函數做用域。函數內部能夠直接讀取全局變量。
函數內部能夠讀取函數外部的變量,可是函數外部是不能讀取到函數內部的變量的,這個在上面已經實驗過了
若是出於種種緣由,須要獲得函數內的局部變量。正常狀況下,這是辦不到的,只有經過變通方法才能實現。那就是在函數的內部,再定義一個函數
function person(){ let age=18; function getAge(){ return age } return getAge } let p=person();// 接收getAge let pAge=p(); console.log(pAge);// 18
上面的代碼 咱們在person內部定義了一個age字段 ,可是咱們想在外部取不到這個值,這個時候咱們能夠在函數內部定義一個函數,這個函數在person 函數內部,是能夠取到person內部的值的,而後咱們再把函數暴露出來,就能夠獲取到這個值了,那此時getAge函數就是閉包,即可以讀取其餘函數內部變量的函數。因爲在 JavaScript 語言中,只有函數內部的子函數才能讀取內部變量,所以能夠把閉包簡單理解成「定義在一個函數內部的函數」,在本質上,閉包就是將函數內部和函數外部鏈接起來的一座橋樑。
閉包的最大用處有兩個,一個是能夠讀取函數內部的變量,另外一個就是讓這些變量始終保持在內存中,即閉包可使得它誕生環境一直存在。請看下面的例子,閉包使得內部變量記住上一次調用時的運算結果
function createIn(count){ return function (){ console.log(++count); } } let result=createIn(0); result();//1 result()//2 result()//3
上面代碼中,count是函數createIn的內部變量。經過閉包,count的狀態被保留了,每一次調用都是在上一次調用的基礎上進行計算。從中能夠看到,閉包result使得函數createIn的內部環境一直存在。因此,閉包能夠看做是函數內部做用域的一個接口。
爲何會這樣呢?緣由就在於result始終在內存中,而result的存在依賴於createIn,所以也始終在內存中,不會在調用結束後,被垃圾回收機制回收
閉包的另外一個用處,是封裝對象的私有屬性和私有方法。
function Person(name) { var _age; function setAge(n) { _age = n; } function getAge() { return _age; } return { name: name, getAge: getAge, setAge: setAge }; } var p1 = Person('張三'); p1.setAge(25); console.log(p1.getAge()) // 25
上面代碼中,函數Person的內部變量_age,經過閉包getAge和setAge,變成了返回對象p1的私有變量。
注意,外層函數每次運行,都會生成一個新的閉包,而這個閉包又會保留外層函數的內部變量,因此內存消耗很大。所以不能濫用閉包,不然會形成網頁的性能問題
在 JavaScript 中,圓括號()是一種運算符,跟在函數名以後,表示調用該函數。好比,print()就表示調用print函數。 有時,咱們須要在定義函數以後,當即調用該函數。這時,你不能在函數的定義以後加上圓括號,這會產生語法錯誤。
function say(){ /* code */ }();// Uncaught SyntaxError: Unexpected token )
產生這個錯誤的緣由是,function這個關鍵字便可以看成語句,也能夠看成表達式。
// 語句 function f() {} // 表達式 var f = function f() {}
爲了不解析上的歧義,JavaScript 引擎規定,若是function關鍵字出如今行首,一概解釋成語句。那不是的就是函數表達式嘍。所以,JavaScript 引擎看到行首是function關鍵字以後,認爲這一段都是函數的定義,不該該以圓括號結尾,因此就報錯了。
下面函數表達式後面跟上圓括號是能夠當即執行的。
// 表達式 直接輸出1 var f = function () { console.log(1) }()
解決方法就是不要讓function出如今行首,讓引擎將其理解成一個表達式。最簡單的處理,就是將其放在一個圓括號裏面。
(function (){console.log(1)})();//1 (function (){console.log(2)}());//2
上面兩種寫法都是以圓括號開頭,引擎就會認爲後面跟的是一個表示式,而不是函數定義語句,因此就避免了錯誤。這就叫作「當即調用的函數表達式」(Immediately-Invoked Function Expression),簡稱 IIFE。 注意,上面兩種寫法最後的分號都是必須的。若是省略分號,遇到連着兩個 IIFE,可能就會報錯
(function (){console.log(1)})()//1 (function (){console.log(2)}())//2 // Uncaught TypeError: (intermediate value)(...) is not a function
上面代碼的兩行之間沒有分號,JavaScript 會將它們連在一塊兒解釋,將第二行解釋爲第一行的參數
推而廣之,任何讓解釋器以表達式來處理函數定義的方法,都能產生一樣的效果,只要讓引擎認爲它是表達式便可。
let i =function (){console.log(1)}();//1 true && function (){console.log(2)}();//2 +function (){console.log(3)}();//3
一般狀況下,只對匿名函數使用這種「當即執行的函數表達式」。它的目的有兩個:一是沒必要爲函數命名,避免了污染全局變量;二是 IIFE 內部造成了一個單獨的做用域,能夠封裝一些外部沒法讀取的私有變量。
eval命令接受一個字符串做爲參數,並將這個字符串看成語句執行。若是參數字符串沒法看成語句運行,那麼就會報錯。放在eval中的字符串,應該有獨自存在的意義,不能用來與eval之外的命令配合使用。舉例來講,下面的代碼將會報錯。若是eval的參數不是字符串,那麼會原樣返回。
eval('var a=1'); console.log(a);//1 console.log(eval(123));//123
eval('3x');//報錯
eval('return')//報錯
eval沒有本身的做用域,都在當前做用域內執行,所以可能會修改當前做用域的變量的值,形成安全問題
var a=0; eval('var a=1'); console.log(a);//1
爲了防止這種風險,JavaScript 規定,若是使用嚴格模式,eval內部聲明的變量,不會影響到外部做用域。
'use strict' var a=0; eval('var a=1'); console.log(a);//0
若是我不聲明,直接修改a,能夠看到仍是能夠修改爲功的。
'use strict' var a=0; eval('a=1'); console.log(a);//1
上面代碼中,嚴格模式下,eval內部仍是改寫了外部變量,可見安全風險依然存在。一般狀況下,eval最多見的場合是解析 JSON 數據的字符串,不過正確的作法應該是使用原生的JSON.parse方法。
let str = '{"name": "qzy", "age": 25}'; let obj = eval('('+str+')');//let obj=JSON.parse(str); console.log(obj); // Object {name: "hanzichi", age: 10}
eval 解析時爲何要加圓括號呢 ?
let obj='{}'; console.log(eval(obj));//undefined console.log(eval('('+obj+')'));//{}
上面說過eval 會執行輸入的字符串,obj是以{開頭、}結尾的。那js 引擎就會把這當成代碼塊,執行裏面的語句。
前面說過eval不利於引擎優化執行速度。更麻煩的是,還有下面這種狀況,引擎在靜態代碼分析的階段,根本沒法分辨執行的是eval。
"use strict" let my=eval; my('var a=1'); console.log(a);//1
爲了保證eval的別名不影響代碼優化,JavaScript 的標準規定,凡是使用別名執行eval,eval內部一概是全局做用域。詳細的看下一篇對象的講解
var a = 1; function f() { var a = 2; var e = eval; e('console.log(a)'); } f() // 1
就一句話,沒事別用eval。