函數
由于JavaScript的函數也是一個對象,所以類似function abs(v){}函數實際上是一個函數對象,而函數名abs
可以視為指向該函數的變量。
因此,第二種定義函數的方式如下:
var abs = function (x) {if (x >= 0) {return x;} else {return -x;} };
在這種方式下,function (x) { ... }
是一個匿名函數,它沒有函數名。但是,這個匿名函數賦值給了變量abs
,所以,通過變量abs
就可以調用該函數。
此方式按照完整語法需要在函數體末尾加一個;
表示賦值語句結束。
由于JavaScript允許傳入任意個參數而不影響調用,因此傳入的參數比定義的參數多也沒有問題,雖然函數內部并不需要這些參數:
abs(10, 'blablabla'); // 返回10 abs(-9, 'haha', 'hehe', null); // 返回9
傳入的參數比定義的少也沒有問題:
abs(); // 返回NaN
此時abs(x)
函數的參數x
將收到undefined
,計算結果為NaN
。
要避免收到undefined
,可以對參數進行檢查:
if (typeof x !== 'number') {throw 'Not a number'; }
arguments
JavaScript還有一個免費贈送的關鍵字arguments
,它只在函數內部起作用,并且永遠指向當前函數的調用者傳入的所有參數。arguments
類似Array
但它不是一個Array
:
function foo(x) {alert(x); // 10for (var i=0; i<arguments.length; i++) {alert(arguments[i]); // 10, 20, 30 } } foo(10, 20, 30);
利用arguments
,你可以獲得調用者傳入的所有參數。也就是說,即使函數不定義任何參數,還是可以拿到參數的值:
function abs() {if (arguments.length === 0) {return 0;}var x = arguments[0];return x >= 0 ? x : -x; } abs(); // 0 abs(10); // 10 abs(-9); // 9
實際上arguments
最常用于判斷傳入參數的個數。你可能會看到這樣的寫法:
// foo(a[, b], c)// 接收2~3個參數,b是可選參數,如果只傳2個參數,b默認為null: function foo(a, b, c) {if (arguments.length === 2) { // 實際拿到的參數是a和b,c為undefinedc = b; // 把b賦給cb = null; // b變為默認值 } }
要把中間的參數b
變為“可選”參數,就只能通過arguments
判斷,然后重新調整參數并賦值。
rest參數
function foo(a, b, ...rest) {console.log('a = ' + a);console.log('b = ' + b);console.log(rest); }
rest參數只能寫在最后,前面用...
標識,從運行結果可知,傳入的參數先綁定a
、b
,多余的參數以數組形式交給變量rest
,所以,不再需要arguments
我們就獲取了全部參數。
如果傳入的參數連正常定義的參數都沒填滿,也不要緊,rest參數會接收一個空數組(注意不是undefined
)。
小心你的return語句
前面我們講到了JavaScript引擎有一個在行末自動添加分號的機制,這可能讓你栽到return語句的一個大坑:
function foo() {return { name: 'foo' }; }foo(); // { name: 'foo' }
如果把return語句拆成兩行:
function foo() {return{ name: 'foo' }; }foo(); // undefined
要小心了,由于JavaScript引擎在行末自動添加分號的機制,上面的代碼實際上變成了:
function foo() {return; // 自動添加了分號,相當于return undefined;{ name: 'foo' }; // 這行語句已經沒法執行到了}
所以正確的多行寫法是:
function foo() {return { // 這里不會自動加分號,因為{表示語句尚未結束name: 'foo'}; }
全局作用域
不在任何函數內定義的變量就具有全局作用域。實際上,JavaScript默認有一個全局對象window
,全局作用域的變量實際上被綁定到window
的一個屬性:
var course = 'Learn JavaScript'; alert(course); // 'Learn JavaScript' alert(window.course); // 'Learn JavaScript'
因此,直接訪問全局變量course
和訪問window.course
是完全一樣的。
你可能猜到了,由于函數定義有兩種方式,以變量方式var foo = function () {}
定義的函數實際上也是一個全局變量,因此,頂層函數的定義也被視為一個全局變量,并綁定到window
對象:
'use strict';function foo() {alert('foo'); } foo(); // 直接調用foo() window.foo(); // 通過window.foo()調用
進一步大膽地猜測,我們每次直接調用的alert()
函數其實也是window
的一個變量:
window.alert('調用window.alert()'); // 把alert保存到另一個變量: var old_alert = window.alert; // 給alert賦一個新函數: window.alert = function () {} // 恢復alert: window.alert = old_alert; alert('又可以用alert()了!');
這說明JavaScript實際上只有一個全局作用域。任何變量(函數也視為變量),如果沒有在當前函數作用域中找到,就會繼續往上查找,最后如果在全局作用域中也沒有找到,則報ReferenceError錯誤。?
變量作用域
在JavaScript中,用var
申明的變量實際上是有作用域的。
如果一個變量在函數體內部申明,則該變量的作用域為整個函數體,在函數體外不可引用該變量:
'use strict';function foo() {var x = 1;x = x + 1; }x = x + 2; // ReferenceError! 無法在函數體外引用變量x
如果兩個不同的函數各自申明了同一個變量,那么該變量只在各自的函數體內起作用。換句話說,不同函數內部的同名變量互相獨立,互不影響:
'use strict';function foo() {var x = 1;x = x + 1; }function bar() {var x = 'A';x = x + 'B'; }
由于JavaScript的函數可以嵌套,此時,內部函數可以訪問外部函數定義的變量,反過來則不行:
'use strict';function foo() {var x = 1;
function bar() {var y = x + 1; // bar可以訪問foo的變量x!}
var z = y + 1; // ReferenceError! foo不可以訪問bar的變量y!
}
如果內部函數和外部函數的變量名重名怎么辦?
'use strict';function foo() {var x = 1; function bar() {var x = 'A';alert('x in bar() = ' + x); // 'A' }alert('x in foo() = ' + x); // 1 bar(); }
這說明JavaScript的函數在查找變量時從自身函數定義開始,從“內”向“外”查找。如果內部函數定義了與外部函數重名的變量,則內部函數的變量將“屏蔽”外部函數的變量。
變量提升
JavaScript的函數定義有個特點,它會先掃描整個函數體的語句,把所有申明的變量“提升”到函數頂部:
'use strict';function foo() {var x = 'Hello, ' + y;alert(x);var y = 'Bob'; }foo();
雖然是strict模式,但語句var x = 'Hello, ' + y;
并不報錯,原因是變量y
在稍后申明了。但是alert
顯示Hello, undefined
,說明變量y
的值為undefined
。這正是因為JavaScript引擎自動提升了變量y
的聲明,但不會提升變量y
的賦值。
對于上述foo()
函數,JavaScript引擎看到的代碼相當于:
function foo() {var y; // 提升變量y的申明var x = 'Hello, ' + y;alert(x);y = 'Bob'; }
由于JavaScript的這一怪異的“特性”,我們在函數內部定義變量時,請嚴格遵守“在函數內部首先申明所有變量”這一規則。最常見的做法是用一個var
申明函數內部用到的所有變量:
function foo() {varx = 1, // x初始化為1y = x + 1, // y初始化為2z, i; // z和i為undefined// 其他語句:for (i=0; i<100; i++) {...} }
全局作用域
不在任何函數內定義的變量就具有全局作用域。實際上,JavaScript默認有一個全局對象window
,全局作用域的變量實際上被綁定到window
的一個屬性:
'use strict';var course = 'Learn JavaScript'; alert(course); // 'Learn JavaScript' alert(window.course); // 'Learn JavaScript'
因此,直接訪問全局變量course
和訪問window.course
是完全一樣的。
你可能猜到了,由于函數定義有兩種方式,以變量方式var foo = function () {}
定義的函數實際上也是一個全局變量,因此,頂層函數的定義也被視為一個全局變量,并綁定到window
對象:
function foo() {alert('foo'); } foo(); // 直接調用foo() window.foo(); // 通過window.foo()調用
進一步大膽地猜測,我們每次直接調用的alert()
函數其實也是window
的一個變量:
window.alert('調用window.alert()'); // 把alert保存到另一個變量: var old_alert = window.alert; // 給alert賦一個新函數: window.alert = function () {} // 恢復alert: window.alert = old_alert; alert('又可以用alert()了!');
名字空間
全局變量會綁定到window
上,不同的JavaScript文件如果使用了相同的全局變量,或者定義了相同名字的頂層函數,都會造成命名沖突,并且很難被發現。
減少沖突的一個方法是把自己的所有變量和函數全部綁定到一個全局變量中。例如:
// 唯一的全局變量MYAPP: var MYAPP = {}; // 其他變量: MYAPP.name = 'myapp'; MYAPP.version = 1.0;// 其他函數: MYAPP.foo = function () {return 'foo'; };
把自己的代碼全部放入唯一的名字空間MYAPP
中,會大大減少全局變量沖突的可能。
許多著名的JavaScript庫都是這么干的:jQuery,YUI,underscore等等。
局部作用域
由于JavaScript的變量作用域實際上是函數內部,我們在for
循環等語句塊中是無法定義具有局部作用域的變量的:
function foo() {for (var i=0; i<100; i++) { // }i += 100; // 仍然可以引用變量i }
為了解決塊級作用域,ES6引入了新的關鍵字let
,用let
替代var
可以申明一個塊級作用域的變量:
function foo() {var sum = 0;for (let i=0; i<100; i++) {sum += i;}i += 1; // SyntaxError }
常量
由于var
和let
申明的是變量,如果要申明一個常量,
const PI = 3.14;
方法
var xiaoming = {name: '小明',birth: 1990,age: function () {var y = new Date().getFullYear();return y - this.birth;} }; xiaoming.age; // function xiaoming.age() xiaoming.age(); // 今年調用是25,明年調用就變成26了
綁定到對象上的函數稱為方法,和普通函數也沒啥區別,但是它在內部使用了一個this
關鍵字,這個東東是什么? 在一個方法內部,this
是一個特殊變量,它始終指向當前對象,也就是xiaoming
這個變量。所以,this.birth
可以拿到xiaoming
的birth
屬性。
More:http://www.liaoxuefeng.com/wiki/001434446689867b27157e896e74d51a89c25cc8b43bdb3000/0014345005399057070809cfaa347dfb7207900cfd116fb000
?
?