聲明函數(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ù),原因如下:
- 具名函數(shù)有更詳細(xì)的錯誤信息和調(diào)用堆棧信息,更方便調(diào)試
- 當(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ù)的特點:
- 箭頭函數(shù)沒有自己的執(zhí)行上下文(execution context), 也就是,它沒有自己的this.
- 它是匿名函數(shù)
- 箭頭內(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")
完全不推薦使用這種方式,原因如下:
- Function對象是在函數(shù)創(chuàng)建時解析的,這比函數(shù)聲明和函數(shù)表達(dá)式更低效。
- 不論在哪里用這種方式聲明函數(shù),它都是在全局作用域中被創(chuàng)建,所以它不能形成閉包。
調(diào)用函數(shù)的方式
四種方式:
- 作為函數(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')
- 作為方法
作為方法的意思就是函數(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
- 作為構(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')
- 使用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了。