JavaScript基礎(6)—— 花裏胡哨的函數

  在正式開始總結JavaScript權威指南第八章——函數之前,先來談談在實際應用中函數是做什麼的。

  通常情況下,我們會將一些常用的工具(如數學方法,數據格式處理,接口處理工具)封裝成一段可重複執行的代碼,這種只定義一次就可以通過函數名被多次執行的代碼就叫做函數。說白了,我們使用函數就是爲了少寫幾行重複代碼。

  FBI warning:本文要介紹的函數跟複用沒有半毛錢關係,如果追求實用主義,完全可以跳過本章內容!

  下面開始正式介紹花裏胡哨的函數技巧,這些技巧大致包含以下內容:

  1.嵌套函數和聽起來很深奧的閉包

  2.三種修改函數上下文(this)的調用(apply,call,bind)

  3.兩種看起來很人性化的鏈式調用和函數柯里化

  4.函數和跟他一點都不像的對象類型之間的愛恨瓜葛

  5.聽起來很牛逼其實就是函數處理函數的高階函數

  6.聽起來很奇怪其實跟函數柯里化差不多的不完全函數

  7.可能有一丟丟軟用的函數記憶

  文章實際內容安排跟書本順序有較大不同,看不懂的建議看原著,你會更看不懂。

  

1.一個函數的基本信息

  根據個人整理,一個函數至少包含以下信息:函數體,參數信息(arguments),length屬性(函數實參數量),上下文(this),函數值(返回值,如果沒有返回值默認是undefined),call()和apply()方法以及從Function.prototype繼承的原型。

  上面提到的函數基本信息裏,常用的只有函數體和實參信息,其他信息我們平時基本都用不到,但如果你想要寫出一個好用且健壯的工具庫,這些方法就變得十分重要了。在本章內容中,我會多次用到上面這些函數的基本屬性和方法,有些東西看似雞肋,但在某些情況下會變得十分有用,因此除了要了解函數的這些基本屬性,最重要的還是要靈活運用!一本正經的抄書從來不是這個系列的目的,我希望各位讀者在看完本章後,會對函數有一個全新的認識(包括我自己)。下面開始一本正經的胡說八道。

2.閉包的不詳細說明

  閉包是JavaScript面試題中永恆的主題,可見閉包在實踐中是多麼糟粕的一種存在,當然我今天要說的不是實踐,這個系列也不是服務於實踐,但是關於閉包,我只談個人看法(理解有明顯錯誤可以評論指出)。

  用一個詞解釋“閉包”:就是局部作用域!在塊級作用域出來之前,JavaScript一直使用函數作用域。當我們使用函數時,就會產生一個“封閉”在函數中的局部作用域,因此所有的函數都可以稱之爲閉包,在塊級作用域出來之後,你也可以認爲下面的代碼存在“閉包”:

if(true){
  const b = 0 // 解析到const,使用塊級作用域,也就是大括號作用域,所以if語法內產生了閉包
  console.log(b)
}

  因此,“閉包”在廣義上並不是一個很難理解的東西,只要產生了局部作用域,就可以稱之爲閉包。但廣義的閉包顯然不是各位想要了解的內容,各位想要了解的閉包,稱之爲——嵌套函數。

  然而嵌套函數只是函數嵌套函數的書本名詞,大部分情況下他依舊不能滿足各位對於閉包的臆想,各位想要了解的閉包,是嵌套函數的一種特殊情況——函數內局部變量的“保存”,下面舉個最簡單例子,來詳解一下各位想要了解的閉包

function increment(){
	let count = 0
	return function(){
		return count++
	}
}
let incrementNum = increment()
incrementNum() //0
incrementNum() //1

  在上例中,函數聲明內部的變量被“保存”在函數作用域內,這個變量沒有被垃圾回收機制自動回收,對這個環節感興趣的可以去了解下JavaScript函數的辣雞回收機制,本例中只簡單解釋下爲什麼count局部變量可以駐紮在內存中:

  1.因爲increment()函數返回一個匿名函數,這個函數被全局變量incrementNum引用,因此這個匿名函數沒有被回收。

  2.由於匿名函數內部使用的變量指向increment()所創建的局部作用域對象中的count屬性,因此count也被強制沒法回收了。

  說來說去可能把某些初學者搞得雲裏霧裏了,想要真正的瞭解閉包,只需要記住一句話就夠了:

  函數的作用域只跟函數定義時的作用域有關!

  要理解這段話,可以看下面的例子:

var scope = 'world'
function a(){
	console.log(scope) // undefined
	var scope = 'hello' // 注意這裏不要用let,嚴格模式下變量不能提升,會導致報錯
	return function(){
		console.log(scope)
	}
}
a()() //hello

 在上例中,定義了兩個函數,一個是嵌套函數a,另一個是匿名函數,嵌套在a函數中。

 a函數定義的時候,他的作用域對象是這樣子的:

//a函數的作用域鏈對象
a的爸爸全局作用域:{
  scope:'world',
  ...
  a自己的作用域:{
    scope:'hello'
    ...
    a的兒子匿名函數的作用域:{
        空空如也
    }
  }
}

  遵循優先找自己作用域內的屬性,其次找父級作用域的屬性,直到全局作用域也找不到返回undefined的套路,我們可以得出以下結論,a函數中打印scope的時候,由於變量提升,因此a函數判斷自己的作用域內存在scope變量,不需要尋求父級幫助,但在打印scope的時候,scope還沒被賦值,因此打印undefined。匿名函數在自己的作用域內找不到scope變量,因此尋求父級a的幫助,找到後發現scope='hello',因此匿名函數打印'hello',而不是全局變量'world'。細細體會這兩個例子,就可以完全瞭解閉包了。

3.call(),apply() 和bind()

  在百度搜索call(),apply()和bind()的區別,類似的文章有一大堆,這裏我囉嗦兩句,給自己做個筆記。

  call(),apply()和bind()都可以看作是函數對象的方法,我們可以通過調用函數對象的call(),apply()方法來實現函數的調用。call和apply的第一個參數會替代函數的原始上下文this,注意this是函數內部的關鍵字,  而不是一個普通變量。下面通過一個簡單例子來了解下call和apply。

function a(){
  console.log(this.x)
}
a()//undefined this指向window
a.call({x:1}) //1
a.apply({x:1}) //1

 從上例中可以看出,call和apply都可以修改函數的上下文,兩者唯一的區別就在於函數傳參的形式不同,如下所示

function a(x,y){
  console.log(x,y,this.z)
}
a.call({z:3},1,2) //1,2,3
a.apply({z:3},[1,2])//1,2,3

  現在你可以使用對象展開符...[1,2]來實現兩者的轉換。

  bind()方法也可以修改函數上下文,但是bind方法返回一個新的函數,而不是直接調用函數,如下所示。

function a(x,y){
  console.log(x,y,this.z)
}
var newFn = a.bind({z:3})
newFn(1,2) //1,2,3

4.一句話搞定鏈式調用

  鏈式調用時函數式編程的技巧之一,其核心代碼卻只有一句話,就是return this。鏈式調用往往注重過程而不注重結果,類似於juery就是借鑑了這種技巧。(我不是說jquery的所有方法都是return this)我們可以手寫一個最簡單的鏈式調用,如下:

let myMath = {
	x:1,
	y:1,
	addX: function(){
		this.x++
		return this
	},
	addY:function(){
		this.y++
		return this
	}
}
let a = myMath.addX().addY().addX().addY().addX()
console.log(a.x,a.y) //4 3

5.函數柯里化是什麼?

  函數柯里化這個名詞對於沒怎麼經歷過筆試和書本的人來說,是個非常陌生的詞。

  在我的文章裏,你永遠不用去關注某個專業術語的名稱,你只需要知道,函數還可以這樣“優化”就可以了。

  那麼函數柯里化究竟做了一件什麼樣的事情呢?舉例說明:

  我們需要一個函數,它可以傳遞任意個參數,且函數可以被一直調用,直至我們不需要繼續調用函數爲止。

  比如我們需要知道一輛出租車一天的公里數,我們可以將一天分爲白天和黑夜,白天統計一次里程數,晚上也統計一次里程數,函數調用如下

  sum(白天的里程數)(晚上的里程數)

  我們也可以把一天分爲24小時,函數調用如下

 sum(0:00)(1:00)(2:00) ....... (23:00)(24:00)

  我們還可以把上午分爲8小時,中午分爲8小時,晚上分爲8小時,調用如下

  sum(0:00,1:00,2:00...)(9:00,10:00,....)(17:00,18:00,...,24:00)

  可以看到,sum函數的參數個數和調用次數都是未知的,因此我們需要解決兩個問題,如何解析參數以及如何多次調用函數。第一個問題很好解決,在函數的基本信息裏包含arguments(參數信息),因此我們的sum函數不需要規定形參的個數,可以直接通過arguments獲取參數。第二個問題是如何讓sum函數一直調用本身,答案也很簡單,利用遞歸的思想即可實現,這個問題的關鍵就是遞歸結束條件是什麼,也就是,我們如何得知本次調用是最後一次調用?答案是:沒辦法!

  那咋辦呢?自己看代碼,自己百度,我也不知道toString()方法爲什麼能作爲遞歸結束的條件,百度上就是這麼寫的。當然我們也可以通過非常樸素的方法:if(arguments.length===0)來作爲結束條件,只是這樣你需要多調用一次函數,如下:

  sum(1,2,3)(4,5)(5,5)(),結果和下面的代碼時相同的,只是下面的調用方式看起來更加“優美”一些。

function sum(){
	let args = Array.from(arguments)
	function add(){
		args = args.concat(Array.from(arguments))
		return add
	}
	add.toString = function(){
		return args.reduce((total,current)=>{
			return total + current
		})
	}
	return add
}
let sums = sum(1,2,3)(4,5)(5,5) // 25

  其實這裏還用到了一些閉包的技巧,add()函數使用了sum()的局部變量,並且在不斷地遞歸中,這個變量可以累積得到所有參數的值,直到最後一次調用時,把所有參數進行累加。

6.函數也是對象,所以我們要利用起來

  這一小節的關鍵字就5個字:函數是對象!

  至於怎麼使用,我會在記憶函數中提到。

 7.高階函數

  所謂高階函數,就是操作函數的函數,它接收一個或多個函數作爲參數,返回一個新的函數。依舊以加減乘除爲例:

function plus(a,b){
	return a+b
}
function reduce(a,b){
	return a-b
}
function mix(plus,reduce){
	return function(a,b,c,d){
		return plus(a,b)*reduce(c,d)
	}
}
let multiply = mix(plus,reduce)
console.log(multiply(1,2,6,4)) //6

 

8.利用函數的對象屬性實現記憶

  在講函數閉包的時候我們提到了“函數內變量駐紮”的概念,

  說到保存一個變量在函數內,其實有兩種方法,但這兩種方法是有區別的:

  第一種就是閉包

  第二種就是利用函數的對象屬性

  函數作爲對象,本身就可以存儲屬性,比如

function fn(){}
fn.x = 0 

  因此當我們需要一個一直被記憶的變量時,就可以把它存儲在函數屬性中,在階乘函數裏就可以用到這個特性。閉包和記憶的不同在於,閉包函數在每次調用時都會創建一個新的變量,而記憶的變量是函數的私有屬性,不會被“重置”。

 

 

  函數部分就講到這,其實還有很多沒用的東西沒講,包括我講的,大部分也沒什麼用,以後寫框架的時候用到再體會和細說吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章