這次說一下對象具體的一個實例:函數,以及其對應的作用域與作用域鏈。簡單的東西大家查下API就行了,這里我更多的是分享自己的理解與技巧。對於作用域和作用域鏈,相信絕大多數朋友看了我的分享都能基本理解,少數人看完之后再努力思考思考,基本也就懂了。最后說一下,不合理的地方,歡迎批評指正。
函數調用
跳過基本的函數定義,直接說函數調用,js中的函數調用有以下四種方式:
1.直接調用
2.作為對象的方法調用
當作為對象調用時,這里的this指向調用方法的對象,而我們所說的鏈式調用即是在函數內部作用域的最后return this。當函數不需要明確的返回值時,我們常常將this上下文返回,養成這種習慣有助於后期使用鏈式調用提高工作效率。
3.通過構造函數調用,值得注意的是,如果構造函數沒有形參,圓括號是可以省略的,如下:
new Array(); new Array;
構造函數的返回值固定為實例對象,無法修改。
另外,當構造函數作為對象的方法調用時,構造函數中的this仍指向實例對象
var obj = { a: function (name, sex) { this.name = name; this.sex = sex; console.log(this) } } var stu = new obj.a('kevin',18) // a {name: "kevin", sex: 18}
4.函數通過call()和apply() 間接調用方法,call和appy的第一個參數為this上下文指向的母對象.
當以對象a的方法來調用fn(x,y)時
fn.call(a, x, y);
fn.apply(a, [x, y]);
當使用 call() 和 apply() 方法時( 以對象a的方法來調用fn() )
fn.call(a)
等價於如下代碼:
a.m = fn; a.m(); delete a.m;
可選形參
當我們在定義函數時,經常需要考慮形參的排列順序。傳入實參的個數可以小於形參,但必須是一一對應,並且可選的形參必須放在形參列表的最后。
使用/*optional*/b 表示b為可選參數
function test (a, /*optional*/b) { // ..... }
與可選形參相比我們則常用對象形參來代替形參列表,此方法常常應用於編寫插件的配置項
作用域
說道函數,就不得不說作用域,我們在工作中,經常會遇到這樣的錯誤
1. XXX is not defined
2. can't find property 'XXX'
常見的原因就是:調用方法的對象未被找的(未成功獲取)或變量在此作用域內未被找到。
而在聲明變量時我們則應該聲明在函數作用域的最頂端,此習慣可以讓開發者對變量所處的作用域一目了然。
js中的作用域為函數作用域,即每個函數內部為一個作用域(作用域也可稱為執行環境)。當系統在執行js代碼時,會創建一個作用域鏈,此作用域鏈的數據結構為類棧(注意不完全等同於棧,這里多說無益)。
每個作用域的代碼在執行時都會創建一個變量對象(VO),此變量對象(VO)中包括了在該作用域中定義的所有變量和函數。由外層向內層的變量對象(VO)會被依次push進作用域鏈中,全局作用域的變量對象(VO)始終會在作用域鏈的最底端(es5的聲明提前所決定),而當前執行代碼的作用域對象(VO)始終在作用域鏈的最頂端。當我們在內部作用域修改全局作用域的變量的值時,由於全局變量對象在棧的最底部,棧的指針需要依次尋找至棧的最底部,並修改全局作用域變量對象的相應值。如果所有變量都定義在全局變量中,內部變量確實是可以訪問得到,但是,執行效率會大大降低。這就是我們為什么需要減少不必要的全局聲明的原因,只調用一次的變量或者函數,盡可能的在其執行環境所對應的作用域中聲明。
執行環境 = { VO:{/*函數中的arguments對象、參數、內部變量以及函數聲明*/} this:{}, Scope:{/*當前作用域的VO以及所有父執行上下文中的VO(與prototype類似)*/} }
注意:在當前作用域的代碼真正執行時,變量對象的值(變量和函數)才會初始化完成。(下文會舉例說明)
在js中每個變量都有其自身的歸屬,我們可以把用戶創建的所有變量類比為一個大家庭中的成員。而全局作用域的變量對象可看做是“祖宗”,而作用域鏈就可看做是家庭的“血緣”,每個函數作用域則可看作是一輩(一代)家庭成員,每個家庭的目標與關注度都放在當前(最新)一輩人【當前執行環境對應的作用域】,而每個家庭也都不能忘本,都要謹記祖宗或長輩的教誨【父作用域的變量】。
一說變量的作用域鏈,就離不開一個老生常談的例子:
// aBtn 為五個按鈕的類數組 // 起初,我們都很渴望打印出 0 1 2 3 4 var aBtn = document.querySelectorAll('button'); for (var i=0; i<aBtn.length; i++) { aBtn[i].onclick = function () { console.log(i) } } // 事實上,全部都是 5
這就用到上文所說的,for循環中的匿名函數在調用之前,i的值已經全部為5,也就是for循環已經執行完。
從個人理解來講,不外呼以下四種方法:
// 1 . // 為btn增加加自定義屬性index,使其在匿名函數中可通過this上下文獲取 var aBtn = document.querySelectorAll('button'); for (var i=0; i<aBtn.length; i++) { aBtn[i].index = i; aBtn[i].onclick = function () { console.log(this.index) } }
// 2. // 在onclik的事件處理函數上級強行增加一個作用域(一代人),並在此作用域內初始化相應的i值 var aBtn = document.querySelectorAll('button'); for (var i=0; i<aBtn.length; i++) { clickFn(aBtn[i], i); } function clickFn (btn, index) { btn.onclick = function () { console.log(index) } }
// 同第二種方法類似,只不過函數改為了匿名函數 var aBtn = document.querySelectorAll('button'); for (var i=0; i<aBtn.length; i++) { (function (i) { aBtn[i].onclick = function () { console.log(i) } })(i) }
// 4. // 使用es6的let聲明變量,則在for循環的{}內也可看做是一個作用域 var aBtn = document.querySelectorAll('button'); for (let i=0; i<aBtn.length; i++) { aBtn[i].onclick = function () { console.log(i) } }
閉包
閉包時作用域鏈的特殊應用的產物,特殊就特殊在閉包所指向的作用域與函數在定義時對應的作用域不同
用一句話概括閉包的形式即:函數b嵌套在函數a內部,函數a返回函數b
function a () { var x = 0; function b () { x++ return x } return b() } console.log(a()) // 1
出現閉包的原因是,有時候根據邏輯需要,我們要在父級作用域中使用局部變量,而閉包就恰好解決了這個問題。另一方面使用閉包獲得的局部變量不會在局部作用域失效后就被清除。而是被保留下來。這是把雙刃劍,而它的缺點就是濫用閉包很容易造成“循環使用”以至於導致內存泄漏。
下面我們看一個特殊的例子:
function counter () { var n = 0; return { count (num) { n = n + num; return n }, reeset () { n = 0 } } } var a = counter(); var b = counter (); console.log(a.count(1)) // 1 console.log(b.count(2)) // 2
這個例子就說明每次調用counter()都會出現一個新的作用域鏈分支和一個新的私有變量n,兩個私有變量互不影響。
像上面的特殊閉包,可以使用對象的存取器屬性實現:
詳細了解Object的屬性,可見上一篇文章:http://www.cnblogs.com/pomelott/p/8082951.html
var obj = { n: 0, get count () { return this.n }, set count (val) { this.n = this.n + val; return this.n } } obj.count = 5; console.log(obj.n) // 5 console.log(obj.count) // 5
函數的其他可挖掘內容和技巧還很多,這期暫時先分享到這。后續請繼續關注。