前言
近期雜事甚多,這些事情的積累對知識體系的提升有好處,但是卻不能整理出來,也整理不出來
比如說我最近研究的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; }
首先,繼承時使用一個空構造函數實現,這樣不會執行原構造函數的實例方法,再規范化必須實現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); }
事件委托由於全部事件是綁定到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>
模擬點擊事件是解決移動端點擊響應的基石,有興趣的同學自己去研究下吧,我這里不多說
延時執行
延時執行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 })
比較狗血的是,他在tap這里使用了settimeout,導致了一個延時,這個延時效果直接的影響便是其event參數失效
也就是這里,touchend時候傳入的event參數不會被tap事件用到,什么e.preventDefault之類的操作便於tap無關了,此類實現至今未改
其它
localstorage
localstorage的使用在我廠webapp的應用中,達到了一個前所未有的高度,我們驚奇的發現,其真實容量是:
localstorage 的最大限制按字符數來算,中英文都是最多500多萬個字符,webkit為5242880個
於是很多時候,localstorage的濫用便會引發localstorage存儲失效,導致業務錯誤
並且localstorage的濫用還表現在存儲業務關鍵信息導致url對外不可用的情況,所以使用localstorage的朋友要慎重!
其它
......
結語
今天我們花了一點時間回顧了一些javascript的核心知識點,希望對各位有用,我這里先撤退了,文中理解有誤請提出