對函數拓展興趣更大一點,優先看,前面字符串後面再說,那些API居多,會使用能記住部分就好。html
1、函數參數可使用默認值數組
1.默認值生效條件瀏覽器
在變量的解構賦值就提到了,函數參數可使用默認值了。正常咱們給默認值是這樣的:app
//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.參數默認值與結構賦值的結合使用this
函數不只能夠直接給參數默認值,還能結合解構賦值的玩法,來看下面的例子:es5
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模式中看到的靜態變量,加上形參造成獨立做用域,致使二者互不干擾。
2、取代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);
3、嚴格模式與函數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
4、箭頭函數
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命令,這個我不是很瞭解,後面看了再說吧。
5、雙冒號運算符(函數綁定運算符)
函數綁定運算符是經過兩個冒號::來取代call,bind,apply方法綁定this的一種提案。
函數綁定運算符左邊是對象,右邊是一個函數,那麼函數執行時,函數的this也就是執行上下文將指向左邊的對象。
obj::func; // 等同於 func.bind(obj);
可是這個貌似還不可用,雙冒號直接報錯了,先做爲了解吧。
6、尾調用優化(只在嚴格模式生效)
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
由於我對於遞歸使用不是很熟練,有些時候甚至用遞歸實現都比較難,這個仍是得培養,這裏就只傳達這個思想了。
忘了說,尾遞歸如今谷歌還不支持,兼容性並非全部瀏覽器都實現了,可是知道也沒壞處。
那麼這章到這裏結束了,我竟然寫了這麼長,鬼看的下去,算了...純當本身學習記錄了。
若是你對函數參數默認值產生的獨立做用域這個概念有所疑慮,歡迎閱讀博主這篇文章,保證能讓你看懂: