對函數拓展興趣更大一點,優先看,前面字符串后面再說,那些API居多,會使用能記住部分就好。
一、函數參數可以使用默認值
1.默認值生效條件
在變量的解構賦值就提到了,函數參數可以使用默認值了。正常我們給默認值是這樣的:
//ES5 function log(x, y) { y = y || "echo"; console.log(x, y); }; log('hello')//hello echo
如果y未賦值則為假,那就取后面的默認賦值,很巧妙,但是有個問題,假設我y就是想傳遞一個false或者一個null,結果會被當假處理,還是執行默認賦值。
function log(x, y) { y = y || "echo"; console.log(x, y); }; log('hello','')//hello echo log('hello',false)//hello echo log('hello',null)//hello echo log('hello',0)//hello echo
很明顯這就不是我們想要的了,我就是想用數字0,就是想用null,結果就是賦值不上去了。怎么解決呢?這里就可以用參數默認賦值了。像這樣:
//ES6 function log(x, y = "echo") { console.log(x, y); }; log("hello", 0);//hello 0 log("hello", null);//hello null log("hello", false);//hello false log("hello", '');//hello
原理就是,只要調用提供的參數不嚴格等於undefined,那就用調用傳遞的參數,否則才考慮使用默認值。
這里數字0,null,false都不嚴格等於undefined,所以起到了作用。
2.參數默認值與結構賦值的結合使用
函數不僅可以直接給參數默認值,還能結合解構賦值的玩法,來看下面的例子:
function foo({ x, y = 5 }) { console.log(x, y); } foo({});//undefined 5 foo();//報錯
為什么foo()報錯了?這是因為上述代碼中,{x,y=5}這一段是結構賦值的默認值,並不是函數形參的默認值,函數foo都沒聲明xy,上哪給你輸出xy去。
foo({})之所以輸出正常,這是因為這種調用等同於以下代碼:
function foo({ x, y = 5 } = {}) { console.log(x, y); } foo(); //undefined 5 foo({}); //undefined 5
這樣寫,直接調用就隨便你傳不傳參數了,所以上面之所以輸出undefined與5是因為x在解構賦值時沒找到對應值,但是y由於解構賦值中傳遞的值嚴格等於undefined,所以默認值生效,這里輸出了5。不理解建議重看解構賦值,應該不難理解....
這里我們一共說了兩個默認值了,解構賦值的默認值,函數形參的默認值,混着說容易糊塗,來看一個有趣的例子:
//默認值給解構賦值 function foo({ x = 1, y = 5 } = {}) { console.log(x, y); } foo(); //1 5 foo({}); //1 5 //默認值給函數形參 function foo1({ x, y } = { x: 1, y: 5 }) { console.log(x, y); } foo1(); //1,5 foo1({}); //undefined undefined
我們分別把默認值給了解構賦值與函數形參,結果兩者在相同調用情況下,還是存在差異。
解構賦值2次都是輸出1,5,理由很簡單,兩次傳遞的參數都相同於undefined,解構賦值默認值始終生效。
而默認值給函數形參,當foo1()調用時,什么都沒傳,解構賦值將1與5賦予給xy;
而foo1({})調用其實存在2次賦值,第一次是函數形參賦值,傳遞了一個空對象,直接將解構賦值右邊替換了。
//step1 function foo1({ x, y } = {}) { console.log(x, y); }; foo1();
第二次就是解構賦值,猶豫xy又沒賦值,又沒有默認值,所以都輸出undefined了。
3.參數默認值建議放在參數尾部
這個建議是考慮到參數簡寫的問題,如果默認值放在參數末尾,調用傳參時可以省略,否則省略了會報錯,舉個例子:
function demo(x = 1, y) { console.log(x, y); } demo(,1)//報錯
但是放在尾部就隨你了,愛傳不傳,不傳當undefined處理,正好默認值生效。
function demo(y, x = 1) { console.log(x, y); } demo(1); //1,1
4.默認值會影響函數的length屬性
我們都知道,函數的length屬性會訪問形參的個數。
console.log(function(a, b, c) {}.length); //3
但是如果形參使用了默認值,length就會受到影響。
console.log(function(a, b, c = 1) {}.length); //2
你以為是有了默認值的不計算在length中了,那你就中招了,當默認值形參是第一個時:
console.log(function(a = 1, b, c) {}.length); //0
讓我們重新理解length,當形參存在默認值時,length屬性會統計函數預期傳入的參數個數(沒默認值的參數),畢竟參數如果默認值都有了,還預期個球;其次,它不統計默認值之后的形參個數。所以上面默認值給了第一個形參,直接length為0了,這對於如果程序用了默認值,又要訪問length的格外需要注意。
5.默認值會創建額外的作用域
如果函數形參使用了默認值,函數在聲明初始化時,參數區域會形成一個看不見的,額外的作用域。不設置默認值不會出現這個作用域。我讀到這句話以為只有函數聲明加默認值才有作用域的問題,其實函數表達式也有這種情況。
var x = 1; function f(x, y = x) { console.log(y); }; f(); //undefined f(2); //2 var x = 1; var f2 = function(x, y = x) { console.log(y); }; f2();//undefined f2(1);//1
這里最讓人疑惑的就是,f()為啥不輸出全局1,居然是undefined。
原因是y=x使用了默認賦值,創建了一個獨立的作用域,y的值從x找,而本作用於中是可以找到第一個參數x的,只是它沒有被賦值,等同於聲明了但沒給值,所以是undefined。理解不了?差不多是這個意思:
var x = 1; { let x; let y = x; console.log(x);//undefined }
但當我們把形參x去掉時,再次調用就發生改變了:
var x = 1; function f3(y = x) { console.log(x, y); } f3(); //1,1 f3(2); //1,2
怎么這下xy都用全局的呢?因為這個獨立作用域沒找到x,剛好外部全局又有個,繼承來了唄,等同於這個意思:
var x = 1; { //x=1 繼承來的 let y = x; console.log(x, y); //1 1 }
我們再來個極端的,看這個代碼,會報錯:
var x = 1; function f4(x = x) { console.log(x); } f4(); //報錯
這就不用解釋了,暫時性死域,未聲明就開始使用,會計作用域肯定不同意啊,等同於這樣:
var x = 1; { //此時里外2個x是互不相干的獨立存在 let x = x; console.log(x, y); //報錯 }
最后再看個稍微復雜點的例子:
var x = 1; function foo(x, y = function() {x = 2;}) { var x = 3; y(); console.log(x); }; foo(); // 3 foo(4); // 3 console.log(x); // 1
在上述代碼中foo函數參數因為用了默認值,所以參數這里出現了一個獨立的作用域,形參x與函數y中變量x同屬於一個作用域。
而在foo函數執行體中,因為使用了var再次聲明了一個x,所以這里的x與參數作用域中的x不同,那么當我們調用foo函數,執行了y()時,只影響了參數作用域中的x,並沒影響全局x與執行體作用域的x,這里輸出了3。
而當我們把這個var去掉,執行體中的x就指向了形參x,所以輸出x這里會變成2。我覺得帶var的情況下,有點像我在JS模式中看到的靜態變量,加上形參形成獨立作用域,導致兩者互不干擾。
二、取代arguments的rest參數
在ES5中去獲取函數形參常常會使用arguments,舉個例子:
function f(){ console.log(arguments); }; f('a','b','c');//一個包含a,b,c的類數組
但在ES6中呢,新增了rest寫法,比如...變量名,還是上面的例子,就成了這樣:
function f(...rest){ console.log(rest.length); console.log(Array.isArray(rest))//true }; f('a','b','c');//3
首先...后面這個變量名隨便取,不是一定要寫rest,其次,這個rest類型是數組!ES5的arguments是類數組,也就是說rest可以直接使用數組方法。
function f1(...rest){ return rest.sort();//雖然這個排序不嚴謹 }; let aa = f1(2,3,5,1);//1,2,3,5 function f2(){ // arguments.sort() 會報錯 return Array.prototype.sort.call(arguments); }; let bb = f2(2,3,5,1);//1,2,3,5
我們對任意數量數字排序,很明顯...rest的寫法更為精簡,請忽略sort排序不嚴謹的地方,這里只是做個寫法對比。
忘了說,rest參數本質上是獲取函數額外的參數,啥意思?就是說,調用函數時,那些沒能跟形參對應上的參數。
function f1(a,b,...c){ console.log(c);//[3,4,5] }; f1(1,2,3,4,5);
這個例子中,參數1,2分別與形參a,b對應,那么...c就對應額外的參數3,4,5了,應該很好理解吧。
另外,...c不算一個形參,所以我們獲取函數length屬性時,是不包括rest參數的,舉個例子:
function f1(a,b,...c){ console.log(f1.length);//2 }; f1(1,2,3,4,5);
最后呢,rest參數必須卸載函數形參尾部,否則就會報錯。
function f1(a, ...c, b) {}; f1(1, 2, 3, 4, 5);
三、嚴格模式與函數name屬性的部分改動
這兩個簡單點說,因為平時也沒怎么用,做個了解就差不多了。
在ES5中,函數內部可以添加嚴格模式,但是ES6開始,如果這個函數使用了參數默認值,或者解構賦值,或者rest參數等,在內部使用嚴格模式就會報錯。
這個我覺得沒啥說的,現在開發基本都是全局嚴格模式,就沒函數里面玩過,誰會閑得蛋疼去函數內部定義嚴格模式....
關於函數的name屬性,我在JS模式也簡單提過,name屬性其實在瀏覽器環境早就支持了,但是這個屬性在ES6才正式納入規范...
var func = function () {}; //ES6 console.log(func.name);//func //es5 console.log(func.name);//"" //ie console.log(func.name);//undefined
我們把一個匿名函數賦予一個變量,在ES5情況,name為空,ES6會將這個變量作為函數的name屬性。其實我覺得將匿名函數賦予變量不就是函數表達式的寫法么。
其次,雖然ES6將函數name屬性納入了規范,但部分瀏覽器實現仍然不同,比如另類的IE在上面的代碼中,輸出居然是undefined。
那如果我們將一個實名函數賦予給一個變量呢,這里需要注意一下:
var func = function demo() {console.log(1)}; console.log(func.name);//1 func();//demo
此時這個函數調用要通過func來調用,但name屬性卻是demo。ie獲取name屬性依舊是undefined
如果是構造函數實例呢,name屬性就是anonymous(匿名的):
//ES6 console.log((new Function).name)//anonymous //IE console.log((new Function).name)//undefined
即便你將這個構造函數賦予給一個變量也如此:
var a = new Function(); console.log(a.name)//anonymous
四、箭頭函數
1.箭頭函數基本用法
ES6引入了箭頭函數,大大簡化了函數的寫法,一個最簡單的例子:
//ES5寫法 var a = function (x){console.log(x)}; //箭頭函數寫法 var a = x => console.log(x); a(1);
var sum = function(a1, b1) { return a1 + b1; }; //箭頭函數寫法 var sum = (a1, b1) => a1 + b1; var a = sum(1, 2); console.log(a); //3
function與return都被省略了。
當然,箭頭函數也有一定規則,假設這個函數沒有形參,或者形參超過了一個,形參的圓括號就不能簡寫:
var a = (x, y) => console.log(x + y); var b = () => console.log(1); a(1,2);//3 b()//1
如果執行塊語句有多條,那花括號就不能省略,必須加上,比如:
var sum = (num1, num2) => { var a = num1 + num2; return a;} sum(1,2)//3
或者代碼塊包含了對象,本身就自帶了花括號,那外層的花括號肯定是不能省略的。ES6入門這本書說如果箭頭函數如果直接返回對象,也必須在對象外面加上花括號,我覺得這句話說的不嚴謹,比如這樣我返回對象還是不用加:
let a = {name:'echo'} let f = () => a; const obj = f(); console.log(obj);//{name:'echo'}
其次,就算加了花括號,內層的對象也並不是返回,我理解的返回就是return,這里有點歧義。
let f = () => {{name:'echo'}}; const obj = f(); console.log(obj);//undefined
箭頭函數能夠與解構賦值結合使用,這肯定是沒問題的,畢竟解構賦值也只是改變了參數傳遞的方式,下面兩種寫法作用相同
let f = ({x,y}) => console.log(x,y); var f = function (obj) { console.log(obj.x,obj.y) };
ES6箭頭函數寫法最重要的就是大大減少了回調函數的代碼量,畢竟回調使用頻率太高了,比如forEach回調:
const arr = [1,2,3,4]; arr.forEach((element,index) => { console.log(index+':'+element); });
2.箭頭函數帶來的使用改變
箭頭函數帶來便捷的同時,也改變了部分規則。我們都知道this這個東西永遠指向它最終的調用者,但是這條規則在箭頭函數中失效了。
var me = {name:'echo'}; var name = '時間跳躍' let f1 = function (){ console.log(this.name)} let f2 = () => console.log(this.name); f1.call(me);//echo f2.call(me);//時間跳躍
上述代碼,我定義了2個相同的函數,只是一個是箭頭函數的寫法,f1輸出echo毋庸置疑,函數執行時,this指向了me對象,所以name屬性是echo。
箭頭函數呢,即便我們使用了call方法,但執行時this依舊指向了window,所以拿到了時間跳躍。
那么問題來了?為啥箭頭函數的this指向了全局window?
首先我們得明白幾個概念:
第一:准確來說,箭頭函數沒有自己的this,它的this是從定義了它的外層代碼塊那里借來的,讀書人的說法不叫偷。
第二:箭頭函數的this是靜態的,從定義好開始,this就老實本分的只從箭頭函數外的作用域借,不受其它誘惑。
那么我們回頭看上面的代碼,來應用這兩個概念,第一f2函數沒有自己的this,它從構造函數外層作用域借,外層是誰?外層是全局,這里的全局就是window。隨便此時我們通過call修改了this指向,很不巧,我箭頭函數的this就是死了心的從外層借。
為了證實這兩個觀點,我們來看兩個例子,首先是普通函數:
function f(){ console.log(this);//{a:1} setTimeout(function () { console.log(this);//window },100); }; f.call({a:1});
函數f被調用時,this肯定指向{a:1},所以函數f中輸出this,指向了該對象。而定時器中的函數輸出時,this是指向window,畢竟定時器中的函數有點自調的意思,類似於這樣:
function f() { console.log(this); //{a:1} (function() { console.log(this);//window })(); } f.call({ a: 1 });
定時器中的函數就差不多這個意思了,普通寫法自然this自然指向window。
現在我們將定時器中的函數修改為箭頭函數,箭頭函數沒this,要從外層作用域借,外層的是對象{a:1},所以這里箭頭函數應該也輸出此對象:
function f() { console.log(this); //{a:1} setTimeout(() => { console.log(this); //{a:1} }, 100); } f.call({ a: 1 });
測試一下,果然沒問題,那么this就先說到這里了。
除了this的變化,箭頭函數不能用在構造函數上,畢竟箭頭函數沒this啊,this都是借來的,this都沒有,還構造個球。
其次,箭頭函數沒有arguments對象,如果要用就得使用rest參數代替,這個前面也有說。
最后,箭頭函數不能使用yield命令,這個我不是很了解,后面看了再說吧。
五、雙冒號運算符(函數綁定運算符)
函數綁定運算符是通過兩個冒號::來取代call,bind,apply方法綁定this的一種提案。
函數綁定運算符左邊是對象,右邊是一個函數,那么函數執行時,函數的this也就是執行上下文將指向左邊的對象。
obj::func; // 等同於 func.bind(obj);
但是這個貌似還不可用,雙冒號直接報錯了,先作為了解吧。
六、尾調用優化(只在嚴格模式生效)
1.尾調用
尾調用是指在函數執行的最后一步調用另一個函數並return。
function f(a){ return f2(a); };
就是說,最后執行的一步,一定是單純的調用了某個函數並返回了。只要加了其它操作的都不叫尾調用:
function f(a){ f2(a); }; function f(a){ return f2(a)+1; }; function f(a){ let func2 = f2(a); return func2; };
第一個沒return函數,本質上最后一步隱性返回了一個undefined,第二個除了調用函數還有加法的操作,第三個最后一步單純return沒調用,都不算尾調用。
另外,除了要求最后一步調用函數外,內部函數被調用時還不能依賴外層函數的內部變量,否則也不屬於尾調用:
function f1(data) { var a = 1; function f2(b){ return b + a; }; return f2(a); }
那么這個尾調用能帶來什么優化?意義是啥?
函數調用時會在內存中形成一個調用記錄,又叫調用幀call frame,用於保存調用的位置與內部變量等信息。
舉個例子,我們首先調用函數A,而函數A又要調用函數B,那么在內存中,A的調用幀上面會有一個函數B的調用幀。此時A,B的調用幀組合起來就形成了一個調用棧call stack。
等到函數B執行完成會將執行結果返回到函數A,函數B的調用幀消失,再執行函數A,完成后A的調用幀消失。畫的比較丑,大概這么個意思:
尾調用比較奇妙的由於它是最后一步調用,比如上面的B,它不會再記錄額外的信息,也不會創建額外的調用幀,非常節約內存。
2.尾遞歸
什么函數調用特別消耗內存?首要想到的---遞歸,遞歸這個東西因為要自己調用自己,處理不好就有棧溢出的問題;那我們能不能讓遞歸結合尾調用來解決遞歸自身函數調用時內存消耗過大的問題,當然可以,這種玩法也叫尾遞歸。
尾遞歸每次調用自己都是最后一步的操作,因此根本不會創建更多的調用幀,完美解決棧溢出的風險,當然,尾遞歸需要改寫原本遞歸的函數。
我們實現一個簡單階乘函數:
//遞歸 function f(a) { if (a === 1) { return a; }; return a * f(a - 1); }; f(5);//120 //尾遞歸 function f(a, total) { if (a === 1) { return total }; return f(a - 1, a * total); }; f(5, 1);//120
很明顯,普通遞歸最后一步還處理了乘法運算,不滿足尾調用,而改寫之后,我們將計算的部分交給了形參,再次調用時,已經是干凈的函數調用返回了,這就是尾調用。
我在 從斐波那契數列淺談遞歸有簡單提及斐波那契數列與遞歸,這里我們也能通過尾遞歸改寫斐波那契數列的計算:
//普通遞歸 比我寫的遞歸好多了.... function Fibonacci(n) { if (n <= 1) { return 1; } return Fibonacci(n - 1) + Fibonacci(n - 2); }; Fibonacci(10)//89 // 尾遞歸 function Fibonacci2(n, ac1 = 1, ac2 = 1) { if (n <= 1) { return ac2; } return Fibonacci2(n - 1, ac2, ac1 + ac2); }; Fibonacci(10)//89
因為我對於遞歸使用不是很熟練,有些時候甚至用遞歸實現都比較難,這個還是得培養,這里就只傳達這個思想了。
忘了說,尾遞歸現在谷歌還不支持,兼容性並不是所有瀏覽器都實現了,但是知道也沒壞處。
那么這章到這里結束了,我居然寫了這么長,鬼看的下去,算了...純當自己學習記錄了。
如果你對函數參數默認值產生的獨立作用域這個概念有所疑慮,歡迎閱讀博主這篇文章,保證能讓你看懂: