js編程中常常遇到的一些問題(持續更新)

一:前言

本文適合有必定JS開發基礎的讀者,文章涉及開發中常常遇到的一些使人疑惑的問題,理解這些問題有助於咱們快速提高對JS這門語言的理解和應用能力。文章只講述具體問題中的關鍵問題,不涵蓋全面的知識點。如想了解具體的知識,能夠參考筆者博客的相關文章。javascript

二:正文

1.丟失的this

在實際應用中, 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.實現手動綁定this

2-1:bind方法的兼容寫法瀏覽器

var bind = Function.prototype.bind || function( context ){
        var self = this; // 保存原函數
        return function(){ // 返回一個新的函數
            return self.apply( context, arguments ); // 執行新的函數的時候,會把以前傳入的context看成新函數體內的this
        }
    };

3.閉包

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結點,這時候就有可能形成內存泄漏。

4.高階函數

(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}

5.函數節流

函數節流也用到了高階函數的知識,由於比較重要,因此單開了一個標題。
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();

6.惰性加載函數

在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須要掌握的重點又是難點的知識,須要多動手實踐才能理解。有關相關知識的詳細講解,能夠參考筆者的相關文章。固然 ,最好的方式是去谷歌而後本身動手實踐。

相關文章
相關標籤/搜索