javascript中的一些核心知識點以及需要注意的地方

葉小釵發表於2014-06-22

前言

近期雜事甚多,這些事情的積累對知識體系的提升有好處,但是卻不能整理出來,也整理不出來

比如說我最近研究的Hybrid線上聯調方案便過於依賴於業務,就算分享也不會有人讀懂,若是抽一點來分享又意義不大

又拿最近做webapp view 轉場動畫研究,就是幾個demo不斷測試,感覺沒有什麼可說的

最後甚至對webapp中的History的處理方案也是有一些心得,一點方案,但是依舊難以整理成文,於是便開始文荒了

這個時候不妨便溫故知新吧,對javascript的一些老知識點進行整理回顧,之後有大動作再說吧!

文中知識僅是個人積累總結,有誤請指出

閉包

作用域鏈

閉包是javascript中一個重要知識點,也是javascript中一塊魔法,我們在不熟悉他的情況下可能便經常使用了,熟悉他了解他是從初級至中級的一個標誌

要真正瞭解閉包,就得從作用域鏈說起

javascript中,作用域鏈的作用是控制變數的訪問順序,僅此而已

首先,javascript在執行時需要一個環境,這個環境便是我們所謂執行上下文(execution context

執行上下文決定了變數或者函式有權利訪問其它資料,每個執行環境都有一個與之關聯的變數物件,用於儲存執行上下文中定義的變數或者函式

一般情況下我們所處的全域性執行上下文便是window物件,所以全域性範圍內建立的所有物件全部是window的屬性或者方法

函式的變數物件一般是其活動物件(activation Object)

其次,javascript沒有塊級作用域的概念,但是每個函式有自己的執行上下文,這個便是變相的塊級作用域

每執行一個函式時,函式的執行上下文會被推入一個上下文棧中,函式若執行結束,這個上下文棧便會被彈出,控制權變回之前的執行上下文

當程式碼在執行上下文中執行時,變回建立一個作用域鏈,這個作用域鏈控制著執行上下文資料訪問順序

function test() {
  var a = 2;
  console.log(a);
}
var a = 1;
test();

在這裡便具有兩個執行上下文,一個是window,一個是test函式

首先,在test執行之前,我們全域性執行上下文已經存在,他便是window,這個時候我們會有a與test在作用域最前端

執行test時候,形成test執行上下文,於是最前端的執行上下文變成了test,這個時候會先形成活動物件,包括arguments以及a

在console.log時,會訪問作用域鏈最近的a變數,也就是2,這個是列印出2的根本原因,若是沒有作用域鏈這個順序就壞了

下面是test執行時候的圖示:

所以作用域鏈相關的知識點是:

① 控制變數訪問順序
② 執行上下文包含一個作用域鏈的指標
③ 該層函式外部有幾個函式,便會有幾個活動物件待處理,作用域鏈指標會指向其外部活動物件
④ 作用域鏈為執行上下文時函式內部屬性,不要妄想去操作

閉包的形成

閉包的形成便是一個函式執行上下文中有一個變數被其內部函式使用了,並且這個內部函式被返回了,便形成了一個閉包

由於函式呼叫後,外部臨時變數儲存著內部的引用,執行時會形成內部上下文環境,內部的函式會包含外部的作用域鏈指向的變數物件,

這個時候就算外部執行環境消耗,由於外部儲存著外部函式的活動物件的引用,所以這個變數物件不會被消耗,這個是閉包產生的原因

function test() {
  var a = 2;
  return function () {
    console.log(a);
  };
}
var b = test();
b();

這裡會形成三個執行環境,一個是全域性的,一個是test的,一個是匿名函式(最終是b函式)的,我們依舊從test執行時說起

當test函式執行時:

var b = test();

會形成一個執行上下文,執行上下文包含一個作用域鏈指標,並且會形成一個活動物件

這裡test的作用域鏈只是一個指標,他只是引用這個活動物件,執行結束後執行上下文會被釋放,作用域鏈也會消失,但是其活動物件未必會GC

在b執行時,其匿名函式的作用域鏈便指向了外部函式的活動物件,不要問他怎麼獲得這個指標引用的,他就是知道,於是test的活動物件將一直被儲存,直到b呼叫結束

這裡b執行的關係是:

經典例子

關於閉包有一個經典的例子,他便是for迴圈的例子:

function createFn() {
  var ret = [], i;
  for (i = 0; i < 10; i++) {
    ret[i] = function () {
      return i;
    };
  }
  return ret;
}
var fns = createFn();

這段程式碼非常簡單,根據一個陣列形成10個函式,每個函式返回其索引值,這類應用在實際工作中會經常用到,只不過我們需要的是其索引對應的資料,而不是簡單的索引了

這類會createFn執行時會有兩個執行環境,一個是自己的,一個是windows的,內部執行環境作用域鏈會指向一個活動物件

當然fns陣列中任意一個函式執行時,其會使用到createFn的活動物件中的資料i,而該活動物件是被10個函式共用的,都是10,所以與預期不合

該問題的處理便是各自形成自己的閉包:

function createFn() {
  var ret = [], i;
  for (i = 0; i < 10; i++) {
    ret[i] = (function (i) {
      return function () {
        return i;
      };
    })(i);
  }
  return ret;
}
var fns = createFn();

這裡迴圈中會形成10個獨立的執行上下文,其中的10個活動物件的arguments都儲存了外部i的獨立資料,而內部又形成一個閉包訪問立即執行函式的資料,所以資料正確了......

其它閉包

requireJS中的閉包

標準的requireJS來說都是一個AMD的模組,比如:

define(function () {
  var add = function (x, y) {
    return x + y;
  };
  return {
    add: add
  };
});

我們知道,requireJS每一次載入其模組皆會被執行一次,並且只會執行一次,這個模組會被requireJS所儲存,所以這個匿名函式活動物件是不會被釋放的,且是唯一的

這個時候我們很多元件便可以統一使用其功能即可,比如生成uuid什麼的......當然,這種不釋放的問題,也會導致heap值的提升,這個是不是有問題便需要各位去驗證了

webapp中的閉包

webapp一般會使用requireJS管理模組,而內部又會形成許多view的例項,這個例項並且會儲存下來,這樣也會導致很多函式的活動物件得不到釋放

一來二往之間,heap值會比傳統網站高,這個是webapp一塊比較頭疼的地方,需要慢慢優化

原型鏈

最初javascript沒有class的概念,我們使用的類是以function模擬,繼承的實現手段一般依靠原型鏈,繼承的使用也是評價一個jser的重要指標

每個函式都會包含一個原型物件prototype

原型物件prototype包含一個指向建構函式的指標constructor

例項物件包含一個內部屬性__proto__指標指向原型物件prototype

這是他們之間的三角關係:

(function () {
    var Person = function (name) {
        this.name = name;
    };
    //Person.prototype = {};//這句將影響十分具有constructor屬性
    Person.prototype.getName = function () {
        return this.name;
    };

    var Student = function (name, sex, id) {
        this.name = name || '無名氏';
        this.sex = sex || '不明';
        this.id = id || '未填'; //學號
    };
    //相當於將其prototype複製了一次,若是包含constructor的話將指向Person
    Student.prototype = new Person();
    Student.prototype.getId = function () {
        return this.id;
    }
    var y = new Person();
    var s = new Student;
    var s1 = y instanceof Person;
    var s2 = s instanceof Student;
    var s3 = s instanceof Person;
    var s4 = Student.prototype.constructor === Person;
    var s5 = Student.constructor === Person;
    var s6 = Student.constructor === Function;

    var s = '';
})();

一般形式的繼承方式如上,偶爾我們會這樣幹:

Student.prototype = {}

但是這樣會導致prototype物件的constructor物件丟失,所以需要找回來,另外一個問題是,這裡繼承需要執行父類的構造方法,這樣是有問題的

比如,父類的建構函式中有一些事件繫結什麼的與子類無關,便會導致該類繼承無用,所以很多時候我們需要自己實現繼承,比較優雅的是prototype的做法,我這裡對其進行了一定改造

var arr = [];
var slice = arr.slice;

function create() {
  if (arguments.length == 0 || arguments.length > 2) throw '引數錯誤';

  var parent = null;
  //將引數轉換為陣列
  var properties = slice.call(arguments);

  //如果第一個引數為類(function),那麼就將之取出
  if (typeof properties[0] === 'function')
    parent = properties.shift();
  properties = properties[0];

  function klass() {
    this.initialize.apply(this, arguments);
  }

  klass.superclass = parent;
  klass.subclasses = [];

  if (parent) {
    var subclass = function () { };
    subclass.prototype = parent.prototype;
    klass.prototype = new subclass;
    parent.subclasses.push(klass);
  }

  var ancestor = klass.superclass && klass.superclass.prototype;
  for (var k in properties) {
    var value = properties[k];

    //滿足條件就重寫
    if (ancestor && typeof value == 'function') {
      var argslist = /^\s*function\s*\(([^\(\)]*?)\)\s*?\{/i.exec(value.toString())[1].replace(/\s/i, '').split(',');
      //只有在第一個引數為$super情況下才需要處理(是否具有重複方法需要使用者自己決定)
      if (argslist[0] === '$super' && ancestor[k]) {
        value = (function (methodName, fn) {
          return function () {
            var scope = this;
            var args = [function () {
              return ancestor[methodName].apply(scope, arguments);
            } ];
            return fn.apply(this, args.concat(slice.call(arguments)));
          };
        })(k, value);
      }
    }

    klass.prototype[k] = value;
  }

  if (!klass.prototype.initialize)
    klass.prototype.initialize = function () { };

  klass.prototype.constructor = klass;

  return klass;
}
View Code

首先,繼承時使用一個空建構函式實現,這樣不會執行原建構函式的例項方法,再規範化必須實現initialize方法,保留建構函式的入口,這類實現比較優雅,建議各位試試

javascript中的DOM事件

事件流

PS:javascript的事件一塊我說的夠多了,這裡再說一次吧......

javascript註冊dom事件的手段很多:

① 直接寫在dom標籤上,onclick的做法

② 在js中這樣寫:el.onclick = function

上述做法事實上是不好的,因為他們無法多次定義,也無法登出,更加不用說使用事件委託機制了

上述兩種做法的最終仍然是呼叫addEventListener方式進行註冊冒泡級別的事件,於是這裡又扯到了javascript事件的幾個階段

在DOM2級事件定義中規定事件包括三個階段,這個是現有DOM事件的基礎,這個一旦改變,前端DOM事件便需要重組
三個階段是事件事件捕獲階段、處於目標階段、冒泡階段
事件捕獲由最先接收到事件的元素往最裡面傳
事件冒泡由最具體元素往上傳至document

一般而言是先捕獲後冒泡,但是處於階段的事件執行只與註冊順序有關,比如:
每次點選一個DOM時候我們會先判斷是否處於事件階段,若是到了處於階段的話便不存在捕獲階段了
直接按照這個DOM的事件註冊順序執行,然後直接進入冒泡階段邏輯,其判斷的依舊是e.target與e.currentTarget是否相等

這個涉及到一個瀏覽器內建事件物件,我們註冊事件方式多種多樣
除了addEventListener可以註冊捕獲階段事件外,其餘方式皆是最後呼叫addEventListener介面註冊冒泡級別事件
註冊的事件佇列會根據DOM樹所處位置進行排列,最先的是body,到最具體的元素
每次我們點選頁面一個區域便會先做判斷,是否處於當前階段,比如:
我當前就是點選的是一個div,如果e.target==e.currentTarget,這個時候便會按註冊順序執行其事件,不會理會事件是捕獲還是冒泡,而跳過捕獲流程,結束後會執行冒泡級別的事件,若是body上有冒泡點選事件(沒有捕獲)也會觸發,以上便是DOM事件相關知識點

事件冒泡是事件委託實現的基石,我們在頁面的每次點選最終都會冒泡到其父元素,所以我們在document處可以捕捉到所有的事件,事件委託實現的核心知識點是解決以下問題:

① 我們事件是繫結到document上面,那麼我怎麼知道我現在是點選的什麼元素呢

② 就算我能根據e.target獲取當前點選元素,但是我怎麼知道是哪個元素具有事件呢

③ 就算我能根據selector確定當前點選的哪個元素需要執行事件,但是我怎麼找得到是哪個事件呢

如果能解決以上問題的話,我們後面的流程就比較簡單了

確定當前元素使用 e.target即可,所以我們問題以解決,其次便根據該節點搜尋其父節點即可,發現父節點與傳入的選擇器有關便執行事件回撥即可

這裡還需要重新e.currentTarget,不重寫全部會繫結至document,簡單實現:

var arr = [];
var slice = arr.slice;
var extend = function (src, obj) {
  var o = {};
  for (var k in src) {
    o[k] = src[k];
  }
  for (var k in obj) {
    o[k] = obj[k];
  }
  return o;
};

function delegate(selector, type, fn) {
  var callback = fn;

  var handler = function (e) {
    //選擇器找到的元素
    var selectorEl = document.querySelector(selector);
    //當前點選元素
    var el = e.target;
    //確定選擇器找到的元素是否包含當前點選元素,如果包含就應該觸發事件
    /*************
    注意,此處只是簡單實現,實際應用會有許多判斷
    *************/
    if (selectorEl.contains(el)) {
      var evt = extend(e, { currentTarget: selectorEl });
      evt = [evt].concat(slice.call(arguments, 1));
      callback.apply(selectorEl, evt);
      var s = '';
    }
    var s = '';
  };

  document.addEventListener(type, handler, false);
}
View Code

事件委託由於全部事件是繫結到document上的,所以會導致阻止冒泡失效,很多初學的同學不知道,這裡要注意

事件模擬

事件模擬是dom事件的一種高階應用,一般情況下用不到,但是一些極端情況下他是解決實際問題的殺手鐗

事件模擬是javascript事件機制中相當有用的功能,理解事件模擬與善用事件模擬是判別一個前端的重要依據,所以各位一定要深入理解

事件一般是由使用者操作觸發,其實javascript也是可以觸發的,比較重要的是,javascript模擬的觸發遵循事件流機制!!!

意思就是,javascript觸發的事件與瀏覽器本身觸發其實是一樣的,簡單模擬事件點選:

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <style type="text/css">
         #p { width: 300px; height: 300px; padding: 10px;  border: 1px solid black; }
         #c { width: 100px; height: 100px; border: 1px solid red; }
    </style>
</head>
<body>
    <div id="p">
        parent
        <div id="c">
            child
        </div>
    </div>
    <script type="text/javascript">
        alert = function (msg) {
            console.log(msg);
        }

        var p = document.getElementById('p'),
        c = document.getElementById('c');
        c.addEventListener('click', function (e) {
            console.log(e);
            alert('子節點捕獲')
        }, true);
        c.addEventListener('click', function (e) {
            console.log(e);
            alert('子節點冒泡')
        }, false);

        p.addEventListener('click', function (e) {
            console.log(e);
            alert('父節點捕獲')
        }, true);

        p.addEventListener('click', function (e) {
            console.log(e);
            alert('父節點冒泡')
        }, false);

        document.addEventListener('keydown', function (e) {
            if (e.keyCode == '32') {
                var type = 'click'; //要觸發的事件型別
                var bubbles = true; //事件是否可以冒泡
                var cancelable = true; //事件是否可以阻止瀏覽器預設事件
                var view = document.defaultView; //與事件關聯的檢視,該屬性預設即可,不管
                var detail = 0;
                var screenX = 0;
                var screenY = 0;
                var clientX = 0;
                var clientY = 0;
                var ctrlKey = false; //是否按下ctrl
                var altKey = false; //是否按下alt
                var shiftKey = false;
                var metaKey = false;
                var button = 0; //表示按下哪一個滑鼠鍵
                var relatedTarget = 0; //模擬mousemove或者out時候用到,與事件相關的物件
                var event = document.createEvent('Events');
                event.myFlag = '葉小釵';
                event.initEvent(type, bubbles, cancelable, view, detail, screenX, screenY, clientX, clientY,
ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget);
                
                console.log(event);
                c.dispatchEvent(event);
            }
        }, false);
    </script>
</body>
</html>
View Code

模擬點選事件是解決移動端點選響應的基石,有興趣的同學自己去研究下吧,我這裡不多說

延時執行

延時執行settimeout是javascript中的一道利器,很多時候一旦解決不了我們便會使用settimeout,但是對settimeout的理解上,很多初學的朋友有一定誤區

初學的朋友一般認為settimeout是在多少毫秒後便會被執行,事實上其後面的資料代表的是一個時間片,或者說是優先順序,settimeout的回撥會在主幹程式之後執行

比如:

var a = 0, b = 1;
setInterval(function () {
  a = 1;
}, 0)
while (1) {
  //...
  b++;
  if(a == 1)
    break;
}

以下程式碼會導致瀏覽器假死,因為settimeout中的程式碼永遠不會執行

settimeout真正的的用法是:

① 延時請求,減少不必要的請求

② 需要過多的操作dom結構時,為了閉包瀏覽器假死,可以使用settimeout

另外,zepto中有一段與settimeout有關的恥辱程式碼,在模擬tap事件時候,zepto使用dom模擬click事件的方式實現了:

.on('touchend MSPointerUp pointerup', function(e){
  if((_isPointerType = isPointerEventType(e, 'up')) &&
    !isPrimaryTouch(e)) return
  cancelLongTap()

  // swipe
  if ((touch.x2 && Math.abs(touch.x1 - touch.x2) > 30) ||
      (touch.y2 && Math.abs(touch.y1 - touch.y2) > 30))

    swipeTimeout = setTimeout(function() {
      touch.el.trigger('swipe')
      touch.el.trigger('swipe' + (swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2)))
      touch = {}
    }, 0)

  // normal tap
  else if ('last' in touch)
    if (deltaX < 30 && deltaY < 30) {
      tapTimeout = setTimeout(function() {

        var event = $.Event('tap')
        event.cancelTouch = cancelAll
        touch.el.trigger(event)

        if (touch.isDoubleTap) {
          if (touch.el) touch.el.trigger('doubleTap')
          touch = {}
        }
        else {
          touchTimeout = setTimeout(function(){
            touchTimeout = null
            if (touch.el) touch.el.trigger('singleTap')
            touch = {}
          }, 250)
        }
      }, 0)
    } else {
      touch = {}
    }
    deltaX = deltaY = 0
})
View Code

比較狗血的是,他在tap這裡使用了settimeout,導致了一個延時,這個延時效果直接的影響便是其event引數失效

也就是這裡,touchend時候傳入的event引數不會被tap事件用到,什麼e.preventDefault之類的操作便於tap無關了,此類實現至今未改

其它

localstorage

localstorage的使用在我廠webapp的應用中,達到了一個前所未有的高度,我們驚奇的發現,其真實容量是:

localstorage 的最大限制按字元數來算,中英文都是最多500多萬個字元,webkit為5242880個

於是很多時候,localstorage的濫用便會引發localstorage儲存失效,導致業務錯誤

並且localstorage的濫用還表現在儲存業務關鍵資訊導致url對外不可用的情況,所以使用localstorage的朋友要慎重!

其它

......

結語

今天我們花了一點時間回顧了一些javascript的核心知識點,希望對各位有用,我這裡先撤退了,文中理解有誤請提出

相關文章