歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> JavaScript函數及作用域的小結

JavaScript函數及作用域的小結

日期:2017/3/1 9:10:51   编辑:Linux編程

文結合基本JavaScript的權威書籍中的內容,根據自己的理解,通過相關示例向大家展示了函數的聲明,參數,調用,閉包等一些內容,希望大家能夠喜歡,如有不足,也希望提出建議,大家共同進步。

JavaScript權威指南(第6版) PDF中文版+英文版+源代碼 下載地址見 http://www.linuxidc.com/Linux/2013-10/91056.htm

在js中使用函數注意三點:
1、函數被調用時,它是運行在他被聲明時的語法環境中的;

2、函數自己無法運行,它總是被對象調用的,函數運行時,函數體內的this指針指向調用該函數的對象,如果調用函數時沒有明確指定該對象, this 默認指向 window ( strict 模式除外,本文不涉及 strict 模式);

3、函數是一種帶有可執行代碼的對象類型數據。

一、聲明函數

1、使用 function 關鍵字

代碼如下:

function myfun(a,b){ //聲明名為myfun的函數

return a+b;

}


2、 聲明匿名函數

function(a,b){ return a+b;}匿名函數自身是無法保存的,由於在js中函數是一種對象型數據,因此可以把匿名函數賦給變量來保存。

var myfun = function(a,b){ return a+b;}

3、使用函數構造器Function //注意首字母大寫

Function 是js內置的一個函數,他是所有函數對象的構造器。(其他數據對象也有自己的內置構造函數,比如Number,Object等,這些構造函數自己的構造器就是Function,因為他們都是函數)。

var myfun = new Function('a,b','return a+b;'); 其中最後一個參數是函數體,前面的參數都是函數的形式參數名,個數不定,因為需要用字符串傳參來構造,函數較長時這種寫法很不方便,一般很少用,也許你會 用它來構造特定的返回值從而取代 eval函數。

需要注意的是,全局變量和全局函數都可以看作window對象的屬性,如果存在同名的函數和變量,只能有一個生效(實際上只有一個屬性),試試下面的代碼。

function a(){ alert('a');}

alert(window.a); //訪問window對象的屬性也可以省去window不寫

var a=1;

alert(window.a);


函數和變量的聲明都發生在代碼解析期,不同的是,變量在解析期只聲明不賦值,因此,同一個作用域內存在同名的函數和變量時,在代碼運行期執行到變量賦值之 前,同名函數生效,同名變量賦值之後(用新的數據覆蓋了該window對象屬性原有的值),變量生效(但是要注意,在firefox 下, 在 with 偽閉包內聲明的函數,只能在聲明之後才能被調用,即,firefox 的 with 內沒有對函數預先聲明)。

代碼如下:
with({}){
a(); //在 firefox 下 a 是未聲明
function a(){ console.log("function a is called");}
}


如果同名稱的函數被多次聲明,後面聲明的將覆蓋前面聲明的,如:

代碼如下:
alert(func1);//彈出func1(){alert(2);}

func1(){

alert(1);

}

alert(func1); //彈出func1(){alert(2);}

func1(){ //這是最後一次聲明的func1,以該函數為准

alert(2);

}

alert(func1); //彈出func1(){alert(2);}

var func1 = function(){ //注意 ,這裡是變量賦值,不是函數聲明

alert(3);

}

alert(func1); //彈出function(){alert(3);}


除了 IE8 及IE8以下的浏覽器,表達式中的函數聲明都會返回匿名函數,不會成功聲明具名函數
代碼如下:
if(function fun(){}){
alert(fun); // error,不會成功聲明名稱為 fun 的函數,但在IE8 及以下的浏覽器中中會成功聲明一個函數 fun

}

(function fun(){ });

alert(fun); //error但是即使在 IE8 一下, 表達式中的具名函數也不能覆蓋該作用於下同名的變量:

var fun = 1; //該變量不能被函數表達式中的函數名覆蓋
var f = function fun(){};
alert(f); //function fun(){};
alert(fun); //1


注意區別:

if(fun = function (){}){
alert(fun); // ok,這裡聲明了一個變量,該變量保存了一個匿名函數

}

js函數是引用型的對象

var a = function(){};
var b=a;
b.x=2;
alert(a.x); //2

二、函數的參數

js函數不會檢查函數調用時傳入的參數個數與定義他時的形式參數個數是否一致,一般地,js函數調用時可以接收的參數個數為25個,當然不同的浏覽器可能有差異,ECMAScript標准對這一點並沒有規范。

如果你不確定函數調用時傳入了多少個參數,可以使用函數的arguments對象。

arguments 有點像數組,被稱為偽數組,它是用來保存實參的數據,arguments.length 為傳入的參數個數,arguments[0] 是第一個參數,arguments[1]是第二個參數,類推...

函數對象的length屬性:這個屬性很少用到,甚至很少人知道,函數的length屬性就是該函數定義時的形式參數個數。

代碼如下:
function myfun(a,b){

alert(arguments.length); //彈出調用時實際傳入的參數個數

alert(arguments[0]); //對應參數a

return a+b;

}

alert(myfun.length); //形參個數,2


arguments對象還有其他屬性,比如常用的arguments.callee ,指向該函數自身。

要注意:如果函數內部聲明了與形參同名的子函數(同域內,變量未賦值時同名函數生效),arguments 的相應值也會被修改,但是,在作用域內使用 var 聲明了同名的 變量則不會導致 arguments 的參數值被函數替換(但firefox 依然替換)。

代碼如下:
function aa(a , b,c){ //js 群的一道題
function a(){}

console.log(a); //function a
console.log(aa);

//如果作用域內沒有 var a ,則 arguments[0] 為 function a (friefox(version 17) 則一定是function a)
console.log(arguments[0]);
var a = "ee"; //注銷此句,考擦 arguments[0] 將變為 a 函數
var aa = "444";
arguments = 6;
console.log(a);
console.log(aa);
console.log(arguments);
}
aa(1,2,3);

三、函數的返回值

js函數使用 return 語句返回值。

一切數據類型都可以作為函數的返回值(包括函數),js函數也可以沒有返回值。

四、函數調用

函數自己是不會運行的,當它運行時,總是存在一個調用它的對象。

默認情況下,在任何語法環境中,如果沒有顯式指定函數的調用對象,就是指通過window對象來調用該函數,此時,函數體內的this指針指向window對象。

代碼如下:
function myfun(a,b){

alert(this);

return a+b;

}

myfun(1,2); // 調用函數並傳入2個參數,這2個參數分別對應形式參數a,b調用函數時,如果傳入的參數個數超過形式參數,就只有用arguments加下標來接收了。


由於沒有顯式指定調用函數的對象,alert(this)將彈出 window對象。這種調用方法是最常見的。 用於顯示指定函數的調用對象方法有三個:

1、如果一個函數被賦為一個對象的屬性值,這個函數只能通過該對象來訪問(但並非是說該函數只能被該對象調用),通過該對象調用這個函數的方式類似以面向對象編程語言中的方法調用(實際上在js中也習慣使用方法這種稱呼)。

代碼如下:
var obj={}; //定義一個對象

obj.fun=function(a,b){

alert(this); //彈出this指針

return a+b;

} //對象屬性值為函數

alert(obj.fun);// 訪問fun函數。 只能通過該對象來訪問這個函數

obj.fun(1,2); //通過obj對象來調用fun函數,將彈出obj對象。這種方式也稱為調用obj對象的fun方法。


2、 任意指定函數的調用對象:在某個語法環境中,如果可以同時訪問到函數fun和對象obj,只要你願意,可以指定通過obj對象來調用fun函數。指定方法 有2種:call方法和apply方法。(因為window對象是浏覽器環境下的頂級對象,在任何語法環境中都能訪問到window對象,因此,任何函數 都可以通過window對象來調用)
代碼如下:
function fun(a,b){

alert(this);

return a+b;

}

var obj={};

fun.call(obj,1,2); //通過obj對象來調用fun函數,並傳入2個參數,彈出的指針為obj對象。

var obj2={};

obj2.fun2 = function(a,b){ //obj2對象的屬性fun2是一個函數

alert(this);

return a+b;

};

obj2.fun2.call(obj,1,2); //通過obj對象來調用obj2對象的fun2屬性值所保存的函數,彈出的this指針是obj對象

//比較隱蔽的方法調用:數組調用一個函數[9,function(){ alert(this[0]); }][1]();

//使用window對象調用函數下面幾種方法是等價的
fun(1,2);
window.fun(1,2); //如果fun函數是全局函數
fun.call(window,1,2);
fun.call(this,1,2); //如果該句代碼在全局環境下(或者被window對象調用的函數體內),因為該語法環境下的this就是指向window對象。
func.call(); //如果函數不需要傳參
func.call(null,1,2);
func.call(undefined,1,2);var name = "window";
function kkk(){
console.log(this.name); // not ie
}
kkk(); //window
kkk.call(kkk); //kkk 函數被自己調用了


另一種比較容易疏忽的錯誤是,在A 對象的方法中,執行了使用了 B 對象的方法調用,試圖在 B 對象的方法裡使用 this 來訪問 A 對象,這在各種回調函數中比較常見,最常見的情形��是 ajax 回調函數中使用 this 。
代碼如下:
var obj = {
data:null,
getData:function(){
$.post(url,{param:token},function(dataBack){ //jQuery ajax post method
this.data = dataBack; //試圖將服務器返回的數據賦給 obj.data ,但這裡的 this 已經指向 jQuery 的 ajax 對象了
},'json');
}
}

//正確做法
var obj = {
data:null,
getData:function(){
var host = this; //保存 obj 對象的引用
$.post(url,{param:"token"},function(dataBack){
host.data = dataBack;
},'json');
}
}


3、apply方法調用:

apply方法與call方法唯一不同的地方是函數傳參方式不同。

obj2.fun2.call(obj,1,2); 改為 apply方式就是obj2.fun2.apply(obj,[1,2]);

apply使用類數組方式傳參,除數組外,還可以使用arguments、HTMLCollection來傳參,但arguments並非數組,如:

var obj={};

function fun_1(x,y){

function fun_2(a,b){

return a+b;

}

fun_2.apply(obj,arguments); //用fun_1的arguments對象來傳參,實際上是接收了x,y

}apply 傳參在IE8 及IE8一下的浏覽器中喲2個問題

在 call 和 apply 調用中,如果傳入標量數據(true/false ,string,number),函數運行時將把他們傳入的基本數據包裝成對象,然後把this指向包裝後的對象,試試下面的代碼。
function a(){

alert(typeof this);

alert(this.constructor);

alert(this);

}

a.call(false);

a.call(100);

a.call('hello');

甚至可以用這個特點來傳參數,但是不建議這種用法:

function a(){ alert(1+this); } //對象在運算中自動進行類型轉換

a.call(100); //101

4、函數作為對象構造器(構造函數)

當函數使用 new 運算作為對象構造器運行時,this 指向新構造出對象,如果該構造函數的返回值不是 null 以外的對象,構造函數運行完畢將返回 this 指向的對象,否則返回原定義的對象。

代碼如下:
function Fun(){

this.a = 1;

this.b = 3;

console.log(this); //{a:1,b:2}

// return {a:999}; //加上此舉 ,將返回 {a:999}

}

var obj = new Fun(); //obj = {a:1,b:2} ,如果沒有參數,也可以寫成 var obj = new Fun;


五、函數作用域

js的變量作用域是函數級的,在js裡沒有類似c語言的塊級作用域。

js編程環境的頂級作用域是window對象下的范圍,稱為全局作用域,全局作用域中的變量稱為全局變量。

js函數內的變量無法在函數外面訪問,在函數內卻可以訪問函數外的變量,函數內的變量稱為局部變量。

js函數可以嵌套,多個函數的層層嵌套構成了多個作用域的層層嵌套,這稱為js的作用域鏈。

js作用域鏈的變量訪問規則是:如果當前作用域內存在要訪問的變量,則使用當前作用域的變量,否則到上一層作用域內尋找,直到全局作用域,如果找不到,則該變量為未聲明。

注意,變量的聲明在代碼解析期完成,如果當前作用域的變量的聲明和賦值語句寫在變量訪問語句後面,js函數會認為當前作用域已經存在要訪問的變量不再向上級作用域查找,但是,由於變量的賦值發生的代碼運行期,訪問的到變量將是undefined。

代碼如下:


var c=1000;

function out(){

var a=1;

var b=2;

function fun(){

alert(a); //undefined

var a=10;

alert(a); //10

alert(b); //2

alert(c); //1000

}

fun();

}

out();

六、匿名函數的調用

匿名函數的使用在js很重要,由於js中一切數據都是對象,包括函數,因此經常使用函數作為另一個函數的參數或返回值。

如果匿名函數沒有被保存,則運行後即被從內存中釋放。

匿名函數的調用方式一般是直接把匿名函數放在括號內替代函數名。如:

(function(a,b){ return a+b;})(1,2); //聲明並執行匿名函數,運行時傳入兩個參數:1和2

//或者

(function(a,b){ return a+b;}(1,2));

//下面這種寫法是錯誤的:

function(a,b){ return a+b;}(1,2);

由於js中語句結束的分號可以省略,js引擎會認為function(a,b){ return a+b;}是一句語句結束,因此匿名函數只聲明了沒有被調用,如果語句沒有傳參(1,2)寫成(),還會導致錯誤,js中空括號是語法錯誤。

下面這種寫法是正確的。

var ab = function(a,b){ return a+b;}(1,2); // ab=3

js 解析語法時,如果表達式出現在賦值運算或操作符運算中,是"貪婪匹配"的(盡量求值)

function(t){ return 1+t;}(); //error
var f = function(t){ return t+1;}(); // ok

~ function(t){return t+1;}(); //ok
+ function(t){return t+1;}(); //ok

如果你只是想把一個匿名函數賦給一個變量,記得在賦值語句後面加上分號,否則,如果後面跟了小括號就變成了函數調用了,尤其是小括號與函數結尾之間分隔了多行時,這種錯誤往往很難發現。

實際開發中,匿名函數可能以運算值的方式返回,這種情況可能不容易看出,比如

var a =1;
var obj = {a:2,f:function(){ return this.a;}};

(1,obj.f)(); //1 逗號表達式反悔了一個匿名函數,當這個匿名函數被調用時,函數體內的 thsi 指向 window

聲 明並立即運行匿名函數被稱為”自執行函數“,自執行函數經常用於封裝一段js代碼。由於函數作用域的特點,自執行函數內的變量無法被外部訪問,放在函數內 的代碼不會對外面的代碼產生影響,可以避免造成變量污染。js開發很容易造成變量污染,在開發中經常引入其他編碼人員開發的代碼,如果不同的編碼人員定義 了同名稱不同含義的全局變量或函數,便造成了變量污染,同一作用域內出現同名的變量或函數,後來的將覆蓋前面的。

(function(){

//自己的代碼.....

})();匿名函數還可以使內存及時釋放:因為變量被聲明在匿名函數內,如果這些變量沒有在匿名函數之外被引用,那麼這個函數運行完畢,裡面的變量所占據的內存就會立即釋放。

函數的name:在firefox等浏覽器,函數有一個name屬性,就是該函數的函數名,但是這個屬性在IE中不存在,另外,匿名函數的name為空值。

var a=function(){}
alert(a.name); //undefined,a是一個存儲了一個匿名函數的變量
function b(){}
alert(b.name); //b ,but undefined for IE

七、函數被調用時,運行在他被定義時的環境中

無論函數在哪裡被調用,被誰調用,都無法改變其被聲明時的語法環境,這決定了函數的運行環境。

代碼如下:
var x=99;

var inerFun=null;

function fun1(){

alert(x);

}

function holder(){

var x = 100;

var fun2 = fun1;

inerFun = function(){ alert(x);}

fun1(); //99

fun2();//99

inerFun(); //100

}

holder();

fun1(); //99

inerFun(); //100

//另一個例子:

var x = 100;
var y=77;
var a1={
x:99,
xx:function(){
//var y=88; //如果注釋這個變量,y將是全局變量的77
alert(y); //沒有使用this指針,調用函數的對象無法影響y的值,函數運行時將從這裡按作用域鏈逐級搜索取值
alert(this.x); //使用了 this 指針,調用函數的
}
}

a1.xx();
a1.xx.call(window);

var jj = a1.xx;

jj(); //效果跟a1.xx.call(window); 一樣//試試下面代碼

var x=99;
function xb(){
this.x=100;
this.a = (function(){return this.x}).call(this); //new 的時候執行了,匿名函數被 實例化的對象 調用
this.b = (function(){return this.x})(); //new 的時候執行了,匿名函數被window調用
this.method = function(){return this.x;}
}


var xbObj = new xb();
console.log(xbObj.x);
console.log(xbObj.a);
console.log(xbObj.b);
console.log(xbObj.method());


注意區分調用函數的對象、函數聲明時的語法環境、函數調用語句的語法環境這幾個概念

1、調用函數的對象(或者說函數的調用方式)決定了函數運行時函數體內的this指針指向誰。

2、函數聲明時的語法環境決定了函數運行時的訪問權限。

3、函數調用語句的語法環境決定了函數是否真的能夠被調用及何時被調用(只有函數在某個語法環境是可見的,這個函數才能被調用)。

函數在運行時,產生一個 arguments 對象可以訪問傳入函數內的參數,arguments 有一個屬性可以指向函數自身:arguments.callee。

函數運行時,函數的 caller 屬性可以指向本函數調用語句所在函數,比如,a函數在b函數體內被調用,則當a函數運行時,a.caller就指向了b函數,如果a 函數在全局環境中被調用則 a.caller=null

arguments 和a.caller 的值與函數的每一次調用直接關聯,他們都是在函數運行時產生的,只能在函數體內訪問。

IE8及IE8以下浏覽器中,a 函數的內的 arguments.caller( IE9之後這個屬性被移除) 指向 a.caller 執行時的 arguments (arguments.caller.callee === a.caller)。

八、字符串實時解析中的函數調用:eval()、new Function()、setTimeout()、setInterval()

eval() 與 window.eval()

代碼如下:
function a(){
console.log('out of b');
}
function b(){
function a(){ console.log("in b"); }
var f = function(){ a(); };
eval('a()'); // in b
window.eval('a()'); //out of b ,ie 6\7\8 in b, ie 9 out of b
(new Function('a();'))(); //out of b
setTimeout('a()',1000); // out of b
setTimeout(f,2000);// in b
}
b();
eval() 中的代碼執行於eval() 語句所處的作用域內:
代碼如下:
var Objinit = function(){
var param = 123;
return {
execute:function(codes){
eval(codes);
},
setCallback:function(f){
this.callback = f;
},
fireCallback:function(){
this.callback && this.callback.call(this);
},
getParam:function(){
return param;
}
}
};

var obj = Objinit ();
var param = 'outerParam';
console.log(param,obj.getParam()); //outerParam 123
obj.execute('param = 456');
console.log(param,obj.getParam()); //outerParam 456
obj.setCallback(function(){ eval("param = 8888")});
obj.fireCallback();
console.log(param,obj.getParam()); //8888 456
obj.setCallback(function(){ eval("eval(param = 9999)")});
obj.fireCallback();
console.log(param,obj.getParam()); //9999 456eval()


字符串中解析出的代碼運在 eval 所在的作用域,window.eval() 則是運行在頂級作用域(低版本 chrome 和 低於IE9 則同 eval()).

IE 中 ,window.execScript(); 相當於 window.eval()

new Function()、setTimeout()、setInterval() 的第一個字符串參數所解析得到的代碼,都是在頂級作用域執行。

九、函數閉包

要理解函數閉包,先了解 js 的垃圾自動回收機制。

number、string、boolean、undefined、null 在運算和賦值操作中是復制傳值,而對象類型的數據按引用傳值。

js 的同一個對象型數據可能被多次引用,如果某個對象不再被引用,或者兩個對象之間互相引用之外不在被第三方所引用,浏覽器會自動釋放其占用的內存空間。

函數被引用:函數被賦為其他對象的屬性值,或者函數內部定義的數據在該函數外被使用,閉包的形成基於後一種情形。

閉包就是一個函數有權限訪問另一個函數。

代碼如下:
var f;

function fun(){

var a =1;

f = function(){ return ++a;};

}

fun(); //產生一個閉包

f(); // 閉包中 a=2

f(); // 閉包中 a =3 ,模擬靜態變量


在 fun 內 聲明的匿名函數賦給 fun 外的變量 f,該匿名函數內使用了在 fun 內聲明的變量 a,於是 f可以訪問變量 a,為了維持這種訪問權限(f執 行時需要訪問a,但何時執行未定), fun() 執行完畢產生的變量 a 不能被釋放(除非f 中的函數被釋放),於是產生了一個閉包(變量 a 被封 閉了,供 f 使用)。

產生閉包的關鍵是,一個在函數 A內的聲明的函數 B被傳出 A 之外,並且 B 函數內使用了在 函數A 內生成的數據(聲明或按值傳參)。

函數B傳出函數A之外的方式有多種,如:

代碼如下:
function fun(){
var a =1;

return {a:123,b:456, c: function(){ return ++a;} };

}

var f = fun();

f.c(); //a=2

廣義上來說,函數運行時都會形成閉包,沒有數據在函數外被引用時,閉包的生命周期很短:函數執行完畢即釋放。

閉包的獨立性:即使由同一個函數產生的多個閉包也是相互獨立的。

代碼如下:
function fun(){

var a =1;

return function(){ return ++a;};

}

var f1 = fun(); //一份閉包

var f2 = fun(); //另一份閉包

alert(f1()); //2

alert(f1()); //3

alert(f2()); //2

alert(f2()); //3

這兩份閉包中的變量 a 是不同的數據,每產生一份閉包, fun() 執行了一次, 變量聲明語句也執行了一次。

最後說一下IE6的內存洩漏和閉包

在IE 6 中,非原生js對象(DOM 等)的循環引用會導致內存洩露,使用閉包時如果涉及非 js 原生對象引用時要注意。

function fun(){

var node = document.getElementById('a');
node.onclick = function(){ alert(node.value); };

node = null; //打斷循環引用防止內存洩露

node 保存的是 DOM 對象,DOM對象存在於 fun 之外(並且一直存在,即使刪除也只是從文檔樹移出),fun 執行後產生閉包,也構成DOM對象與回調函數的循環引用(node-function-node),在IE 6 下發生內存洩露。

Copyright © Linux教程網 All Rights Reserved