今天在寫代碼的時候,我犯了一個很low的錯誤,廢話不多說,直接上代碼: 大家看到之後,第一反應肯定會認為是個語法錯誤,可是自己仔細想想,這是什麼原因?似乎還不能解釋清楚,好奇寶寶模式立即啟動,經過查閱相關資料得到了答案,接下來我們一起來探討下其中的原理。 疑惑解答 大家有沒有考慮過為什麼上面這種寫法 ...
今天在寫代碼的時候,我犯了一個很low的錯誤,廢話不多說,直接上代碼:
1 function () { 2 console.log('hello world'); 3 }()
大家看到之後,第一反應肯定會認為是個語法錯誤,可是自己仔細想想,這是什麼原因?似乎還不能解釋清楚,好奇寶寶模式立即啟動,經過查閱相關資料得到了答案,接下來我們一起來探討下其中的原理。
疑惑解答
大家有沒有考慮過為什麼上面這種寫法會報錯?
原來,瀏覽器遇到function關鍵字的時候會認為這是一個函數聲明,函數聲明必須包括:關鍵字function、函數名、形參、函數體。在解析上面代碼的時候,解析器發現沒有出現函數名而直接出現了(),瀏覽器便會認為這種定義不符合規範,所以就報錯了。
既然是缺少函數名,如果我們給它添加函數名,是不是會正確調用?
1 function hello () { 2 console.log('hello world'); 3 }()
讓我們靜靜等待奇跡出現!
哎,瀏覽器在解析的時候怎麼又報錯了?
其實是函數聲明的緣故,也就是說通過聲明的函數會被提升到其他代碼的前面。提升之後應該是這樣了:
1 function hello () { 2 console.log('hello world'); 3 } 4 ...// do something 5 (); // 咦?這是什麼東東?
解析器對此也很茫然,不知道該按照什麼標準去解析,只能告訴你寫的不夠規範了。 大家看一下下麵的這種用法,會不會感覺很熟悉?
1 var hello = function () { 2 console.log('hello world'); 3 } 4 hello();
試想一下,如果用()把上面的函數名hello包裹起來,會發生什麼
1 var hello = function () { 2 console.log('hello world'); 3 } 4 (hello)();
Bingo,瀏覽器輸出了“hello world”,這種寫法是不是特別像數學中的結合律。
繼續對上面的函數調用做一些改變,如果把函數名hello替換成匿名函數,猜想應該也可以調用成功。
1 (function () { 2 console.log('hello world'); 3 })();
果然達到了預期的結果,調用成功了!這是為什麼?
因為用()把匿名函數包裹起來,解析器便會認為這是一個函數表達式,函數表達式的後面添加()顯然是可以正確執行的。此外,除了()之外,也可以使用!等常見的一元運算符來執行匿名函數。
1 !function() { 2 console.log('hello world'); 3 }();
上面這段代碼會輸出“hello world”。
Tips:
說到函數定義,需要註意函數聲明和函數表達式兩者的區別(大神可以跳過喔~):前者有個函數聲明提升的過程,在代碼預解析的時候,會把函數聲明提升到代碼的頂部,所以在聲明函數位置之前調用函數並不會出錯;而後者只是進行變數提升,因此,解析器只有執行函數表達式的代碼之後才可以調用該函數。
說到這裡,想起了一個老生常談的問題,函數總是在特定的作用域中執行,函數中this的指向是不是也把好多同學弄的一知半解呢?讓我們來繼續研究一下~~~
走進函數的this
一般情況下,哪個對象調用函數(方法),則this便指向哪個對象。
通過一個例子來說明該如何確定this的指向。
1 var obj= { 2 number: 1, 3 getOwnNumber: function () { 4 var number = 2; 5 return this.number; 6 }, 7 getNumber: function () { 8 var number = 3; 9 return function () { 10 var number = 4; 11 return this.number; 12 }; 13 } 14 } 15 console.log(obj.getOwnNumber()); 16 console.log(obj.getNumber()());
大家猜一下,第一個輸出結果是多少?大家不要猶豫,答案是1,就這麼簡單!
很明顯,是obj調用的getOwnNumber方法,而obj對象內部定義的number值為1,所以第一個輸出結果理所應當是1。
問題來了,第二個輸出結果是多少?1?2?3?4?
還是同樣的方法,我們找一找this到底指向哪個對象。obj.getNumber()運行結果是一個匿名函數,然後再執行匿名函數。我們可以把這個過程拆分一下,如下:
1 var fun = obj.getNumber() // 返回一個匿名函數 2 fun();
經過分解之後,很明顯,函數是在全局作用域中調用的,所以此處的this理應指向window對象,window對象中沒有定義number,所以結果是undefined。
假想一下,當代碼複雜之後,確定this的指向是不是更加麻煩?別急,ES6規範提供了箭頭函數的語法,可以幫助我們解決這個問題。箭頭函數是何方神聖,該怎麼定義呢?箭頭函數的基本寫法如下:
1 const hello = () => { 2 console.log('hello'); 3 }
我們嘗試使用箭頭函數來改造obj對象的getNumber方法:
1 const obj= { 2 number: 1, 3 getOwnNumber() { 4 const number = 2; 5 return this.number; 6 }, 7 getNumber() { 8 const number = 3; 9 return () => { 10 const number = 4; 11 return this.number; 12 }; 13 } 14 } 15 console.log(obj.getOwnNumber()); 16 console.log(obj.getNumber()());
運行之後會發現兩個輸出結果都是1。這是為什麼?
因為箭頭函數內部的this是指向定義函數時所在的對象,在上面的代碼中,this指向了obj對象,所以輸出1。有了箭頭函數之後,我們不再需要通過變數保存對象的this指針或者通過bind方法改變this的指針,輕鬆實現我們預期的效果。
箭頭函數和普通函數的主要區別:(摘抄自阮一峰大神的《ES6標準入門》):
- 箭頭函數體內的this就是定義時所在的對象,而不是使用時所在的對象。
- 箭頭函數不可以當做構造函數。也就是說,不可以使用new命令,否則會拋出一個錯誤。
- 不可以使用arguments對象,該對象在函數體內不存在。可以使用rest參數代替。
- 不可以使用yield命令,因此箭頭函數不能用作Generator函數
但是箭頭函數也不是可以在任何場景下都能使用,如果我們要改變this的指向該怎麼辦?在ES6出現之前,我們可以使用call、apply和bind方法來改變函數中this的指向,ES7提出了使用雙冒號(::)的運算符,使得我們可以通過這個運算符來改變箭頭函數中this的指向。
除了箭頭函數之外,ES6又對之前的函數做了哪些優化?
構造函數
在其他語言中可以通過類實現面向對象的功能,但是在ES6規範公佈之前,JavaScript中並沒有類的概念,面向對象的語法只能通過構造函數的方式來實現。
1 function Zhuanzhuan (name) { 2 this.name = name; 3 } 4 Zhuanzhuan.prototype.say = function () { 5 console.log('hi~I am zhuanzhuan'); 6 }
要想實例化這個“類”,必須使用new關鍵字
1 var zhuanzhuan = new Zhuanzhuan('zhuanzhuan');
在ES6之前,使用構造函數來創建對象,閱讀起來並不是很清晰。ES6提供的class語法跟其他面向對象的語言語法較為接近。 使用ES6的語法來改寫一下上面用構造函數定義的“類”。
1 class Zhuanzhuan { 2 constructor (name) { 3 this.name = name; 4 } 5 say () { 6 console.log('hi~I am zhuanzhuan'); 7 } 8 } 9 let zhuanzhuan = new Zhuanzhuan('zhuanzhuan');
看起來是不是特別像C++語言中的面向對象風格呢,出現class之後,媽媽再也不用擔心我寫的類不夠清晰了~~~
JavaScript語言中的函數內容相當豐富,需要我們不斷去通過實踐去加深理解。騏驥一躍,不能十步,駑馬十駕,功在不捨。多總結,多思考。大家如果有好的想法,可以相互交流和分享,每天進步一點點,不斷提高自己的專業技能。
這就是我因為一個小錯誤,引發的思考。
如果你喜歡我們的文章,關註我們的公眾號和我們互動吧。