1. 程式人生 > >JavaScript 系列博客(三)

JavaScript 系列博客(三)

slice 自身 lee cto 數值 bug 執行 可見 set

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 系列博客(三)