function
命令後面是函數名,函數名後面是一對圓括號,裏面是傳入函數的參數。函數體放在大括號裏面。function print(s) { console.log(s); }
function
命令後面通常不帶有函數名。若是加上函數名,該函數名只在函數體內部有效,在函數體外部無效。var print = function x(){ console.log(typeof x); }; x // ReferenceError: x is not defined print() // function
可是,咱們在函數表達式中,每每會加入這個函數名x。雖然這個x
只在函數體內部可用,指代函數表達式自己,其餘地方都不可用。可是這種寫法有兩個好處:(1)能夠在函數體內部調用自身,方便遞歸;(2)方便除錯(除錯工具顯示函數調用棧時,將顯示函數名,而再也不顯示這裏是一個匿名函數)編程
若一個函數被屢次聲明,後邊的聲明會覆蓋前邊的聲明(這裏要注意的是,因爲函數名的提高,前一次聲明在任什麼時候候都是無效的,並非在第二次聲明前還有效,而是所有覆蓋)數組
function f() { console.log(1); } f() // 2 function f() { console.log(2); } f() // 2
因爲函數與其它數據類型地位平等,因此js中又稱函數爲第一等公民。安全
function add(x, y) { return x + y; } // 將函數賦值給一個變量 var operator = add; // 將函數做爲參數和返回值 function a(op){ return op; } a(add)(1, 1) // 2
因爲js引擎將函數名視同變量名,因此也遵循變量聲明的原則,會被提高到代碼頭部閉包
所以,此代碼不報錯函數
f(); function f() {}
可是,若採用賦值語句定義函數,就會發生錯誤。這與變量名聲明提早相似,只是將函數聲明提早,而沒有將函數的賦值提早工具
f(); var f = function (){}; // TypeError: undefined is not a function
var f; f(); f = function () {};
再看下這個例子性能
var f = function () { console.log('1'); } function f() { console.log('2'); } f() // 1
先var f,而後第二個函數聲明覆蓋了第一個函數聲明,接着第一個函數的賦值覆蓋了第二個函數的聲明,最後仍是第一個函數的賦值優化
當變量賦值定義函數時,若爲匿名函數,返回變量名;如有具體函數名,返回function後的函數名prototype
var f3 = function myName() {}; f3.name // 'myName'
name屬性的重要做用,就是獲取傳入的函數參數的函數名code
var myFunc = function () {}; function test(f) { console.log(f.name); } test(myFunc) // myFunc
function (){[native code]}
;能夠返回函數內部註釋,而且能夠藉助這一點變相實現多行字符var multiline = function (fn) { var arr = fn.toString().split('\n'); return arr.slice(1, arr.length - 1).join('\n'); }; function f() {/* 這是一個 多行註釋 */} multiline(f); // " 這是一個 // 多行註釋"
js只有兩種做用域:(1)全局做用域:在函數外部聲明;(2)函數做用域:在函數內部定義,外部沒法讀取
值得注意的是,函數內部定義的變量,會在該做用域覆蓋掉同名全局變量
另外,只有在函數內部聲明的纔是局部變量,在其餘區塊中聲明,儘管和函數同樣都有區塊,一概是全局變量
與全局做用域同樣,函數做用域內部也會產生「變量提高」現象。var
命令聲明的變量,無論在什麼位置,變量聲明都會被提高到函數體的頭部。
function foo(x) { if (x > 100) { var tmp = x - 100; } } // 等同於 function foo(x) { var tmp; if (x > 100) { tmp = x - 100; }; }
在js中,函數是一等公民,與值相同,所以也有本身的做用域,其做用域與變量同樣,就是其聲明時所在的做用域,與運行時所在的做用域無關。
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
的內部變量。
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
,致使報錯。
一樣的,函數體內部聲明的函數,做用域綁定函數體內部。
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
。正是這種機制,構成了下文要講解的「閉包」現象。
首先注意,參數是能夠省略的。不管函數定義時定義了多少參數,可是運行時提供多少參數甚至不提供參數,js都不會報錯。省略的參數的值就變爲undefined
。須要注意的是,函數的length
屬性與實際傳入的參數個數無關,只反映函數預期傳入的參數個數。
function f(a, b) { return a; } f(1, 2, 3) // 1 f(1) // 1 f() // undefined f.length // 2
可是,沒有辦法只省略靠前的參數,而保留靠後的參數。若是必定要省略靠前的參數,只有顯式傳入undefined
。
function f(a, b) { return a; } f( , 1) // SyntaxError: Unexpected token ,(…) f(undefined, 1) // undefined
上面代碼中,若是省略第一個參數,就會報錯
var p = 2; function f(p) { p = 3; } f(p); p // 2
var obj = { p: 1 }; function f(o) { o.p = 2; } f(obj); obj.p // 2
如有同名的參數,則取最後出現的那個值,即便後邊的參數沒有值或者被省略,也是以其爲準
function f(a, a) { console.log(a); } f(1) // undefined
這時,若要得到第一個a的值,可使用arguments對象
function f(a, a) { console.log(arguments[0]); } f(1) // 1
arguments對象包含了函數運行時的全部參數,arguments[0]
就是第一個參數,arguments[1]
就是第二個參數,以此類推。這個對象只有在函數體內部,纔可使用。
var f = function (one) { console.log(arguments[0]); console.log(arguments[1]); console.log(arguments[2]); } f(1, 2, 3) // 1 // 2 // 3
通常狀況下,arguments對象能夠在運行時修改,例如,函數f()
調用時傳入的參數,在函數內部被修改爲3
和2
。
var f = function(a, b) { arguments[0] = 3; arguments[1] = 2; return a + b; } f(1, 1) // 5
可是若使用嚴格模式,修改arguments
對象就不會影響到實際的函數參數。
var f = function(a, b) { 'use strict'; // 開啓嚴格模式 arguments[0] = 3; arguments[1] = 2; return a + b; } f(1, 1) // 2
經過arguments
對象的length
屬性,能夠判斷函數調用時到底帶幾個參數。
function f() { return arguments.length; } f(1, 2, 3) // 3 f(1) // 1 f() // 0
值得注意的是,雖然arguments
很像數組,但它是一個對象。數組專有的方法(好比slice
和forEach
),不能在arguments
對象上直接使用。
若是要讓arguments
對象使用數組方法,真正的解決方法是將arguments
轉爲真正的數組。下面是兩種經常使用的轉換方法:slice
方法和逐一填入新數組。
var args = Array.prototype.slice.call(arguments); // 或者 var args = []; for (var i = 0; i < arguments.length; i++) { args.push(arguments[i]); }
js語言特有鏈式做用域結構,子對象會一級一級的向上尋找全部父對象的變量。即全部父對象的變量對子對象都是可見的,可是子對象的變量對父對象不可見(老父親了)
例如,函數內部能夠直接讀取全局變量,函數外部卻沒法讀取函數內部的變量;F2能夠讀取F1的變量,F1卻不能讀取F2的變量
function f1() { var n = 999; function f2() { console.log(n); // 999 } }
下邊咱們思考一個問題,原本f1外部是讀取不到f1的變量的,可是f2能夠讀取到父對象f1的變量,若咱們把f2做爲返回值,不就能夠在f1外部讀取f1的內部變量了嗎!
function f1() { var n = 999; function f2() { console.log(n); } return f2; } var result = f1(); result(); // 999
這就是閉包的概念,簡單來講,閉包就是定義在函數內部的函數,可以讀取父函數內部變量的f2.本質上講,閉包可以記住誕生的環境,是將函數內部和函數外部連接起來的一座橋樑
start
是函數createIncrementor
的內部變量。經過閉包,start
的狀態被保留了,每一次調用都是在上一次調用的基礎上進行計算。從中能夠看到,閉包inc
使得函數createIncrementor
的內部環境,一直存在。因此,閉包能夠看做是函數內部做用域的一個接口。
爲何會這樣呢?緣由就在於inc
始終在內存中,而inc
的存在依賴於createIncrementor
,所以也始終在內存中,不會在調用結束後,被垃圾回收機制回收。
function createIncrementor(start) { return function () { return start++; }; } var inc = createIncrementor(5); inc() // 5 inc() // 6 inc() // 7
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
的私有變量。
在js 中,圓括號()
是一種運算符,跟在函數名以後,表示調用該函數。好比,print()
就表示調用print
函數。
可是,當咱們在定義函數後當即調用函數時,不能定義後加括號,不然會產生語法錯誤。這是由於function便可以當作語句,也能夠當作表達式,而js引擎規定,若function出如今句首,一概解釋爲語句。因此,JavaScript 引擎看到行首是function
關鍵字以後,認爲這一段都是函數的定義,不該該以圓括號結尾,因此就報錯了。
// 語句 function f() {} // 表達式 var f = function f() {}
解決方法也很簡單,就是將函數放到一個圓括號裏。這就是IIFE(當即調用的函數表達式)
值得注意的是,最後的分號是必須的,不然會報錯
(function(){ /* code */ }()); // 或者 (function(){ /* code */ })();
一般狀況下,只對匿名函數使用這種「當即執行的函數表達式」。它的目的有兩個:一是沒必要爲函數命名,避免了污染全局變量;二是 IIFE 內部造成了一個單獨的做用域,能夠封裝一些外部沒法讀取的私有變量。
// 寫法一 var tmp = newData; processData(tmp); storeData(tmp); // 寫法二 (function () { var tmp = newData; processData(tmp); storeData(tmp); }());
上面代碼中,寫法二比寫法一更好,由於徹底避免了污染全局變量。
eval
命令接受一個字符串做爲參數,並將這個字符串看成語句執行。
eval('var a = 1;'); a // 1
若是參數字符串沒法看成語句運行,那麼就會報錯。
eval('3x') // Uncaught SyntaxError: Invalid or unexpected token
放在eval
中的字符串,應該有獨自存在的意義,不能用來與eval
之外的命令配合使用。舉例來講,下面的代碼將會報錯。
eval('return;'); // Uncaught SyntaxError: Illegal return statement
上面代碼會報錯,由於return
不能單獨使用,必須在函數中使用。
若是eval
的參數不是字符串,那麼會原樣返回。
eval(123) // 123
eval
沒有本身的做用域,都在當前做用域內執行,所以可能會修改當前做用域的變量的值,形成安全問題。
var a = 1; eval('a = 2'); a // 2
上面代碼中,eval
命令修改了外部變量a
的值。因爲這個緣由,eval
有安全風險。
爲了防止這種風險,JavaScript 規定,若是使用嚴格模式,eval
內部聲明的變量,不會影響到外部做用域。
(function f() { 'use strict'; eval('var foo = 123'); console.log(foo); // ReferenceError: foo is not defined })()
上面代碼中,函數f
內部是嚴格模式,這時eval
內部聲明的foo
變量,就不會影響到外部。
不過,即便在嚴格模式下,eval
依然能夠讀寫當前做用域的變量。
(function f() { 'use strict'; var foo = 1; eval('foo = 2'); console.log(foo); // 2 })()
上面代碼中,嚴格模式下,eval
內部仍是改寫了外部變量,可見安全風險依然存在。
總之,eval
的本質是在當前做用域之中,注入代碼。因爲安全風險和不利於 JavaScript 引擎優化執行速度,因此通常不推薦使用。一般狀況下,eval
最多見的場合是解析 JSON 數據的字符串,不過正確的作法應該是使用原生的JSON.parse
方法。
前面說過eval
不利於引擎優化執行速度。更麻煩的是,還有下面這種狀況,引擎在靜態代碼分析的階段,根本沒法分辨執行的是eval
。
var m = eval; m('var x = 1'); x // 1
上面代碼中,變量m
是eval
的別名。靜態代碼分析階段,引擎分辨不出m('var x = 1')
執行的是eval
命令。
爲了保證eval
的別名不影響代碼優化,JavaScript 的標準規定,凡是使用別名執行eval
,eval
內部一概是全局做用域。
var a = 1; function f() { var a = 2; var e = eval; e('console.log(a)'); } f() // 1
上面代碼中,eval
是別名調用,因此即便它是在函數中,它的做用域仍是全局做用域,所以輸出的a
爲全局變量。這樣的話,引擎就能確認e()
不會對當前的函數做用域產生影響,優化的時候就能夠把這一行排除掉。
eval
的別名調用的形式五花八門,只要不是直接調用,都屬於別名調用,由於引擎只能分辨eval()
這一種形式是直接調用。
eval.call(null, '...') window.eval('...') (1, eval)('...') (eval, eval)('...')
上面這些形式都是eval
的別名調用,做用域都是全局做用域。