JS學習筆記之再理解一等公民--函式(基礎篇)

duanhao發表於2021-09-09

宣告函式的方式

這裡其實我比較迷惑,我以前認為宣告函式只有函式宣告方式和函式表示式,其它的所有情況比如在類裡面的,物件裡面的都歸於這兩個,最近看資料又覺得其它方式可以單獨成為一種宣告函式的方式,所以跑回來完善了一下文章。

方式1. 函式宣告(Function declartion)

function 函式名([形參列表]) { 
    //函式體 }

函式宣告會被提升到作用域頂部,也就是說,你可以在某個函式宣告前呼叫它而不會報錯。
函式宣告的函式名是必須的,所以它有name屬性。


方式2. 函式表示式(Function expression)

let 變數名 = function [函式名]([形參列表]) { 
    //函式體 
}

在某個物件中的函式表示式:

const obj = {
  sum: function [函式名]([形參列表]) {    //函式體
  }
}

函式表示式又分為具名函式和匿名函式,以上,如果有“函式名”就是具名函式,反之是匿名函式。

對於具名函式,函式名的作用域只在函式內部,而變數名的作用域是全域性的,所以在函式內部即可以使用函式名也可以使用變數名呼叫自身,在函式外部則只能使用變數名呼叫。

//函式表示式--具名函式let factorial = function fact(x) {    if (x 

具名函式有name屬性,匿名函式沒有。

推薦使用具名函式,原因如下:

  1. 具名函式有更詳細的錯誤資訊和呼叫堆疊資訊,更方便除錯

  2. 當在函式內部有遞迴呼叫時,使用函式名呼叫比使用變數名呼叫效率更高

函式表示式不會被提升到作用域頂部,原因是函式表示式是將函式賦值給一個變數,而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 變數名 = (形參列表) => {  //函式體}

箭頭函式的特點:

  1. 箭頭函式沒有自己的執行上下文(execution context), 也就是,它沒有自己的this.

  2. 它是匿名函式

  3. 箭頭內部也沒有arguments物件


方式5. 函式建構函式(function constructor)

在js中,每個函式實際都是一個Function物件,而Function物件是由Function建構函式建立的。

const 變數名 = new Function([字串形式的引數列表],字串形式的函式體)

比如:

const adder = new Function("a", "b", "return a + b")

完全不推薦使用這種方式,原因如下:

  1. Function物件是在函式建立時解析的,這比函式宣告和函式表示式更低效。

  2. 不論在哪裡用這種方式宣告函式,它都是在全域性作用域中被建立,所以它不能形成閉包。

呼叫函式的方式

四種方式:

  1. 作為函式
    作為函式的意思就是在全域性作用域下、某個函式體內部或者某個塊語句內部呼叫
    當以此方式呼叫函式時,一般不會使用到this關鍵字(這也是它和作為方法呼叫時的最大區別),因為此時的this要麼指向全域性物件window(非嚴格模式下)要麼為undefined(嚴格模式下)

  2. 作為方法
    作為方法的意思就是函式作為一個物件裡的屬性被呼叫,此時函式的this指向該物件,並且函式可以訪問到該物件的所有屬性。

  3. 作為建構函式
    作為建構函式呼叫時,函式名前面會有new關鍵字,如果函式沒有引數,那麼是不需要在函式名後面跟()的。此時不管是函式還是方法,this指向的既不是物件也不是window(或undefined),而是一個被稱為“原型”的物件

  4. 使用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/,如需轉載,請註明出處,否則將追究法律責任。

相關文章