一、閉包(Closure)1.1、閉包相關的問題1.2、理解閉包二、對象2.1、對象常量(字面量)2.2、取值2.3、枚舉(遍歷)2.4、更新與添加2.5、對象的原型2.6、刪除2.7、封裝三、函數3.1、參數對象 (arguments)3.2、構造函數 3.3、函數調用3.3.1、call3.3.... ...
一、閉包(Closure)
1.1、閉包相關的問題
請在頁面中放10個div,每個div中放入字母a-j,當點擊每一個div時顯示索引號,如第1個div顯示0,第10個顯示9;方法:找到所有的div,for迴圈綁定事件。
示例代碼:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>閉包</title> <style type="text/css"> div { width: 100px; height: 100px; background: lightgreen; float: left; margin: 20px; font: 30px/100px "microsoft yahei"; text-align: center; } </style> </head> <body> <div>a</div> <div>b</div> <div>c</div> <div>d</div> <div>e</div> <div>f</div> <div>g</div> <div>h</div> <div>i</div> <div>j</div> <script type="text/javascript"> var divs=document.getElementsByTagName("div"); for (var i=0;i<divs.length;i++) { divs[i].onclick=function(){ alert(i); } } </script> </body> </html>
運行結果:
因為點擊事件的函數內部使用外部的變數i一直在變化,當我們指定click事件時並沒有保存i的副本,這樣做也是為了提高性能,但達不到我們的目的,我們要讓他執行的上下文保存i的副本,這種機制就是閉包。
修改後的代碼:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>閉包</title> <style type="text/css"> div { width: 100px; height: 100px; background: lightgreen; float: left; margin: 20px; font: 30px/100px "microsoft yahei"; text-align: center; } </style> </head> <body> <div>a</div> <div>b</div> <div>c</div> <div>d</div> <div>e</div> <div>f</div> <div>g</div> <div>h</div> <div>i</div> <div>j</div> <script type="text/javascript"> var divs=document.getElementsByTagName("div"); for (var i=0;i<divs.length;i++) { divs[i].onclick=(function(n){ return function(){ alert(n); } })(i); } </script> </body> </html>
運行結果:
n是外部函數的值,但是內部函數(點擊事件)需要使用,返回函數前的n被臨時駐留在記憶體中給點擊事件使用,簡單說就是函數的執行上下文被保存起來,i生成了多個副本。
1.2、理解閉包
閉包概念:當一個內部函數被調用,就會形成閉包,閉包就是能夠讀取其他函數內部變數的函數,定義在一個函數內部的函,創建一個閉包環境,讓返回的這個子程式抓住i,以便在後續執行時可以保持對這個i的引用。內部函數比外部函數有更長的生命周期;函數可以訪問它被創建時所處的上下文環境。
Javascript語言特有的"鏈式作用域"結構(chain scope),子對象會一級一級地向上尋找所有父對象的變數
二、對象
對象就是“鍵/值”對的集合併擁有一個連接到原型(prototype)對隱藏連接。
2.1、對象常量(字面量)
一個對象字面量就是包含在一對花括弧中的零個或多個“鍵/值”對。對象字面量可以出現在任何允許表達式出現的地方。
對象的定義:
//空對象 var obj1={}; //對象中的屬性 var obj2={name:"foo",age:19}; var obj3={"nick name":"dog"}; //對象中的方法 var obj4={ price:99, inc:function(){ this.price+=1; } }
對象中可包含的內容:
對象常量可以出現在任何允許表達式出現的地方,對象、數組、函數可以相互間嵌套,形式可以多種多樣。對象的值可以是:數組,函數,對象,基本數據類型等。
//對象中可包含的內容 var obj5 = [{ name: "jack" }, { name: "lucy", //常量 hobby:["讀書","上網","代碼"], //數組 friend:{name:"mark",height:198,friend:{}}, //對象 show:function(){ //函數 console.log("大家好,我是"+this.name); } }]; //對象中的this是動態的,指向的是:調用者 obj5[1].show();
輸出:大家好,我是lucy
2.2、取值
方法一:直接使用點號運算
//3取值 var obj6={"nick name":"pig",realname:"Rose"}; console.log(obj6.realname); //console.log(obj6.nick name); 錯誤
方法二:使用索引器,當對象中的key有空格是
//3取值 var obj6={"nick name":"pig",realname:"Rose"}; console.log(obj6["realname"]); console.log(obj6["nick name"]);
2.3、枚舉(遍歷)
方法一:
var obj7={weight:"55Kg","nick name":"pig",realname:"Rose"}; for (var key in obj7) { console.log(key+":"+obj7[key]); }
運行結果:
輸出順序是不能保證的。
2.4、更新與添加
如果對象中存在屬性就修改對應值,如果不存在就添加。對象通過引用傳遞,它們永遠不會被覆制
var obj8={realname:"King"}; obj8.realname="Queen"; //修改 obj8.weight=1000; //添加屬性 obj8.show=function() //添加方法 { console.log(this.realname+","+this.weight); } obj8.show();
輸出:
Queen,1000
var obj8={realname:"King"}; obj8.realname="Queen"; //修改 obj8.weight=1000; //添加屬性 obj8.show=function() //添加方法 { console.log(this.realname+","+this.weight); } obj8.show(); //引用 var obj9=obj8; //obj9指向obj8的引用 obj9.realname="Jack"; obj8.show();
輸出:
2.5、對象的原型
javascript是一種動態語言,與C#和Java這樣的靜態語言是不一樣的;javascript並沒有嚴格的類型,可以簡單認為javascript是由對象組成的,對象間連接到原型(prototype)實現功能的擴展與繼承。每個對象都鏈接到一個原型對象,並且可以從中繼承屬性,所有通過常量(字面量)創建的對象都連接到Object.prototype,它是JavaScript中的頂級(標配)對象,類似高級語言中的根類。
現在我們修改系統中的Object對象,添加一個創建方法,指定要創建對象的原型,實現類似繼承功能:
<script type="text/javascript"> if(typeof Object.beget !== "function") { Object.create = function(o) { //構造函數,用於創建對象 var F = function() {}; //指定由構造函數創建的對象的原型 F.prototype = o; //調用構造方法創建新對象 return new F(); } } var rose={ name:"rose", show:function(){ console.log("姓名:"+this.name); } }; rose.show(); //輸出 var lucy=Object.create(rose); //簡單認為是:創建一個對象且繼承rose lucy.name="lucy"; //重寫 lucy.show(); </script>
運行結果:
原型關係是一種動態關係,如果修改原型,該原型創建的對象會受到影響。
var lucy=Object.create(rose); //簡單認為是:創建一個對象且繼承rose lucy.name="lucy"; //重寫 var jack=Object.create(rose); jack.name="jack"; //修改原型中的方法 rose.show=function(){ console.log("姓名->"+this.name); } lucy.show(); jack.show();
結果:
關於原型在函數中會再講到。
2.6、刪除
//刪除屬性 delete mark.name; //調用方法,輸出:姓名:undefined mark.show(); //刪除函數 delete mark.show; //錯誤,mark.show is not a function mark.show();
刪除不用的屬性是一個好習慣,在某些情況下可能引發記憶體泄漏。
2.7、封裝
使用對象封裝的好處是可以減少全局變數的污染機會,將屬性,函數都隸屬一個對象。
封裝前:
var name="foo"; //name是全局的,被暴露 i=1; //全局的,沒有var關鍵字聲明的變數是全局的,與位置關係不大 function show(){ //show 是全局的,被暴露 console.log("name->"+name); console.log(++i); } //i是全局的 2 show(); //3 show();
封裝後:
//對外只暴露bar,使用閉包封裝 var bar=function(){ var i=1; return{ name:"bar", show:function(){ console.log("name->"+this.name); console.log(++i); } }; }; var bar1=bar(); //2 bar1.show(); //3 bar1.show(); var bar2=bar(); //2,因為被封裝,且閉包,i是局部私有的 bar2.show();
運行結果:
三、函數
javascript中的函數就是對象,對象就是“鍵/值”對的集合併擁有一個連接到原型對隱藏連接。
3.1、參數對象 (arguments)
第一個函數中有一個預設對象叫arguments,類似數組,但不是數組,該對象是傳遞給函數的參數。
<script type="text/javascript"> function counter(){ var sum=0; for(var i=0;i<arguments.length;i++){ sum+=arguments[i]; } return sum; } console.log(counter(199,991,1,2,3,4,5)); console.log(counter()); </script>
運行結果:
1205
0
這裡的arguments是一個隱式對象,不聲明也在函數中,內部函數可以訪問外部函數的任意內容,但是不能直接訪問外部函數的arguments與this對象。
function f1() { console.log(arguments.length); f2=function() { console.log(arguments.length); } return f2; } var f=f1(1,2,3); f();
運行結果:
3
0
3.2、構造函數
在javascript中對象構造函數可以創建一個對象。
<script type="text/javascript"> /*構造函數*/ //可以簡單的認為是一個類型的定義 function Student(name,age){ this.name=name; this.age=age; this.show=function(){ console.log(this.name+","+this.age); } } //通過new關鍵字調用構造函數,創建一個對象tom var rose=new Student("rose",18); var jack=new Student("jack",20); rose.show(); jack.show(); </script>
3.3、函數調用
3.3.1、call
調用一個對象的一個方法,以另一個對象替換當前對象
call([thisObj[,args])
hisObj 可選項。將被用作當前對象的對象。args 將被傳遞方法參數序列。
call 方法可以用來代替另一個對象調用一個方法。call 方法可將一個函數的對象上下文從初始的上下文改變為由 thisObj 指定的新對象。
示例:
/*構造函數*/ function Student(name,age){ this.name=name; this.age=age; } show=function(add){ console.log(add+":"+this.name+","+this.age); } //通過new關鍵字調用構造函數,創建一個對象tom var rose=new Student("rose",18); var jack=new Student("jack",20); //調用show方法,指定上下文,指定調用對象,this指向rose,“大家好是參數” show.call(rose,"大家好"); show.call(jack,"Hello");
運行結果:
call方法中的參數都可以省去,第1個參數表示在哪個對象上調用該方法,或this指向誰,如果不指定則會指向window對象。
示例:
var name="無名"; var age=18; show.call();
結果:
undefined:無名,18
3.3.2、apply
apply([thisObj[,argArray]])
應用某一對象的一個方法,用另一個對象替換當前對象,與call類似。
如果 argArray 不是一個有效的數組或者不是arguments對象,那麼將導致一個 TypeError。
如果沒有提供 argArray 和 thisObj 任何一個參數,那麼 Global 對象將被用作 thisObj, 並且無法被傳遞任何參數。
對於第一個參數意義都一樣,但對第二個參數:
apply傳入的是一個參數數組,也就是將多個參數組合成為一個數組傳入,而call則作為call的參數傳入(從第二個參數開始)。
如 func.call(func1,var1,var2,var3)對應的apply寫法為:func.apply(func1,[var1,var2,var3])
同時使用apply的好處是可以直接將當前函數的arguments對象作為apply的第二個參數傳入
示例代碼:
/*構造函數*/ function Student(name,age){ this.name=name; this.age=age; } show=function(greeting,height){ console.log(greeting+":"+this.name+","+this.age+","+height); } //通過new關鍵字調用構造函數,創建一個對象tom var rose=new Student("rose",18); var jack=new Student("jack",20); //調用show方法,指定上下文,指定調用對象,this指向rose,“大家好是參數” show.apply(rose,["大家好","178cm"]); show.apply(jack,["Hello","188cm"]);
運行結果:
從上面的示例中可以發現apply的第2個參數是一個數組,數組中的內容將映射到被調用方法的參數中,如果單這樣看發現不如call方便,其實如果直接取方法的參數arguments則apply要方便一些。通過簡單的變化就可以替代call。
function display(){ show.apply(jack,arguments); } display("hi","224cm");
結果:
hi:jack,20,224cm
javascript里call和apply操作符可以隨意改變this指向
如果在javascript語言里沒有通過new(包括對象字面量定義)、call和apply改變函數的this指針,函數的this指針都是指向window的。
關於this指針,我的總結是:是誰調用的函數,那麼這個函數中的this指針就是它;如果沒有明確看出是誰調用的,那麼應該就是window調用的,那麼this指針就是window。
3.3.3、caller
在一個函數調用另一個函數時,被調用函數會自動生成一個caller屬性,指向調用它的函數對象。如果該函數當前未被調用,或並非被其他函數調用,則caller為null。
在JavaScript的早期版本中,Function對象的caller屬性是對調用當前函數的函數的引用
function add() { console.log("add被調用"); //add方法的調用函數,如果調用add方法的不是函數則為null console.log(add.caller); } function calc(){ add(); } //直接調用add方法 add(); //間接通過calc方法調用 calc();
運行結果:
caller與this還是有區別的,this是指調用方法的對象,而caller是指調用函數的函數。
<script type="text/javascript"> function add(n) { console.log("add被調用"); if(n<=2){ return 1; } return add.caller(n-1)+add.caller(n-2); } function calc(n){ console.log("calc被調用"); return add(n); } //1 1 2 console.log(calc(3)); </script>
結果:
3.3.4、Callee
當函數被調用時,它的arguments.callee對象就會指向自身,也就是一個對自己的引用
function add(n1,n2){ console.log(n1+n2); //arguments.callee(n1,n2); //指向add方法 return arguments.callee; } add(1,2)(3,4)(5,6)(7,8)(8,9);
運行結果:
當第1次調用add方法時輸入3,立即將函數返回再次調用,每次調用後又返回自己,這樣可以實現鏈式編程。
3.5、立即執行函數表達式 (IIFE)
IIFE即Immediately-Invoked Function Expression,立即執行函數表達式
3.5.1、匿名函數與匿名對象
匿名函數就是沒有名稱的函數,javascript中經常會使用匿名函數實現事件綁定,回調,實現函數級的私有作用域,如下所示:
function(){ console.log("這是一個匿名函數"); };
匿名對象:
{ name:"foo", show:function(){ console.log(this.name); } }
沒有名稱的匿名函數也叫函數表達式,它們間是有區別的。
3.5.2、函數與函數表達式
下麵是關於函數與函數表達式定義時的區別
a)、函數定義(Function Declaration)
function Identifier ( Parameters ){ FunctionBody }
function 函數名稱(參數){函數主體}
在函數定義中,參數(Parameters)標識符(Identifier )是必不可少的。如果遺漏,會報提示錯誤:
代碼:
function(){ console.log("這是一個匿名函數"); };
結果:
b)、函數表達式(Function Expression)
function Identifier(Parameters){ FunctionBody }
函數表達式中,參數和標識符都是可選的,與函數定義的區別是標識符可省去。
其實,"function Identifier(Parameters){ FunctionBody }"並不是一個完整的函數表達式,完整的函數的表達式,需要一個賦值操作。
比如: var name=function Identifier(Parameters){ FunctionBody }
3.5.3、立即執行函數表達式與匿名對象
//1 正常定義函數 function f1(){ console.log("正常定義f1函數"); }; //2 被誤解的函數表達式 function(){ console.log("報錯Unexpected token ("); }(); //3 IIFE,括弧中的內容被解釋成函數表達式 (function(){ console.log("IIFE,正常執行"); })(); //4 函數表達式 var f2=function(){ console.log("這也被視為函數表達式"); };
第3種寫法為什麼這樣就能立即執行並且不報錯呢?因為在javascript里,括弧內部不能包含語句,當解析器對代碼進行解釋的時候,先碰到了(),然後碰到function關鍵字就會自動將()裡面的代碼識別為函數表達式而不是函數聲明。
如果需要將函數表達式或匿名對象立即執行,可以使用如下方法:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>IIFE</title>
</head>
<body>
<script type="text/javascript">
//調用匿名函數
(function() {
console.log("這是一個函數表達式");
})();
//調用匿名對象
({
name: "foo",
show: function() {
console.log(this.name);
}
}).show();
console.log({
a: 1
}.a);
console.log({
a: function() {}
}.a());
</script>
</body>
</html>
運行結果:
3.5.4、各種IIFE的寫法
//最常用的兩種寫法 (function(){ /* code */ }()); // 老師推薦寫法 (function(){ /* code */ })(); // 當然這種也可以 // 括弧和JS的一些操作符(如 = && || ,等)可以在函數表達式和函數聲明上消除歧義 // 如下代碼中,解析器已經知道一個是表達式了,於是也會把另一個預設為表達式 // 但是兩者交換則會報錯 var i = function(){ return 10; }(); true && function(){ /* code */ }(); 0, function(){ /* code */ }(); // 如果你不怕代碼晦澀難讀,也可以選擇一元運算符 !function(){ /* code */ }(); ~function(){ /* code */ }(); -function(){ /* code */ }(); +function(){ /* code */ }(); // 你也可以這樣 new function(){ /* code */ } new function(){ /* code */ }() // 帶參
如果是函數表達式,可直接在其後加"()"立即執行。
如果是函數聲明,可以通過"()"、"+"、"-"、"void"、"new"等運算符將其轉換為函數表達式,然後再加"()"立即執行。
3.5.5、參數
函數表達式也是函數的一種表達形式,同樣可以像函數一樣使用參數,如下所示:
(function (n){ console.log(n); })(100);
輸出:100
其實通過IIFE還能形成一個類似的塊級作用域,當塊內的程式在使用外部對象時將優先查找塊內的對象,再查找塊外的對象,依次向上。
(function(win,undfd){ win.console.log("Hello"==undfd); })(window,undefined);
3.5.6、添加分號
為了避免與其它的javascript代碼產生影響後報錯,常常會在IIFE前增加一個分號,表示前面所有的語句都結束了,開始新的一語句。
var k=100 (function (n){ console.log(n); })(k);
上面的腳本會報錯,因為javascript解釋器會認為100是函數名。
var k=100 ;(function (n){ console.log(n); })(k);
這樣就正確了,在javascript中一行語句的結束可以使用分號,也可以不使用分號,因為一般的自定義插件會使用IIFE,這是一段獨立的代碼,在應用過程中不能保證用戶會加上分號,所以建議在IIFE前加上分號。
3.5.7、IIFE的作用
1)、提高性能
減少作用域查找時間。使用IIFE的一個微小的性能優勢是通過匿名函數的參數傳遞常用全局對象window、document、jQuery,在作用域內引用這些全局對象。JavaScript解釋器首先在作用域內查找屬性,然後一直沿著鏈向上查找,直到全局範圍。將全局對象放在IIFE作用域內提升js解釋器的查找速度和性能。
function(window, document, $) { }(window, document, window.jQuery);
2)、壓縮空間
通過參數傳遞全局對象,壓縮時可以將這些全局對象匿名為一個更加精簡的變數名
function(w, d, $) { }(window, document, window.jQuery);
3)、避免衝突
匿名函數內部可以形成一個塊級的私有作用域。
4)、依賴載入
可以靈活的載入第三方插件,當然使用模塊化載入更好(AMD,CMD),示例如下。
A.html與B.html文件同時引用公用的common.js文件,但是只有A.html需要使用到StuObj對象,B.html不需要,但使用其它方法。
Student.js
var StuObj = { getStu: function(name) { return new Student(name); } } /*構造函數*/ function Student(name) { this.name = name; this.show = function() { console.log("Hello," + this.name); } }
Common.js
function other1() {} function other2() {} (function($) { if($) { $.getStu("Tom").show(); } })(typeof StuObj=="undefined"?false:StuObj);
A.HTML
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>A</title> </head> <body> <script src="js/Student.js" type="text/javascript" charset="utf-8"></script<