1. 函數聲明和函數表達式有什么區別 (*)
- 函數在JS中有三種方式來定義:
- 函數聲明(function declaration)
- 函數表達式(function expression)
- 調用new function返回
- 區別:函數聲明是和變量聲明類似,函數聲明的解析是在預執行(pre-execution)階段,也就是瀏覽器準備執行代碼的時候,因此,通過函數聲明來定義函數,可以在定義前或后被調用。然而函數表達式不能做到這點,因為函數表達式是將函數賦值給變量,是將函數放在語句中而不是代碼主流中,只有當瀏覽器解析到該語句的時候函數才能被調用,并且不能在其他位置調用,就是其實際作用是給變量賦值而非作為一個函數被調用。
- 例子:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>task17</title>
</head>
<body>
<script>
var test1 = function say() {
console.log('hello world');
}; // 函數表達式,有分號,是語句
test1(); // hello world
sayName(); // 大阿群
say(); // error
function sayName() {
console.log('大阿群');
} // 函數聲明,無分號
</script>
</body>
</html>
2. 什么是變量的聲明前置?什么是函數的聲明前置 (**)
- 變量的聲明前置:JS語法中,在指定的作用域內,聲明的變量會在提升到代碼的頂部,而賦值操作不會跟隨提升,如果未聲明變量,則無論變量在任何作用域中,都視為該變量為全局變量。
- 函數的聲明前置:JS語法中,如果函數定義是通過函數聲明的,那么在指定作用域內,函數會提升到代碼頂部,并且放在變量聲明的下面。
- 例子:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>task17</title>
</head>
<body>
<script>
console.log(a); // undefined
var a = 1;
demo();
console.log(a); // 2
function demo() {
a = 2;
}
console.log(b); // error
</script>
</body>
</html>
可以看出,未定義的b會出現錯誤,而a變量聲明前置,但是執行第一個
console.log
的時候a未初始化,所以值未undefined,繼續往下執行demo()函數,雖然demo()函數聲明是在執行之后,但是由于函數聲明前置,所以正確執行顯示2。
3. arguments 是什么 (*)
- arguments意為參數。
- 在JavaScript中,arguments對象是比較特別的一個對象,實際上是當前函數的一個內置屬性。arguments非常類似Array,但實際上又不是一個Array實例。可以通過如下代碼得以證實(當然,實際上,在函數funcArg中,調用arguments是不必要寫成funcArg.arguments,直接寫arguments即可)。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>task17</title>
</head>
<body>
<script>
Array.prototype.testArg = "test";
function funcArg() {
console.log(funcArg.arguments[0]);
console.log(funcArg.arguments.testArg);
}
console.log(new Array().testArg);
funcArg(1,2);
</script>
</body>
</html>
- arguments對象的長度是由實參個數而不是形參個數決定的。形參是函數內部重新開辟內存空間存儲的變量,但是其與arguments對象內存空間并不重疊。對于arguments和值都存在的情況下,兩者值是同步的,但是針對其中一個無值的情況下,對于此無值的情形值不會得以同步。如下代碼可以得以驗證。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>task17</title>
</head>
<body>
<script>
function f(a, b, c) {
console.log(arguments.length); // 2
console.log(a); // 1
a = 100 ;
console.log(a); // 100
console.log(arguments[0]); // 100
console.log(c); // undefined
c = 200;
console.log(arguments[2]); // undefined
}
f(1,2);
</script>
</body>
</html>
- 由JavaScript中函數的聲明和調用特性,可以看出JavaScript中函數是不能重載的。
- Javascript函數的聲明是沒有返回值類型這一說法的;
- JavaScript中形參的個數嚴格意義上來講只是為了方便在函數中的變量操作,實際上實參已經存儲在arguments對象中了。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>task17</title>
</head>
<body>
<script>
function f(a) {
return a + 10;
}
function f(a) {
return a - 10;
}
console.log(f(1));
</script>
</body>
</html>

* arguments對象中有一個非常有用的屬性:callee。arguments.callee返回此arguments對象所在的當前函數引用。在使用函數遞歸調用時推薦使用arguments.callee代替函數名本身。
``` html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>task17</title>
</head>
<body>
<script>
function count(a) {
if(a == 1) {
return 1;
}
return a + arguments.callee(--a);
}
var mm = count(10);
console.log(mm);
</script>
</body>
</html>
不過call屬性在嚴格模式下被禁用了,這點需要注意。
參考:arguments對象
4. 函數的重載怎樣實現 (**)
在一些編程語言中,函數的返回值類型和參考不同會使得同一個函數有不同的功能一同實現,這是函數重載;而在Javascript中沒有重載的概念,所以正常情況下的函數是無法重載的:
不過可以用arguments屬性來判斷實參的個數,模擬重載:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>task17</title>
</head>
<body>
<script>
function test() {
if(arguments.length == 1) {
console.log(arguments[0] + 3);
} else if(arguments.length == 2) {
console.log(arguments[0] * arguments[1]);
}
}
test(1, 2);
</script>
</body>
</html>

5. 立即執行函數表達式是什么?有什么作用 (***)
- 用
()
將函數聲明變為表達式并立即執行,就是立即執行表達式(IIFE):
(function() {statement} ) ();
(function() {statement} ());
兩種寫法都可以,推薦使用后者; -
作用:
- 模擬塊作用域:
眾所周知,JavaScript沒有C或Java中的塊作用域(block),只有函數作用域,在同時調用多個庫的情況下,很容易造成對象或者變量的覆蓋,比如:
liba.js
- 模擬塊作用域:
var num = 1;
// code....
libb.js
var num = 2;
// code....
如果在頁面中同時引用liba.js和liba.js兩個庫,必然導致num變量被覆蓋,為了解決這個問題,可以通過IIFE來解決:
liba.js
(function(){
var num = 1;
// code....
}());
libb.js
(function(){
var num = 2;
// code....
}());
經過改造之后,兩個庫的代碼就完全獨立,并不會互相影響。
- 解決閉包沖突(待補充)
- 與自執行函數表達式的區別:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>task17</title>
</head>
<body>
<script>
// 這是一個自執行的函數,函數內部執行自身,遞歸
function foo() { foo(); }
// 這是一個自執行的匿名函數,因為沒有標示名稱
// 必須使用arguments.callee屬性來執行自己
var foo = function () { arguments.callee(); };
// 這可能也是一個自執行的匿名函數,僅僅是foo標示名稱引用它自身
// 如果你將foo改變成其它的,你將得到一個used-to-self-execute匿名函數
var foo = function () { foo(); };
// 有些人叫這個是自執行的匿名函數(即便它不是),因為它沒有調用自身,它只是立即執行而已。
(function () { /* code */ } ());
// 為函數表達式添加一個標示名稱,可以方便Debug
// 但一定命名了,這個函數就不再是匿名的了
(function foo() { /* code */ } ());
// 立即調用的函數表達式(IIFE)也可以自執行,不過可能不常用罷了
(function () { arguments.callee(); } ());
(function foo() { foo(); } ());
// 另外,下面的代碼在黑莓5里執行會出錯,因為在一個命名的函數表達式里,他的名稱是undefined
// 呵呵,奇怪
(function foo() { foo(); } ());
</script>
</body>
</html>
6. 什么是函數的作用域鏈 (****)
-
javascript作用域:
任何程序設計語言都有作用域的概念,簡單的說,作用域就是變量與函數的可訪問范圍,即作用域控制著變量與函數的可見性和生命周期。在JavaScript中,變量的作用域有全局作用域和局部作用域兩種。-
全局作用域(Global Scope)
在代碼中任何地方都能訪問到的對象擁有全局作用域,一般來說以下幾種情形擁有全局作用域: - 最外層函數和在最外層函數外面定義的變量擁有全局作用域,例如:
-
全局作用域(Global Scope)
var a = 1;
function add() {
var c = 1 + 2;
}
-
所有末定義直接賦值的變量自動聲明為擁有全局作用域,例如:
未聲明局部變量 - 所有window對象的屬性擁有全局作用域
-
局部作用域(Local Scope)
和全局作用域相反,局部作用域一般只在固定的代碼片段內可訪問到,最常見的例如函數內部,所有在一些地方也會看到有人把這種作用域稱為函數作用域:
局部作用域 -
作用域鏈:
作用域鏈是內部上下文所有變量對象(包括父變量對象)的列表,用來變量查詢。在代碼執行的過程中,所用到的變量會在當前作用域中進行尋找,如果找不到,就會往沿著作用域鏈向上一級進行尋找,一直到全局作用域為止,如果找到便會停止(而不理會上一級是否有同名的變量),如果找不到,就會報錯。
function add(num1,num2) {
var sum = num1 + num2;
return sum;
}
在函數add創建時,它的作用域鏈中會填入一個全局對象,該全局對象包含了所有全局變量,如下圖所示(注意:圖片只例舉了全部變量中的一部分):
執行代碼:
var total = add(5, 10);
首先在本身內部作用域中尋找所需對象,當不存在時,像外層找,執行過程中按照從上到下順序執行,執行一層就會類似于編鎖鏈上的一節,將其保留下來,在其生命周期內反復被查找。
參考:
JavaScript 開發進階:理解 JavaScript 作用域和作用域鏈
深入理解JavaScript系列(14):作用域鏈(Scope Chain)
代碼:
1. 以下代碼輸出什么? (難度**)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>task17-1</title>
</head>
<body>
<script>
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('男');
</script>
</body>
</html>

2. 寫一個函數,返回參數的平方和?如 (難度**)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>task17-2</title>
</head>
<body>
<script>
function sumOfSquare(){
var sum = 0;
for(var i = 0; i < arguments.length; i++){
sum = arguments[i] * arguments[i] + sum;
}
console.log(sum);
}
sumOfSquare(2,3,4);
sumOfSquare(1,3);
</script>
</body>
</html>
3. 如下代碼的輸出?為什么 (難度*)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>task17-3</title>
</head>
<body>
<script>
console.log(a);
var a = 1;
console.log(b);
</script>
</body>
</html>
4. 如下代碼的輸出?為什么 (難度*)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>task17-4</title>
</head>
<body>
<script>
sayName('world');
sayAge(10);
function sayName(name){
console.log('hello', name);
}
var sayAge = function(age){
console.log(age);
};
</script>
</body>
</html>
5. 如下代碼的輸出?為什么 (難度**)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>task17-5</title>
</head>
<body>
<script>
function fn(){}
var fn = 3;
console.log(fn);
</script>
</body>
</html>

6. 如下代碼的輸出?為什么 (難度***)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>task17-6</title>
</head>
<body>
<script>
function fn(fn2){
console.log(fn2);
var fn2 = 3;
console.log(fn2);
console.log(fn);
function fn2(){
console.log('fnnn2');
}
}
fn(10);
</script>
</body>
</html>
按照執行順序重寫函數:
<script>
function fn(fn2){
var fn2;
function fn2(){
console.log('fnnn2');
}
console.log(fn2);
fn2 = 3;
console.log(fn2);
console.log(fn);
}
fn(10);
</script>
7. 如下代碼的輸出?為什么 (難度***)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>task17-7</title>
</head>
<body>
<script>
var fn = 1;
function fn(fn){
console.log(fn);
}
console.log(fn(fn));
</script>
</body>
</html>
執行順序:
<script>
var fn;
function fn(fn){
console.log(fn);
}
fn = 1;
console.log(fn(fn));
</script>
fn = 1為最終結果,無法進行()操作,所以顯示錯誤。
8. 如下代碼的輸出?為什么 (難度**)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>task17-8</title>
</head>
<body>
<script>
console.log(j);
console.log(i);
for(var i = 0; i < 10; i++){
var j = 100;
}
console.log(i);
console.log(j);
</script>
</body>
</html>
for是循環語句,不是函數,首先并不會前置,其次其定義的自然就是全局變量,所以能夠被解析,正常順序執行并顯示。
9. 如下代碼的輸出?為什么 (難度****)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>task17-9</title>
</head>
<body>
<script>
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;
}
}
</script>
</body>
</html>
執行順序:
<script>
var i, fn;
function fn(){
var i;
function fn2(){
i = 100;
}
console.log(i);
i = 99;
fn2();
console.log(i);
}
fn();
i = 10;
fn = 20;
console.log(i);
</script>
- 首先將變量和函數前置,并且將函數內部嵌套函數作用域內也進行前置操作;
- 然后按照順序,首先執行fn(),會遇到第一個console.log(i),由于此時i未賦值,所以為undefined,然后執行fn2()函數,由于fn2()函數中的i未聲明,所以定義的是全局變量,所以下一個console.log(i)為100;
- 繼續往下執行,i = 10定義全局變量,所以最后一個console.log(i)為10。
10. 如下代碼的輸出?為什么 (難度*****)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>task17-10</title>
</head>
<body>
<script>
var say = 0;
(function say(n){
console.log(n);
if(n < 3) return;
say(n - 1);
}(10));
console.log(say);
</script>
</body>
</html>

- 首先,放在一個()中,并且有分號結尾,說明其實一個語句,即“立即執行的函數表達式”,所以函數不會前置,按照順序執行,立即執行的表達式中發生迭代,顯示10,9,8......3,2,滿足if條件,return跳出函數,繼續執行下面的console.log(say);語句,由于立即執行的函數表達式中的say生命周期已經結束,所以console.log尋找全局變量say,為0。
本文版權歸本人和饑人谷所有,轉載請注明來源,謝謝