前言:本文將詳細的介紹JS中函數(shù)的相關(guān)概念(包括函數(shù)的call stack
论皆、this
、作用域、閉包俊扭、柯里化萨惑、高階函數(shù)等)庸蔼,總結(jié)函數(shù)中比較容易理解錯的坑贮匕,讓我們更加全面的認識函數(shù)刻盐。部分概念與代碼參考阮一峰JS教程敦锌。
1乙墙、什么是函數(shù)
函數(shù)是一段可以反復(fù)調(diào)用的代碼塊听想。函數(shù)還能接受輸入的參數(shù),不同的參數(shù)會返回不同的值肛走。
函數(shù)的本質(zhì)就是對象朽色,或者說可以執(zhí)行代碼的對象就是函數(shù)葫男。
因此我們甚至可以使用寫純對象的方式來寫一個函數(shù)梢褐,讓我們更了解函數(shù)的本質(zhì):
var fn = {} ;
fn.params = ['x','y'] ; // 參數(shù)
fn.fbody = 'console.log(1)' ; //函數(shù)體
fn.call = function(){
eval(fn.fbody)
}
fn.call() ; // 調(diào)用函數(shù)的call方法來執(zhí)行函數(shù)
// 1
如上述代碼盈咳,fn作為一個對象鱼响,使用call()
方法執(zhí)行的效果和執(zhí)行函數(shù)是一樣的,當然fn只是個長得像函數(shù)的對象筐骇,只是為了便于我們理解函數(shù)的本質(zhì)铛纬。
注:上面代碼中使用了eval()
方法告唆,那么就在這里提一下:
eval
命令的作用是悔详,將字符串當作語句執(zhí)行茄螃。eval()
方法是window
的全局對象归苍。
如eval('var a = 1;'); a // 1
一般情況下盡量避免使用該方法运怖。
2摇展、函數(shù)的五種聲明方法
注:函數(shù)體內(nèi)部的return
語句咏连,表示返回祟滴。JavaScript 引擎遇到return
語句垄懂,就直接返回return
后面的那個表達式的值,后面即使還有語句匙头,也不會得到執(zhí)行乾胶。也就是說,return
語句所帶的那個表達式脑融,就是函數(shù)的返回值肘迎。return
語句不是必需的锻煌,如果沒有的話宋梧,該函數(shù)就不返回任何值捂龄,或者說返回undefined
倦沧。
- 具名函數(shù)的聲明
function fn1() {
return undefined; // 如果不寫return 展融,瀏覽器默認添加
}
匿名函數(shù)的聲明
var fn2 = function() {}
結(jié)合上面兩種方式(謹慎使用)
var fn3 = function fn4() {}
使用
Function
函數(shù)對象來聲明
var fn5 = new Function(
'x',
'y',
'return x + y'
);
其中new Function()
中告希,最后一個參數(shù)表示函數(shù)體暂雹,前面的參數(shù)表示傳入函數(shù)的參數(shù)杭跪。
- 最酷炫的聲明方法:箭頭函數(shù)(詳情可見本文第14條)
var fn6 = (x,y) => {return x+y}
箭頭前面表示傳入函數(shù)的參數(shù),箭頭后面表示函數(shù)體檬贰。
如果只有一個參數(shù)翁涤,參數(shù)的圓括號可以省略:
var fn7 = x => {return x*2}
如果函數(shù)體只有一句話葵礼,可以同時省略函數(shù)體的大括號及return
:
var fn8 = x => x*x
3鸳粉、函數(shù)的調(diào)用
眾所周知届谈,函數(shù)的調(diào)用很簡單艰山,比如有這樣一個函數(shù)
function fn (){ return undefined; }
我們只需fn();
即可執(zhí)行該函數(shù)曙搬。
但是作為初學者织鲸,更建議使用call()
方法搂擦,比如有函數(shù):
function fn(x,y){ return x+y; }
就可以這么調(diào)用:fn.call(undefined,1,2) // 結(jié)果為3
等價于這么寫fn(1,2)
之所以建議初學者使用call()
方法,因為這么寫更便于理解this
和arguments
比如上面的那句調(diào)用哗脖,call()
的第一個參數(shù)就是this
瀑踢,后面的參數(shù)就是arguments
4、函數(shù)的常用屬性和方法
-
name
屬性
每一個函數(shù)都有name
屬性才避,你以為name
屬性就真的是表示函數(shù)的名字而且很好理解嗎橱夭?
明顯答案是否定的,使用不同的函數(shù)聲明方式桑逝,其name
屬性的值可能就不是你想象中的樣子。下面舉例說明:
// 具名函數(shù)的name屬性 楞遏,表示函數(shù)的名字
function fn1() {}
fn1.name // "fn1"
// 匿名函數(shù)的name屬性茬暇,指的是接收函數(shù)的變量名
var fn2 = function () {}
fn2.name // "fn2"
// 下面這種聲明方法首昔,在外部獲取不到fn4函數(shù)
// 注:fn3.name返回函數(shù)表達式的名字。注意糙俗,真正的函數(shù)名還是fn3勒奇,而fn4這個名字只在函數(shù)體內(nèi)部可用。
var fn3 = function fn4(){}
fn3.name // "fn4"
// 使用Function() 方法構(gòu)造函數(shù)巧骚,函數(shù)的name屬性值為"anonymous"
var fn5 = new Function()
fn5.name // "anonymous"
// 箭頭函數(shù)的name屬性赊颠,指的也是接收該函數(shù)的變量名
var fn6 = () => {}
fn6.name // "fn6"
-
length
屬性
函數(shù)的length
屬性返回函數(shù)預(yù)期傳入的參數(shù)個數(shù),即函數(shù)定義之中的參數(shù)個數(shù)劈彪。
如:
function f(a, b) {}
f.length // 2
上面代碼定義了空函數(shù)f竣蹦,它的length屬性就是定義時的參數(shù)個數(shù)。不管調(diào)用時輸入了多少個參數(shù)沧奴,length屬性始終等于2草添。
注:length
屬性提供了一種機制,判斷定義時和調(diào)用時參數(shù)的差異扼仲,以便實現(xiàn)面向?qū)ο缶幊痰摹狈椒ㄖ剌d“(overload)。
-
toString()
方法
函數(shù)的toString
方法返回一個字符串抄淑,內(nèi)容是函數(shù)的源碼屠凶,包括函數(shù)中的注釋也會被打印出來。如:
function f() {
var a = 1
/* 這是一個
多行注釋*/
}
f.toString()
// "function f() {
// var a = 1
// /* 這是一個0
// 多行注釋*/
// }"
5肆资、call
& apply
& bind
的用法
在 javascript 中矗愧,call
和 apply
都是為了改變某個函數(shù)運行時的上下文(context)而存在的,換句話說郑原,就是為了改變函數(shù)體內(nèi)部 this 的指向唉韭。
call()
方法調(diào)用一個函數(shù), 其具有一個指定的this
值和分別地提供的參數(shù)(參數(shù)的列表)。apply()
方法調(diào)用一個函數(shù), 其具有一個指定的this
值犯犁,以及作為一個數(shù)組(或類似數(shù)組的對象)提供的參數(shù)属愤。bind()
方法創(chuàng)建一個新的函數(shù),被調(diào)用時酸役,將其this
關(guān)鍵字設(shè)置為提供的值住诸,在調(diào)用新函數(shù)時,在任何提供之前提供一個給定的參數(shù)序列涣澡。對于
apply
贱呐、call
二者而言,作用完全一樣入桂,只是接受參數(shù)的方式不太一樣奄薇,call
需要把參數(shù)按順序傳遞進去,而apply
則是把參數(shù)放在數(shù)組里抗愁。apply
馁蒂、call
呵晚、bind
三者都是用來改變函數(shù)的this對象的指向的;
apply
远搪、call
劣纲、bind
三者第一個參數(shù)都是this要指向的對象,也就是想指定的上下文谁鳍;
apply
癞季、call
、bind
三者都可以利用后續(xù)參數(shù)傳參倘潜;
bind
是返回對應(yīng)函數(shù)绷柒,便于稍后調(diào)用;apply
涮因、call
則是立即調(diào)用 废睦。
舉例說明:
var obj = {
x: 81,
};
var foo = {
getX: function() {
return this.x;
}
}
console.log(foo.getX.bind(obj)()); //81
console.log(foo.getX.call(obj)); //81
console.log(foo.getX.apply(obj)); //81
// 三個輸出的都是81,但是注意看使用 bind() 方法的养泡,他后面多了對括號嗜湃。
//也就是說,區(qū)別是澜掩,當你希望改變上下文環(huán)境之后并非立即執(zhí)行购披,而是回調(diào)執(zhí)行的時候,使用 bind() 方法肩榕。而 apply/call 則會立即執(zhí)行函數(shù)刚陡。
6、call stack 調(diào)用棧
"調(diào)用棧"(call stack)表示函數(shù)或子例程像堆積木一樣存放株汉,以實現(xiàn)層層調(diào)用筐乳。
舉個函數(shù)嵌套調(diào)用的例子:
function a(){
console.log('a1')
b.call()
console.log('a2')
return 'a'
}
function b(){
console.log('b1')
c.call()
console.log('b2')
return 'b'
}
function c(){
console.log('c')
return 'c'
}
a.call()
console.log('end')
其執(zhí)行順序如圖所示:
下面是嵌套調(diào)用和遞歸調(diào)用時,函數(shù)的執(zhí)行順序:
7乔妈、函數(shù)中的this
和 arguments
(本文只介紹this和arguments是什么蝙云,暫時不介紹他們的用法,關(guān)于this
更深入的認識路召,可見我的另一篇關(guān)于JS面向?qū)ο蟮牟┛?/a> 第7條)
首先舉個例子:
聲明function fn(x,y){ return x+y }
調(diào)用fn.call( undefined , 1 ,2 )
其中 贮懈,undefined
指的就是函數(shù)fn的this
,即优训,this
是call()
的第一個參數(shù)朵你。1,2
指的就是函數(shù)的arguments
,需要注意的一點是:arguments
是一個類數(shù)組對象揣非。注意:在普通模式下抡医,this是undefined時,瀏覽器會將其變?yōu)?code>window
在嚴格模式下,this
是undefined
忌傻,打印出來的就是undefined
大脉。舉例:
// 普通模式下:
function f(){
console.log(this)
}
f.call(1) // Number {1}
// 嚴格模式下:
function f(){
'use strict'
console.log(this)
}
f.call(1) // 1
即,普通模式下水孩,瀏覽器會把this
轉(zhuǎn)化為對象镰矿,嚴格模式下會禁止這樣的轉(zhuǎn)換。
-
注意:
this
必須是對象俘种,或者這么說秤标,this
就是函數(shù)與對象之間的羈絆。
舉個例子:fn.call(10)
宙刘,意思是想讓函數(shù)fn
的this
是數(shù)字10苍姜,但實際上
JS會將其變成一個數(shù)字對象,即Number(10)
悬包。
8衙猪、詞法作用域(也叫靜態(tài)作用域)
作用域(scope)指的是變量存在的范圍。在 ES5 的規(guī)范中布近,Javascript 只有兩種作用域:一種是全局作用域垫释,變量在整個程序中一直存在,所有地方都可以讀瘸徘啤棵譬;另一種是函數(shù)作用域,變量只在函數(shù)內(nèi)部存在季蚂。ES6 又新增了塊級作用域。
規(guī)則:
按照抽象語法樹琅束,就近原則
注意:我們只能確定變量是哪個變量扭屁,但是不能確定變量的值
在判斷作用域時,還要注意函數(shù)作用域內(nèi)部會產(chǎn)生的“變量提升”現(xiàn)象涩禀。var命令聲明的變量料滥,不管在什么位置,變量聲明都會被提升到函數(shù)體的頭部艾船。
9葵腹、什么是閉包
(本節(jié)只介紹閉包的概念,暫不深入屿岂,這里提供方方的一篇介紹閉包的文章用于深入了解践宴,或可見我寫的一篇關(guān)于前端基礎(chǔ)知識十題中的第四題)
如果一個函數(shù)使用了它范圍外的變量,那么(這個函數(shù)和這個變量)就叫做閉包爷怀。
閉包(closure)是 Javascript 語言的一個難點阻肩,也是它的特色,很多高級應(yīng)用都要依靠閉包實現(xiàn)运授。
理解閉包烤惊,首先必須理解變量作用域乔煞。前面提到,JavaScript 有兩種作用域:全局作用域和函數(shù)作用域柒室。函數(shù)內(nèi)部可以直接讀取全局變量渡贾。
10、柯里化
柯里化是一個聽起來很高大上的概念雄右,但也很好理解空骚。簡單來說就是一個返回函數(shù)的函數(shù),然后將函數(shù)的參數(shù)中的一個或幾個參數(shù)確定下來不脯,如將 f(x,y) 變成 f(x=1)(y) 或 f(y=1)x府怯,柯里化可以將真實計算拖延到最后再做。這里貼兩個詳細介紹柯里化的文章:http://www.yinwang.org/blog-cn/2013/04/02/currying 防楷、https://zhuanlan.zhihu.com/p/31271179
- 舉兩個例子:
// 第一個例子:
// 柯里化之前
function sum(x,y){
return x+y
}
// 柯里化之后
function addOne(y){
return sum(1, y)
}
// 第二個例子:
// 柯里化之前
function Handlebar(template, data){
return template.replace('{{name}}', data.name)
}
// 柯里化之后
function Handlebar(template){
return function(data){
return template.replace('{{name}}', data.name)
}
}
上面的代碼中牺丙,第一個例子好理解,我們來看第二個例子复局。
柯里化之前冲簿,我們要調(diào)用函數(shù)Handlebar
,每次都要傳一個template模板亿昏,比如Handlebar('<h1>I'm {{name}}</h1>',{name:'noch'})
峦剔,如果這個一個模板(即參數(shù)template
為'<h1>I'm {{name}}</h1>'
) 我們需要用到很多次,難道每次調(diào)用都要賦一次值嗎角钩?為了簡化函數(shù)Handlebar
的這種情況下的調(diào)用吝沫,我們就對該函數(shù)進行了柯里化。之后再使用時递礼,首先這么調(diào)用var newHandlebar= Handlebar('<h1>I'm {{name}}</h1>')
惨险,相當于給template
賦了值,然后調(diào)用函數(shù)newHandlebar
:newHandlebar({name:'enoch'})
即可
- 一道關(guān)于柯里化的題目:
請寫出一個柯里化其他函數(shù)的函數(shù) curry脊髓,這個函數(shù)能夠?qū)⒔邮芏鄠€參數(shù)的函數(shù)辫愉,變成多個接受一個參數(shù)的函數(shù),具體見示例(這是 lodash.curry 的文檔示例):
function curry(???){
???
return ???
}
var abc = function(a, b, c) {
return [a, b, c];
};
var curried = curry(abc);
curried(1)(2)(3);
// => [1, 2, 3]
curried(1, 2)(3);
// => [1, 2, 3]
curried(1, 2, 3);
// => [1, 2, 3]
答案如下:
function curry(func , fixedParams){
if ( !Array.isArray(fixedParams) ) { fixedParams = [ ] }
return function(){
let newParams = Array.prototype.slice.call(arguments); // 新傳的所有參數(shù)
if ( (fixedParams.length+newParams.length) < func.length ) {
return curry(func , fixedParams.concat(newParams));
}else{
return func.apply(undefined, fixedParams.concat(newParams));
}
};
}
11将硝、高階函數(shù)
- 在數(shù)學和計算機科學中恭朗,高階函數(shù)是至少滿足下列一個條件的函數(shù)(也就是說至少滿足下列一個條件的函數(shù)就被稱為高階函數(shù)):
- 接受一個或多個函數(shù)作為輸入:如:
forEach
、sort
依疼、map
痰腮、filter
、reduce
- 輸出一個函數(shù):如:
bind
律罢、lodash.curry
- 不過它也可以同時滿足兩個條件:如:
Function.prototype.bind
- 接受一個或多個函數(shù)作為輸入:如:
- 那么高階函數(shù)有什么用呢:最重要的一條就是可以將函數(shù)任意的組合诽嘉。(如react中的很多應(yīng)用)
12、回調(diào)(callback
)
- 名詞形式:被當做參數(shù)的函數(shù)就是回調(diào)
- 動詞形式:調(diào)用這個回調(diào)
- 舉個例子:
fn(function(){})
:函數(shù)fn
中的參數(shù)是一個函數(shù),在fn
中調(diào)用了這個函數(shù)虫腋,那么這個函數(shù)就是回調(diào)函數(shù)骄酗,調(diào)用的過程就是回調(diào)。 - 注意:回調(diào)跟異步?jīng)]有任何關(guān)系
13悦冀、構(gòu)造函數(shù)
簡單的來說返回對象的函數(shù)就是構(gòu)造函數(shù) 趋翻。(具體用法可看我的一篇介紹面向?qū)ο蟮牟┛?/a> 第五條)。
比如new Number()
得到的就是一個數(shù)值對象盒蟆,一般的踏烙,構(gòu)造函數(shù)的函數(shù)名首字母要大寫。
再舉個栗子历等,我們自己寫一個構(gòu)造函數(shù)讨惩。
function Person(){} // 這就是個構(gòu)造函數(shù),何以見得寒屯,就要看你如何調(diào)用
var person1 = Person() // 這么寫的話荐捻,函數(shù)Person什么也沒有返回
var person2 = new Person() //使用new,函數(shù)Person中默認會多出幾行寡夹,其中就包括會返回一個空對象
// 如果想在函數(shù)中添加屬性处面,可以使用this,即:
function Person(){
this.name = ''
return this // 這一行可以不用寫
}
var person3 = new Person({}) // 這么調(diào)用即可菩掏,會為傳入的空對象增加一個name的屬性
14魂角、箭頭函數(shù)
箭頭函數(shù)的形式在本文第2條已經(jīng)介紹,這里不再贅述智绸。接下來主要介紹一下箭頭函數(shù)和普通的函數(shù)的區(qū)別:
-
箭頭函數(shù)沒有
this
野揪,對,這就是它們之間唯一的區(qū)別瞧栗。具體的說就是:箭頭函數(shù)中如果使用了this
斯稳,此時this
就是相當于一個普通的參數(shù),由于箭頭函數(shù)本身沒有this
沼溜,那么他就會找它的父級中的this
來確定this
的值平挑。 - 舉個例子:
setTimeout(function(){
console.log(this)
}.bind({name:'enoch'}),1000)
// 一秒后打印出 {name: "enoch"}
// 如果沒有用bind綁定this 游添,打印出的是window對象
setTimeout(function(){
console.log(this) // A
setTimeout(function(){
console.log(this) // B
},1000)
}.bind({name:'enoch'}),1000)
// 第一秒打印出 {name: "enoch"}系草,第二秒打印出window對象
第二個代碼塊中:setTimeout
中又有一個setTimeout
,兩個setTimeout
都會執(zhí)行打印this
這句話唆涝,而A處的this
和B處的this
顯然不是一個值(A處的this
為{name: "enoch"}
找都,B處的 this
為window
對象)。
如果你想讓兩個this
都是{name: "enoch"}
廊酣,可以這么做能耻,里面的函數(shù)也用bind
綁定外面的this
:
setTimeout(function(){
console.log(this) // A
setTimeout(function(){
console.log(this) // B
}.bind(this),1000)
}.bind({name:'enoch'}),1000)
或者使用箭頭函數(shù)
setTimeout(function(){
console.log(this) // A
setTimeout(()=>{
console.log(this) // B
},1000)
}.bind({name:'enoch'}),1000)
那么如果我硬要用call
來指定箭頭函數(shù)的this
可以嘛,答案是否定的,請看代碼:
var fn = ()=>{console.log(this)}
fn() // 此時打印出的是window對象
fn.call({name:'enoch'})
// 此時打印出來的還是window對象晓猛,箭頭函數(shù)沒有接收你指定的this
所以使用箭頭函數(shù)可以很容易的做到函數(shù)里面的this
就是外面的this
饿幅,不用擔心函數(shù)的this
會莫名的改變。
15戒职、一些易錯的坑
看了這么多概念栗恩,覺得函數(shù)貌似也不太難呢?嘿嘿洪燥,那么請繼續(xù)磕秤,下面將寫一些函數(shù)中易錯的坑。
- 題目1:求f1.call()的結(jié)果
var a = 1
function f1(){
alert(a) // 是多少
var a = 2
}
f1.call()
- 題目2:
var a = 1
function f1(){
var a = 2
f2.call()
}
function f2(){
console.log(a) // 是多少
}
f1.call()
- 題目3:點擊第3個 li 時捧韵,打印 2 還是打印 6市咆?
var liTags = document.querySelectorAll('li')
for(var i = 0; i<liTags.length; i++){
liTags[i].onclick = function(){
console.log(i) // 點擊第3個 li 時,打印 2 還是打印 6再来?
}
}
- 題目4:這兩個代碼塊的結(jié)果一樣嗎蒙兰?
function f(){
console.log(this)
}
f.call(1)
function f(){
'use strict'
console.log(this)
}
f.call(1)
- 題目5:下面的幾個代碼塊,a的值分別為什么
var a = console.log(1);
function f(){
return 1
}
a = f
function f(){
return 1
}
var a = f.call()
- 題目6:下面的幾個代碼塊其弊,a的值分別為什么
var a = 1,2
var a = (1,2)
var a = (1, console.log(2))
- 題目7:
function f(){
return function f2(){}
}
var a = f.call()
function f(){
return function f2(){}
}
var a = f.call()
var b = a.call()
function f(){
return function f2(){}
}
var a = f.call().call()
- 題目8:
function f1(){
console.log(this)
function f2(){
}
}
var obj = {name: 'obj'}
f1.call( obj )
function f1(){
function f2(){
console.log(this)
}
f2.call()
}
var obj = {name: 'obj'}
f1.call( obj )
- 題目9:
function f1(){
console.log(this) // 第一個 this
function f2(){
console.log(this) // 第二個 this
}
f2.call()
}
var obj = {name: 'obj'}
f1.call( obj )
為什么兩個 this 值不一樣癞己?
本題答案:
=>每個函數(shù)都有自己的 this,為什么會一樣梭伐?
=>this 就是 call 的第一個參數(shù)痹雅,第一個 this 對應(yīng)的 call 是 f1.call(obj),第二個 this 對應(yīng)的 call 是 f2.call()
=>this 和 arguments 都是參數(shù)糊识,參數(shù)都要在函數(shù)執(zhí)行(call)的時候才能確定