# 函數
函數是一段可以反復調用的代碼塊。函數還能接受輸入的參數,不同的參數會返回不同的值。
## 概述
### 函數的聲明
JavaScript 有三種聲明函數的方法。
**(1)function 命令**
`function`命令聲明的代碼區塊,就是一個函數。`function`命令后面是函數名,函數名后面是一對圓括號,里面是傳入函數的參數。函數體放在大括號里面。
```javascript
function print(s) {
console.log(s);
}
```
上面的代碼命名了一個`print`函數,以后使用`print()`這種形式,就可以調用相應的代碼。這叫做函數的聲明(Function Declaration)。
**(2)函數表達式**
除了用`function`命令聲明函數,還可以采用變量賦值的寫法。
```javascript
var print = function(s) {
console.log(s);
};
```
這種寫法將一個匿名函數賦值給變量。這時,這個匿名函數又稱函數表達式(Function Expression),因為賦值語句的等號右側只能放表達式。
采用函數表達式聲明函數時,`function`命令后面不帶有函數名。如果加上函數名,該函數名只在函數體內部有效,在函數體外部無效。
```javascript
var print = function x(){
console.log(typeof x);
};
x
// ReferenceError: x is not defined
print()
// function
```
上面代碼在函數表達式中,加入了函數名`x`。這個`x`只在函數體內部可用,指代函數表達式本身,其他地方都不可用。這種寫法的用處有兩個,一是可以在函數體內部調用自身,二是方便除錯(除錯工具顯示函數調用棧時,將顯示函數名,而不再顯示這里是一個匿名函數)。因此,下面的形式聲明函數也非常常見。
```javascript
var f = function f() {};
```
需要注意的是,函數的表達式需要在語句的結尾加上分號,表示語句結束。而函數的聲明在結尾的大括號后面不用加分號。總的來說,這兩種聲明函數的方式,差別很細微,可以近似認為是等價的。
**(3)Function 構造函數**
第三種聲明函數的方式是`Function`構造函數。
```javascript
var add = new Function(
'x',
'y',
'return x + y'
);
// 等同于
function add(x, y) {
return x + y;
}
```
上面代碼中,`Function`構造函數接受三個參數,除了最后一個參數是`add`函數的“函數體”,其他參數都是`add`函數的參數。
你可以傳遞任意數量的參數給`Function`構造函數,只有最后一個參數會被當做函數體,如果只有一個參數,該參數就是函數體。
```javascript
var foo = new Function(
'return "hello world";'
);
// 等同于
function foo() {
return 'hello world';
}
```
`Function`構造函數可以不使用`new`命令,返回結果完全一樣。
總的來說,這種聲明函數的方式非常不直觀,幾乎無人使用。
### 函數的重復聲明
如果同一個函數被多次聲明,后面的聲明就會覆蓋前面的聲明。
```javascript
function f() {
console.log(1);
}
f() // 2
function f() {
console.log(2);
}
f() // 2
```
上面代碼中,后一次的函數聲明覆蓋了前面一次。而且,由于函數名的提升(參見下文),前一次聲明在任何時候都是無效的,這一點要特別注意。
### 圓括號運算符,return 語句和遞歸
調用函數時,要使用圓括號運算符。圓括號之中,可以加入函數的參數。
```javascript
function add(x, y) {
return x + y;
}
add(1, 1) // 2
```
上面代碼中,函數名后面緊跟一對圓括號,就會調用這個函數。
函數體內部的`return`語句,表示返回。JavaScript 引擎遇到`return`語句,就直接返回`return`后面的那個表達式的值,后面即使還有語句,也不會得到執行。也就是說,`return`語句所帶的那個表達式,就是函數的返回值。`return`語句不是必需的,如果沒有的話,該函數就不返回任何值,或者說返回`undefined`。
函數可以調用自身,這就是遞歸(recursion)。下面就是通過遞歸,計算斐波那契數列的代碼。
```javascript
function fib(num) {
if (num === 0) return 0;
if (num === 1) return 1;
return fib(num - 2) + fib(num - 1);
}
fib(6) // 8
```
上面代碼中,`fib`函數內部又調用了`fib`,計算得到斐波那契數列的第6個元素是8。
### 第一等公民
JavaScript 語言將函數看作一種值,與其它值(數值、字符串、布爾值等等)地位相同。凡是可以使用值的地方,就能使用函數。比如,可以把函數賦值給變量和對象的屬性,也可以當作參數傳入其他函數,或者作為函數的結果返回。函數只是一個可以執行的值,此外并無特殊之處。
由于函數與其他數據類型地位平等,所以在 JavaScript 語言中又稱函數為第一等公民。
```javascript
function add(x, y) {
return x + y;
}
// 將函數賦值給一個變量
var operator = add;
// 將函數作為參數和返回值
function a(op){
return op;
}
a(add)(1, 1)
// 2
```
### 函數名的提升
JavaScript 引擎將函數名視同變量名,所以采用`function`命令聲明函數時,整個函數會像變量聲明一樣,被提升到代碼頭部。所以,下面的代碼不會報錯。
```javascript
f();
function f() {}
```
表面上,上面代碼好像在聲明之前就調用了函數`f`。但是實際上,由于“變量提升”,函數`f`被提升到了代碼頭部,也就是在調用之前已經聲明了。但是,如果采用賦值語句定義函數,JavaScript 就會報錯。
```javascript
f();
var f = function (){};
// TypeError: undefined is not a function
```
上面的代碼等同于下面的形式。
```javascript
var f;
f();
f = function () {};
```
上面代碼第二行,調用`f`的時候,`f`只是被聲明了,還沒有被賦值,等于`undefined`,所以會報錯。
注意,如果像下面例子那樣,采用`function`命令和`var`賦值語句聲明同一個函數,由于存在函數提升,最后會采用`var`賦值語句的定義。
```javascript
var f = function () {
console.log('1');
}
function f() {
console.log('2');
}
f() // 1
```
上面例子中,表面上后面聲明的函數`f`,應該覆蓋前面的`var`賦值語句,但是由于存在函數提升,實際上正好反過來。
## 函數的屬性和方法
### name 屬性
函數的`name`屬性返回函數的名字。
```javascript
function f1() {}
f1.name // "f1"
```
如果是通過變量賦值定義的函數,那么`name`屬性返回變量名。
```javascript
var f2 = function () {};
f2.name // "f2"
```
但是,上面這種情況,只有在變量的值是一個匿名函數時才是如此。如果變量的值是一個具名函數,那么`name`屬性返回`function`關鍵字之后的那個函數名。
```javascript
var f3 = function myName() {};
f3.name // 'myName'
```
上面代碼中,`f3.name`返回函數表達式的名字。注意,真正的函數名還是`f3`,而`myName`這個名字只在函數體內部可用。
`name`屬性的一個用處,就是獲取參數函數的名字。
```javascript
var myFunc = function () {};
function test(f) {
console.log(f.name);
}
test(myFunc) // myFunc
```
上面代碼中,函數`test`內部通過`name`屬性,就可以知道傳入的參數是什么函數。
### length 屬性
函數的`length`屬性返回函數預期傳入的參數個數,即函數定義之中的參數個數。
```javascript
function f(a, b) {}
f.length // 2
```
上面代碼定義了空函數`f`,它的`length`屬性就是定義時的參數個數。不管調用時輸入了多少個參數,`length`屬性始終等于2。
`length`屬性提供了一種機制,判斷定義時和調用時參數的差異,以便實現面向對象編程的“方法重載”(overload)。
### toString()
函數的`toString()`方法返回一個字符串,內容是函數的源碼。
```javascript
function f() {
a();
b();
c();
}
f.toString()
// function f() {
// a();
// b();
// c();
// }
```
上面示例中,函數`f`的`toString()`方法返回了`f`的源碼,包含換行符在內。
對于那些原生的函數,`toString()`方法返回`function (){[native code]}`。
```javascript
Math.sqrt.toString()
// "function sqrt() { [native code] }"
```
上面代碼中,`Math.sqrt()`是 JavaScript 引擎提供的原生函數,`toString()`方法就返回原生代碼的提示。
函數內部的注釋也可以返回。
```javascript
function f() {/*
這是一個
多行注釋
*/}
f.toString()
// "function f(){/*
// 這是一個
// 多行注釋
// */}"
```
利用這一點,可以變相實現多行字符串。
```javascript
var multiline = function (fn) {
var arr = fn.toString().split('\n');
return arr.slice(1, arr.length - 1).join('\n');
};
function f() {/*
這是一個
多行注釋
*/}
multiline(f);
// " 這是一個
// 多行注釋"
```
上面示例中,函數`f`內部有一個多行注釋,`toString()`方法拿到`f`的源碼后,去掉首尾兩行,就得到了一個多行字符串。
## 函數作用域
### 定義
作用域(scope)指的是變量存在的范圍。在 ES5 的規范中,JavaScript 只有兩種作用域:一種是全局作用域,變量在整個程序中一直存在,所有地方都可以讀取;另一種是函數作用域,變量只在函數內部存在。ES6 又新增了塊級作用域,本教程不涉及。
對于頂層函數來說,函數外部聲明的變量就是全局變量(global variable),它可以在函數內部讀取。
```javascript
var v = 1;
function f() {
console.log(v);
}
f()
// 1
```
上面的代碼表明,函數`f`內部可以讀取全局變量`v`。
在函數內部定義的變量,外部無法讀取,稱為“局部變量”(local variable)。
```javascript
function f(){
var v = 1;
}
v // ReferenceError: v is not defined
```
上面代碼中,變量`v`在函數內部定義,所以是一個局部變量,函數之外就無法讀取。
函數內部定義的變量,會在該作用域內覆蓋同名全局變量。
```javascript
var v = 1;
function f(){
var v = 2;
console.log(v);
}
f() // 2
v // 1
```
上面代碼中,變量`v`同時在函數的外部和內部有定義。結果,在函數內部定義,局部變量`v`覆蓋了全局變量`v`。
注意,對于`var`命令來說,局部變量只能在函數內部聲明,在其他區塊中聲明,一律都是全局變量。
```javascript
if (true) {
var x = 5;
}
console.log(x); // 5
```
上面代碼中,變量`x`在條件判斷區塊之中聲明,結果就是一個全局變量,可以在區塊之外讀取。
### 函數內部的變量提升
與全局作用域一樣,函數作用域內部也會產生“變量提升”現象。`var`命令聲明的變量,不管在什么位置,變量聲明都會被提升到函數體的頭部。
```javascript
function foo(x) {
if (x > 100) {
var tmp = x - 100;
}
}
// 等同于
function foo(x) {
var tmp;
if (x > 100) {
tmp = x - 100;
};
}
```
### 函數本身的作用域
函數本身也是一個值,也有自己的作用域。它的作用域與變量一樣,就是其聲明時所在的作用域,與其運行時所在的作用域無關。
```javascript
var a = 1;
var x = function () {
console.log(a);
};
function f() {
var a = 2;
x();
}
f() // 1
```
上面代碼中,函數`x`是在函數`f`的外部聲明的,所以它的作用域綁定外層,內部變量`a`不會到函數`f`體內取值,所以輸出`1`,而不是`2`。
總之,函數執行時所在的作用域,是定義時的作用域,而不是調用時所在的作用域。
很容易犯錯的一點是,如果函數`A`調用函數`B`,卻沒考慮到函數`B`不會引用函數`A`的內部變量。
```javascript
var x = function () {
console.log(a);
};
function y(f) {
var a = 2;
f();
}
y(x)
// ReferenceError: a is not defined
```
上面代碼將函數`x`作為參數,傳入函數`y`。但是,函數`x`是在函數`y`體外聲明的,作用域綁定外層,因此找不到函數`y`的內部變量`a`,導致報錯。
同樣的,函數體內部聲明的函數,作用域綁定函數體內部。
```javascript
function foo() {
var x = 1;
function bar() {
console.log(x);
}
return bar;
}
var x = 2;
var f = foo();
f() // 1
```
上面代碼中,函數`foo`內部聲明了一個函數`bar`,`bar`的作用域綁定`foo`。當我們在`foo`外部取出`bar`執行時,變量`x`指向的是`foo`內部的`x`,而不是`foo`外部的`x`。正是這種機制,構成了下文要講解的“閉包”現象。
## 參數
### 概述
函數運行的時候,有時需要提供外部數據,不同的外部數據會得到不同的結果,這種外部數據就叫參數。
```javascript
function square(x) {
return x * x;
}
square(2) // 4
square(3) // 9
```
上式的`x`就是`square`函數的參數。每次運行的時候,需要提供這個值,否則得不到結果。
### 參數的省略
函數參數不是必需的,JavaScript 允許省略參數。
```javascript
function f(a, b) {
return a;
}
f(1, 2, 3) // 1
f(1) // 1
f() // undefined
f.length // 2
```
上面代碼的函數`f`定義了兩個參數,但是運行時無論提供多少個參數(或者不提供參數),JavaScript 都不會報錯。省略的參數的值就變為`undefined`。需要注意的是,函數的`length`屬性與實際傳入的參數個數無關,只反映函數預期傳入的參數個數。
但是,沒有辦法只省略靠前的參數,而保留靠后的參數。如果一定要省略靠前的參數,只有顯式傳入`undefined`。
```javascript
function f(a, b) {
return a;
}
f( , 1) // SyntaxError: Unexpected token ,(…)
f(undefined, 1) // undefined
```
上面代碼中,如果省略第一個參數,就會報錯。
### 傳遞方式
函數參數如果是原始類型的值(數值、字符串、布爾值),傳遞方式是傳值傳遞(passes by value)。這意味著,在函數體內修改參數值,不會影響到函數外部。
```javascript
var p = 2;
function f(p) {
p = 3;
}
f(p);
p // 2
```
上面代碼中,變量`p`是一個原始類型的值,傳入函數`f`的方式是傳值傳遞。因此,在函數內部,`p`的值是原始值的拷貝,無論怎么修改,都不會影響到原始值。
但是,如果函數參數是復合類型的值(數組、對象、其他函數),傳遞方式是傳址傳遞(pass by reference)。也就是說,傳入函數的原始值的地址,因此在函數內部修改參數,將會影響到原始值。
```javascript
var obj = { p: 1 };
function f(o) {
o.p = 2;
}
f(obj);
obj.p // 2
```
上面代碼中,傳入函數`f`的是參數對象`obj`的地址。因此,在函數內部修改`obj`的屬性`p`,會影響到原始值。
注意,如果函數內部修改的,不是參數對象的某個屬性,而是替換掉整個參數,這時不會影響到原始值。
```javascript
var obj = [1, 2, 3];
function f(o) {
o = [2, 3, 4];
}
f(obj);
obj // [1, 2, 3]
```
上面代碼中,在函數`f()`內部,參數對象`obj`被整個替換成另一個值。這時不會影響到原始值。這是因為,形式參數(`o`)的值實際是參數`obj`的地址,重新對`o`賦值導致`o`指向另一個地址,保存在原地址上的值當然不受影響。
### 同名參數
如果有同名的參數,則取最后出現的那個值。
```javascript
function f(a, a) {
console.log(a);
}
f(1, 2) // 2
```
上面代碼中,函數`f()`有兩個參數,且參數名都是`a`。取值的時候,以后面的`a`為準,即使后面的`a`沒有值或被省略,也是以其為準。
```javascript
function f(a, a) {
console.log(a);
}
f(1) // undefined
```
調用函數`f()`的時候,沒有提供第二個參數,`a`的取值就變成了`undefined`。這時,如果要獲得第一個`a`的值,可以使用`arguments`對象。
```javascript
function f(a, a) {
console.log(arguments[0]);
}
f(1) // 1
```
### arguments 對象
**(1)定義**
由于 JavaScript 允許函數有不定數目的參數,所以需要一種機制,可以在函數體內部讀取所有參數。這就是`arguments`對象的由來。
`arguments`對象包含了函數運行時的所有參數,`arguments[0]`就是第一個參數,`arguments[1]`就是第二個參數,以此類推。這個對象只有在函數體內部,才可以使用。
```javascript
var f = function (one) {
console.log(arguments[0]);
console.log(arguments[1]);
console.log(arguments[2]);
}
f(1, 2, 3)
// 1
// 2
// 3
```
正常模式下,`arguments`對象可以在運行時修改。
```javascript
var f = function(a, b) {
arguments[0] = 3;
arguments[1] = 2;
return a + b;
}
f(1, 1) // 5
```
上面代碼中,函數`f()`調用時傳入的參數,在函數內部被修改成`3`和`2`。
嚴格模式下,`arguments`對象與函數參數不具有聯動關系。也就是說,修改`arguments`對象不會影響到實際的函數參數。
```javascript
var f = function(a, b) {
'use strict'; // 開啟嚴格模式
arguments[0] = 3;
arguments[1] = 2;
return a + b;
}
f(1, 1) // 2
```
上面代碼中,函數體內是嚴格模式,這時修改`arguments`對象,不會影響到真實參數`a`和`b`。
通過`arguments`對象的`length`屬性,可以判斷函數調用時到底帶幾個參數。
```javascript
function f() {
return arguments.length;
}
f(1, 2, 3) // 3
f(1) // 1
f() // 0
```
**(2)與數組的關系**
需要注意的是,雖然`arguments`很像數組,但它是一個對象。數組專有的方法(比如`slice`和`forEach`),不能在`arguments`對象上直接使用。
如果要讓`arguments`對象使用數組方法,真正的解決方法是將`arguments`轉為真正的數組。下面是兩種常用的轉換方法:`slice`方法和逐一填入新數組。
```javascript
var args = Array.prototype.slice.call(arguments);
// 或者
var args = [];
for (var i = 0; i < arguments.length; i++) {
args.push(arguments[i]);
}
```
**(3)callee 屬性**
`arguments`對象帶有一個`callee`屬性,返回它所對應的原函數。
```javascript
var f = function () {
console.log(arguments.callee === f);
}
f() // true
```
可以通過`arguments.callee`,達到調用函數自身的目的。這個屬性在嚴格模式里面是禁用的,因此不建議使用。
## 函數的其他知識點
### 閉包
閉包(closure)是 JavaScript 語言的一個難點,也是它的特色,很多高級應用都要依靠閉包實現。
理解閉包,首先必須理解變量作用域。前面提到,JavaScript 有兩種作用域:全局作用域和函數作用域。函數內部可以直接讀取全局變量。
```javascript
var n = 999;
function f1() {
console.log(n);
}
f1() // 999
```
上面代碼中,函數`f1`可以讀取全局變量`n`。
但是,正常情況下,函數外部無法讀取函數內部聲明的變量。
```javascript
function f1() {
var n = 999;
}
console.log(n)
// Uncaught ReferenceError: n is not defined(
```
上面代碼中,函數`f1`內部聲明的變量`n`,函數外是無法讀取的。
如果出于種種原因,需要得到函數內的局部變量。正常情況下,這是辦不到的,只有通過變通方法才能實現。那就是在函數的內部,再定義一個函數。
```javascript
function f1() {
var n = 999;
function f2() {
console.log(n); // 999
}
}
```
上面代碼中,函數`f2`就在函數`f1`內部,這時`f1`內部的所有局部變量,對`f2`都是可見的。但是反過來就不行,`f2`內部的局部變量,對`f1`就是不可見的。這就是 JavaScript 語言特有的"鏈式作用域"結構(chain scope),子對象會一級一級地向上尋找所有父對象的變量。所以,父對象的所有變量,對子對象都是可見的,反之則不成立。
既然`f2`可以讀取`f1`的局部變量,那么只要把`f2`作為返回值,我們不就可以在`f1`外部讀取它的內部變量了嗎!
```javascript
function f1() {
var n = 999;
function f2() {
console.log(n);
}
return f2;
}
var result = f1();
result(); // 999
```
上面代碼中,函數`f1`的返回值就是函數`f2`,由于`f2`可以讀取`f1`的內部變量,所以就可以在外部獲得`f1`的內部變量了。
閉包就是函數`f2`,即能夠讀取其他函數內部變量的函數。由于在 JavaScript 語言中,只有函數內部的子函數才能讀取內部變量,因此可以把閉包簡單理解成“定義在一個函數內部的函數”。閉包最大的特點,就是它可以“記住”誕生的環境,比如`f2`記住了它誕生的環境`f1`,所以從`f2`可以得到`f1`的內部變量。在本質上,閉包就是將函數內部和函數外部連接起來的一座橋梁。
閉包的最大用處有兩個,一個是可以讀取外層函數內部的變量,另一個就是讓這些變量始終保持在內存中,即閉包可以使得它誕生環境一直存在。請看下面的例子,閉包使得內部變量記住上一次調用時的運算結果。
```javascript
function createIncrementor(start) {
return function () {
return start++;
};
}
var inc = createIncrementor(5);
inc() // 5
inc() // 6
inc() // 7
```
上面代碼中,`start`是函數`createIncrementor`的內部變量。通過閉包,`start`的狀態被保留了,每一次調用都是在上一次調用的基礎上進行計算。從中可以看到,閉包`inc`使得函數`createIncrementor`的內部環境,一直存在。所以,閉包可以看作是函數內部作用域的一個接口。
為什么閉包能夠返回外層函數的內部變量?原因是閉包(上例的`inc`)用到了外層變量(`start`),導致外層函數(`createIncrementor`)不能從內存釋放。只要閉包沒有被垃圾回收機制清除,外層函數提供的運行環境也不會被清除,它的內部變量就始終保存著當前值,供閉包讀取。
閉包的另一個用處,是封裝對象的私有屬性和私有方法。
```javascript
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);
p1.getAge() // 25
```
上面代碼中,函數`Person`的內部變量`_age`,通過閉包`getAge`和`setAge`,變成了返回對象`p1`的私有變量。
注意,外層函數每次運行,都會生成一個新的閉包,而這個閉包又會保留外層函數的內部變量,所以內存消耗很大。因此不能濫用閉包,否則會造成網頁的性能問題。
### 立即調用的函數表達式(IIFE)
根據 JavaScript 的語法,圓括號`()`跟在函數名之后,表示調用該函數。比如,`print()`就表示調用`print`函數。
有時,我們需要在定義函數之后,立即調用該函數。這時,你不能在函數的定義之后加上圓括號,這會產生語法錯誤。
```javascript
function(){ /* code */ }();
// SyntaxError: Unexpected token (
```
產生這個錯誤的原因是,`function`這個關鍵字即可以當作語句,也可以當作表達式。
```javascript
// 語句
function f() {}
// 表達式
var f = function f() {}
```
當作表達式時,函數可以定義后直接加圓括號調用。
```javascript
var f = function f(){ return 1}();
f // 1
```
上面的代碼中,函數定義后直接加圓括號調用,沒有報錯。原因就是`function`作為表達式,引擎就把函數定義當作一個值。這種情況下,就不會報錯。
為了避免解析的歧義,JavaScript 規定,如果`function`關鍵字出現在行首,一律解釋成語句。因此,引擎看到行首是`function`關鍵字之后,認為這一段都是函數的定義,不應該以圓括號結尾,所以就報錯了。
函數定義后立即調用的解決方法,就是不要讓`function`出現在行首,讓引擎將其理解成一個表達式。最簡單的處理,就是將其放在一個圓括號里面。
```javascript
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();
```
上面兩種寫法都是以圓括號開頭,引擎就會認為后面跟的是一個表達式,而不是函數定義語句,所以就避免了錯誤。這就叫做“立即調用的函數表達式”(Immediately-Invoked Function Expression),簡稱 IIFE。
注意,上面兩種寫法最后的分號都是必須的。如果省略分號,遇到連著兩個 IIFE,可能就會報錯。
```javascript
// 報錯
(function(){ /* code */ }())
(function(){ /* code */ }())
```
上面代碼的兩行之間沒有分號,JavaScript 會將它們連在一起解釋,將第二行解釋為第一行的參數。
推而廣之,任何讓解釋器以表達式來處理函數定義的方法,都能產生同樣的效果,比如下面三種寫法。
```javascript
var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();
```
甚至像下面這樣寫,也是可以的。
```javascript
!function () { /* code */ }();
~function () { /* code */ }();
-function () { /* code */ }();
+function () { /* code */ }();
```
通常情況下,只對匿名函數使用這種“立即執行的函數表達式”。它的目的有兩個:一是不必為函數命名,避免了污染全局變量;二是 IIFE 內部形成了一個單獨的作用域,可以封裝一些外部無法讀取的私有變量。
```javascript
// 寫法一
var tmp = newData;
processData(tmp);
storeData(tmp);
// 寫法二
(function () {
var tmp = newData;
processData(tmp);
storeData(tmp);
}());
```
上面代碼中,寫法二比寫法一更好,因為完全避免了污染全局變量。
## eval 命令
### 基本用法
`eval`命令接受一個字符串作為參數,并將這個字符串當作語句執行。
```javascript
eval('var a = 1;');
a // 1
```
上面代碼將字符串當作語句運行,生成了變量`a`。
如果參數字符串無法當作語句運行,那么就會報錯。
```javascript
eval('3x') // Uncaught SyntaxError: Invalid or unexpected token
```
放在`eval`中的字符串,應該有獨自存在的意義,不能用來與`eval`以外的命令配合使用。舉例來說,下面的代碼將會報錯。
```javascript
eval('return;'); // Uncaught SyntaxError: Illegal return statement
```
上面代碼會報錯,因為`return`不能單獨使用,必須在函數中使用。
如果`eval`的參數不是字符串,那么會原樣返回。
```javascript
eval(123) // 123
```
`eval`沒有自己的作用域,都在當前作用域內執行,因此可能會修改當前作用域的變量的值,造成安全問題。
```javascript
var a = 1;
eval('a = 2');
a // 2
```
上面代碼中,`eval`命令修改了外部變量`a`的值。由于這個原因,`eval`有安全風險。
為了防止這種風險,JavaScript 規定,如果使用嚴格模式,`eval`內部聲明的變量,不會影響到外部作用域。
```javascript
(function f() {
'use strict';
eval('var foo = 123');
console.log(foo); // ReferenceError: foo is not defined
})()
```
上面代碼中,函數`f`內部是嚴格模式,這時`eval`內部聲明的`foo`變量,就不會影響到外部。
不過,即使在嚴格模式下,`eval`依然可以讀寫當前作用域的變量。
```javascript
(function f() {
'use strict';
var foo = 1;
eval('foo = 2');
console.log(foo); // 2
})()
```
上面代碼中,嚴格模式下,`eval`內部還是改寫了外部變量,可見安全風險依然存在。
總之,`eval`的本質是在當前作用域之中,注入代碼。由于安全風險和不利于 JavaScript 引擎優化執行速度,所以一般不推薦使用。通常情況下,`eval`最常見的場合是解析 JSON 數據的字符串,不過正確的做法應該是使用原生的`JSON.parse`方法。
### eval 的別名調用
前面說過`eval`不利于引擎優化執行速度。更麻煩的是,還有下面這種情況,引擎在靜態代碼分析的階段,根本無法分辨執行的是`eval`。
```javascript
var m = eval;
m('var x = 1');
x // 1
```
上面代碼中,變量`m`是`eval`的別名。靜態代碼分析階段,引擎分辨不出`m('var x = 1')`執行的是`eval`命令。
為了保證`eval`的別名不影響代碼優化,JavaScript 的標準規定,凡是使用別名執行`eval`,`eval`內部一律是全局作用域。
```javascript
var a = 1;
function f() {
var a = 2;
var e = eval;
e('console.log(a)');
}
f() // 1
```
上面代碼中,`eval`是別名調用,所以即使它是在函數中,它的作用域還是全局作用域,因此輸出的`a`為全局變量。這樣的話,引擎就能確認`e()`不會對當前的函數作用域產生影響,優化的時候就可以把這一行排除掉。
`eval`的別名調用的形式五花八門,只要不是直接調用,都屬于別名調用,因為引擎只能分辨`eval()`這一種形式是直接調用。
```javascript
eval.call(null, '...')
window.eval('...')
(1, eval)('...')
(eval, eval)('...')
```
上面這些形式都是`eval`的別名調用,作用域都是全局作用域。
## 參考鏈接
- Ben Alman, [Immediately-Invoked Function Expression (IIFE)](http://benalman.com/news/2010/11/immediately-invoked-function-expression/)
- Mark Daggett, [Functions Explained](http://markdaggett.com/blog/2013/02/15/functions-explained/)
- Juriy Zaytsev, [Named function expressions demystified](http://kangax.github.com/nfe/)
- Marco Rogers polotek, [What is the arguments object?](http://docs.nodejitsu.com/articles/javascript-conventions/what-is-the-arguments-object)
- Juriy Zaytsev, [Global eval. What are the options?](http://perfectionkills.com/global-eval-what-are-the-options/)
- Axel Rauschmayer, [Evaluating JavaScript code via eval() and new Function()](http://www.2ality.com/2014/01/eval.html)
- 前言
- 入門篇
- 導論
- 歷史
- 基本語法
- 數據類型
- 概述
- null,undefined 和布爾值
- 數值
- 字符串
- 對象
- 函數
- 數組
- 運算符
- 算術運算符
- 比較運算符
- 布爾運算符
- 二進制位運算符
- 其他運算符,運算順序
- 語法專題
- 數據類型的轉換
- 錯誤處理機制
- 編程風格
- console 對象與控制臺
- 標準庫
- Object 對象
- 屬性描述對象
- Array 對象
- 包裝對象
- Boolean 對象
- Number 對象
- String 對象
- Math 對象
- Date 對象
- RegExp 對象
- JSON 對象
- 面向對象編程
- 實例對象與 new 命令
- this 關鍵字
- 對象的繼承
- Object 對象的相關方法
- 嚴格模式
- 異步操作
- 概述
- 定時器
- Promise 對象
- DOM
- 概述
- Node 接口
- NodeList 接口,HTMLCollection 接口
- ParentNode 接口,ChildNode 接口
- Document 節點
- Element 節點
- 屬性的操作
- Text 節點和 DocumentFragment 節點
- CSS 操作
- Mutation Observer API
- 事件
- EventTarget 接口
- 事件模型
- Event 對象
- 鼠標事件
- 鍵盤事件
- 進度事件
- 表單事件
- 觸摸事件
- 拖拉事件
- 其他常見事件
- GlobalEventHandlers 接口
- 瀏覽器模型
- 瀏覽器模型概述
- window 對象
- Navigator 對象,Screen 對象
- Cookie
- XMLHttpRequest 對象
- 同源限制
- CORS 通信
- Storage 接口
- History 對象
- Location 對象,URL 對象,URLSearchParams 對象
- ArrayBuffer 對象,Blob 對象
- File 對象,FileList 對象,FileReader 對象
- 表單,FormData 對象
- IndexedDB API
- Web Worker
- 附錄:網頁元素接口
- a
- img
- form
- input
- button
- option
- video,audio