JS學習筆記之再理解一等公民--函式(基礎篇)
宣告函式的方式
這裡其實我比較迷惑,我以前認為宣告函式只有函式宣告方式和函式表示式,其它的所有情況比如在類裡面的,物件裡面的都歸於這兩個,最近看資料又覺得其它方式可以單獨成為一種宣告函式的方式,所以跑回來完善了一下文章。
方式1. 函式宣告(Function declartion)
function 函式名([形參列表]) { //函式體 }
函式宣告會被提升到作用域頂部,也就是說,你可以在某個函式宣告前呼叫它而不會報錯。
函式宣告的函式名是必須的,所以它有name屬性。
方式2. 函式表示式(Function expression)
let 變數名 = function [函式名]([形參列表]) { //函式體 }
在某個物件中的函式表示式:
const obj = { sum: function [函式名]([形參列表]) { //函式體 } }
函式表示式又分為具名函式和匿名函式,以上,如果有“函式名”就是具名函式,反之是匿名函式。
對於具名函式,函式名的作用域只在函式內部,而變數名的作用域是全域性的,所以在函式內部即可以使用函式名也可以使用變數名呼叫自身,在函式外部則只能使用變數名呼叫。
//函式表示式--具名函式let factorial = function fact(x) { if (x具名函式有name屬性,匿名函式沒有。
推薦使用具名函式,原因如下:
具名函式有更詳細的錯誤資訊和呼叫堆疊資訊,更方便除錯
當在函式內部有遞迴呼叫時,使用函式名呼叫比使用變數名呼叫效率更高
函式表示式不會被提升到作用域頂部,原因是函式表示式是將函式賦值給一個變數,而js對提升變數的操作是隻提升變數的宣告而不會提升變數的賦值,所以不能在某個函式表示式之前呼叫它。
注意
1. 函式表示式可以出現在任何地方,函式宣告不能出現在迴圈、條件判斷、try/catch、with語句中。
注:只有在嚴格模式下,在塊語句中使用了函式宣告才會報錯。
2. 立即執行函式只能是函式表示式而不能是函式宣告,但使用函式宣告不會報錯,只是不會執行
例2://函式宣告方式function square(a){ console.log(a * a); }(5)//函式表示式方式let square = function(a){ console.log(a * a); }(5)//錯誤的方式function(a){ console.log(a * a); }(5)上面的程式碼第一段不會列印出值,第二段能列印出值,出現這種區別的原因是隻有函式宣告可以提升,函式宣告後面的()直接被忽略掉了,所以它不能立即執行。而第三段程式碼會報錯,因為它既沒有函式名又沒有賦值給變數,js引擎就會將其解析成函式宣告。為了避免在某些情況下js解析函式產生歧義,js建議在立即執行函式的函式體外面加一對圓括號:
例3:(function square(a){ console.log(a * a) ; }(5)) (function(a){ console.log(a * a) ; }(5))上面的程式碼都可以正常執行了,js會將其正確解析成函式表示式。
方式3. 速記方法定義(Shorthand method definition)
在物件裡:
const obj = { 函式名([形參列表]) { //函式體 } }在類裡面(React裡面就是這種方式):
class Person { constructor() {} 函式名([形參列表]) { //函式體 } }這種方式定義的方法是具名函式。
比起const obj = {add: function() {} }
,更推薦這種方式。方式4. 箭頭函式(Arrow function)
const 變數名 = (形參列表) => { //函式體}箭頭函式的特點:
箭頭函式沒有自己的執行上下文(execution context), 也就是,它沒有自己的this.
它是匿名函式
箭頭內部也沒有arguments物件
方式5. 函式建構函式(function constructor)
在js中,每個函式實際都是一個Function物件,而Function物件是由Function建構函式建立的。
const 變數名 = new Function([字串形式的引數列表],字串形式的函式體)比如:
const adder = new Function("a", "b", "return a + b")完全不推薦使用這種方式,原因如下:
Function物件是在函式建立時解析的,這比函式宣告和函式表示式更低效。
不論在哪裡用這種方式宣告函式,它都是在全域性作用域中被建立,所以它不能形成閉包。
呼叫函式的方式
四種方式:
作為函式
作為函式的意思就是在全域性作用域下、某個函式體內部或者某個塊語句內部呼叫
當以此方式呼叫函式時,一般不會使用到this關鍵字(這也是它和作為方法呼叫時的最大區別),因為此時的this要麼指向全域性物件window(非嚴格模式下)要麼為undefined(嚴格模式下)作為方法
作為方法的意思就是函式作為一個物件裡的屬性被呼叫,此時函式的this指向該物件,並且函式可以訪問到該物件的所有屬性。作為建構函式
作為建構函式呼叫時,函式名前面會有new關鍵字,如果函式沒有引數,那麼是不需要在函式名後面跟()的。此時不管是函式還是方法,this指向的既不是物件也不是window(或undefined),而是一個被稱為“原型”的物件。使用call(),apply()或者bind()方法
這三個方法都是可以顯示指定this的指向的,即任何函式都可以作為任何物件的方法來呼叫。這四種方式最大的不同就是this的指向問題,首先,作為函式呼叫的this是最好理解的,而作為方法呼叫看起來也不難,無非就是方法是哪個物件的屬性this就指向誰嘛,但兩個結合起來可能就比較容易迷惑人:
例4:let obj = { name: 'melody', age: 18, sayHello: function() { //sayHello()是obj物件的屬性 console.log(this.name); sayAge(); function sayAge() { //sayAge()是sayHello()的內部函式 console.log(this.age) } } } obj.sayHello();首先,
sayHello()
方法定義在obj
物件上,那麼sayHello()
裡面的this
就指向了obj
,所以第一個會列印出melody
,接著sayHello()
呼叫了它的內部函式sayAge()
,此時sayAge()
裡面的this.age
應該是什麼?是obj
物件上的age
嗎?其實不是,在sayAge()
裡面列印出this
會發現this
是指向window
物件的,所以第二個console
會列印出undefined
。因為這時候外面多了一個物件,我們就容易被這個物件迷惑,以為巢狀函式的this和外層函式的this的指向是一樣的,而其實此時我們遵循的原則應該是第一條:當作為函式呼叫時,this要麼指向全域性物件window(非嚴格模式下)要麼為undefined(嚴格模式下),也就是外層函式是作為方法呼叫,而巢狀函式依然是作為函式呼叫的,它們各自遵循各自的規則。如果想讓巢狀函式和外層函式的this都指向同一個,以前的方法是將this的值儲存在一個變數裡面:
... sayHello: function() { let that = this; function sayAge() { console.log(that.age) //18 } } ...或者使用ES6新增的箭頭函式:
... sayHello: function() { console.log(this.name); //melody let sayAge = () => { console.log(this.age) //18 } sayAge(); } ...關於箭頭函式和普通函式的this的區別,後面再詳細講吧~
作為建構函式就很強了,這就涉及到js裡面最難也最重要到部分:原型和繼承,它們重要到這篇文章都沒資格展開,所以就略過吧~嗯...我的意思是下一次總結。
call(),apply()和bind()
相同之處:
第一個引數都是指定this的值
不同之處:
從第二個引數開始,call()和bind()是函式的引數列表,apply()是引數陣列。
call()和apply()是立即呼叫函式,bind()是建立一個新函式,將繫結的this值傳給新函式,但新函式不會立即呼叫,除非你手動呼叫它。
舉例說明這三個方法的基本用法:
例5:
let color = { color: 'yellow', getColor: function(name) { console.log(`${name} like ${this.color}`); } }let redColor = { color: 'red'} color.getColor.call(redColor, 'melody') color.getColor.apply(redColor, ['melody']) color.getColor.bind(redColor, 'melody')()
首先,apply()方法的第二個引數是陣列,call()和bind()是引數列表,其次,apply()和call()會立即呼叫函式而bind()不會,所以要想bind()後能立即執行函式,需要在最後加一對括號。
apply()和call()
前面也說了,這兩個函式的唯一區別就是第二個引數的格式,apply()的第二個引數是陣列,call()從第二個引數開始是函式的引數列表,並且引數順序需要和函式的引數順序一致,如下:
let obj = {}; //模擬thisfunction fn(arg1,arg2) {}//呼叫fn.call(obj, arg1, arg2); fn.apply(obj, [arg1, arg2]);
注意:目前的主流瀏覽器幾乎都支援apply()方法的第二個引數是類陣列物件,我在Chrome, Firefox, Opera, Safari上面都測試過,只要是類陣列物件就可以,不過低版本可能會不支援,所以建議先將類陣列轉換成陣列再傳給apply()方法。
用法一:類陣列物件借用陣列方法
常見的類陣列物件有:
arguments物件,
getElementsByTagName(), getElementsByClassName (), getElementsByName(), querySelectorAll()方法獲取到的節點列表。
注:類陣列物件就是擁有length屬性的特殊物件
例6:將類陣列物件轉換成陣列
Array.prototype.slice.call(arguments); [].slice.call(arguments);//或者Array.prototype.slice.apply(arguments); [].slice.apply(arguments);
因為此時不需要給slice()
方法傳入引數,所以call()
和apply()
都可以實現。
例7:借用其它陣列方法
//類陣列物件let objLikeArr = {'0': 'melody','1': 18,'2': 'sleep',length: 3}//借用陣列的indexOf()方法Array.prototype.indexOf.call(objLikeArr, 18); //1Array.prototype.indexOf.apply(objLikeArr, ['sleep']); //2
用法二:求陣列最大(小)值Math.max()
和Math.min()
可以找出一組數字中的最大(小)值,但是當引數為陣列時,結果是NaN
,這時候用apply()方法可以解決這個問題,因為apply()的第二個引數接收的是陣列。
例8:
let arr1 = [1,2,12,8,9,34];Math.max.apply(null, arr1); //34
數字字串也可以:
例9:
let a = '1221679183';Math.max.apply(null, a.split('')); //9
用法三:借用toString()方法判斷資料型別
這不是最好用的判斷資料型別的方法,但是是最有效的方法。
例10:
//基本資料型別 let null1 = null; let undefined1 = undefined; let str = "hello"; let num = 123; let bool = true; let symbol = Symbol("hello");//引用資料型別 let obj = {}; let arr = []; let fun = function() {}; let reg = new RegExp(/a+b/, 'g'); let date = new Date(); Object.prototype.toString.call(null1) //[object Null] Object.prototype.toString.call(undefined1) //[object Undefined] Object.prototype.toString.call(str) //[object String] Object.prototype.toString.call(num) //[object Number] Object.prototype.toString.call(bool) //[object Boolean] Object.prototype.toString.call(symbol) //[object Symbol] Object.prototype.toString.call(obj) //[object Object] Object.prototype.toString.call(arr) //[object Array] Object.prototype.toString.call(fun) //[object Function] Object.prototype.toString.call(reg) //[object RegExp] Object.prototype.toString.call(date) //[object Date]
用法四:實現函式不定參
一個常見的用法是實現console可接收多個引數的功能:
例11:
function log() { console.log.apply(console, arguments) } log('hello'); //hellolog('hello', 'melody'); // hello melody
es6新增的 ... 運算子其實更方便:
function log(...arg) { console.log(...arg); }
還可以加預設的列印值:
function logToHello() { let args = Array.prototype.slice.call(arguments); args.unshift('(melody say)'); console.log.apply(console, args) } logToHello('thank you.', 'I hope you have a good day'); logToHello('thank you.');
bind()
bind() 函式會建立一個新函式,稱為繫結函式,繫結函式與原函式具有相同的函式體。當繫結函式被呼叫時 this 值繫結到 bind() 的第一個引數,並且該引數不能被重寫,也就是繫結的this就不再改變了。
用法一:解決將方法賦值給另一個變數時this指向改變的問題
當函式作為物件的屬性被呼叫時,如果這時候是先將方法賦值給一個變數,再透過這個變數來呼叫方法,此時this的指向就會發生變化,不再是原來的物件了,這時候,就算該函式使用箭頭函式的寫法也無濟於事了。解決方法是在賦值時使用bind()方法繫結this。:
例12:
name = "Tiya"; //全域性作用域的變數let obj1 = { name: 'melody', //區域性作用域的變數 sayHello: function() { console.log(this.name); }, }let sayHello1 = obj1.sayHello; sayHello1() //Tiya,this的指向發生了變化,指向全域性作用域let sayHello = obj1.sayHello.bind(obj1); sayHello() //melody
用法二:解決dom元素上繫結事件,當事件觸發時this指向改變的問題
這個問題最常出現在使用某些框架的時候,比如React,寫過React的小夥伴肯定對於this.xxx.bind(this)
這種寫法再熟悉不過了,因為React內部並沒有幫我們繫結好this,所以需要我們手動繫結this,否則就會出錯。
例13:
//模擬的dom元素let ele = document.getElementById("container");let user = { data: { name: "melody", }, clickHandler: function() { ele.innerHTML = this.data.name; } } ele.addEventListener("click", user.clickHandler); //報錯 Cannot read property 'name' of undefined
我們在一個dom元素上監聽了點選事件,當該事件觸發時,將user
物件上的一個變數值顯示在該元素上,但如果直接使用ele.addEventListener("click", user.clickHandler)
,此時,clickHandler
事件內部的this
已經變成了這個節點而不再是
user
本身了,正確的做法是呼叫時給clickHandler
繫結this
:
ele.addEventListener("click", user.clickHandler.bind(user));
實參、形參和arguments物件
簡單來說,形參是宣告函式時的引數,實參是呼叫函式時傳入的引數。
例14:
function getName(name) { //此處為形參 console.log(`my name is ${name}`); } getName('melody'); //此處為實參
js的函式,呼叫時傳入的引數和宣告時的引數個數可以不一致,型別可以不一致(也沒有宣告型別的機會),這就是為什麼js沒有函式過載概念的原因。
情況一:實引數量 >形引數量
此時函式會忽略多餘的實參,就比如說前面的例子:
function log(name) { console.log(name); } log('world', 'hello'); //world
情況二:實引數量
此時多餘的引數的值為undefined
,比如:
function log(name, age) { console.log(name, age); } log('world'); //world undefined
arguments是函式內部可以獲取到傳入的引數的類陣列物件,要注意的是arguments的長度代表的是實參的數量,而不是形參的數量。
前面說到js沒有函式過載的概念,但可以用arguments物件模擬函式的過載:
function overloading() { switch(arguments.length) { case 1: return arguments[0]; break; case 2: return arguments[0] + arguments[1]; break; default: return 0; break; } }
es6以後,js慢慢有了比arguments更好的方式去處理函式的引數,比如rest引數,前面的例子也提到過:
function log(...arg) { console.log(...arg); } log(1,2)
它看起來比arguments更容易理解也更簡潔,js應該也有想淘汰arguments的想法,所以建議大家能用es6語法實現的就不要用arguments了。
寫在最後
感覺最後一節寫的有點水,還請大家原諒~
本來今年的目標是在簡書上擁有100個粉絲的,但是有了更重要的事情要做,所以今年都不會再更新技術文章了~
現在有36個粉絲,還是超級開心的~
我文筆很爛,技術又很爛,雖然很用心很認真在寫文章,但離優秀還有很遠的距離,很想謝謝願意看我文章的人,你們都不會嫌棄我寫的不好~
我讀的大學是一個普通二本,專業還不太對口,入前端坑真的是場意外,但我幸運的是我畢業那年前端需求量很大,所以雖然我很菜,但工作還是找得到的,不過現在卻有些迷茫,感覺自己無法進步,這大概就是人們說的瓶頸期吧,我以為瘋狂補js基礎,看框架原始碼,總結技術文章就能突破當前的困境,但事實是我能感覺到自己在進步,卻也能感覺到自己離突破這個瓶頸還有一段距離,所以我做了一個非常重要的決定,所以我要閉關去啦~
這一次,不論成敗,因為過程的意義已經遠超於結果。
這一次,不論艱辛,因為這種生活不叫忙碌而叫充實。
作者:大柚子08
連結:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1343/viewspace-2814435/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 再談函式和一等公民函式
- javascript一等公民------函式JavaScript函式
- 前端亂彈99日之函式是一等公民前端函式
- JavaScript學習筆記(七)—— 再說函式JavaScript筆記函式
- 工廠方法、建構函式、原型物件——JS基礎學習筆記(四)函式原型物件JS筆記
- js純函式學習筆記(一)JS函式筆記
- MySQL學習筆記【基礎篇】MySql筆記
- Go函式介紹與一等公民Go函式
- go 學習筆記之學習函數語言程式設計前不要忘了函式基礎Go筆記函數程式設計函式
- JS的學習理解--->函式JS函式
- Vue.js 學習筆記之四:Vue 元件基礎Vue.js筆記元件
- Python零基礎學習筆記(二十四)——函式Python筆記函式
- JS學習理解之閉包和高階函式JS函式
- 飛機的 PHP 學習筆記之語言基礎篇PHP筆記
- 技術分享 | Kubernetes 學習筆記之基礎知識篇筆記
- 零基礎學習 Python 之函式Python函式
- 安心學習,重學前端之(js基礎篇(1))前端JS
- pandas之常用基本函式學習筆記函式筆記
- JS 基礎篇(七):JS中的遍歷函式JS函式
- 前端學習之PHP基礎函式總結前端PHP函式
- hive學習筆記之九:基礎UDFHive筆記
- hive學習筆記之六:HiveQL基礎Hive筆記
- day10學習筆記之函式上筆記函式
- hive學習筆記之七:內建函式Hive筆記函式
- js加固之正規表示式學習筆記JS筆記
- async函式學習筆記。函式筆記
- 生成函式 學習筆記函式筆記
- JS學習筆記之this指向JS筆記
- Python 3 學習筆記之——基礎語法Python筆記
- Golang學習筆記-1.6 函式Golang筆記函式
- JavaScript學習筆記 - 原生函式JavaScript筆記函式
- MYSQL學習筆記14: 函式MySql筆記函式
- python學習筆記(六)——函式Python筆記函式
- TS學習筆記(四):函式筆記函式
- Oracle學習筆記(6)——函式Oracle筆記函式
- CSS 基礎學習筆記CSS筆記
- node基礎學習筆記筆記
- Python基礎學習筆記Python筆記