JS學(xué)習(xí)筆記之再理解一等公民--函數(shù)(基礎(chǔ)篇)

聲明函數(shù)的方式

這里其實我比較迷惑,我以前認(rèn)為聲明函數(shù)只有函數(shù)聲明方式和函數(shù)表達(dá)式,其它的所有情況比如在類里面的,對象里面的都?xì)w于這兩個,最近看資料又覺得其它方式可以單獨成為一種聲明函數(shù)的方式,所以跑回來完善了一下文章。

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

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

函數(shù)聲明會被提升到作用域頂部,也就是說,你可以在某個函數(shù)聲明前調(diào)用它而不會報錯。
函數(shù)聲明的函數(shù)名是必須的,所以它有name屬性。


方式2. 函數(shù)表達(dá)式(Function expression)

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

在某個對象中的函數(shù)表達(dá)式:

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

函數(shù)表達(dá)式又分為具名函數(shù)和匿名函數(shù),以上,如果有“函數(shù)名”就是具名函數(shù),反之是匿名函數(shù)。

對于具名函數(shù),函數(shù)名的作用域只在函數(shù)內(nèi)部,而變量名的作用域是全局的,所以在函數(shù)內(nèi)部即可以使用函數(shù)名也可以使用變量名調(diào)用自身,在函數(shù)外部則只能使用變量名調(diào)用。

//函數(shù)表達(dá)式--具名函數(shù)
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); //錯誤

具名函數(shù)有name屬性,匿名函數(shù)沒有。

推薦使用具名函數(shù),原因如下:

  1. 具名函數(shù)有更詳細(xì)的錯誤信息和調(diào)用堆棧信息,更方便調(diào)試
  2. 當(dāng)在函數(shù)內(nèi)部有遞歸調(diào)用時,使用函數(shù)名調(diào)用比使用變量名調(diào)用效率更高

函數(shù)表達(dá)式不會被提升到作用域頂部,原因是函數(shù)表達(dá)式是將函數(shù)賦值給一個變量,而js對提升變量的操作是只提升變量的聲明而不會提升變量的賦值,所以不能在某個函數(shù)表達(dá)式之前調(diào)用它。


注意

1. 函數(shù)表達(dá)式可以出現(xiàn)在任何地方,函數(shù)聲明不能出現(xiàn)在循環(huán)、條件判斷、try/catch、with語句中。

注:只有在嚴(yán)格模式下,在塊語句中使用了函數(shù)聲明才會報錯。

2. 立即執(zhí)行函數(shù)只能是函數(shù)表達(dá)式而不能是函數(shù)聲明,但使用函數(shù)聲明不會報錯,只是不會執(zhí)行
例2:

//函數(shù)聲明方式
function square(a){
    console.log(a * a);
}(5)
//函數(shù)表達(dá)式方式
let square = function(a){
    console.log(a * a);
}(5)
//錯誤的方式
function(a){
    console.log(a * a);
}(5)

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

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

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


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

在對象里:

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

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

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

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


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

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

箭頭函數(shù)的特點:

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

方式5. 函數(shù)構(gòu)造函數(shù)(function constructor)

在js中,每個函數(shù)實際都是一個Function對象,而Function對象是由Function構(gòu)造函數(shù)創(chuàng)建的。

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

比如:

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

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

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

調(diào)用函數(shù)的方式

四種方式:

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

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

let _sayHello = person.sayHello
// 此時的this指向的對象已經(jīng)變成了`window`而不是`person`,`this.name`的值為`undefined`
_sayHello() // hello undefined
  1. 作為構(gòu)造函數(shù)
    作為構(gòu)造函數(shù)調(diào)用時,this指向構(gòu)造函數(shù)的實例
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的指向的,即任何函數(shù)都可以作為任何對象的方法來調(diào)用

這四種方式最大的不同就是this的指向問題,首先,作為函數(shù)調(diào)用的this是最好理解的,而作為方法調(diào)用看起來也不難,無非就是方法是哪個對象的屬性this就指向誰嘛,但兩個結(jié)合起來可能就比較容易迷惑人:
例4:

let obj = {
    name: 'melody',
    age: 18,

    sayHello: function() { //sayHello()是obj對象的屬性
        console.log(this.name);
        sayAge();
        function sayAge() { //sayAge()是sayHello()的內(nèi)部函數(shù)
            console.log(this.age)
        }
    }
}
obj.sayHello();

首先,sayHello()方法定義在obj對象上,那么sayHello()里面的this就指向了obj,所以第一個會打印出melody,接著sayHello()調(diào)用了它的內(nèi)部函數(shù)sayAge(),此時sayAge()里面的this.age應(yīng)該是什么?是obj對象上的age嗎?其實不是,在sayAge()里面打印出this會發(fā)現(xiàn)this是指向window對象的,所以第二個console會打印出undefined

因為這時候外面多了一個對象,我們就容易被這個對象迷惑,以為嵌套函數(shù)的this和外層函數(shù)的this的指向是一樣的,而其實此時我們遵循的原則應(yīng)該是第一條:當(dāng)作為函數(shù)調(diào)用時,this要么指向全局對象window(非嚴(yán)格模式下)要么為undefined(嚴(yán)格模式下),也就是外層函數(shù)是作為方法調(diào)用,而嵌套函數(shù)依然是作為函數(shù)調(diào)用的,它們各自遵循各自的規(guī)則。如果想讓嵌套函數(shù)和外層函數(shù)的this都指向同一個,以前的方法是將this的值保存在一個變量里面:

...
    sayHello: function() {
        let that = this;
        function sayAge() {
            console.log(that.age) //18
        }
    }
...

或者使用ES6新增的箭頭函數(shù):

...
    sayHello: function() {
        console.log(this.name); //melody
        let sayAge = () => {
            console.log(this.age) //18
        }
        sayAge();
    }
...

關(guān)于箭頭函數(shù)和普通函數(shù)的this的區(qū)別,后面再詳細(xì)講吧~

作為構(gòu)造函數(shù)就很強了,這就涉及到j(luò)s里面最難也最重要到部分:原型和繼承,它們重要到這篇文章都沒資格展開,所以就略過吧~嗯...我的意思是下一次總結(jié)。

call(),apply()和bind()

相同之處:

  • 第一個參數(shù)都是指定this的值

不同之處:

  • 從第二個參數(shù)開始,call()和bind()是函數(shù)的參數(shù)列表,apply()是參數(shù)數(shù)組。
  • call()和apply()是立即調(diào)用函數(shù),bind()是創(chuàng)建一個新函數(shù),將綁定的this值傳給新函數(shù),但新函數(shù)不會立即調(diào)用,除非你手動調(diào)用它。

舉例說明這三個方法的基本用法:
例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()方法的第二個參數(shù)是數(shù)組,call()和bind()是參數(shù)列表,其次,apply()和call()會立即調(diào)用函數(shù)而bind()不會,所以要想bind()后能立即執(zhí)行函數(shù),需要在最后加一對括號。

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

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

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

用法一:類數(shù)組對象借用數(shù)組方法
常見的類數(shù)組對象有:

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

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

例6:將類數(shù)組對象轉(zhuǎn)換成數(shù)組

Array.prototype.slice.call(arguments);
[].slice.call(arguments);
//或者
Array.prototype.slice.apply(arguments);
[].slice.apply(arguments);

因為此時不需要給slice()方法傳入?yún)?shù),所以call()apply()都可以實現(xiàn)。

例7:借用其它數(shù)組方法

//類數(shù)組對象
let objLikeArr = {'0': 'melody','1': 18,'2': 'sleep',length: 3}

//借用數(shù)組的indexOf()方法
Array.prototype.indexOf.call(objLikeArr, 18); //1
Array.prototype.indexOf.apply(objLikeArr, ['sleep']); //2

用法二:求數(shù)組最大(小)值
Math.max()Math.min()可以找出一組數(shù)字中的最大(小)值,但是當(dāng)參數(shù)為數(shù)組時,結(jié)果是NaN,這時候用apply()方法可以解決這個問題,因為apply()的第二個參數(shù)接收的是數(shù)組。
例8:

let arr1 = [1,2,12,8,9,34];
Math.max.apply(null, arr1); //34

數(shù)字字符串也可以:
例9:

let a = '1221679183';
Math.max.apply(null, a.split('')); //9

用法三:借用toString()方法判斷數(shù)據(jù)類型
這不是最好用的判斷數(shù)據(jù)類型的方法,但是是最有效的方法。
例10:

//基本數(shù)據(jù)類型
    let null1 = null;
    let undefined1 = undefined;
    let str = "hello";
    let num = 123;
    let bool = true;
    let symbol = Symbol("hello");

//引用數(shù)據(jù)類型
    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]

用法四:實現(xiàn)函數(shù)不定參
一個常見的用法是實現(xiàn)console可接收多個參數(shù)的功能:
例11:

function log() {
    console.log.apply(console, arguments)
}
log('hello'); //hello
log('hello', 'melody'); // hello melody

es6新增的 ... 運算符其實更方便:

function log(...arg) {
    console.log(...arg);
}

還可以加默認(rèn)的打印值:

    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() 函數(shù)會創(chuàng)建一個新函數(shù),稱為綁定函數(shù),綁定函數(shù)與原函數(shù)具有相同的函數(shù)體。當(dāng)綁定函數(shù)被調(diào)用時 this 值綁定到 bind() 的第一個參數(shù),并且該參數(shù)不能被重寫,也就是綁定的this就不再改變了。

用法一:解決將方法賦值給另一個變量時this指向改變的問題
當(dāng)函數(shù)作為對象的屬性被調(diào)用時,如果這時候是先將方法賦值給一個變量,再通過這個變量來調(diào)用方法,此時this的指向就會發(fā)生變化,不再是原來的對象了,這時候,就算該函數(shù)使用箭頭函數(shù)的寫法也無濟于事了。解決方法是在賦值時使用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元素上綁定事件,當(dāng)事件觸發(fā)時this指向改變的問題
這個問題最常出現(xiàn)在使用某些框架的時候,比如React,寫過React的小伙伴肯定對于this.xxx.bind(this)這種寫法再熟悉不過了,因為React內(nèi)部并沒有幫我們綁定好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)聽了點擊事件,當(dāng)該事件觸發(fā)時,將user對象上的一個變量值顯示在該元素上,但如果直接使用ele.addEventListener("click", user.clickHandler),此時,clickHandler事件內(nèi)部的this已經(jīng)變成了<div id="container"></div>這個節(jié)點而不再是user本身了,正確的做法是調(diào)用時給clickHandler綁定this

ele.addEventListener("click", user.clickHandler.bind(user));

實參、形參和arguments對象

簡單來說,形參是聲明函數(shù)時的參數(shù),實參是調(diào)用函數(shù)時傳入的參數(shù)。
例14:

function getName(name) { //此處為形參
    console.log(`my name is ${name}`);
}
getName('melody'); //此處為實參

js的函數(shù),調(diào)用時傳入的參數(shù)和聲明時的參數(shù)個數(shù)可以不一致,類型可以不一致(也沒有聲明類型的機會),這就是為什么js沒有函數(shù)重載概念的原因。
情況一:實參數(shù)量 >形參數(shù)量
此時函數(shù)會忽略多余的實參,就比如說前面的例子:

function log(name) {
    console.log(name);
}
log('world', 'hello'); //world

情況二:實參數(shù)量 <形參數(shù)量
此時多余的參數(shù)的值為undefined,比如:

function log(name, age) {
    console.log(name, age);
}
log('world'); //world undefined

arguments是函數(shù)內(nèi)部可以獲取到傳入的參數(shù)的類數(shù)組對象,要注意的是arguments的長度代表的是實參的數(shù)量,而不是形參的數(shù)量。

前面說到j(luò)s沒有函數(shù)重載的概念,但可以用arguments對象模擬函數(shù)的重載:

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更好的方式去處理函數(shù)的參數(shù),比如rest參數(shù),前面的例子也提到過:

function log(...arg) {
    console.log(...arg);
}
log(1,2)

它看起來比arguments更容易理解也更簡潔,js應(yīng)該也有想淘汰arguments的想法,所以建議大家能用es6語法實現(xiàn)的就不要用arguments了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,415評論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,104評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,647評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,130評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,366評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,887評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,737評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,174評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,608評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,914評論 2 372