天天看點

javascript 函數及作用域(轉)

在js中使用函數注意三點:

1、函數被調用時,它是運作在他被聲明時的文法環境中的;

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

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

一、聲明函數

1、使用 function 關鍵字

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

return a+b;

}      

2、 聲明匿名函數

function(a,b){ return      

匿名函數自身是無法儲存的,由于在js中函數是一種對象型資料,是以可以把匿名函數賦給變量來儲存。

var myfun = function(a,b){ return      

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

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

var myfun = new      

其中最後一個參數是函數體,前面的參數都是函數的形式參數名,個數不定,因為需要用字元串傳參來構造,函數較長時這種寫法很不友善,一般很少用,也許你會用它來構造特定的傳回值進而取代 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個問題,看這裡:

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      

由于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 456      

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

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

關于 eval 的執行環境, 看這裡更專業的講解:

​​javascript:void(0)​​7

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() 執行了一次,  變量聲明語句也執行了一次。

js oop 程式設計中閉包可以用于模拟私有成員、構造單體類

function MakeItem(name,val){
 var myName,myVal; //私有屬性

//私有方法
 function setName(name){
     myname=name; 
}

 //私有 方法
function setVal(val){
    myVal=val;
 }

 //執行new構造對象時調用内部私有方法 
setName(name);
setVal(val);

//公共方法
this.getName=function(){ 
    return myName; 
} 

 this.getVal=function(){ 
    return myVal;
 }
}

var obj = new MakeItem("name",100);
obj.myname; //undefined 無法在外面通路私有屬性
obj.getName(); //ok      

下面是一種單體類建構方法

var Singleton = (function(){

    var instance = null; //在閉包中儲存單體類的執行個體

    var args = null;

    var f = function(){

        if(!instance){

            if(this===window){                

               args = Array.prototype.slice.call(arguments,0);                

                instance = new arguments.callee();

            }else{

               this.init.apply(this,args||arguments);

                instance = this;

            }

        }

        return instance;

    };

    f.prototype = {

        init:function(a,b,c){

            this.a = a;

            this.b = b;

            this.c = c;       

            this.method1 = function(){ console.log("method 1"); };

            this.method1 = function(){ console.log("method 1"); };

            console.log("init instance");

        }

    };

    f.prototype.constructor = f.prototype.init;

    return f;

 

})();

 
//單體的使用
var obj1 =  Singleton(1,2,3);

var obj2 = new Singleton();

var obj3 = new Singleton();

console.log(obj1===obj2,obj2===obj3); //true

console.log(obj1);

 

 

//一個單體類聲明函數

var SingletonDefine= function(fun){

    return (function(){

        var instance = null;

        var args = null;       

        var f = function(){

            if(!instance){

                if(this===window){

                    args = Array.prototype.slice.call(arguments,0);                    

                    instance = new arguments.callee();

                }else{

                    fun.apply(this,args||arguments);                    

                    instance = this;

                }

            }

            return instance;

        };

 

        f.prototype = fun.prototype; 

        f.prototype.constructor = fun;             

        return f;

    })();

};

var fun = function(a,b,c){

    this.a = a;

    this.b = b;

    this.c = c;

    this.method1 = function(){ console.log("method 1"); };

    console.log("init instance");

};

fun.prototype.method2 = function(){ console.log('method 2'); };

 
//單體類聲明函數用法
var Singleton = SingletonDefine(fun);

var obj1 =  Singleton(8,9,10);

var obj2 = new Singleton();

var obj3 = new Singleton(3,2,1);

console.log(obj1===obj2,obj2===obj3);

console.log(obj1);

//console.log(obj1.toSource()); //firefox

obj1.method1();

obj1.method2();      

IE6 的記憶體洩露與閉包

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

function fun(){

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

node = null; //打斷循環引用防止記憶體洩露