本文適合有必定JS開發基礎的讀者,文章涉及開發中常常遇到的一些使人疑惑的問題,理解這些問題有助於咱們快速提高對JS這門語言的理解和應用能力。文章只講述具體問題中的關鍵問題,不涵蓋全面的知識點。如想了解具體的知識,能夠參考筆者博客的相關文章。javascript
在實際應用中, this的指向大體分爲如下四種:
(1)做爲對象方法的調用
(2)做爲普通函數調用
(3)構造器調用
(4)Function.prototype.call或Function.prototype.applyhtml
1-1閱讀下面代碼:java
//1.做爲對象方法的調用this老是指向那個對象 window.name = 'globalName'; var getName = function(){ return this.name; }; console.log( getName() ); // 輸出:globalName //2.做爲普通函數的調用:非嚴格模式下this老是指向window,嚴格模式下 undefined window.name = 'globalName'; var myObject = { name: 'sven', getName: function(){ return this.name; } }; var getName = myObject.getName;//關鍵:這裏保留了一個普通函數的引用 console.log( getName() ); // globalName
經過以上兩個對比,理解使用方法不一樣,this指向不一樣node
1-2閱讀下面的代碼:web
var getId = function( id ){ return document.getElementById( id ); }; getId( 'div1' ); //咱們也許思考過爲何不能用下面這種更簡單的方式: var getId = document.getElementById; getId( 'div1' );
document.getElementById方法須要用到this。這個this原本被指望指向document,當getElementById被看成 document的屬性被調用時,方法內部的this確實是指向document.
可是當使用getId來引用document.getElementById以後,在調用getId,此時就變成了普通函數調用,內部的this就指向了window。
利用call或者apply更正this指向:
//咱們能夠嘗試利用apply 把document 看成this 傳入getId 函數,幫助「修正」this:編程
document.getElementById = (function( func ){ return function(){ return func.apply( document, arguments ); } })( document.getElementById ); var getId = document.getElementById; var div = getId( 'div1' ); alert (div.id); // 輸出: div1
2-1:bind方法的兼容寫法瀏覽器
var bind = Function.prototype.bind || function( context ){ var self = this; // 保存原函數 return function(){ // 返回一個新的函數 return self.apply( context, arguments ); // 執行新的函數的時候,會把以前傳入的context看成新函數體內的this } };
3-1.如今來看看下面這段代碼:安全
var func = function(){ var a = 1; return function(){ a++; alert ( a ); } }; var f = func(); f(); // 輸出:2 f(); // 輸出:3 f(); // 輸出:4 f(); // 輸出:5
當執行f = func()時,f返回了一個匿名函數的引用,它能夠訪問到func()被調用時產生的環境,而局部變量a一直處在這個環境裏。這個變量就有了不被銷燬的理由,這裏就產生了一個閉包結構。
3-2常見的閉包的問題:閉包
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> </head> <body> <div>1</div> <div>2</div> <div>3</div> <div>4</div> <div>5</div> <script type="text/javascript"> var node = document.getElementsByTagName('div'); for(var i=0;len=node.length;i<len;i++){ nodes[i].onclick=function(){ alert(i); } } //不管點擊哪一個結點,都返回5 //這是由於onclick事件是異步的,當事件觸發的時候,for循環早已經結束 //保存了函數的引用,順着做用域鏈從內到外查找i時,查到的值老是5 //解決方法就是在閉包的幫助下,把每次循環的i都封閉起來。 /* for(var i=0;len=node.length;i<len;i++){ nodes[i].onclick=(function(i){ alert(i); })(i); }*/ </script> </body> </html>
3-3.利用閉包延續局部變量的壽命app
//img 對象常常用於進行數據上報,以下所示: var report = function( src ){ var img = new Image(); img.src = src; }; report( 'http://xxx.com/getUserInfo' ); //丟失數據的緣由是img是report函數中的局部變量,當函數調用以後局部變量就銷燬了,而此時或許還沒來得及發起http請求 //如今咱們把img 變量用閉包封閉起來,便能解決請求丟失的問題: var report = (function(){ var imgs = []; return function( src ){ var img = new Image(); imgs.push( img ); img.src = src; } })();
閉包與內存管理
閉包會使一些數據沒法被及時的銷燬,若是未來須要回收這些變量,咱們能夠手動把這些變量設置爲null。
跟閉包和內存泄漏有關係的地方是,使用閉包的同時容易造成循環引用,若是閉包的做用域鏈中保存着一些DOM結點,這時候就有可能形成內存泄漏。
(1)函數能夠做爲參數被傳遞
(2)函數能夠做爲返回值輸出
4-1.函數做爲參數傳遞
Array.prototype.sort方法:
var array = ['10','5','12','3']; array.sort(); //array:['10','12','3','5'] //如代碼那樣,排序的結果並非咱們想要的,這與sort函數的比較規則有關係 array.sort(function(a,b){return a-b;}); //array:['3','5','10','12'] 傳入一個比較的函數,就能夠按照數字大小的規則進行正確的比較了。
4-2.函數做爲返回值輸出
var getSingle = function ( fn ) { var ret; return function () { return ret || ( ret = fn.apply( this, arguments ) ); }; };
4-3.函數做爲參數被傳遞而且返回另外一個函數
var getScript = getSingle(function(){ return document.createElement( 'script' ); }); var script1 = getScript(); var script2 = getScript(); alert ( script1 === script2 ); // 輸出:true
4-4.高階函數應用
(1)高階函數實現AOP
AOP(面向切面編程)的主要做用是把一些跟核心業務邏輯模塊無關的功能抽離出來,這些業務邏輯無關的功能包括日誌統計、控制安全、異常處理等。把這些功能抽離出來以後,再經過「動態織入」的方式摻入業務邏輯模塊中。
下面代碼經過擴展Function.prototype來實現把一個函數「動態織入」
Function.prototype.before = function( beforefn ){ var __self = this; // 保存原函數的引用 return function(){ // 返回包含了原函數和新函數的"代理"函數 beforefn.apply( this, arguments ); // 執行新函數,修正this return __self.apply( this, arguments ); // 執行原函數 } }; Function.prototype.after = function( afterfn ){ var __self = this; return function(){ var ret = __self.apply( this, arguments ); afterfn.apply( this, arguments ); return ret; } }; var func = function(){ console.log( 2 ); }; func = func.before(function(){ console.log( 1 ); }).after(function(){ console.log( 3 ); }); func();
(2)柯里化
一個currying函數首先會接受一些參數,接受了這些參數以後,該函數不會當即求值,而是繼續返回另一個函數,剛纔傳入的參數在函數造成的閉包中被保存了下來。待到函數真正須要求值的時候,以前傳入的全部參數都會一次性用於求值。
一個經典的柯里化:
function curry(fn){ var arr1 = Array.prototype.slice.call(arguments,1); return function(){ var arg2 = Array.prototype.slice.call(arguments); var array = arr1.concat(arr2); return fn.apply(null,array); } }
不斷累積的柯里化:
var currying = function( fn ){ var args = [];//外層函數變量:用來累積 return function(){ if ( arguments.length === 0 ){ return fn.apply( this, args ); }else{ [].push.apply( args, arguments ); return arguments.callee; } } };
(3)uncurrying
在javascript中,當咱們調用對象的某個方法時,其實不用關心對象本來是否被設計爲擁有這個方法,這是動態類型語言的特色,也就是常說的鴨子類型思想。
同理,一個對象也未必只能使用它本身的方法,其實能夠借用本來不屬於他的方法: call apply
Function.prototype.uncurrying = function () { var self = this; return function() { var obj = Array.prototype.shift.call( arguments ); return self.apply( obj, arguments ); }; }; var push = Array.prototype.push.uncurrying(); var obj = { "length": 1, "0": 1 }; push( obj, 2 );//將2使用push的方法做用到obj上 console.log( obj ); // 輸出:{0: 1, 1: 2, length: 2}
函數節流也用到了高階函數的知識,由於比較重要,因此單開了一個標題。
javascript中的函數在大多數狀況下都是由用戶主動調用觸發的,除非是函數自己的實現不合理。可是在一些少數狀況下,函數可能被很頻繁的調用,而形成大的性能問題。
(1)函數被頻繁調用的場景
1.window.onresize事件 2.mousemove事件 3.上傳進度
(2)函數節流的原理
解決函數觸發頻率過高的問題,須要咱們按照時間段來忽略一些事件請求。
(3)函數節流的代碼實現
詳情能夠參考
Underscore.js#throttle
Underscore.js#debounce
簡單實現:
將即將被執行的函數用steTimeout延時一段時間執行。若是該次延時執行尚未完成,就忽略掉接下來調用該函數的請求。
var throttle = function ( fn, interval ) { var __self = fn, // 保存須要被延遲執行的函數引用 timer, // 定時器 firstTime = true; // 是不是第一次調用 return function () { var args = arguments, __me = this; if ( firstTime ) { // 若是是第一次調用,不需延遲執行 __self.apply(__me, args); return firstTime = false; } if ( timer ) { // 若是定時器還在,說明前一次延遲執行尚未完成 return false; timer = setTimeout(function () { // 延遲一段時間執行 clearTimeout(timer); timer = null; __self.apply(__me, args); }, interval || 500 ); }; }; window.onresize = throttle(function(){ console.log( 1 ); }, 500 );
另外一種實現函數節流的方法-分時函數
某些函數確實是用戶主動調用的,可是由於一些客觀的緣由,這些函數會嚴重的影響頁面的性能。
一個例子就是建立QQ好友列表。若是一個好友列表用一個節點表示,當咱們在頁面中渲染這個列表的時候,可能要一次性的網頁面中建立成百上千個節點。
var ary = []; for ( var i = 1; i <= 1000; i++ ){ ary.push( i ); // 假設ary 裝載了1000 個好友的數據 }; var renderFriendList = function( data ){ for ( var i = 0, l = data.length; i < l; i++ ){ var div = document.createElement( 'div' ); div.innerHTML = i; document.body.appendChild( div ); } }; renderFriendList( ary );
在短期內網頁面中大量添加DOM節點顯然也會讓瀏覽器吃不消。
這個問題的解決方案之一是下面的timeChunk函數:讓建立節點的工做分批進行
//第一個參數是建立節點時須要的數據,第二個參數封裝了建立節點邏輯的函數,第三個參數表示每一批建立節點的數量。 var timeChunk = function( ary, fn, count ){ var obj, t; var len = ary.length; var start = function(){ for ( var i = 0; i < Math.min( count || 1, ary.length ); i++ ){ var obj = ary.shift(); fn( obj ); } }; return function(){ t = setInterval(function(){ if ( ary.length === 0 ){ // 若是所有節點都已經被建立好 return clearInterval( t ); } start(); }, 200 ); // 分批執行的時間間隔,也能夠用參數的形式傳入 }; }; var ary = []; for ( var i = 1; i <= 1000; i++ ){ ary.push( i ); }; var renderFriendList = timeChunk( ary, function( n ){ var div = document.createElement( 'div' ); div.innerHTML = n; document.body.appendChild( div ); }, 8 ); renderFriendList();
在web開發中,由於瀏覽器之間的實現差別,一些嗅探工做老是不可避免。
var addEvent = function( elem, type, handler ){ if ( window.addEventListener ){ return elem.addEventListener( type, handler, false ); } if ( window.attachEvent ){ return elem.attachEvent( 'on' + type, handler ); } };
這個函數的缺點是,當它每次被調用的時候都會執行裏面的if條件分支。
下面這個函數雖然仍然有一些分支判斷,可是在第一次進入條件分支以後,在函數內部就會重寫這個函數,重寫以後的函數就是咱們但願的addEvent函數。
var addEvent = function(ele,type,handler){ if(window.addEventListener){ addEvent = function(ele,type,handler){ elem.addEventListener( type, handler, false ); } } if(window.attachEvent){ addEvent = function(ele,type,handler){ elem.attachEvent( 'on' + type, handler ); } } addEvent(ele,type,handler); }
文章介紹的都是JS須要掌握的重點又是難點的知識,須要多動手實踐才能理解。有關相關知識的詳細講解,能夠參考筆者的相關文章。固然 ,最好的方式是去谷歌而後本身動手實踐。