JavaScript是一門函數式的面向對象編程語言。瞭解函數將會是瞭解對象建立和操做、原型及原型方法、模塊化編程等的重要基礎。函數包含一組語句,它的主要功能是代碼複用、隱藏信息和組合調用。咱們編程就是把一個需求拆分紅若干函數和數據結構的組合實現,其中算法又是實現正確函數的方法論。咱們先介紹基礎知識:① 在JavaScript中,函數對象背後到底有什麼;② 函數調用的模式有多少種;③ 做用域與閉包。至於遞歸、記憶、回調、級聯、模塊、柯里化等,咱們放到進階知識中再涉獵。html
1、 函數對象程序員
前面咱們提到,在JavaScript中,函數也是對象,通常對象的原型鏈接到Object.prototype,函數對象則鏈接到Function.prototype,再鏈接到Object.prototype。咱們能夠看看這兩個對象中具備什麼樣的屬性:web
1 var sum = function(a, b) { 2 return a + b; 3 } 4 5 console.log(sum.prototype);
輸出發現,這個Function.prototype中有一個constructor構造器屬性,值就是剛纔咱們定義的這個函數的內容。而Function.prototype則鏈接到Object.prototype。也就是說當咱們建立一個函數對象時,Function的構造器會自動運行相似這樣的一些代碼: this.prototype = {constructor: this}; 。換句話說,其實constructor屬性的意義不大,由於它本身自己就是這個屬性。只是由於JavaScript爲了模仿其餘的面嚮對象語言,作出了這樣一個「僞類」,以constructor做爲中間層而已。算法
2、函數調用編程
函數的特別之處在於:它能夠被調用。調用函數時,操做系統會暫停當前函數的執行,把控制權和參數傳遞給調用的函數。函數除了收到給出的形式參數,還會接收兩個新參數:this 和 arguments。咱們在面向對象的編程中最須要注意的就是須要用到的方法裏面this的值究竟是什麼。實際上,this的值取決於函數調用的模式。在JavaScript中,函數調用的模式一共有4種,分別是:方法調用模式、函數調用模式、構造器調用模式和apply調用模式:數組
1. 方法調用模式瀏覽器
對象中的函數咱們稱爲方法。此時,該函數中this的值爲直接所屬的對象。數據結構
注:但因爲設計失誤,若是將對象中的方法存入一個變量中,再調用這個變量,這一個this又將指向全局變量:閉包
1 var obj = { 2 property: 'hello', 3 4 method: function() { 5 console.log(this.property); 6 } 7 }; 8 9 var func = obj.method; 10 func();
像上述這種狀況,最終會輸出undefined,由於當你將方法賦值給func變量時,func中就只有這個代碼段的空殼而已,調用它就像調用一個普通的函數,this指向的是全局變量。只有單獨調用 obj.method(); 纔不會出現上述狀況。app
爲此,ECMA後來提出了一個解決方案:在一整段JavaScript代碼的開頭一行 'use strict'; ,讓瀏覽器執行嚴格模式排除錯誤。但當你看到前面這句話時,就要立馬反應過來我所使用的瀏覽器的版本是否能夠執行嚴格模式了(假如你的瀏覽器版本不支持嚴格模式,那這一行代碼只會被當成普通的字符串執行,不會有什麼結果)。若是在上述的代碼放入嚴格模式下執行,this會被JavaScript定向爲undefined,接着拋出一個TypeError錯誤。
2. 函數調用模式
當函數不是對象的屬性,也就是在通常狀況下,咱們像上面舉例的代碼同樣直接聲明的一個函數。此時,該函數中this的值指向全局對象。這種調用方法是最簡單直接的方法:
1 var sub = function(a, b) { 2 return a - b; 3 }
但須要注意的是,因爲語言設計的失誤,一個函數的內部函數this的值,本應該爲這個函數this的值,而真實狀況是它卻指向了全局對象,所以,咱們須要更機智地提供一種解決方法:
1 var motherLyn = { 2 generation: 'mother', 3 name: 'Lyn', 4 getFullName: function() { 5 var that = this; 6 7 var getGeneration = function() { 8 return that.generation; 9 } 10 11 var getName = function() { 12 return that.name; 13 } 14 15 return getGeneration() + getName(); 16 } 17 }
若是咱們缺乏了第五行的代碼,因爲getGeneration和getName兩個函數處於getFullName函數的內部,它們的this會指向window對象(全局對象),而window對象中沒有generation和name屬性,將會返回undefined。而在getFullName函數中聲明一個that變量並讓它指向this,避免了在內部函數中使用this,才能讓代碼向咱們指望的方向運行。
還有一種解決方案,就是使用ES6中的箭頭函數:
1 var motherLyn = { 2 generation: 'mother', 3 name: 'Lyn', 4 getFullName: function() { 5 var getGeneration = () => { 6 return this.generation; 7 }; 8 var getName = () => { 9 return this.name; 10 } 11 12 return getGeneration() + getName(); 13 } 14 }
ES6中,箭頭函數能夠取代內部函數調用,它的出現正是爲了修正內部函數this指引不正確的問題。
3. 構造器調用模式
使用這種方式時,咱們務必要把函數名字的首字母大寫,以與函數調用方式區分開來,每當看到首字母大寫的函數就會本能地加上new關鍵字。這也是你們的一種約定,使咱們不會由於疏忽而調用時忘記添加new關鍵字,增長測試工做:
1 var MotherLyn = function(generation, name) { 2 this.generation = generation; 3 this.name = name; 4 }; 5 6 var person = new MotherLyn("mother", "Lyn");
這時候this仍然指向全局變量,只有使用new關鍵字時this纔會指向函數對象自己,JavaScript也會提示此構造函數可能會轉換爲類聲明。在能夠不使用new的狀況下,咱們能夠儘可能不使用這種形式的構造器,由於當發生錯誤時,既沒有編譯時警告,也沒有運行時警告。
在之後關於對象建立的講解中咱們將看到多種建立對象的方式,也包括徹底不使用new的建立方法,咱們須要結合不一樣狀況使用。
在之後關於原型的講解中咱們會看到一個對象的實例、它的構造器和它的原型三者之間的關係,這很是重要。
4. Apply調用模式
前面提到,函數本質上就是對象,所以函數是能夠具備方法的。例如使用Function.apply方法,咱們能夠重定義某個方法內this的值,以數組的形式傳遞指望傳入的參數。這樣哪怕一個對象沒有繼承另外一個對象,也可使用它裏面的方法:
1 var myArray = [5, 6]; 2 var addArray = sum.apply(null, myArray); //調用到文章首部的sum函數,結果值爲11 3 4 var myObj = { 5 generation: 'my', 6 name: 'Obj' 7 }; 8 var getObjName = motherLyn.getFullName.apply(myObj); 9 //調用到上面的motherLyn對象中的getFullName方法,結果輸出myObj
咱們發現,哪怕上述的myObj並無繼承自motherLyn對象,它仍然能經過apply方法,重定義this的值,重用其中的方法。
注:上面咱們用到了apply方法以數組的形式重定義了arguments參數,但實際上因爲語言設計的失誤,arguments參數並非一個數組,而是一個array-like對象。也就是說,它除了有一個length屬性之外,沒有Array.prototype中的像concat這樣的其餘方法。
3、 做用域與閉包
1. 做用域
在編程語言中,做用域控制變量的可見性、生命週期、名稱衝突和內存管理,對於程序員來講是一項重要的服務。儘管像其餘類C語法的語言同樣,JavaScript也擁有函數做用域,但是直到ES5標準卻一直沒有塊級做用域。這一點也是設計上比較糟糕的地方:
1 for(var i = 0; i < 5; i++) { 2 console.log(i); 3 }; 4 5 console.log(i);
像以上的代碼會輸出從0到5的六個i,緣由是由於JavaScript缺乏塊級做用域,i的確從for語句中被泄露出來了。爲此在ES6標準中let和const兩種聲明變量的方式被提出了(固然這兩個關鍵字還會解決不少其餘問題),這一點咱們會放到後續的進階知識中講解到。像以上這個代碼使用let取代var保證了i不會被泄露,並且i不會被聲明爲window對象的屬性,有效避免了污染全局對象的問題。
在不少現代語言中,咱們更加推薦延遲聲明變量。但在JavaScript中,因爲缺乏塊級做用域,儘管使用var聲明變量還會獲得變量提高(先使用再聲明也是能夠的),但延遲聲明變量可能會編寫出混亂的難以維護的代碼。所以咱們仍是要在函數體的頂部將全部須要使用到的變量所有聲明出來。
2. 閉包
所謂閉包,就是能夠訪問被它被建立時所處的上下文環境的函數。閉包支持了JavaScript實現更靈活更有邏輯性的表達方式,先前咱們提到這麼屢次「因爲設計失誤」,如今咱們終於能夠誇獎一次「設計很是精彩」了。閉包最多見的用法就是返回一個函數:
1 var getMe = function() { 2 var name = 'MotherLyn'; 3 var displayName = function() { 4 console.log(name); 5 } 6 return displayName; 7 }
上述例子中的displayName函數就是典型的一個閉包,它能夠得到它被建立時上下文環境(也就是getMe函數)中的變量,這些變量將持續地保留直至內部函數再也不須要使用(固然這必定程度上也會影響性能)。當咱們須要調用這個displayName函數,咱們這樣來寫:
1 var me = getMe(); 2 me();
第一句調用到了getMe函數,將它的返回值給到了內部函數,並賦值給了me,此時name值已經肯定好了。最後調用到me函數來調用displayName函數。觀察以上的函數,或許咱們能夠考慮下閉包的做用:
① 在DOM操做中,咱們的代碼一般是做爲用戶行爲的回調函數執行,也就是說爲了響應用戶的某些行爲而存在。所以在編寫可複用的web代碼時,閉包具備重要意義:
1 <a href="#" id="red">Red</a> 2 <a href="#" id="green">Green</a> 3 <a href="#" id="blue">Blue</a>
1 function changeColor(color) { 2 return function() { 3 document.body.style.backgroundColor = color; 4 } 5 } 6 7 var change2Red = changeColor('red'); 8 var change2Green = changeColor('green'); 9 var change2Blue = changeColor('blue'); 10 11 document.getElementById('red').onclick = change2Red; 12 document.getElementById('green').onclick = change2Green; 13 document.getElementById('blue').onclick = chage2Blue ;
上面的代碼跟面向對象的代碼有點相似,它先創建了一個改換背景顏色的模板函數,經過賦不一樣的值建立不一樣的函數對象進行應用。
② 數據隱藏和封裝(這也是模塊化編程的基礎):
1 var motherLyn = function(generation, name) { 2 var myGeneration = generation; 3 var myName = name; 4 var str = ''; 5 return { 6 getFullName: function() { 7 return str + myGeneration + myName; 8 } 9 } 10 } 11 12 var me = motherLyn('mother', 'Lyn'); 13 14 console.log(me.getFullName())
在這個例子中,motherLyn做爲一個構造函數,返回一個對象,咱們不能使用new關鍵字建立實例,所以函數名咱們採用了小寫開頭。創造了一個me實例之後,沒法直接訪問myGeneration、myName和str三個變量,只能獲得getFullName函數的返回值,這樣的封裝效果就很是強了。因爲JavaScript中內部函數的生命週期比它的外部函數要長,咱們利用這一點模仿了面向對象的私有對象。
③ 設計失誤(for循環閉包詳解)
JavaScript中有一個很是常見的關於閉包的設計失誤,當咱們在for循環中加入一層閉包,將會出現意外的結果:
1 <p>1</p> 2 <p>2</p> 3 <p>3</p> 4 <p>4</p>
1 var pList = document.querySelectorAll('p'); 2 3 for(var i = 0; i < pList.length; i++) { 4 pList[i].onclick = function() { 5 console.log(i); 6 } 7 }
觀察代碼,咱們獲取到HTML中的全部四個p結點,做爲一個結點數組。咱們指望循環這個數組,讓其每個結點被點擊時輸出它在數組中的位置。但不幸的是,結果是每一次都輸出for循環結束之後i的值。也就是在以上的例子中會永遠輸出4。緣由是內部函數實際上訪問外部函數的實際變量而非它的複製,對於這個問題,咱們有許多種解決方案,最簡單的莫過於:
1 for(let i = 0; i < pList.length; i++) { 2 pList[i].onclick = function() { 3 console.log(i); 4 } 5 }
使用ES6標準中的let取代var,使i的做用域變爲塊級做用域,這樣閉包中訪問到的i也被修正爲循環過程當中的i。咱們也能夠建立一個輔助函數,讓這個輔助函數返回綁定了當前i值的函數:
1 var helper = function(i) { 2 return function() { 3 console.log(i); 4 } 5 } 6 7 for(var i = 0; i < pList.length; i++) { 8 pList[i].onclick = helper(i); 9 }
固然還有其餘的方法,咱們比較不推薦的是將i綁定到循環當前的對象中做爲一個屬性存在,這樣會污染當前的對象。
4、 箭頭函數
在常人的理解裏,在內部函數中,this的指向應該是跟隨它的外部函數的。在講述函數調用模式時咱們發現了這個問題,因爲JavaScript的設計失誤,內部函數的this竟然指向全局對象。咱們只有使用這種hack寫法,聲明一個that變量指向跟this指向同樣的內容來做爲修正:
1 var obj = { 2 name: 'an object', 3 4 oldExpression: function() { 5 var that = this; 6 var intervalFunction = function() { 7 return that.name; 8 } 9 } 10 }
爲了修正這一問題,箭頭函數被提出,用以取代普通的內部函數寫法了:
1 var obj = { 2 name: 'an object', 3 4 newExpression: function() { 5 var intervalFunction = () => { 6 return this.name; 7 } 8 } 9 }
箭頭函數的語法爲: () => {code} ,括號內填入參數,大括號內填入內部函數的全部內容,內部的this被修正爲外部函數的this。常見的用法例如:
1 let array = [5, 2, 3, 8, 1, 6, 4]; 2 3 // 從小到大排序 4 array.sort((x, y) => {return x - y;});
總結:1. 在JavaScript中函數也是一個對象,它的原型被鏈接到Function.prototype,再鏈接到Object.prototype;
2. 函數有四種調用模式,分別是:① 方法調用模式, ② 函數調用模式, ③ 構造器調用模式, ④ Apply調用模式;
3. 爲了合理運用函數,咱們須要掌握兩個要點,分別是① 做用域(ES5中只有函數做用域沒有塊做用域),② 閉包(尤爲是for循環閉包的解決方案例子)。
4. 箭頭函數的使用。
原文出處:https://www.cnblogs.com/BlogOfMotherLyn/p/11266530.html