JavaScript 系列博客(三) 前言 本篇介紹 JavaScript 中的函數知識。 函數的三種聲明方法 function 命令 可以類比為 python 中的 def 關鍵詞。 function 命令聲明的代碼區塊,就是一個函數。命令後面是函數名,函數名後面的圓括弧裡面是要傳入的形參名。函數 ...
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的私有變數。
註意:外城函數每次運行,都會產生一個新的閉包,而這個閉包又會保留外城函數的內部變數,所以記憶體消耗很大。