函數以及函數做用域詳解

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions_and_function_scope
Javascript 函數 函數做用域javascript


概述

一般來講,函數就是一個能夠被外部調用的(或者函數自己遞歸調用)的「子程序」。和程序自己同樣,一個函數的函數體是由一系列的語句組成的,函數能夠接受函數,也能夠返回一個返回值。前端

在Javascript中函數也是對象,也能夠像其餘對象同樣,添加屬性和方法等。具體來說,它們是Function對象。java

用法簡介

在Javascript中,每一個函數其實都是一個Function對象,查看Function界面,瞭解Function屬性和方法。node

要想返回一個返回值,函數必須使用return語句指定所要返回的值(使用new關鍵詞的構造函數除外)。若是一個函數沒有return語句,則它默認返回undefined。算法

調用函數時,傳入函數的值爲函數的實參,對應位置的函數參數名爲形參。若是實參是一個包含原始值(數字,字符串,布爾值)的變量,則就算函數在內部改變了形參的值,返回後該實參的值也不會變壞。若是實參是一個對象引用,則對應形參和實參會指向同一個對象,若是函數內部形參引用對象的值發生了改變,則實參指向的引用對象的值也會改變:express

/*定義函數 myFunc*/
function myFunc(theObject) {
    theObject.brand = "Toyota";
}

/*
 *定義變量 mycar;
 *建立並初始化一個對象;
 *將對象的引用賦值給變量mycar;
 */
var mycar = {
    brand : "Honda",
    model : "Accord",
    year : 1998
};

window.alert(mycar.brand);  //彈出「Honda」

myFunc(mycar);

window.alert(mycar.brand);  //彈出「Toyota」

函數執行時,this不會指向正在運行的函數自己,而是指向調用該函數的對象。若是你想在函數內部獲取自身的引用,只能使用函數名或者arguments.callee屬性。編程

定義函數

定義函數一共有三種方法:數組

函數聲明(function語句)

有一個特別的語法來聲明函數:瀏覽器

function name([param,[param,[...param]]]) {
        //statements 
}

name
函數名安全

param
函數的參數名稱,一個函數最多有255個參數。

statements
函數體內執行的語句

函數表達式(function操做符)

函數表達式很函數聲明有着不少相同之處,他們甚至有着相同的語法:

function[name]([param,[param,[...param]]]) {
        //statements
}

name
能夠省略

param
函數的參數名稱,一個函數最多有255個參數。

statements
函數體內執行的語句

Function構造函數

和其餘類型同樣,Function對象可使用new操做符來建立:

[new] Function (arg1, arg2, ... argN, functionBody)

arg1, arg2, ... argN
一個或多個變量名稱,來做爲函數的形參名.類型爲字符串,值必須爲一個合法的JavaScript標識符,例如Function("x", "y","alert(x+y)"),或者逗號鏈接的多個標識符,例如Function("x,y","alert(x+y)")

functionBody
一個字符串,包含了組成函數的函數體的一條或多條語句.
即便不使用new操做符,直接調用Function函數,效果也是同樣的,仍然能夠正常建立一個函數.

注意: 不推薦使用Function構造函數來建立函數.由於在這種狀況下,函數體必須由一個字符串來指定.這樣會阻止一些JS引擎作程序優化,還會引發一些其餘問題.

arguments對象

在函數內部,你可使用arguments對象獲取到該函數的全部傳入參數. 查看 arguments.

做用域和函數堆棧

調用其餘函數時的做用域部分和函數部分

遞歸

函數能指向並引用它本身,有三種引用方法引用函數它本身:

  1. 經過函數名

  2. arguments.callee

  3. 指向該函數的變量

考慮到一下的函數定義:

var foo = function bar() {
    //statements
}

在這個函數內部,下面就是上面所說的三種:

  1. bar()

  2. arguments.callee

  3. foo()

一個函數調用它本身叫作遞歸函數。在某些方面,遞歸函數像一個循環(loop),一樣的代碼屢次執行同時有一個控制語句(爲了不無限循環),好比下面的循環:

var x = 0;
while (x < 10){
    x++
};
console.log(x);    //輸出10

能變換爲迭代函數並調用該函數:

function loop(x) {
    if(x>=10){console.log(x);}
    else{loop(x+1);}
}
loop(0);    //返回10

然而,一些算法並非簡單的迭代循環。好比,得到DOM節點使用遞歸就更方便:

function walkTree(node) {
    if (node == null) return;
    for(var i = 0; i < node.childNodes.length; i++){
        walkTree(node.childNodes[i]);
    }
}

比較這些循環方程,每個遞歸調用都會調用更多的調用。
將遞歸算法轉換成非遞歸算法大可能是能實現的,可是常常邏輯會變得更加複雜而且要使用更多的堆棧,事實上,遞歸自己就使用堆棧:函數堆棧。
這種相似堆棧的行爲能在下圖中看到:

function foo(i){
    if(i<0) return;
    console.log('begin: ' + i);
    foo(i - 1);
    console.log('end: ' + i);
}
foo(3);

嵌套函數和閉包

你能夠將一個函數嵌套在另外一個函數內部,被嵌套函數只是他外部函數的一個私有函數,這種狀況咱們將它叫作閉包。

閉包是一個擁有自由訪問另外一個函數做用域的表達式(一般是一個函數)。

當一個被嵌套函數是一個閉包時,意味着一個被嵌套函數能繼承它的外部函數的參數(arguments)和變量(variables)。意味着內部函數擁有外部函數的做用域。
總而言之:

  • 內部函數能擁有訪問外部函數的語句

  • 內部的函數定義了一個閉包:內部函數能使用外部函數的參數(arguments)和變量(variables),可是外部函數不能使用內部函數的參數(arguments)和變量(variables)。
    下例演示一個函數閉包:

function addSquares(a,b){
    function square(x){
        return x*x;
        }
    return square(a)+square(b);
}
console.log(addSquares(1,2));      //返回5

因爲內部函數造成了一個閉包,你能夠把內部函數看成返回值返回:

function outSide(x){
    function inSide(y){
        return x + y;
    }
    return inSide;
}
fnInside = outSide(1);
console.log(fnInside(2));       //返回3
console.log(outSide(2)(10));       //返回12

變量的保存

注意,x變量是如何在inside函數返回後被保存的。一個閉包必須保存它的做用域中的參數和變量。每一次調用有可能提供不一樣的參數,這樣一個新的閉包都會在外層調用時被建立,只有當要返回的內部函數再也不被訪問時內存纔會被清空。
這和保存來自其餘對象的引用沒有區別,可是後一種狀況更加少見由於一個函數不會直接設置引用而且不能檢查它們。

多層嵌套

函數能多層嵌套,A函數中B函數,B函數中包含C函數。在這個函數裏B和C都是閉包。所以B函數能訪問A函數,C函數能訪問B函數。而且,C函數也能訪問A函數。這樣看,閉包能包含多層嵌套。它們遞歸的包含外層的做用域,這種狀況咱們稱之爲做用域鏈。
例子以下:

function A(x){
    function B(y){
        function C(z){
            console.log(x + y + z);
        }
        C(2);
    }
    B(6);
}
A(1);

此例中C能訪問B中y和A中的x,這些之因此能成立是由於:

1.B爲A的閉包,B能訪問A的變量和參數。
2.C爲B的閉包
3.由於B包含A的做用域,因此C也包含A的做用域。換言之,C包含B和A的做用域鏈。

然而反過來卻不行,A不能訪問B,B也不能訪問C

命名衝突

當閉包中兩個變量或者參數擁有相同的名字,這種狀況叫作命名衝突。越內層的函數擁有越高的優先權,最內層的做用域擁有最高的優先權,最外層的做用域的優先權最低。做用域鏈的原理如此。做用域最前端是最內層的做用域,最後是最外層。考慮以下例子

function outside(){
    var x = 10;
    function inside(x){
    return x;
    }
    return inside;
}
result = outside()(20);       //返回20

上例中的命名衝突發生在return的x是返回inside裏的參數x仍是外層代碼的x,此處的做用域鏈爲(inside,outside,全局),然而內層的x優先級比外層的x優先級高,因而返回的是內層的x。

函數構造器vs.函數聲明vs.函數表達式

比較以下:
1.用函數構造器構造函數並添加參數:

var multiply = new Function("x", "y", "return x*y;");

2.函數聲明:

function multiply(x, y) {
    return x*y; 
}

3.函數表達式:

var multiply = function(x, y) {
    return x*y;
}

4.函數命名錶達式

var multiply = function func_name(x, y) {
    return x*y;
}

上面的代碼大約都是乾的一樣的事情,除了一些微妙的區別:

  • 函數名和函數指定的變量名是有區別的:

    • 函數名是不能改變的,函數指定變量名是能夠改變的

    • 函數名的使用只能和函數體一塊兒使用,若是嘗試在函數體外使用函數名的話會報錯error或者返回undefined。好比:

    1. y = function x(){};
      alert(x);

    另外一方面,函數指定變量名(好比最後一例中的multiply)只被它自身(變量名)的做用域所約束,這個做用域必定包含函數聲明的做用域。

    • 如同以上四個例子所示,函數名與函數指定變量名不一樣。它們彼此之間沒有關聯。

  • 函數聲明一樣會建立一個和函數名同樣的變量。不像那些在函數表達式中定義的那樣,在函數聲明中定義的函數能在它們定義的做用域中經過它們的命名被訪問:

    function x(){
    }
    console.log(x);   //返回function(){};

    接下來的例子會展現函數名和函數指定變量是如何無關的。即便函數名被指定給了其餘的變量名,返回的仍然是函數名:

    function foo(){};
    console.log(foo);
    var bar = foo;
    console.log(bar);
  • 一個由「new Function()」建立的新函數是沒有函數名的。在spiderMonkey的javascript引擎中,若是函數名爲「anonymous」序列化的函數將會顯示,好比下例中使用「console.log(new Function())」的輸出:

    function anonymous(){};
    console.log(new Function());  //返回「function anonymous(){}」
  • 不像函數表達式定義的函數調用必須在函數表達式後面,函數聲明的函數調用能夠在函數的前面,以下:

    foo();
    function foo(){
        alert("FOOO");
    }
  • 函數表達式中定義的函數會繼承當前的做用域。就是說該函數是一個閉包。而使用Function建立的函數不會繼承任何的做用域除了全局做用域(全部的函數都會繼承)。

  • 函數表達式和函數聲明定義的函數只會解析一次,但那些使用Function構造的卻不是。就是說,經過new Function方式構建函數時內部的字符每一次都會重解析,函數自己不會重分析,因而函數表達式比「new Function(...)」這種方式要快。因此經過函數構造方式分構造函數不管什麼時候都是應該被避免的。這應該被記下來,然而,函數表達式和函數聲明嵌套在經過函數構造器構造的中且當即執行就不會重複解析屢次,以下例:

var foo = (new Function("var bar=\"fooo!\";\nreturn(function(){\n\talert(\"bar\");\n})"))();
foo();

函數聲明一般會很輕易的變成函數表達式,函數聲明再也不是函數聲明的緣由多是由於它知足如下兩個條件:
1.變成了表達式的一部分。
2.它再也不是函數的「資源元素」,一個「資源元素」是腳本或者函數中的一段非嵌套語句:

var x = 0;                  //資源元素
if (x == 0) {               //資源元素
    x = 10;                 //不是資源元素
    function boo() {}       //不是資源元素
}
function foo(){              //資源元素
    var y = 20;              //資源元素
    function bar() {}           //資源元素
    while(y == 10){             //資源元素
        function blah(){}      //不是資源元素
        y++;                   //不是資源元素
    }
}

例如:

//函數聲明
function foo(){}

//函數表達式
(function bar(){})

//函數表達式
var x = function hello(){}
if(x){
    //函數表達式
    function hello(){}
}
//函數聲明
function a(){
    //函數聲明
    function b(){}
    if(0) {
        //函數表達式
        function c(){}
    }
}

條件執行函數

函數能夠定義在條件語句裏經過普通的function語句和new Function語句。但請注意如下這種狀況ECMAScript5中再也不容許出現函數語句,因此這個特性在跨瀏覽器中並不能表現的很好,你不能再編程中徹底依賴它。
在下面的代碼中,這個zero函數永遠都不能定義和執行。由於if(0)永遠都返回false;

if(0) {
    function zero(){
        document.writeln("This is a zero.");
    }
}

若是這個條件發生改變,if(1)那麼zero就會被定義。

注意:儘管這一類函數看上去就像是函數聲明,可是它其實是函數表達式。由於它被包含在其餘的條件語句中。看函數表達式和函數聲明有何不一樣.

注意:一些javascript的解析器,不包括SpiderMonkey,錯誤的把命名函數表達式當成了函數定義。這樣會致使zero會被定義即便if返回的是false。安全的方法是將匿名函數指定給變量:

if(0) {
    var zero = function(){
        document.writeln("This is zero.");
    }
}

函數和事件處理程序

在JavaScript中, DOM事件處理程序只能是函數(相反的,其餘DOM規範的綁定語言還可使用一個包含handleEvent方法的對象作爲事件處理程序)(譯者注:這句話已通過時,目前在大部分非IE[6,7,8]的瀏覽器中,都已經支持一個對象做爲事件處理程序). 在事件被觸發時,該函數會被調用,一個 event 對象會做爲第一個也是惟一的一個參數傳入該函數.像其餘參數同樣,若是不須要使用該event對象, 則對應的形參能夠省略.

一般的HTML事件對象包括:window(Window對象,包括frames),document(HTMLdocument對象和elements(各類元素對象)。在HTMLDOM中,事件對象擁有時間處理程序屬性,這個屬性一般是小寫的有on前綴的,好比:onfocus。一個更加靈活的添加事件對象的方法有DOM2級事件

注意:事件是DOM的一部分,而不是javascript的一部分。

下例會個window對象的onfocus事件綁定一個函數:

window.onfocus = function() {
    document.body.style.backgroundColor = 'blue';
}

若是函數綁定到了變量上,那麼能夠將事件指向該變量。以下:

var setBgColor = new Function('document.body.style.backgroundColor = "while" ');

你能夠像以下的方法那樣使用它們:
1.給DOM的事件屬性指向變量:

document.form1.colorbutton.onclick = setBgColor;

2.HTML標籤屬性:

<input name="colorbutton" type="button" onclick="setBgColor()" />

上例在執行過程當中的效果以下代碼:

document.form1.colorbutton.onclick = function onclick(){
    setBgColor();
}

注意:怎樣在HTML中調用一個onclick返回事件的屬性。能夠像以下這樣使用:

<input ...
    onclick="alert(event.target.tagName)"

就像其餘的參數參考的函數同樣,事件處理程序一樣是方法,在函數中返回的this對象會指向調用該方法的對象。好比:

window.onfocus

上例中在onfocus上綁定的事件處理程序的this對象就會指向window對象。
給事件綁定的有傳參的事件處理程序,必須被包含在其餘的函數中:

document.form1.button1.onclick = function(){
    setBgColor("some color");
}

函數的局部變量

arguments:一個"類數組"的對象,包含了傳入當前函數的全部實參;
arguments.callee:指向當前函數;
arguments.caller:指向調用當前函數的函數,請使用arguments.callee.caller代替;
arguments.length:arguments對象的中元素的個數。

同步於我的博客:http://penouc.com

相關文章
相關標籤/搜索