最近對函數式編程產生了興趣,因而複習了下關於函數的相關知識點,一塊兒學習把~~javascript
函數是能夠經過外部代碼調用的一個「子程序」。前端
在 js 中,函數是一等公民(first-class),由於函數除了能夠擁有本身的屬性和方法,還能夠被看成程序同樣被調用。在 js 中,函數實際上就是對象,每一個函數都是 Function
構造函數的實例,所以函數名/變量名實際上也是一個指向函數對象的指針,一個變量名只能指向一個內存地址。也正因如此 js 中函數沒有重載,由於兩個同名函數,後面的函數會覆蓋前面的函數。vue
length
屬性表示函數預期接收的命名參數的個數,未定義參數不計算在內。java
name
屬性返回函數的名稱。若是有函數名,就返回函數名;若是沒有函數名,就返回被賦值的變量名或對象屬性名。es6
prototype
屬性是函數的原型對象,通常用來給實例添加公共屬性和方法,是保存它們實例方法的真正所在。chrome
function SuperType(name){
this.name = name
}
SuperType.prototype.sayName = function(){
alert(this.name);
}
let instance1 = new SuperType('幻靈兒')
let instance2 = new SuperType('夢靈兒')
instance1.sayName === instance2.sayName // true
SuperType.prototype.constructor === SuperType // true
複製代碼
兩個實例擁有公共方法sayName
。原型對象的constructor
指向構造函數自己。編程
es6 加入了元屬性new.target
,用來判斷函數是否經過 new
關鍵字調用。當函數是經過new
關鍵字調用的時候,new.target
的值爲該構造函數;當函數不是經過new
調用的時候,new.target
爲undefined
。數組
參數有形參(parameter)和實參(argument)的區別,形參至關於函數中定義的變量,實參是在運行時的函數調用時傳入的參數。瀏覽器
函數能夠經過函數聲明建立,也能夠經過函數表達式建立:閉包
// 函數聲明
function bar() {}
console.log(bar) // ƒ bar() {}
// 函數表達式
var foo = function bar() {}
console.log(bar) // Uncaught ReferenceError: bar is not defined
// 當即調用函數表達式(IIFE)
(function bar(){})()
console.log(bar) // Uncaught ReferenceError: bar is not defined
複製代碼
簡單來講,函數聲明是 function
處在聲明中的第一個單詞的函數,不然就是函數表達式。
函數表達式var foo = function bar() {}
中的foo
是函數的變量名,bar
是函數名。函數名和變量名存在着差異:函數名不能被改變,但變量名卻可以被從新賦值;函數名只能在函數體內使用,而變量名卻能在函數所在的做用域中使用。其實,函數聲明也是同時也建立了一個和函數名相同的變量名:(值得一提的是 es6 中的 class 表達式也是一樣的設計)
function bar () {}
var foo = bar
bar = 1
console.log(bar) // 1
console.log(foo) // ƒ bar () {}
複製代碼
能夠看出,bar
函數被賦值給變量foo
,就算給變量bar
從新賦值,foo
變量仍然是ƒ bar () {}
。因此,就算是函數聲明,咱們日常在函數外調用函數的時候也是使用變量名而不是函數名調用的。 平時絕對不要輕易修改函數聲明的變量名,不然會形成語義上的理解困難。
函數聲明和函數表達式的最重要的區別是,函數聲明存在函數提高,而函數表達式只存在變量提高(用var
聲明的有變量提高,let const
聲明的沒有變量提高)。函數提高會在引擎解析代碼的時候,把整個函數提高到代碼最頂層;變量提高只會把變量名提高到代碼最頂層,此時變量名的值爲undefined
;而且函數提高優先於變量提高,也就是說若是代碼中同時存在變量a
和函數a
,那麼變量a
會覆蓋函數a
:
var a = 1
function a (){}
console.log(a) // 1
複製代碼
js 中除了箭頭函數,全部的函數均可以做爲構造函數。但按照慣例,構造函數的首字母應該爲大寫字母。js 的Object Array Function Boolean String Number
都是構造函數。構造函數配合關鍵字new
能夠創造一個實例對象,如:let instance = new Object()
便創造了一個對象,let instance = new Function()
便建立了一個函數對象,let instance = new String()
便建立了一個字符串包裝對象等等等。構造函數除了用來生成一個對象,還能夠用來模擬繼承,有興趣看這篇文章。
es6 是對 js 的一次大升級,使得 js 的使用溫馨度大大提高。es6 對函數的擴展讓函數的使用體驗更加酸爽,其新功能以下:(本文中 es6 是指 ES2015 以後版本的統稱)
es6 中新增了箭頭函數,極大的提升了函數書寫溫馨度。
// es5 寫法
let f = function (v) { return v }
// es6 寫法
let f = (v) => { return v }
// 像這樣只有一個參數或代碼塊只有一條語句的,能夠省略括號或者大括號,此時箭頭後面的是函數返回值
let f= v => v
// 若是不須要返回值,能夠用 void 關鍵字
let f = (fn) => void fn()
// 若是沒有形參,則須要括號
let f = () => { console.log('個人參數去哪了') }
// 函數參數是對象的話,可使用變量的解構賦值
const full = function ({ first, last }){ return first + last }
full({first: '幻靈', last: '爾依'}) // 幻靈爾依
// 箭頭函數使用解構賦值更簡便,但此時參數必須用括號
const full = ({ first, last }) => first + last
full({first: '幻靈', last: '爾依'}) // 幻靈爾依
複製代碼
箭頭函數除了書寫簡便以外,還有以下特徵:
this、super、arguments
和 new.target
:箭頭函數內部的這些值直接取自 定義時的外圍非箭頭函數,且不可改變;this
值不受 call()、apply()、bind() 方法的影響:由於箭頭函數根本沒有本身的this
;this
,而構造函數須要有本身的this
指向實例對象,因此若是經過 new
關鍵字調用箭頭函數會拋錯Uncaught TypeError: arrowFunction is not a constructor
。又由於不能做爲構造函數,因此箭頭函數乾脆也沒有本身的prototype
屬性。即便咱們手動給箭頭函數添加了prototype
屬性,它也不能被用做構造函數;Generator
函數沒有本身的this
是箭頭函數最大的特色。由於這個特性,箭頭函數不宜用做對象的方法,由於點調用和call/bind/ayyly
綁定都沒法改變箭頭函數的this
:
let obj = {
arrow: () => { return this.god },
foo() { return this.god },
god: '幻靈爾依'
}
obj.foo() // '幻靈爾依'
obj.arrow() // undefined
obj.arrow.call(obj) // undefined
複製代碼
也正是由於這個特性,使得在vue
等框架中使用this
爽的暢快淋漓。由於這些框架通常都把vue
實例對象綁定在鉤子函數或methods
中函數的this
對象上,在這些函數中使用箭頭函數方便咱們在函數嵌套的時候直接使用this
而不用老套又沒有語法高亮的let _this = this
:
export default {
data() {
return {
name: '幻靈爾依'
}
},
created() {
console.log(this.name) // '幻靈爾依'
setTimeout(() => {
this.name = '好人卡'
console.log(this.name) // '好人卡'
setTimeout(() => {
this.name = '你媽叫你回家吃飯了'
console.log(this.name) // '你媽叫你回家吃飯了'
}, 1000)
}, 1000)
}
}
複製代碼
能夠看到,只要是箭頭函數,不管嵌套多深,this
永遠都是外圍非箭頭函數created
鉤子函數中的那個this
。
函數參數的默認值對於一些須要參數有默認值的函數很是方便:
// 當參數設置默認值,就算只有一個參數,也必須用括號
let f = (v = '幻靈爾依') => v
// 參數是最後一個參數的話能夠不填,此時使用默認值
f() // '幻靈爾依'
// 傳入 undefined 則使用默認值
f(undefined) // '幻靈爾依'
// 傳入 undefined 以外的值不會使用默認值
f(null) // null
複製代碼
默認值能夠和解構賦值一塊兒使用:
let f = ({ x, y = 1 }) => { console.log(x, y) }
f({}) // undefined 1
f({ x: 2, y: 2 }) // 2 2
f({ x: 1 }) // 1 1
// 此時必須傳入一個對象,不然會拋錯
f() // Uncaught TypeError: Cannot destructure property `x` of 'undefined' or 'null'.
// 也能夠再給對象一個默認參數
let f = ({ x = 1 , y = 1} = {}) => { console.log(x, y) }
// 此時調用能夠不傳參數,就至關於傳了個空對象
f() // 1 1
複製代碼
參數指定了默認值以後,函數的length
屬性將不計算該參數。若是設置了默認值的參數不是尾參數,那麼length
屬性也再也不計入後面的參數了。下文的 rest
參數也不會計入length
屬性。這是由於length屬性的含義是,該函數預期傳入的參數個數。
一旦設置了參數的默認值,函數進行聲明初始化時,參數會造成一個單獨的做用域。這就至關於使用了參數默認值以後,函數外面又包裹了一層參數用let
聲明的塊級做用域:
var x = 1
function foo(x = x) {
return x
}
foo() // Uncaught ReferenceError: Cannot access 'x' before initialization
// 其實上面代碼至關因而這樣的,因爲存在暫時性死區,`let x = x`會報錯
var x = 1
{
let x = x
function foo() {
return x
}
}
foo () // Uncaught ReferenceError: Cannot access 'x' before initialization
// 再看個例子
var x = 1
function foo(x, y = function() { x = 2; }) {
x = 3
y()
console.log(x)
}
foo() // 2
x // 1
// 上面代碼至關於
var x = 1
{
let x
let y = function() { x=2 }
function foo() {
x = 3
y()
console.log(x)
}
}
foo() // 2
x // 1
複製代碼
其實就把默認參數的括號想象成是let
聲明的塊級做用域就好了。
剩餘參數,顧名思義就是剩餘的參數的集合,因此剩餘參數後面不能再有參數。剩餘參數就是擴展運算符+變量名:
// 剩餘參數代替僞數組對象`arguments`
let f = function(...args) { return args } // 箭頭函數寫法更簡單 let f = (...arg) => arg
let arr = f(1, 2, 3, 4) // [1, 2, 3, 4]
// 還能夠用擴展運算符展開一個數組看成函數實參
f(...arr) // [1, 2, 3, 4]
複製代碼
尾調用(Tail Call)優化是指某個函數的最後一步是返回並調用另外一個函數,因此函數執行的最後一步必定要是return
一個函數調用:
function f(x){
return g(x)
}
複製代碼
函數調用會在執行棧建立一個「執行上下文」,函數中調用另外一個函數則會建立另外一個「執行上下文」並壓在棧頂,若是函數嵌套過多,執行棧中函數的執行上下文堆疊過多,內存得不到釋放,就可能會發生真正的stack overflow
。
可是若是一個函數調用是發生在當前函數中的最後一步,就不須要保留外層函數的執行上下文了,由於這時候要調用的函數的參數值已經肯定,再也不須要用到外層函數的內部變量了。尾調用優化就是當符合這個條件的時候刪除外曾函數的執行上下文,只保留內部調用函數的執行上下文。
尾調用優化對遞歸函數意義重大(後面會將介紹遞歸)。
ES2017 規定函數形參和實參結尾能夠有逗號,以前,函數形參和實參結尾都不能有逗號。
ES2019 規定Function.prototype.toString()
要返回如出一轍的原始代碼的字符串,以前返回的字符串會省略註釋和空格。
ES2019 規定catch
能夠省略參數,如今能夠這樣寫了:try{...}catch{...}
es6 還引入了 Promise
構造函數和 async
函數,使得異步操做變得更加方便。還引入了class
繼承,想了解的去看阮一峯ECMAScript 6 入門。
es6 就介紹到這,都是從阮一峯哪學的。
高階函數是指有如下特徵之一的函數:
js 內置了不少高階函數,像forEach map every some filter reduce find findIndex
等,都是把函數做爲參數傳遞,即回調函數:
[1, 2, 3, 4].map(v => v * 2) // [2, 4, 6, 8] 返回二倍數組
[1, 2, 3, 4].filter(v => !(v % 2)) // [2, 4] 返回偶數組成的數組
[1, 2, 3, 4].findIndex(v=> v === 3) // 2 返回第一次值爲3的項的下標
[1, 2, 3, 4].reduce((prev, cur) => prev + cur) // 10 返回數組各項之和
複製代碼
像經常使用的節流防抖函數,都是即以函數爲參數,又在函數中返回另外一個函數:
// 防抖
function _debounce (fn, wait = 250) {
let timer
return function (...agrs) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, wait)
}
}
// 節流
function _throttle (fn, wait = 250) {
let last = 0
return function (...args) {
let now = Date.now()
if (now - last > wait) {
last = now
fn.apply(this, args)
}
}
}
// 應用
button.onclick = _debounce (function () { ... })
input.keyup = _throttle (function () { ... })
複製代碼
節流和防抖函數都是在函數中返回另外一個函數,並利用閉包保存須要的變量,避免了污染外部做用域。
上面節流防抖函數用到了閉包。很長時間以來我對閉包都停留在「定義在一個函數內部的函數」這樣膚淺的理解上。事實上這只是閉包造成的必要條件之一。直到後來看了kyle大佬的《你不知道的javascript》上冊關於閉包的定義,我才豁然開朗:
當函數可以記住並訪問所在的詞法做用域時,就產生了閉包。
let single = (function(){
let count = 0
return {
plus(){
count++
return count
},
minus(){
count--
return count
}
}
})()
single.plus() // 1
single.minus() // 0
複製代碼
這是個單例模式,這個模式返回了一個對象並賦值給變量single
,變量single
中包含兩個函數plus
和minus
,而這兩個函數都用到了所在詞法做用域中的變量count
。正常狀況下count
和所在的執行上下文會在函數執行結束時被銷燬,可是因爲count
還在被外部環境使用,因此在函數執行結束時count
和所在的執行上下文不會被銷燬,這就產生了閉包。每次調用single.plus()
或者single.minus()
,就會對閉包中的count
變量進行修改,這兩個函數就保持住了對所在的詞法做用域的引用。
閉包實際上是一種特殊的函數,它能夠訪問函數內部的變量,還可讓這些變量的值始終保持在內存中,不會在函數調用後被垃圾回收機制清除。
看個經典安利:
// 方法1
for (var i = 1; i <= 5; i++) {
setTimeout(function() {
console.log(i)
}, 1000)
}
// 方法2
for (let i = 1; i <= 5; i++) {
setTimeout(function() {
console.log(i)
}, 1000)
}
複製代碼
方法1中,循環設置了五個定時器,一秒後定時器中回調函數將執行,打印變量i
的值。毋庸置疑,一秒以後i
已經遞增到了5,因此定時器打印了五次5 。(定時器中並無找到當前做用域的變量i
,因此沿做用域鏈找到了全局做用域中的i
)
方法2中,因爲es6的let
會建立局部做用域,因此循環設置了五個做用域,而五個做用域中的變量i
分佈是1-5,每一個做用域中又設置了一個定時器,打印一秒後變量i
的值。一秒後,定時器從各自父做用域中分別找到的變量i
是1-5 。這是個利用閉包解決循環中變量發生異常的新方法。
閉包是一些經常使用的工具函數的常客,柯里化/組合函數/節流防抖函數等都使用了閉包。
遞歸就是在函數中調用自身:
function factorial(n) {
if (n === 1) return 1
return n * factorial(n - 1)
}
複製代碼
上面就是一個遞歸實現的階乘,因爲返回值中還有n
,因此外層函數的執行環境理論上不能被銷燬。可是 chrome 瀏覽器如此強大,factorial(10000)
並無爆棧。不過看到瀏覽器幾千個調用棧也是嚇了一跳:
上文中介紹了尾調用優化:指某個函數的最後一步是返回並調用另外一個函數。在遞歸中使用尾調用優化成爲尾遞歸。上面階乘函數改寫爲尾遞歸以下:
function factorial(n, total = 1) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
複製代碼
這樣就符合尾調用優化的規則了,理論上如今應該只有一個調用棧了。然而通過測試,chrome 瀏覽器(版本 76.0.3809.100)目前並不支持尾調用優化(目前好像尚未瀏覽器支持):
反而由於執行上下文中存的變量多了個total
,執行factorial(10000)
會爆棧。
組合(compose)函數會接收若干個函數做爲參數,每一個參數函數執行後的輸出做爲下一個函數的輸入,直至最後一個函數的輸出做爲最終的結果。實現效果以下:
function compose(...fns){ ... }
compose(f,g)(x) // 至關於 f(g(x))
compose(f,g,m)(x) // 至關於 f(g(m(x))
compose(f,g,m)(x) // 至關於 f(g(m(x))
compose(f,g,m,n)(x) // 至關於 f(g(m(n(x))
···
複製代碼
組合函數的實現很簡單:
function compose (...fns) {
return function (...args) {
return fns.reduceRight((arg , fn, index) => {
if (index === fns.length - 1) {
return fn(...arg)
}
return fn(arg)
}, args)
}
}
複製代碼
注意reduceRight
第三個參數index
也是倒序的。
開閉原則:軟件中的對象(類,模塊,函數等等)應該對於擴展是開放的,可是對於修改是封閉的。
開閉原則是咱們編程中的基本原則之一,咱們前端近些年發展的組件化 模塊化 顆粒化
的也暗合了開閉原則。基於這個原則,利用組合函數可以幫咱們實現適用性更強、更易擴展的代碼。
假如咱們有個應用要作各類字符串處理,爲了方便調用,咱們能夠將一些字符串要用到的方法封裝成純函數:
function toUpperCase(str) {
return str.toUpperCase()
}
function split(str){
return str.split('');
}
function reverse(arr){
return arr.reverse();
}
function join(arr){
return arr.join('');
}
function wrap(...args){
return args.join('\r\n')
}
複製代碼
若是咱們要將一個字符串let str = 'emosewa si nijeuj'
轉化成大寫,而後逆序,能夠這樣寫:join(reverse(split(toUpperCase(str))))
。而後咱們又要轉換另外一個字符串let str2 = 'dlrow olleh'
,又得寫:join(reverse(split(toUpperCase(str2))))
這樣一長串。如今有了組合函數,咱們能夠簡單寫:
let turnStr = compose(join, reverse, split, toUpperCase)
turnStr(str) // JUEJIN IS AWESOME
turnStr(str2) // HELLO WORLD
// 還能夠傳多個參數,見 turnStr2
let turnStr2 = compose(join, reverse, split, toUpperCase, wrap)
turnStr2(str, str2) // HELLO WORLD JUEJIN IS AWESOME
複製代碼
還有一種管道函數從左至右處理數據流,即把組合函數的參數倒着傳,感受上比較符合傳參的邏輯,可是從右向左執行更加可以反映數學上的含義。因此更推薦組合函數,就不介紹管道了,避免你的選擇困難症。
柯里化,是把多參函數轉換爲一系列單參函數的技術。具體實現就是柯里化函數會接收若干參數,而後不會當即求值,而是繼續返回一個新函數,將傳入的參數經過閉包的形式保存,等到被真正求值的時候,再一次性把全部傳入的參數進行求值。
關於柯里化,這裏有篇深度好文,我寫不出來的那種:JavaScript 專題之函數柯里化。
這裏是柯里化的一種實現方式:
function sub_curry(fn) {
var args = [].slice.call(arguments, 1);
return function() {
return fn.apply(this, args.concat([].slice.call(arguments)));
};
}
function curry(fn, length) {
length = length || fn.length;
var slice = Array.prototype.slice;
return function() {
if (arguments.length < length) {
var combined = [fn].concat(slice.call(arguments));
return curry(sub_curry.apply(this, combined), length - arguments.length);
} else {
return fn.apply(this, arguments);
}
};
}
var fn = curry(function(a, b, c) {
return [a, b, c];
});
fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]
複製代碼
這段看起來比較困難,建議代碼複製到瀏覽器中加斷點調試下。
經常使用的工具函數就介紹到這裏,下篇函數式編程主食敬請期待~