一:理解call和apply 及arguments.callee
ECMAScript3給Function的原型定義了兩個方法,他們是Function.prototype.call
和 Function.prototype.apply
. 其實他們的作用是一樣的,只是傳遞的參數不一樣而已;
1. apply; 接受2個參數,第一個參數指定了函數體內this對象的指向,第二個參數爲一個類似數組的集合,比如如下代碼:
var yunxi = function(a,b){
console.log([a,b]); // [1,2]
console.log(this === window); // true
};
yunxi.apply(null,[1,2]);
如上代碼,我們第一個參數傳入null,函數體內默認會指向與宿主對象,即window對象;因此我們可以在yunxi函數內打印下值爲true即可看到:
下面我們來看看使用call方法的實例如下:
var yunxi = function(a,b){
console.log([a,b]); // [1,2]
console.log(this === window); // true
};
yunxi.call(null,1,2);
可以看到 call方法的第二個參數是以逗號隔開的參數;
那麼call和apply用在什麼地方呢?
1.call和apply 最常見的用途是改變函數體內的this指向,如下代碼:
var longen = {
name:'yunxi'
};
var longen2 = {
name: '我叫塗根華'
};
var name = "我是來測試的";
var getName = function(){
return this.name;
};
console.log(getName()); // 打印 "我是來測試的";
console.log(getName.call(longen)); // 打印 yunxi
console.log(getName.call(longen2)); // 打印 "我叫塗根華"
第一次調用 getName()
方法,因爲它是普通函數調用,所以它的this指向與window,因此打印出全局對象的name的值;
第二次調用getName.call(longen);
執行這句代碼後,getName這個方法的內部指針this指向於longen這個對象了,因此打印this.name
實際上是longen.name,因此返回的是name=”yunxi”;
但是this指針也有列外的情況,比如一個點擊元素,當我們點擊一個元素的時候,this指針就指向與那個點擊元素,但是當我們在內部再包含一個函數後,在函數內再繼續調用this的話,那麼現在的this指針就指向了window了;比如如下代碼:
document.getElementById("longen").onclick = function(){
console.log(this); // this 就指向於div元素對象了
var func = function(){
console.log(this); // 打印出window對象
}
func();
}
如上代碼。可以看到外部this指向與被點擊的那個元素,內部普通函數調用,this指針都是指向於window對象。但是我們可以使用call或者apply方法來改變this的指針的;如下代碼:
document.getElementById("longen").onclick = function(){
console.log(this); // this 就指向於div元素對象了
var func = function(){
console.log(this); // 就指向於div元素對象了
}
func.call(this);
}
如上代碼我們使用call方法調用func函數,使this指向與func這個對象了,當然上面的方法我們還可以不使用call或者apply方法來改變this的指針,我們可以在外部先使用一個變量來保存this的指針,在內部調用的時候我們可以使用哪個變量即可,如下代碼演示:
document.getElementById("longen").onclick = function(){
console.log(this); // this 就指向於div元素對象了
var self = this;
var func = function(){
console.log(self); // 就指向於div元素對象了
}
func();
}
arguments.callee的理解
callee是arguments的一個屬性,它可以被用作當前函數或函數體執行的環境中,或者說調用一個匿名函數;返回的是當前正在被執行的Function對象;簡單的來說就是當前執行環境的函數被調用時候,arguments.callee對象會指向與自身,就是當前的那個函數的引用;
如下代碼:
var count = 1;
var test = function() {
console.log(count + " -- " + (test.length == arguments.callee.length) );
// 打印出 1 -- true 2 -- true 3 -- true
if (count++ < 3) {
// 調用test()函數自身
arguments.callee();
}
};
test();
arguments.callee()
的含義是調用當前正在執行的函數自身,比如上面的test的匿名函數;
Function.prototype.bind介紹
目前很多高級瀏覽器都支持Function.prototype.bind
方法,該方法用來指定函數內部的this指向。爲了支持各個瀏覽器,我們也可以自己來簡單的模擬一個~
如下代碼:
Function.prototype.bind = function(context) {
var self = this;
return function(){
return self.apply(context,arguments);
}
}
var yunxi = {
name: 'yunxi'
};
var func = function(){
console.log(this.name); // yunxi
}.bind(yunxi);
func();
如上代碼所示:func這個函數使用調用bind這個方法,並且把對象yunxi作爲參數傳進去,然後bind函數使用return返回一個函數,當我們調用func()
執行這個方法的時候,其實我們就是在調用bind方法內的return返回的那個函數,在返回的那個函數內context的上下文其實就是我們以參數yunxi對象傳進去的,因此this指針指向與yunxi這個對象了~ 所以打印出this.name
就是yunxi那個對象的name了;
除了上面我們看到的介紹apply或者call方法可以改變this指針外,我們還可以使用call或者apply來繼承對象的方法;實質也就是改變this的指針了;
比如有如下代碼:
var Yunxi = function(name){
this.name = name;
};
var Longen = function(){
Yunxi.apply(this,arguments);
};
Longen.prototype.getName = function(){
return this.name;
};
var longen = new Longen("tugenhua");
console.log(longen.getName()); // 打印出tugenhua
如上代碼:我先實例化Longen這個對象,把參數傳進去,之後使用Yunxi.apply(this,arguments)
這句代碼來改變Longen這個對象的this的指針,使他指向了Yunxi這個對象,因此Yunxi這個對象保存了longen這個實例化對象的參數tugenhua,因此當我們調用longen.getName
這個方法的時候,我們返回this.name
,即我們可以認爲返回的是Yunxi.name
因此返回的是 tugenhua,我們只是借用了下Yunxi這個對象內的this.name
來保存Longen傳進去的參數而已;
二:閉包的理解
閉包的結構有如下2個特性
1.封閉性:外界無法訪問閉包內部的數據,如果在閉包內聲明變量,外界是無法訪問的,除非閉包主動向外界提供訪問接口;
2.持久性:一般的函數,調用完畢之後,系統自動註銷函數,而對於閉包來說,在外部函數被調用之後,閉包結構依然保存在
系統中,閉包中的數據依然存在,從而實現對數據的持久使用。
缺點:
使用閉包會佔有內存資源,過多的使用閉包會導致內存溢出等.
如下代碼:
function a(x) {
var a = x;
var b = function(){
return a;
}
return b;
}
var b = a(1);
console.log(b()); // 1
首先在a函數內定義了2個變量,1個是存儲參數,另外一個是閉包結構,在閉包結構中保存着b函數內的a變量,默認情況下,當a函數調用完之後a變量會自動銷燬的,但是由於閉包的影響,閉包中使用了外界的變量,因此a變量會一直保存在內存當中,因此變量a參數沒有隨着a函數銷燬而被釋放,因此引申出閉包的缺點是:過多的使用閉包會佔有內存資源,或內存溢出等肯能性;
// 經典的閉包實列如下:
function f(x){ //外部函數
var a = x; // 外部函數的局部變量,並傳遞參數
var b = function(){ // 內部函數
return a; // 訪問外部函數中的局部變量
};
a++; // 訪問後,動態更新外部函數的變量
return b; // 返回內部函數
}
var c = f(5); // 調用外部函數並且賦值
console.log(c()); // 調用內部函數,返回外部函數更新後的值爲6
下面我們來看看如下使用閉包的列子
在如下代碼中有2個函數,f函數的功能是:把數組類型的參數中每個元素的值分別封裝在閉包結構中,然後把閉包存儲在一個數組中,並返回這個數組,但是在函數e中調用函數f並向其傳遞一個數組["a","b","c"]
,然後遍歷返回函數f返回數組,我們運行打印後發現都是c undefined,那是因爲在執行f函數中的循環時候,把值雖然保存在temp中,但是每次循環後temp值在不斷的變化,當for循環結束後,此時temp值爲c,同時i變爲3,因此當調用的時候 打印出來的是temp爲3,arrs[3]變爲undefined;因此打印出 c undefined
解決閉包的缺陷我們可以再在外面包一層函數,每次循環的時候,把temp參數和i參數傳遞進去 如代碼二
// 代碼一
function f(x) {
var arrs = [];
for(var i = 0; i < x.length; i++) {
var temp = x[i];
arrs.push(function(){
console.log(temp + ' ' +x[i]); // c undefined
});
}
return arrs;
}
function e(){
var ar = f(["a","b","c"]);
for(var i = 0,ilen = ar.length; i < ilen; i++) {
ar[i]();
}
}
e();
// 代碼二:
function f2(x) {
var arrs = [];
for(var i = 0; i < x.length; i++) {
var temp = x[i];
(function(temp,i){
arrs.push(function(){
console.log(temp + ' ' +x[i]); // c undefined
});
})(temp,i);
}
return arrs;
}
function e2(){
var ar = f2(["a","b","c"]);
for(var i = 0,ilen = ar.length; i < ilen; i++) {
ar[i]();
}
}
e2();
三:javascript中的this詳解
this的指向常見的有如下幾點需要常用到:
- 全局對象的this是指向與window;
- 作爲普通函數調用。
- 作爲對象方法調用。
- 構造器調用。
-
Function.prototype.call
或Function.prototype.apply
調用。
下面我們分別來介紹一下
1.全局對象的this;
console.log(this); // this指向於window
setTimeout() 和 setInterval()函數內部的this指針是指向於window的,如下代碼:
function test(){
console.log(11);
}
setTimeout(function(){
console.log(this === window); // true
this.test(); // 11
});
2.作爲普通函數調用;
如下代碼:
var name = "longen";
function test(){
return this.name;
}
console.log(test()); // longen
當作爲普通函數調用時候,this總是指向了全局對象,在瀏覽器當中,全局對象一般指的是window;
3.作爲對象的方法調用。
如下代碼:
var obj = {
"name": "我的花名改爲云溪了,就是爲了好玩",
getName: function(){
console.log(this); // 在這裏this指向於obj對象了
console.log(this.name); // 打印 我的花名改爲云溪了,就是爲了好玩
}
};
obj.getName(); // 對象方法調用
但是呢,我們不能像如下一樣調用對象了,如下調用對象的話,this還是執行了window,如下代碼:
var name = "全局對象名字";
var obj = {
"name": "我的花名改爲云溪了,就是爲了好玩",
getName: function(){
console.log(this); // window
console.log(this.name); // 全局對象名字
}
};
var yunxi = obj.getName;
yunxi();
運行yunxi()函數,還是會像調用普通函數一樣,this指向了window的;
4.構造器調用。
Javascript中不像Java一樣,有類的概念,而JS中只能通過構造器創建對象,通過new 對象,當new運算符調用函數時候,該函數會返回一個對象,一般情況下,構造器裏面的this就是指向返回的這個對象;
如下代碼:
var Obj = function(){
this.name = "yunxi";
};
var test = new Obj();
console.log(test.name); // yunxi
注意:構造器函數第一個字母需要大寫,這是爲了區分普通函數還是構造器函數而言;
如上代碼:通過調用new Obj()
方法 返回值保存到test變量中,那麼test就是那個對象了,所以內部的this就指向與test對象了,因此test.name
就引用到了內部的this.name
即輸出 “yunxi”字符串;
但是也有例外的情況,比如構造器顯示地返回了一個對象的話,那麼這次繼續調用的話,那麼會最終會返回這個對象,比如如下代碼:
var obj = function(){
this.name = "yunxi";
return {
"age": "27"
}
};
var test = new obj();
console.log(test.name); // undefined
那麼繼續調用的話,會返回unedfined,因爲返回的是那個對象,對象裏面沒有name這個屬性,因此值爲undefined;
四:理解函數引用和函數調用的區別
看下面的代碼分析:
// 函數引用 代碼一
function f(){
var x = 5;
return x;
}
var a = f;
var b = f;
console.log(a===b); // true
// 函數調用 代碼二
function f2() {
var x = 5;
return x;
}
var a2 = f2();
var b2 = f2();
console.log(a2 === b2);
// 函數調用 代碼三
function f3(){
var x = 5;
return function(){
return x;
}
}
var a3 = f3();
var b3 = f3();
console.log(a3 === b3); // false
如上的代碼:代碼一和代碼二分部是函數引用和函數調用的列子,返回都爲true,代碼三也是函數調用的列子,返回且爲false
我們現在來理解下函數引用和函數調用的本質區別:當引用函數時候,多個變量內存存儲的是函數的相同的入口指針,因此對於同一個函數來講,無論多少個變量引用,他們都是相等的,因爲對於引用類型(對象,數組,函數等)都是比較的是內存地址,如果他們內存地址一樣的話,說明是相同的;但是對於函數調用來講,比如代碼三;每次調用的時候,都被分配一個新的內存地址,所以他們的內存地址不相同,因此他們會返回false,但是對於代碼二來講,我們看到他們沒有返回函數,只是返回數值,他們比較的不是內存地址,而是比較值,所以他們的值相等,因此他們也返回true,我們也可以看看如下實列化一個對象的列子,他們也被分配到不同的內存地址,因此他們也是返回false的;如下代碼測試:
function F(){
this.x = 5;
}
var a = new F();
var b = new F();
console.log(a === b); // false
五:理解js中的鏈式調用
我們使用jquery的時候,jquery的簡單的語法及可實現鏈式調用方法,現在我們自己也封裝一個鏈式調用的方法,來理解下 jquery中如何封裝鏈式調用 無非就是每次調用一個方法的時候 給這個方法返回this即可,this指向該對象自身,我們看看代碼:
// 定義一個簡單的對象,每次調用對象的方法的時候,該方法都返回該對象自身
var obj = {
a: function(){
console.log("輸出a");
return this;
},
b:function(){
console.log("輸出b");
return this;
}
};
console.log(obj.a().b()); // 輸出a 輸出b 輸出this指向與obj這個對象
// 下面我們再看下 上面的通過Function擴展類型添加方法的demo如下:
Function.prototype.method = function(name,func) {
if(!this.prototype[name]) {
this.prototype[name] = func;
return this;
}
}
String.method('trim',function(){
return this.replace(/^\s+|\s+$/g,'');
});
String.method('log2',function(){
console.log("鏈式調用");
return this;
});
String.method('r',function(){
return this.replace(/a/,'');
});
var str = " abc ";
console.log(str.trim().log2().r()); // 輸出鏈式調用和 bc
六:理解使用函數實現歷史記錄--提高性能
函數可以使用對象去記住先前操作的結果,從而避免多餘的運算。比如我們現在測試一個費波納茨的算法,我們可以使用遞歸函數計算fibonacci數列,一個fibonacci數字是之前兩個fibonacci數字之和,最前面的兩個數字是0和1;代碼如下:
var count = 0;
var fibonacci = function(n) {
count++;
return n < 2 ? n : fibonacci(n-1) + fibonacci(n-2);
};
for(var i = 0; i <= 10; i+=1) {
console.log(i+":"+fibonacci(i));
}
console.log(count); // 453
我們可以看到如上 fibonacci函數總共調用了453次,for循環了11次,它自身調用了442次,如果我們使用下面的記憶函數的話,那麼就可以減少他們的運算次數,從而提高性能;
思路:先使用一個臨時數組保存存儲結果,當函數被調用的時候,先看是否已經有存儲結果 如果有的話,就立即返回這個存儲結果,否則的話,調用函數運算下;代碼如下:
var count2 = 0;
var fibonacci2 = (function(){
var memo = [0,1];
var fib = function(n) {
var result = memo[n];
count2++;
if(typeof result !== 'number') {
result = fib(n-1) + fib(n-2);
memo[n] = result;
}
return result;
};
return fib;
})();
for(var j = 0; j <= 10; j+=1) {
console.log(j+":"+fibonacci2(j));
}
console.log(count2); // 29
這個函數也返回了同樣的結果,但是隻調用了函數29次,循環了11次,也就是說函數自身調用了18次,從而減少無謂的函數的調用及運算,下面我們可以把這個函數進行抽象化,以構造帶記憶功能的函數,如下代碼:
var count3 = 0;
var memoizer = function(memo,formula) {
var recur = function(n) {
var result = memo[n];
count3++; // 這句代碼只是說明運行函數多少次,在代碼中並無作用,實際使用上可以刪掉
if(typeof result !== 'number') {
result = formula(recur,n);
memo[n] = result;
}
return result;
};
return recur;
};
var fibonacci3 = memoizer([0,1],function(recur,n){
return recur(n-1) + recur(n-2);
});
// 調用方式如下
for(var k = 0; k <=10; k+=1) {
console.log(k+":"+fibonacci3(k));
}
console.log(count3); // 29
如上封裝 memoizer 裏面的參數是實現某個方法的計算公式,具體的可以根據需要自己手動更改,這邊的思路無非就是想習慣使用對象去保存臨時值,從而減少不必要的取值存儲值的操作;
七:理解通過Function擴展類型
javascript 允許爲語言的基本數據類型定義方法。通過Object.prototype
添加原型方法,該方法可被所有的對象使用。
這對函數,字符串,數字,正則和布爾值都適用,比如如下現在給Function.prototype
增加方法,使該方法對所有函數都可用,代碼如下:
Function.prototype.method = function(name,func) {
if(!this.prototype[name]) {
this.prototype[name] = func;
return this;
}
}
Number.method('integer',function(){
return Math[this < 0 ? 'ceil' : 'floor'](this);
});
console.log((-10/3).integer()); // -3
String.method('trim',function(){
return this.replace(/^\s+|\s+$/g,'');
});
console.log(" abc ".trim()); // abc
八:理解使用模塊模式編寫代碼
使用函數和閉包可以構建模塊,所謂模塊,就是一個提供接口卻隱藏狀態與實現的函數或對象。使用函數構建模塊的優點是:減少全局變量的使用;
比如如下:我想爲String擴展一個方法,該方法的作用是尋找字符串中的HTML字符字體並將其替換爲對應的字符;
// 如下代碼:
Function.prototype.method = function(name,func) {
if(!this.prototype[name]) {
this.prototype[name] = func;
return this;
}
}
String.method('deentityify',function(){
var entity = {
quot: '"',
It: '<',
gt: '>'
};
return function(){
return this.replace(/&([^&;]+);/g,function(a,b){
var r = entity[b];
return typeof r === 'string' ? r : a;
});
}
}());
console.log("&It;">".deentityify()); // <">
模塊模式利用函數作用域和閉包來創建綁定對象與私有成員的關聯,比如在上面的deentityify()方法纔有權訪問字符實體表entity這個數據對象;
模塊開發的一般形式是:定義了私有變量和函數的函數,利用閉包創建可以訪問到的私有變量和函數的特權函數,最後返回這個特權函數,或把他們保存到可以訪問的地方。
模塊模式一般會結合實例模式使用。javascript的實例就是使用對象字面量表示法創建的。對象的屬性值可以是數值或者函數,並且屬性值在該對象的生命週期中不會發生變化;比如如下代碼屬於模塊模式:定義了一個私有變量name屬性,和一個實例模式(對象字面量obj)並且返回這個對象字面量obj,對象字面量中的方法與私有變量name進行了綁定;
// 比如如下經典的模塊模式
var MODULE = (function(){
var name = "tugenhua";
var obj = {
setName: function() {
this.name = name;
},
getName: function(){
return this.name;
}
};
return obj;
})();
MODULE.setName()
console.log(MODULE.getName()); // tugenhua
九:理解惰性實列化
在頁面中javascript初始化執行的時候就實例化類,如果在頁面中沒有使用這個實列化的對象,就會造成一定的內存浪費和性能損耗;這時候,我們可以使用惰性實列化來解決這個問題,惰性就是把實列化推遲到需要使用它的時候纔去做,做到 "按需供應";
// 惰性實列化代碼如下
var myNamespace = function(){
var Configure = function(){
var privateName = "tugenhua";
var privateGetName = function(){
return privateName;
};
var privateSetName = function(name) {
privateName = name;
};
// 返回單列對象
return {
setName: function(name) {
privateSetName(name);
},
getName: function(){
return privateGetName();
}
}
};
// 存儲Configure實列
var instance;
return {
init: function(){
// 如果不存在實列,就創建單列實列
if(!instance) {
instance = Configure();
}
// 創建Configure單列
for(var key in instance) {
if(instance.hasOwnProperty(key)) {
this[key] = instance[key];
}
}
this.init = null;
return this;
}
}
}();
// 調用方式
myNamespace.init();
var name = myNamespace.getName();
console.log(name); // tugenhua
如上代碼是惰性化實列代碼:它包括一個單體Configure實列,直接返回init函數,先判斷該單體是否被實列化,如果沒有被實列化的話,則創建並執行實列化並返回該實列化,如果已經實列化了,則返回現有實列;執行完後,則銷燬init方法,只初始化一次
十:推薦分支函數(解決兼容問題的更好的方法)
分支函數的作用是:可以解決兼容問題if或者else的重複判斷的問題,我們一般的做法是:根據兼容的不同寫if,else等,這些判斷來實現兼容,但是這樣明顯就有一個缺點,每次執行這個函數的時候,都需要進行if和else的檢測,效率明顯不高,我們現在使用分支函數來實現當初始化的時候進行一些檢測,在之後的運行代碼過程中,代碼就無需檢測了;
// 我們先來看看傳統的封裝ajax請求的函數
//創建XMLHttpRequest對象:
var xmlhttp;
function createxmlhttp(){
if (window.XMLHttpRequest){
// code for IE7+, Firefox, Chrome, Opera, Safari
xmlhttp=new XMLHttpRequest();
}
else{
// code for IE6, IE5
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
}
// 下面我們看看分支函數代碼如下:
var XHR = (function(){
var standard = {
createXHR : function() {
return new XMLHttpRequest();
}
};
var oldActionXObject = {
createXHR : function(){
return new ActiveXObject("Microsoft.XMLHTTP");
}
};
var newActionXObject = {
createXHR : function(){
return new ActiveXObject("Msxml2.XMLHTTP");
}
};
if(standard.createXHR) {
return standard;
}else {
try{
newActionXObject.createXHR();
return newActionXObject;
}catch(e){
oldActionXObject.createXHR();
return oldActionXObject;
}
}
})();
console.log(XHR.createXHR()); //xmlHttpRequest對象
上面的代碼就是分支函數,分支的原理是:聲明幾個不同名稱的對象,且爲該不同名稱對象聲明一個相同的方法,然後根據不同的瀏覽器設計來實現,接着開始進行瀏覽器檢測,並且根據瀏覽器檢測來返回哪一個對象,不論返回的是哪一個對象,最後它一致對外的接口都是createXHR方法的;
十一:惰性載入函數(也是解決兼容問題的)
和上面分支的原理是一樣的,代碼也可以按照上面的推薦分支風格編碼的;解決的問題也是解決多個if條件判斷的;代碼如下:
// 代碼如下:
var addEvent = function(el,type,handler){
addEvent = el.addEventListener ? function(el,type,handler){
el.addEventListener(type,handler,false);
} : function(el,type,handler) {
el.attachEvent("on" + type,handler);
}
addEvent(el,type,handler);
};
惰性載入函數也是在函數內部改變自身的一種方式,在重複執行的時候就不會再進行檢測的;惰性載入函數的分支只會執行一次,即第一次調用的時候,其優點如下:
要執行的適當代碼只有在實際調用函數時才執行。
第一次調用該函數的時候,緊接着內部函數也會執行,但是正因爲這個,所以後續繼續調用該函數的話,後續的調用速度會很快;因此避免了多重條件;
十二:理解函數節流
DOM操作的交互需要更多的內存和CPU時間,連續進行過多的DOM相關的操作可能會導致瀏覽器變慢甚至崩潰,函數節流的設計思想是讓某些代碼可以在間斷的情況下連續重複執行,實現該方法可以使用定時器對該函數進行節流操作;
比如:第一次調用函數的時候,創建一個定時器,在指定的時間間隔下執行代碼。當第二次執行的時候,清除前一次的定時器並設置另一個,將其替換成一個新的定時器;
// 如下簡單函數節流代碼演示
var throttle = {
timeoutId: null,
// 需要執行的方法
preformMethod: function(){
},
// 初始化需要調用的方法
process: function(){
clearTimeout(this.timeoutId);
var self = this;
self.timeoutId = setTimeout(function(){
self.preformMethod();
},100);
}
};
// 執行操作
throttle.process();
函數節流解決的問題是一些代碼(比如事件)無間斷的執行,這可能會影響瀏覽器的性能,比如瀏覽器變慢或者直接崩潰。比如對於mouseover事件或者click事件,比如點擊tab項菜單,無限的點擊,有可能會導致瀏覽器會變慢操作,這時候我們可以使用函數節流的操作來解決;
原文地址:http://www.cnblogs.com/tugenhua0707/p/5046854.html