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

聲明函數的方式

這里其實我比較迷惑,我以前認為聲明函數只有函數聲明方式和函數表達式动雹,其它的所有情況比如在類里面的槽卫,對象里面的都歸于這兩個,最近看資料又覺得其它方式可以單獨成為一種聲明函數的方式胰蝠,所以跑回來完善了一下文章歼培。

方式1. 函數聲明(Function declartion)

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

函數聲明會被提升到作用域頂部,也就是說茸塞,你可以在某個函數聲明前調用它而不會報錯躲庄。
函數聲明的函數名是必須的,所以它有name屬性钾虐。


方式2. 函數表達式(Function expression)

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

在某個對象中的函數表達式:

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

函數表達式又分為具名函數和匿名函數噪窘,以上,如果有“函數名”就是具名函數效扫,反之是匿名函數倔监。

對于具名函數,函數名的作用域只在函數內部菌仁,而變量名的作用域是全局的浩习,所以在函數內部即可以使用函數名也可以使用變量名調用自身,在函數外部則只能使用變量名調用掘托。

//函數表達式--具名函數
let factorial = function fact(x) {
    if (x <= 1)  return 1;
    else         return x * fact(x-1)瘦锹;//正確
    //else         return x * factorial(x-1);//正確
}
factorial(5); //正確
fact(5); //錯誤

具名函數有name屬性闪盔,匿名函數沒有弯院。

推薦使用具名函數同廉,原因如下:

  1. 具名函數有更詳細的錯誤信息和調用堆棧信息叼旋,更方便調試
  2. 當在函數內部有遞歸調用時,使用函數名調用比使用變量名調用效率更高

函數表達式不會被提升到作用域頂部,原因是函數表達式是將函數賦值給一個變量邀层,而js對提升變量的操作是只提升變量的聲明而不會提升變量的賦值异赫,所以不能在某個函數表達式之前調用它椅挣。


注意

1. 函數表達式可以出現在任何地方,函數聲明不能出現在循環(huán)塔拳、條件判斷鼠证、try/catch、with語句中靠抑。

注:只有在嚴格模式下量九,在塊語句中使用了函數聲明才會報錯。

2. 立即執(zhí)行函數只能是函數表達式而不能是函數聲明颂碧,但使用函數聲明不會報錯荠列,只是不會執(zhí)行
例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)

上面的代碼第一段不會打印出值,第二段能打印出值载城,出現這種區(qū)別的原因是只有函數聲明可以提升肌似,函數聲明后面的()直接被忽略掉了,所以它不能立即執(zhí)行诉瓦。而第三段代碼會報錯川队,因為它既沒有函數名又沒有賦值給變量,js引擎就會將其解析成函數聲明睬澡。為了避免在某些情況下js解析函數產生歧義呼寸,js建議在立即執(zhí)行函數的函數體外面加一對圓括號:
例3:

(function square(a){
    console.log(a * a) ;
}(5))
(function(a){
    console.log(a * a) ;
}(5))

上面的代碼都可以正常執(zhí)行了,js會將其正確解析成函數表達式猴贰。


方式3. 速記方法定義(Shorthand method definition)

在對象里:

const obj = {
  函數名([形參列表]) {
    //函數體
  }
}

在類里面(React里面就是這種方式):

class Person {
  constructor() {}
  函數名([形參列表]) {
    //函數體
  }
}

這種方式定義的方法是具名函數对雪。
比起 const obj = {add: function() {} } ,更推薦這種方式。


方式4. 箭頭函數(Arrow function)

const 變量名 = (形參列表) => {
  //函數體
}

箭頭函數的特點:

  1. 箭頭函數沒有自己的執(zhí)行上下文(execution context), 也就是米绕,它沒有自己的this.
  2. 它是匿名函數
  3. 箭頭內部也沒有arguments對象

方式5. 函數構造函數(function constructor)

在js中瑟捣,每個函數實際都是一個Function對象,而Function對象是由Function構造函數創(chuàng)建的栅干。

const 變量名 = new Function([字符串形式的參數列表]迈套,字符串形式的函數體)

比如:

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

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

  1. Function對象是在函數創(chuàng)建時解析的碱鳞,這比函數聲明和函數表達式更低效桑李。
  2. 不論在哪里用這種方式聲明函數,它都是在全局作用域中被創(chuàng)建,所以它不能形成閉包贵白。

調用函數的方式

四種方式:

  1. 作為函數
    作為函數的意思就是在全局作用域下率拒、某個函數體內部或者某個塊語句內部調用
    當以此方式調用函數時,一般不會使用到this關鍵字(這也是它和作為方法調用時的最大區(qū)別)禁荒,因為此時的this要么指向全局對象window(非嚴格模式下)要么為undefined(嚴格模式下)
let sayHello = function(name) {
  console.log(`hello ${name}`)
}
sayHello('melody')
  1. 作為方法
    作為方法的意思就是函數作為一個對象里的屬性被調用猬膨,此時函數的this指向該對象,并且函數可以訪問到該對象的所有屬性呛伴。
let person = {
  name: 'melody',
  sayHello: function() {
     console.log(`hello ${this.name}`)
  }
}
person.sayHello() // hello melody

Note: 這種方式要注意this 可能會改變的情況

let _sayHello = person.sayHello
// 此時的this指向的對象已經變成了`window`而不是`person`勃痴,`this.name`的值為`undefined`
_sayHello() // hello undefined
  1. 作為構造函數
    作為構造函數調用時,this指向構造函數的實例
function Person(name, age) {
    this.name = name
    this.savor = age
}

    let person1 = new Person('melody', 'sleeping')
    let person2 = new Person('shelly', 'singing')
  1. 使用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會發(fā)現this是指向window對象的贴捡,所以第二個console會打印出undefined

因為這時候外面多了一個對象村砂,我們就容易被這個對象迷惑烂斋,以為嵌套函數的this和外層函數的this的指向是一樣的,而其實此時我們遵循的原則應該是第一條:當作為函數調用時础废,this要么指向全局對象window(非嚴格模式下)要么為undefined(嚴格模式下)汛骂,也就是外層函數是作為方法調用,而嵌套函數依然是作為函數調用的评腺,它們各自遵循各自的規(guī)則帘瞭。如果想讓嵌套函數和外層函數的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的區(qū)別蒿讥,后面再詳細講吧~

作為構造函數就很強了蝶念,這就涉及到js里面最難也最重要到部分:原型和繼承抛腕,它們重要到這篇文章都沒資格展開,所以就略過吧~嗯...我的意思是下一次總結媒殉。

call()担敌,apply()和bind()

相同之處:

  • 第一個參數都是指定this的值

不同之處:

  • 從第二個參數開始,call()和bind()是函數的參數列表适袜,apply()是參數數組柄错。
  • call()和apply()是立即調用函數舷夺,bind()是創(chuàng)建一個新函數苦酱,將綁定的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()后能立即執(zhí)行函數尾序,需要在最后加一對括號。

apply()和call()
前面也說了躯砰,這兩個函數的唯一區(qū)別就是第二個參數的格式每币,apply()的第二個參數是數組,call()從第二個參數開始是函數的參數列表琢歇,并且參數順序需要和函數的參數順序一致兰怠,如下:

let obj = {}; //模擬this
function fn(arg1,arg2) {}
//調用
fn.call(obj, arg1, arg2);
fn.apply(obj, [arg1, arg2]);

注意:目前的主流瀏覽器幾乎都支持apply()方法的第二個參數是類數組對象,我在Chrome, Firefox, Opera, Safari上面都測試過李茫,只要是類數組對象就可以揭保,不過低版本可能會不支持,所以建議先將類數組轉換成數組再傳給apply()方法魄宏。

用法一:類數組對象借用數組方法
常見的類數組對象有:

  • arguments對象,
  • getElementsByTagName(), getElementsByClassName (), getElementsByName(), querySelectorAll()方法獲取到的節(jié)點列表秸侣。

注:類數組對象就是擁有l(wèi)ength屬性的特殊對象

例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); //1
Array.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'); //hello
log('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() 函數會創(chuàng)建一個新函數略吨,稱為綁定函數集币,綁定函數與原函數具有相同的函數體。當綁定函數被調用時 this 值綁定到 bind() 的第一個參數翠忠,并且該參數不能被重寫鞠苟,也就是綁定的this就不再改變了。

用法一:解決將方法賦值給另一個變量時this指向改變的問題
當函數作為對象的屬性被調用時秽之,如果這時候是先將方法賦值給一個變量当娱,再通過這個變量來調用方法,此時this的指向就會發(fā)生變化考榨,不再是原來的對象了跨细,這時候,就算該函數使用箭頭函數的寫法也無濟于事了河质。解決方法是在賦值時使用bind()方法綁定this冀惭。:
例12:

name = "Tiya"; //全局作用域的變量
let obj1 = {
    name: 'melody', //局部作用域的變量
    sayHello: function() { 
        console.log(this.name);
    },
}
let sayHello1 = obj1.sayHello;
sayHello1() //Tiya,this的指向發(fā)生了變化掀鹅,指向全局作用域

let sayHello = obj1.sayHello.bind(obj1);
sayHello() //melody

用法二:解決dom元素上綁定事件散休,當事件觸發(fā)時this指向改變的問題
這個問題最常出現在使用某些框架的時候,比如React乐尊,寫過React的小伙伴肯定對于this.xxx.bind(this)這種寫法再熟悉不過了戚丸,因為React內部并沒有幫我們綁定好this,所以需要我們手動綁定this科吭,否則就會出錯昏滴。
例13:

//模擬的dom元素
<div id="container"></div>

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元素上監(jiān)聽了點擊事件,當該事件觸發(fā)時对人,將user對象上的一個變量值顯示在該元素上谣殊,但如果直接使用ele.addEventListener("click", user.clickHandler),此時牺弄,clickHandler事件內部的this已經變成了<div id="container"></div>這個節(jié)點而不再是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了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末阔挠,一起剝皮案震驚了整個濱河市飘庄,隨后出現的幾起案子脑蠕,更是在濱河造成了極大的恐慌购撼,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谴仙,死亡現場離奇詭異迂求,居然都是意外死亡,警方通過查閱死者的電腦和手機晃跺,發(fā)現死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門揩局,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人掀虎,你說我怎么就攤上這事凌盯。” “怎么了烹玉?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵驰怎,是天一觀的道長。 經常有香客問我二打,道長县忌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任继效,我火速辦了婚禮症杏,結果婚禮上,老公的妹妹穿的比我還像新娘瑞信。我一直安慰自己厉颤,他們只是感情好,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布凡简。 她就那樣靜靜地躺著逼友,像睡著了一般绩郎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上翁逞,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天肋杖,我揣著相機與錄音,去河邊找鬼挖函。 笑死状植,一個胖子當著我的面吹牛,可吹牛的內容都是我干的怨喘。 我是一名探鬼主播津畸,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼必怜!你這毒婦竟也來了肉拓?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤梳庆,失蹤者是張志新(化名)和其女友劉穎暖途,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體膏执,經...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡驻售,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了更米。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡征峦,死狀恐怖迟几,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情栏笆,我是刑警寧澤类腮,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站竖伯,受9級特大地震影響存哲,放射性物質發(fā)生泄漏。R本人自食惡果不足惜七婴,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一祟偷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧打厘,春花似錦修肠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽饲化。三九已至,卻和暖如春吗伤,著一層夾襖步出監(jiān)牢的瞬間吃靠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工足淆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留巢块,地道東北人。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓巧号,卻偏偏與公主長得像族奢,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子丹鸿,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355