聲明函數的方式
這里其實我比較迷惑,我以前認為聲明函數只有函數聲明方式和函數表達式动雹,其它的所有情況比如在類里面的槽卫,對象里面的都歸于這兩個,最近看資料又覺得其它方式可以單獨成為一種聲明函數的方式胰蝠,所以跑回來完善了一下文章歼培。
方式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屬性闪盔,匿名函數沒有弯院。
推薦使用具名函數同廉,原因如下:
- 具名函數有更詳細的錯誤信息和調用堆棧信息叼旋,更方便調試
- 當在函數內部有遞歸調用時,使用函數名調用比使用變量名調用效率更高
函數表達式不會被提升到作用域頂部,原因是函數表達式是將函數賦值給一個變量邀层,而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 變量名 = (形參列表) => {
//函數體
}
箭頭函數的特點:
- 箭頭函數沒有自己的執(zhí)行上下文(execution context), 也就是米绕,它沒有自己的this.
- 它是匿名函數
- 箭頭內部也沒有arguments對象
方式5. 函數構造函數(function constructor)
在js中瑟捣,每個函數實際都是一個Function對象,而Function對象是由Function構造函數創(chuàng)建的栅干。
const 變量名 = new Function([字符串形式的參數列表]迈套,字符串形式的函數體)
比如:
const adder = new Function("a", "b", "return a + b")
完全不推薦使用這種方式,原因如下:
- Function對象是在函數創(chuàng)建時解析的碱鳞,這比函數聲明和函數表達式更低效桑李。
- 不論在哪里用這種方式聲明函數,它都是在全局作用域中被創(chuàng)建,所以它不能形成閉包贵白。
調用函數的方式
四種方式:
- 作為函數
作為函數的意思就是在全局作用域下率拒、某個函數體內部或者某個塊語句內部調用
當以此方式調用函數時,一般不會使用到this關鍵字(這也是它和作為方法調用時的最大區(qū)別)禁荒,因為此時的this要么指向全局對象window(非嚴格模式下)要么為undefined(嚴格模式下)
let sayHello = function(name) {
console.log(`hello ${name}`)
}
sayHello('melody')
- 作為方法
作為方法的意思就是函數作為一個對象里的屬性被調用猬膨,此時函數的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
- 作為構造函數
作為構造函數調用時,this指向構造函數的實例
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的指向的沛申,即任何函數都可以作為任何對象的方法來調用。
這四種方式最大的不同就是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了。