函數

一、函數聲明和函數表達式有什么區別?(*)

ECMAScript里面規定了三種聲明函數的方式:

  • 構造函數
    函數也是對象的一種,可以通過其構造函數,使用new來創建一個函數對象。
var printName = new Function("console.log('Cttin');");

但是這種方法不推薦使用。

  • 函數聲明
    函數聲明通過關鍵字function來聲明,關鍵詞后面是函數名,名稱后面有個小括號,括號里面放的是函數的參數,最后是一對花括號,函數的代碼塊就放在這個花括號里面。
function  printName(){
    console.log('Cttin');
}
printName();
  • 函數表達式
    函數表達式不是以function開始,而是一般出現在代碼中間的部分,后面緊跟可選的函數名,然后是小括號,里面可以放置函數的參數,最后是花括號,用于放置函數主體。
var printName = function(){
    console.log('Cttin');
};

函數聲明和函數表達式比較容易混淆,它們的區別主要有:

  • 函數聲明總是以function關鍵詞開始,如果不是,那么它就是函數表達式。
  • 函數聲明最后面一般不寫分號,而函數表達式有分號。
  • 還有一個很重要的區別,對于函數聲明,并不僅僅是函數名被提前了,整個函數的定義也被提前了。而對于函數表達式,規則和聲明變量提前一樣。也就是說,函數表達式只是變量被提前,它所賦的值并沒有被提前。這一點,在下面將會詳細說明。

二、什么是變量的聲明前置?什么是函數的聲明前置 ?(**)

JS和C、Java等語言不同,JS能夠在變量和函數被聲明之前使用它們。

  • 變量的聲明提前
    先來看個例子。
    DEMO

    運行上面的代碼,結果會報錯。這也是在預料之中,因為變量a并沒有聲明。
    然后再修改一下上面的代碼看看。
    DEMO

    雖然變量是在語句調用之后定義的,但是結果并沒有報錯。這就變量聲明提前的作用,它其實相當于如下的代碼:
var a;
console.log(a);
a = 718;

JS的解析器會把當前作用域內聲明的所有變量和函數都放到作用域的開始處。但是對于變量來說,它只是把變量的聲明提前到作用域的開始處,而變量的賦值仍然按照順序執行。而根據JS的語法,未被賦值的變量會自動賦值為undefined,所以這里運行的結果就是undefined
再來看個例子。
DEMO


在上面的代碼中,我們首先聲明了一個全局變量name,然后在函數內部定義一個局部變量。本來是希望第一次打印的輸出是全局范圍內定義的name變量,第二次打印出局部變量name的值。但是輸出并沒有和我們設想的一樣,原因就是定義的局部變量在其作用域內聲明提前了。它其實相當于如下的代碼:

var name = "hunger";
(function(){
  var name;
  console.log("Original name was " + name);
  name = "cttin";
  console.log("New name was " + name);
})();

所以第一次打印出的是沒有賦值的undefined,第二次才打印出cttin

  • 函數的聲明前置
    函數的聲明前置包括兩種情況,分別為函數聲明和函數表達式。
  • 函數聲明
    DEMO

    JS解釋器允許你在函數聲明之前使用,即函數聲明并不僅僅是函數名被提前,整個函數的定義也被提前了。它相當于如下的代碼:
function fn(){
    console.log('1');
}
fn();
  • 函數表達式
    還是先來看個例子。
    DEMO

    在上面的代碼中,sayAge變量被前置了,但是它的賦值并沒有提前,這樣看來函數表達式的提前就和上面所說的變量提前是一回事。上面的代碼相當于:
var sayAge;
sayAge(10);
sayAge = function(age){
console.log(age);
}

因為被提前的變量的默認值是 undefined,undefined不是函數,當然不能被調用,所以報的錯誤屬于“類型不匹配”。

總結:

  • 變量聲明會提前到作用域的頂部,而賦值會被保留在原地,依然是按次序執行。
  • 函數聲明整個會被前置到變量聲明的后面,即使函數寫在最后也可以在前面語句調用。
  • 函數作為值賦給變量時,只有變量提前,函數并沒有被提前。和變量聲明提前一樣。

所以我們在分析代碼的時候,可以把變量和函數聲明放在作用域的頂部,這樣分析出來的結果一般不容易出錯。

三、arguments 是什么?(*)

arguments是一個類數組對象,代表傳給一個function的參數列表。arguments對象是函數內部的本地變量,不再是函數的屬性。
arguments只在函數內部有效,可以在函數內部通過使用 arguments對象來獲取函數的所有參數。這個對象為傳遞給函數的每個參數建立一個條目,條目的索引號從 0 開始。參數也可以被重新賦值。
DEMO
arguments對象類似于數組,但它并不是真正的數組,除了length,它沒有數組所特有的屬性和方法。
arguments主要用途:arguments對于參數數量是一個可變量的函數來說比較有用。 當這個函數的參數數量比它顯式聲明的參數數量更多的時候,你就可以使用 arguments對象獲取到該函數的所有傳入參數。

四、函數的重載怎樣實現?(**)

重載是很多面向對象語言實現多態的手段之一,相同名字的函數參數個數不同或者順序不同都被認為是不同的函數,稱為函數重載。
但是在JS中,沒有函數重載的概念,函數通過名字唯一確定,就算參數不同也被認為是相同的函數,后面的會覆蓋前面的。
DEMO


上面的代碼出現問題就是因為JS沒有函數重載的概念,后面的函數會覆蓋前面的。所以運行的結果相當于是計算“1+2+undefined”,結果自然就是NaN啦。
在JS中,對于參數不確定的函數,可以通過arguments來解決。
DEMO

五、立即執行函數表達式(IIFE)是什么?有什么作用?(***

  • 立即執行函數表達式(Immediately-Invoked Function Expression)
    在JS中,()在函數名之后,是一種運算符,表示調用這個函數。有的時候,我們需要在定義函數之后,立即調用該函數。你不能在函數的定義之后加上圓括號,這會產生語法錯誤。例如:
function(n){
    var i = 888;
    console.log(n);
    console.log('hello hunger',i);
}


原因就是function這個關鍵字可以當作語句,也可以當作表達式。所以為了避免歧義,JS引擎規定,如果function出現在首行,就是解析成語句。所以在上面的代碼中,JS會把它當作是函數的定義,而不應該以括號結尾。
為了讓JS引擎解析成一個表達式,就不能讓function出現在句首,這樣就出現了立即執行函數,就是將其用括號包裹住。它有兩種寫法,分別為:

(function(){ 
...
})();
(function(){
}());  //注意后面的分號

利用立即執行函數上面的代碼就可以成功的運行啦。



立即執行函數表達式一般不需要給函數命名,因為只需要立刻執行就行。如果要命名,一般是用于遞歸函數。例如:

(function say(n){
    var i = 888;
    console.log(n);
    console.log('hello hunger' ,i);
    if(i<0)  return;
    say(n-1);
})(10);
  • IIFE的作用
  • 函數都有一個作用域,立即執行函數表達式包裹一段代碼,讓它有自己的作用域,這也是封裝的第一步。生成一個局部變量,執行完就銷毀。
  • 可以不必為函數命名,避免了污染全局變量。

可以參考立即調用的函數表達式(IIFE)

六、什么是函數的作用域鏈 (****

  • 作用域
    作用域就是函數和變量可以訪問的范圍,JS中變量的作用域分為全局作用域和局部作用域。JS的作用域是靠函數來形成的,也就是說一個函數內定義的變量函數外不可以訪問,變量在聲明它們的函數體及其子函數內是可見的。
    這里需要注意一下語句的變量作用域范圍。
console.log(j);  //undefined
console.log(i);  //undefined
for(var i=0;i<10;i++){   //不是函數,是控制語句,所以這里的i和j是全局作用域。
    var j = 100;
}
console.log(i);  //10
console.log(j);  //100

最外層函數和在最外層函數外面定義的變量擁有全局作用域,只有函數才有局部作用域。

for(var i=0;i<10;i++){
    var j = 100;
}
function fn(){
    var i = 99;
    console.log(i);   //只有函數才有局部的作用域,先在內部找(找不到再一層層往上面的作用域找),所以輸出為99。
}
fn();  // 99
console.log(i); //10,只能取到for里面的10.但是如果上面函數里面的var i = 99;改成i = 99;打印結果為99,所以如果不加var就是全局變量。

當然也不是說不帶var的就是全局作用域,再來看個例子。

for(var i=0;i<10;i++){
    var j = 100;
}
function fn2(){
    console.log(i);
    var i = 99;   //如果去掉var,結果應該是10,100,100
function fn2(){   
    i = 100;     //沒有加var,可以認為這個函數里面變量的作用域在父親的范圍內
}
fn2();     //再執行這個,也就是內部的fn2(),得到100
console.log(i);
}
fn2();     //先執行這個調用,也就是大函數fn2(),得到undefined
console.log(i);   //最后執行。這里是全局變量的i,為10

總的來說

  • 變量沒有在函數內聲明或者聲明的時候沒有帶var就是全局變量(除了在函數內部定義的子函數情況),擁有全局作用域;
  • window對象的所有屬性擁有全局作用域,在代碼任何地方都可以訪問;
  • 函數內部聲明并且以var修飾的變量就是局部變量,只能在函數體內使用;
  • 函數的參數雖然沒有使用var,但仍然是局部變量。
  • 作用域鏈
    代碼在執行的過程中,會創建一個作用域鏈,用來保證執行的環境對變量和函數的有序訪問。在函數運行過程中標識符的解析是沿著作用域鏈一級一級搜索的過程,從當前所在的作用域開始,逐級向上尋找,直到找到同名標識符為止,找到后不再繼續遍歷,找不到就報錯。
    詳細資料還可以參考:
    JavaScript 開發進階:理解 JavaScript 作用域和作用域鏈
    JavaScript作用域鏈

七、代碼

1.以下代碼輸出什么? (難度**)

function getInfo(name, age, sex){ 
    console.log('name:',name); 
    console.log('age:', age);
    console.log('sex:', sex); 
    console.log(arguments); 
    arguments[0] = 'valley'; 
    console.log('name', name);
 } 
getInfo('hunger', 28, '男'); 
getInfo('hunger', 28);
getInfo('男');
getInfo('hunger', 28, '男');

getInfo('hunger', 28);

getInfo('男');

2.寫一個函數,返回參數的平方和?如 (難度**)

function sumOfSquares(){
 } 
sumOfSquares(2,3,4); // 29 
sumOfSquares(1,3); // 10
function sumOfSquares(){
    sum = 0;
    for(var i = 0;i<arguments.length;i++){
        sum = sum + arguments[i]*arguments[i];
  }
    console.log(sum);
}
sumOfSquares(2,3,4); //29
sumOfSquares(1,3); //10
運行結果

3.如下代碼的輸出?為什么 (難度*)

console.log(a); 
var a = 1; 
console.log(b);  

根據變量提升,以上代碼相當于:

var a;
console.log(a);
a = 1;
console.log(b);
運行結果

4.如下代碼的輸出?為什么(難度*)

sayName('world'); 
sayAge(10); 
function sayName(name){ 
console.log('hello ', name);
} 
var sayAge = function(age){ 
console.log(age); 
};

以上代碼相當于:

var sayAge;
function sayName(name){
    console.log('hello ', name);
}
sayName('world');
sayAge(10);
sayAge = function(age){
    console.log(age);
};
運行結果

5.如下代碼的輸出?為什么(難度**)

function fn(){} 
var fn = 3; 
console.log(fn);

以上代碼相當于:

var fn;
function fn(){}  //變量的聲明會提升到函數聲明的前面,函數的聲明會覆蓋方法的聲明
fn = 3;  //變量的賦值會覆蓋方法的聲明。如果沒有這句賦值語句,輸出就是function fn(){}
console.log(fn);


6.如下代碼的輸出?為什么 (難度***

function fn(fn2){
console.log(fn2);
var fn2 = 3;
console.log(fn2);
console.log(fn);
function fn2(){
    console.log('fnnn2');
}
}
fn(10);

以上的代碼相當于:

function fn(fn2){
var fn2;  //先是變量聲明提前
function fn2(){
    console.log('fnnn2');
}  //再是函數聲明提前到變量聲明的后面
console.log(fn2);  //當函數執行有命名沖突的時候,函數執行時載入順序是變量、函數、參數,所以此處的輸出應該是上面的函數。
fn2 = 3; 
console.log(fn2);  //經過上面的賦值語句,覆蓋了上面的方法聲明,所以輸出3
console.log(fn);  //打印出函數fn
}
fn(10);  //根據此處的函數調用,執行上面的fn函數

運行結果

7.如下代碼的輸出?為什么(難度***

var fn = 1; 
function fn(fn){ 
console.log(fn);
 } 
console.log(fn(fn)); 

上述代碼相當于:

var fn;  //先聲明變量fn
function fn(fn){ 
console.log(fn);
 }   //再聲明函數,函數的聲明會覆蓋變量的聲明
fn = 1;  //然后給fn賦值為1,會覆蓋上面的方法聲明,此時fn為數字1
console.log(fn(fn));  //這里是調用函數fn,但是此時fn是數字1,不能當作函數來調用,所以會報錯
運行結果

8.如下代碼的輸出?為什么(難度**)

//作用域 console.log(j);
console.log(i);
for(var i=0; i<10; i++){
var j = 100; 
}
console.log(i); 
console.log(j);

因為for是控制語句,不是函數,所以它里面的變量i和j都是全局的,所以它相當于如下的代碼:

var i;
var j;
console.log(i);
for(i=0; i<10; i++){
  j = 100; 
}
console.log(i); 
console.log(j);

運行結果

9.如下代碼的輸出?為什么(難度****

fn();
var i = 10;
var fn = 20; 
console.log(i);
function fn(){ 
    console.log(i); 
    var i = 99; 
    fn2(); 
    console.log(i); 
    function fn2(){
    i = 100; 
    }
}

上述代碼相當于:

var i;  //聲明變量
var fn;  //聲明變量
function fn(){ 
    var i;
    function fn2(){
    i = 100; 
    }
    console.log(i);  //因為上面函數fn2中的變量i是局部作用域,只在函數內部有效,外部無法訪問,所以這里的輸出結果為undefined
    i = 99; 
    fn2();  //調用fn2函數,此時i為100
    console.log(i);  //輸出i的值100
}  //聲明函數
fn();  //調用函數,執行函數fn
i = 10;  //函數執行完后,i被賦值為10
fn = 20;   //fn被賦值為20,此時fn已經不是函數了,因為被賦值語句覆蓋了,變成了數字20
console.log(i);  //打印出i的值10

運行結果

10.如下代碼的輸出?為什么(難度*****

var say = 0; 
(function say(n){ 
console.log(n); 
if(n<3) return; 
say(n-1); 
}( 10 )); 
console.log(say);

function外部加了個括號,所以為立即執行函數,里面又嵌套了say(n-1),所以為遞歸函數。把函數最后面的參數10傳遞進去,所以一開始打印的就是10,依次遞減直到等n小于3,也就是當n等于2的時候退出整個循環。
然后再執行最后一句,打印出say的值,注意這里并不是函數調用哦,后面沒有括號。

運行結果

如果把它改成函數調用就會報錯啦,原因就和上面分析的一樣,這里的say被賦值為0,已經不是函數了,不能調用。

var say = 0; 
var n = 10;
function say(n){ 
console.log(n); 
if(n<3) return; 
say(n-1); 
}; 
console.log(say());
運行結果

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,362評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,013評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,346評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,421評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,146評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,534評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,585評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,767評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,318評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,074評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,258評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,828評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,486評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,916評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,156評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,993評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,234評論 2 375

推薦閱讀更多精彩內容

  • 函數聲明和函數表達式有什么區別 (*)解析器會率先讀取函數聲明,并使其在執行任何代碼之前可以訪問;函數表達式則必須...
    coolheadedY閱讀 399評論 0 1
  • 1.函數聲明和函數表達式有什么區別 (*) 區別: 函數聲明后面的分號可加可不加,不加也不影響接下來語句的執行,但...
    Sheldon_Yee閱讀 409評論 0 1
  • 問答題 函數聲明和函數表達式有什么區別 (*)答://函數聲明function hello(){ conso...
    饑人谷_桶飯閱讀 249評論 0 0
  • 1. 函數聲明和函數表達式有什么區別 (*) 函數在JS中有三種方式來定義:函數聲明(function decla...
    進擊的阿群閱讀 449評論 0 1
  • linux系統的軟件庫確實是沒有windows下的軟件庫豐富了。但是其中仍然不乏一些好用的軟件。接下來的幾周,筆者...
    Vongola閱讀 670評論 1 0