一、函數聲明和函數表達式有什么區別?(*)
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('男');
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());