JavaScript 系列博客(三)
JavaScript 系列博客(三)
前言
本篇介紹 JavaScript 中的函數知識。
函數的三種聲明方法
function 命令
可以類比為 python 中的 def 關鍵詞。
function 命令聲明的代碼區塊,就是一個函數。命令後面是函數名,函數名後面的圓括號裏面是要傳入的形參名。函數體放在大括號裏面。
function fn(name) {
console.log(name);
}
使用 function 命名了一個 fn 函數,以後可以通過調用 fn 來運行該函數。這叫做函數的聲明(Function Declaration)。
函數表達式
除了使用 function 命令聲明函數外,可以采用變量賦值的寫法。(匿名函數)
var fn = function(name) {
console.log(name);
};
這種寫法將一個匿名函數賦值給變量。這時,這個匿名函數又稱之為函數表達式(Function Expression),因為賦值語句的等號右側只能放表達式。
采用函數表達式聲明函數時,function 命令後面不帶有函數名。如果加上函數名,該函數名只能在函數體內訪問,在函數體外部無效。
var fn = function x(name) { console.log(typeof x); }; x // ReferenceError: x is not defined fn(); // function
聲明函數時,在函數表達式後加了函數名 x,這個 x 只可以在函數內部使用,指代函數表達式本身。這種寫法有兩個用處:一可以在函數體內部調用自身;二方便debug(debug 顯示函數調用棧時,會顯示函數名)。需要註意的是,函數表達式需要在語句的結尾加上分號,表示語句結束。而函數的聲明在結尾的大括號後面不用加分號。
Function 構造函數
第三種聲明函數的方法是通過構造函數,可以理解為 python 中的函數類,通過傳入參數並且返回結果就可以創建一個函數。
構造函數接收三個參數,最後一個為 add函數的‘’函數體‘’,其他參數為add 函數的參數。可以為構造函數傳遞任意數量的參數,不過只有最後一個參數被當做函數體,如果只有一個參數,該參數就是函數體。
Function 構造函數也可以不用 new 命令,結果一樣。這種聲明函數的方式不直觀,使用概率很少。
函數的調用
和 python 一樣,調用一個函數通過圓括號,圓括號中是要傳入的實參。
函數體內部的 return 語句,表示返回。JavaScript 引擎遇到 return 時,就直接返回 return 後面表達式的值(和 python 一樣),所以 return 後面的代碼是無意義的,如果沒有 return 那麽就會返回 undefined(python 中返回 None)。
函數作用域
作用域的定義
作用域指的是變量存在的範圍。在 ES5中,JavaScript 只有兩種作用域:一種是全局作用域,變量在整個程序中一直存在,任意位置可以訪問到;另一種是函數作用域,也稱之為局部作用域,變量只有在函數內部才能訪問到。ES6新增了塊級作用域,等價於局部作用域一樣,就是新增了一種產生局部作用域的方式。通過大括號產生塊級作用域。
在函數外部聲明的變量就是全局變量,可以在任意位置讀取。
在函數內部定義的變量,外部無法讀取,只有在函數內部可以訪問到。並且函數內部定義的同名變量,會在函數內覆蓋全局變量。
註意:對於 var 命令來說,局部變量只可以在函數內部聲明,在其他區塊中聲明,一律都是全局變量。ES6中聲明變量的命令改為 let,在區塊中聲明變量產生塊級作用域。
函數內部的變量提升
與全局作用域一樣,函數作用域也會產生‘’變量提升‘’現象。var 命令生命的變量,不管在什麽位置,變量聲明都會被提升到函數體的頭部。
function foo(x) {
if (x > 100) {
var tmp = x - 100;
}
}
// 等同於
function foo(x) {
var tmp;
if (x > 100) {
tmp = x - 100;
}
}
函數本身的作用域
函數和其他值(數值、字符串、布爾值等)地位相同。凡是可以使用值得地方,就可以使用函數。比如,可以把函數賦值給變量和對象的屬性,也可以當做參數傳入其他函數,或者作為函數的結果返回。函數是一個可以執行的值,此外沒有特殊之處。
函數也有自己的作用域,函數的作用域稱為局部作用域。與變量一樣,就是其生命時所在的作用域,與其運行時所在的作用域無關(閉包、裝飾器)。通俗地講就是在定義函數的時候,作用域已經就確定好了,那麽在訪問變量的時候就開始從本作用域開始查找,而與函數的調用位置無關。
var x = function () {
var a = 1;
console.log(a);
};
function y() {
var a = 2;
x();
}
y(); // 1
函數 x 是在函數 f 的外部生命的,所以它的作用域綁定外層,內部變量 a 不會到函數 f 體內取值,所以輸出1,而不是2。
總之,函數執行時所在的作用域,是定義時的作用域,而不是調用時所在的作用域。
函數參數
調用函數時,有時候需要外部傳入的實參,傳入不同的實參會得到不同的結果,這種外部數據就叫參數。
參數的省略
在 JavaScript 中函數參數不是必需的,就算傳入的參數和形參的個數不相等也不會報錯。調用時無論提供多少個參數(或者不提供參數),JavaScript 都不會報錯。省略的參數的值變為 undefined。需要註意的是,函數的 length 屬性值與實際傳入的參數個數無關,只反映函數預期傳入的參數個數。
但是,JavaScript 中的參數都是位置參數,所以沒有辦法只省略靠前的參數,而保留靠後的參數。如果一定要省略靠前的參數,只有顯示的傳入 undefined。
傳遞方式
函數參數如果是原始類型的值(數值、字符串、布爾值),傳遞方式是傳值傳遞(pass by value)。這意味著,在函數體內修改參數值,不會影響到函數外部(局部變量的修改不會影響到全局變量:對於基本數據類型)。
但是,如果函數參數是復合類型的值(數組、對象、其他函數),因為傳值方式為地址傳遞(pass by reference)。也就是說,傳入函數的原始值的地址,因此在函數內部修改參數,將會影響到原始值。
註意:如果函數內部修改的不是參數對象的某個屬性,而是直接替換掉整個參數,這時不會影響到原始值。
var obj = [1, 2, 3];
function f(o) {
o = [2, 3, 4];
}
f(obj);
obj // [1, 2, 3]
上面代碼,在函數 f 內部,參數對象 obj 被整個替換成另一個值。這時不會影響到原始值。這是因為,形式參數(o)的值實際上是參數 obj 的地址,重新對o 賦值導致 o 指向另一個地址,保存在原地址上的數據不會被改變。
同名參數
如果有同名的參數,則取最後出現的那個值。
function f(a, a) {
console.log(a);
}
f(1, 2) // 2
上面代碼中,函數 f 有兩個參數,且參數名都是 a。取值的時候,以後面的 a 為準,即使後面的a 沒有值或被省略,也是以其為準。
function f(a, a) {
console.log(a);
}
f(1) // undefined
調用函數 f 時,沒有提供第二個參數,a 的取值就變成了 undefined。這時,如果要獲得第一個 a 的值,可以使用 arguments 對象(類比linux 中的arg)。
function f(a, a) {
console.log(arguments[0]);
}
f(1) // 1
arguments 對象
定義
由於 JavaScript 允許函數有不定數目的參數,所以需要一種機制,可以在函數體內部讀取所有參數。這就是 arguments 對象的由來。
arguments 對象包含了函數運行時的所有參數,arguments[0]就是第一個參數,以此類推。註意:該對象只有在函數體內部才可以使用。
正常模式下,arguments 對象可以在運行時修改。
var f = function(a, b) {
arguments[0] = 3;
arguments[1] = 3;
return a + b;
}
f(1, 1) // 5
上面代碼中,調用 f 時傳入的參數,在函數體內被修改了,那麽結果也會修改。
嚴格模式下,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
與數組的關系
需要註意的是,雖然 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]);
}
callee 屬性
arguments 對象帶有一個 callee 屬性,返回它所對應的原函數。
var f = function() {
console.log(arguments.callee === f);
}
f(); // true
可以通過 arguments.callee,達到調用自身的目的。這個屬性在嚴格模式裏面是禁用的,不建議使用。
函數閉包
閉包是所有編程語言的難點,在 python 中閉包的多應用於裝飾器中。在 JavaScript 中閉包多用於創建作用域,或者解決變量汙染的問題。
理解閉包,首先需要理解變量作用域。在 ES5中,JavaScript 只有兩種作用域:全局作用於和函數作用域。函數內部可以直接讀取全局變量。
var n = 999;
function f1() {
console.log(n);
}
f1(); // 999,n是全局變量,可以被訪問到
但是函數外部無法讀物函數內部聲明的變量。
function f1() {
var n = 999;
}
console.log(n);
// Uncaught ReferenceError: n is not defined
因為變量作用域的關系,在外部需要訪問到局部變量在正常情況下是做不到的,這就可以通過閉包來實現。下來來看一個經典例子:循環綁定事件產生的變量汙染
<div class="box">
0000001
</div>
<div class="box">
0000002
</div>
<div class="box">
0000003
</div>
<script>
var divs = document.querySelectorAll(".box");
// 存在汙染的寫法
for (var i =0; i < divs.length; i++) {
divs.onclick = function () {
console.log(‘xxx‘, i)
}
}
// 運行結果顯示4
</script>
會產生變量汙染的原因是作用域,因為 var 並不產生作用域,所以在 for循環中的變量就是全局變量,只要 for循環結束那麽 i 的值就確定了,除非在極限情況下,你的手速比 cpu 還要快,那麽可能會看到小於4的值。這樣的問題可以通過函數的閉包來解決。產生新的作用域用來保存 i 的值。
for (var i = 0; i < divs.length; i++) {
(function () {
var index = i;
divs[index].onclick = function () {
console.log(‘xxx‘, index);
}
})()
}
// 另一種版本
for (var i = 0; i < divs.length; i++) {
function(i) {
divs[i].onclick = function () {
console.log(‘yyy‘, i)
}
}(i)
}
利用閉包原理產生新的作用域用來保存變量 i 的值,這樣就解決了變量汙染的問題,還有利用ES6的聲明變量關鍵詞 let,也會產生新的作用域(塊級作用域)也可以解決變量汙染的問題。
在 JavaScript 中,嵌套函數中的子函數中可以訪問到外部函數中的局部變量,但是外部函數訪問不到子函數中的局部變量,這是 JavaScript 中特有的‘’鏈式作用域‘’結構(python 也一樣),子對象會一級一級的向上尋找所有父對象的變量。所以,父對象的所有變量,對子對象都是可見的,反之則不成立。可以簡單地把閉包理解為‘’定義在一個函數內部的函數‘’,閉包最大的特點就是它可以‘’記住‘’誕生的環境,在本質上閉包就是將函數內部和函數外連接起來的一座橋梁。
必報的最大用處有兩個,一個是可以讀取函數內部的變量,另一個就是讓這些變量始終保持在內存中,即閉包可以使得它誕生的環境一直存在。下面的例子:
function createIncrementor(start) {
return function () {
return start++;
};
}
var inc = createIncrementor(5);
inc(); // 5
inc(); // 6
inc(): // 7
上面代碼中,start 是函數 createIncrementor 的內部變量。通過閉包,start 的狀態被保存,每一次調用都是在上一次調用的基礎上進行計算。從中可以看出,閉包 inc 使得函數 createIncrementor 的內部環境一直存在。所以閉包可以看做是函數內部作用域的一個接口。為什麽會這樣呢?原因就在於 inc 始終在內存中,而 inc 的存在依賴於 createIncrementor,因此也一直存在於內存中,不會再外層函數調用結束後 start 變量被垃圾回收機制回收。
閉包的另外一個用處是封裝對象的私有屬性和私有方法。(這部分還不太懂,還需要琢磨)
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的私有變量。
註意:外城函數每次運行,都會產生一個新的閉包,而這個閉包又會保留外城函數的內部變量,所以內存消耗很大。
JavaScript 系列博客(三)