函數(shù)是一段可以反復(fù)調(diào)用的代碼塊窃植。函數(shù)還能接受輸入的參數(shù)帝蒿,不同的參數(shù)會(huì)返回不同的值。
概述
函數(shù)的聲明
JavaScript有三種聲明函數(shù)的方法巷怜。
1.function 命令
function
命令聲明的代碼區(qū)塊葛超,就是一個(gè)函數(shù)。function
命令后面是函數(shù)名延塑,函數(shù)名后面是一對(duì)圓括號(hào)绣张,里面是傳入函數(shù)的參數(shù)。函數(shù)體放在大括號(hào)里面关带。
function print(s) {
console.log(s);
}
上面的代碼命名了一個(gè)print
函數(shù)侥涵,以后使用print()
這種形式,就可以調(diào)用相應(yīng)的代碼宋雏。這叫做函數(shù)的聲明(Function Declaration
)芜飘。
2.函數(shù)表達(dá)式
除了用function
命令聲明函數(shù),還可以采用變量賦值的寫法磨总。
var print = function(s) {
console.log(s);
};
這種寫法將一個(gè)匿名函數(shù)賦值給變量嗦明。這時(shí),這個(gè)匿名函數(shù)又稱函數(shù)表達(dá)式(Function Expression
)蚪燕,因?yàn)橘x值語句的等號(hào)右側(cè)只能放表達(dá)式娶牌。
采用函數(shù)表達(dá)式聲明函數(shù)時(shí)奔浅,function
命令后面不帶有函數(shù)名。如果加上函數(shù)名诗良,該函數(shù)名只在函數(shù)體內(nèi)部有效汹桦,在函數(shù)體外部無效。
var print = function x(){
console.log(typeof x);
};
x // ReferenceError: x is not defined
print() // function
上面代碼在函數(shù)表達(dá)式中鉴裹,加入了函數(shù)名x
营勤。這個(gè)x
只在函數(shù)體內(nèi)部可用,指代函數(shù)表達(dá)式本身壹罚,其他地方都不可用。這種寫法的用處有兩個(gè)寿羞,一是可以在函數(shù)體內(nèi)部調(diào)用自身猖凛,二是方便除錯(cuò)(除錯(cuò)工具顯示函數(shù)調(diào)用棧時(shí),將顯示函數(shù)名绪穆,而不再顯示這里是一個(gè)匿名函數(shù))辨泳。因此,下面的形式聲明函數(shù)也非常常見玖院。
var f = function f() {};
需要注意的是菠红,函數(shù)的表達(dá)式需要在語句的結(jié)尾加上分號(hào),表示語句結(jié)束难菌。而函數(shù)的聲明在結(jié)尾的大括號(hào)后面不用加分號(hào)试溯。總的來說郊酒,這兩種聲明函數(shù)的方式遇绞,差別很細(xì)微,可以近似認(rèn)為是等價(jià)的燎窘。
3.Function 構(gòu)造函數(shù)
第三種聲明函數(shù)的方式是Function
構(gòu)造函數(shù)摹闽。
var add = new Function(
'x',
'y',
'return x + y'
);
// 等同于
function add(x, y) {
return x + y;
}
上面代碼中,Function
構(gòu)造函數(shù)接受三個(gè)參數(shù)褐健,除了最后一個(gè)參數(shù)是add
函數(shù)的“函數(shù)體”付鹿,其他參數(shù)都是add
函數(shù)的參數(shù)。
可以傳遞任意數(shù)量的參數(shù)給Function
構(gòu)造函數(shù)蚜迅,只有最后一個(gè)參數(shù)會(huì)被當(dāng)做函數(shù)體舵匾,如果只有一個(gè)參數(shù),該參數(shù)就是函數(shù)體慢叨。
var foo = new Function(
'return "hello world";'
);
// 等同于
function foo() {
return 'hello world';
}
Function
構(gòu)造函數(shù)可以不使用new
命令纽匙,返回結(jié)果完全一樣。
總的來說拍谐,這種聲明函數(shù)的方式非常不直觀烛缔,幾乎無人使用馏段。
函數(shù)的重復(fù)聲明
如果同一個(gè)函數(shù)被多次聲明,后面的聲明就會(huì)覆蓋前面的聲明践瓷。
function f() {
console.log(1);
}
f() // 2
function f() {
console.log(2);
}
f() // 2
上面代碼中院喜,后一次的函數(shù)聲明覆蓋了前面一次。而且晕翠,由于函數(shù)名的提升喷舀,前一次聲明在任何時(shí)候都是無效的,這一點(diǎn)要特別注意淋肾。
圓括號(hào)運(yùn)算符硫麻,return 語句和遞歸
調(diào)用函數(shù)時(shí),要使用圓括號(hào)運(yùn)算符樊卓。圓括號(hào)之中拿愧,可以加入函數(shù)的參數(shù)。
function add(x, y) {
return x + y;
}
add(1, 1) // 2
上面代碼中碌尔,函數(shù)名后面緊跟一對(duì)圓括號(hào)浇辜,就會(huì)調(diào)用這個(gè)函數(shù)。
函數(shù)體內(nèi)部的return
語句唾戚,表示返回柳洋。JavaScript 引擎遇到return
語句,就直接返回return
后面的那個(gè)表達(dá)式的值叹坦,后面即使還有語句熊镣,也不會(huì)得到執(zhí)行。也就是說立由,return
語句所帶的那個(gè)表達(dá)式轧钓,就是函數(shù)的返回值。return
語句不是必需的锐膜,如果沒有的話毕箍,該函數(shù)就不返回任何值,或者說返回undefined
道盏。
函數(shù)可以調(diào)用自身而柑,這就是遞歸(recursion
)。下面就是通過遞歸荷逞,計(jì)算斐波那契數(shù)列的代碼媒咳。
function fib(num) {
if (num === 0) return 0;
if (num === 1) return 1;
return fib(num - 2) + fib(num - 1);
}
fib(6) // 8
上面代碼中,fib
函數(shù)內(nèi)部又調(diào)用了fib
种远,計(jì)算得到斐波那契數(shù)列的第6個(gè)元素是8涩澡。
第一等公民
JavaScript語言將函數(shù)看作一種值,與其它值(數(shù)值坠敷、字符串妙同、布爾值等等)地位相同射富。凡是可以使用值的地方,就能使用函數(shù)粥帚。比如胰耗,可以把函數(shù)賦值給變量和對(duì)象的屬性,也可以當(dāng)作參數(shù)傳入其他函數(shù)芒涡,或者作為函數(shù)的結(jié)果返回柴灯。函數(shù)只是一個(gè)可以執(zhí)行的值,此外并無特殊之處费尽。
由于函數(shù)與其他數(shù)據(jù)類型地位平等赠群,所以在JavaScript語言中又稱函數(shù)為第一等公民。
function add(x, y) {
return x + y;
}
// 將函數(shù)賦值給一個(gè)變量
var operator = add;
// 將函數(shù)作為參數(shù)和返回值
function a(op){
return op;
}
a(add)(1, 1)
// 2
函數(shù)名的提升
JavaScript引擎將函數(shù)名視同變量名旱幼,所以采用function
命令聲明函數(shù)時(shí)乎串,整個(gè)函數(shù)會(huì)像變量聲明一樣,被提升到代碼頭部速警。所以,下面的代碼不會(huì)報(bào)錯(cuò)鸯两。
f();
function f() {}
表面上闷旧,上面代碼好像在聲明之前就調(diào)用了函數(shù)f
。但是實(shí)際上钧唐,由于“變量提升”忙灼,函數(shù)f
被提升到了代碼頭部,也就是在調(diào)用之前已經(jīng)聲明了钝侠。但是该园,如果采用賦值語句定義函數(shù),JavaScript就會(huì)報(bào)錯(cuò)帅韧。
f();
var f = function (){};
// TypeError: undefined is not a function
上面的代碼等同于下面的形式里初。
var f;
f();
f = function () {};
上面代碼第二行,調(diào)用f
的時(shí)候忽舟,f
只是被聲明了双妨,還沒有被賦值,等于undefined
叮阅,所以會(huì)報(bào)錯(cuò)刁品。因此,如果同時(shí)采用function
命令和賦值語句聲明同一個(gè)函數(shù)浩姥,最后總是采用賦值語句的定義挑随。
var f = function () {
console.log('1');
}
function f() {
console.log('2');
}
f() // 1
函數(shù)的屬性和方法
name屬性
函數(shù)的name
屬性返回函數(shù)的名字。
function f1() {}
f1.name // "f1"
如果是通過變量賦值定義的函數(shù)勒叠,那么name
屬性返回變量名兜挨。
var f2 = function () {};
f2.name // "f2"
但是膏孟,上面這種情況,只有在變量的值是一個(gè)匿名函數(shù)時(shí)才是如此暑劝。如果變量的值是一個(gè)具名函數(shù)骆莹,那么name
屬性返回function
關(guān)鍵字之后的那個(gè)函數(shù)名。
var f3 = function myName() {};
f3.name // 'myName'
上面代碼中担猛,f3.name
返回函數(shù)表達(dá)式的名字咨演。注意饺饭,真正的函數(shù)名還是f3
,而myName
這個(gè)名字只在函數(shù)體內(nèi)部可用。
name
屬性的一個(gè)用處片排,就是獲取參數(shù)函數(shù)的名字。
var myFunc = function () {};
function test(f) {
console.log(f.name);
}
test(myFunc) // myFunc
上面代碼中惊楼,函數(shù)test
內(nèi)部通過name
屬性捻艳,就可以知道傳入的參數(shù)是什么函數(shù)。
length 屬性
函數(shù)的length
屬性返回函數(shù)預(yù)期傳入的參數(shù)個(gè)數(shù)比驻,即函數(shù)定義之中的參數(shù)個(gè)數(shù)该溯。
function f(a, b) {}
f.length // 2
上面代碼定義了空函數(shù)f
,它的length
屬性就是定義時(shí)的參數(shù)個(gè)數(shù)别惦。不管調(diào)用時(shí)輸入了多少個(gè)參數(shù)狈茉,length
屬性始終等于2。
length
屬性提供了一種機(jī)制掸掸,判斷定義時(shí)和調(diào)用時(shí)參數(shù)的差異氯庆,以便實(shí)現(xiàn)面向?qū)ο缶幊痰摹胺椒ㄖ剌d”(overload
)。
toString()
函數(shù)的toString
方法返回一個(gè)字符串扰付,內(nèi)容是函數(shù)的源碼堤撵。
function f() {
a();
b();
c();
}
f.toString()
// function f() {
// a();
// b();
// c();
// }
對(duì)于那些原生的函數(shù),toString()
方法返回function (){[native code]}
羽莺。
Math.sqrt.toString()
// "function sqrt() { [native code] }"
上面代碼中实昨,Math.sqrt
是JavaScript引擎提供的原生函數(shù),toString()
方法就返回原生代碼的提示盐固。
函數(shù)內(nèi)部的注釋也可以返回屠橄。
function f() {/*
這是一個(gè)
多行注釋
*/}
f.toString()
// "function f(){/*
// 這是一個(gè)
// 多行注釋
// */}"
利用這一點(diǎn),可以變相實(shí)現(xiàn)多行字符串闰挡。
var multiline = function (fn) {
var arr = fn.toString().split('\n');
return arr.slice(1, arr.length - 1).join('\n');
};
function f() {/*
這是一個(gè)
多行注釋
*/}
multiline(f);
// " 這是一個(gè)
// 多行注釋"
函數(shù)作用域
定義
作用域(scope
)指的是變量存在的范圍锐墙。在ES5的規(guī)范中,JavaScript只有兩種作用域:一種是全局作用域长酗,變量在整個(gè)程序中一直存在溪北,所有地方都可以讀取;另一種是函數(shù)作用域之拨,變量只在函數(shù)內(nèi)部存在茉继。ES6又新增了塊級(jí)作用域蚀乔。
對(duì)于頂層函數(shù)來說烁竭,函數(shù)外部聲明的變量就是全局變量(global variable
),它可以在函數(shù)內(nèi)部讀取吉挣。
var v = 1;
function f() {
console.log(v);
}
f() // 1
在函數(shù)內(nèi)部定義的變量派撕,外部無法讀取,稱為“局部變量”(local variable
)睬魂。
function f(){
var v = 1;
}
v // ReferenceError: v is not defined
上面代碼中终吼,變量v
在函數(shù)內(nèi)部定義,所以是一個(gè)局部變量氯哮,函數(shù)之外就無法讀取际跪。
函數(shù)內(nèi)部定義的變量,會(huì)在該作用域內(nèi)覆蓋同名全局變量喉钢。
var v = 1;
function f(){
var v = 2;
console.log(v);
}
f() // 2
v // 1
上面代碼中姆打,變量v
同時(shí)在函數(shù)的外部和內(nèi)部有定義。結(jié)果肠虽,在函數(shù)內(nèi)部定義穴肘,局部變量v
覆蓋了全局變量v
。
注意舔痕,對(duì)于var
命令來說,局部變量只能在函數(shù)內(nèi)部聲明豹缀,在其他區(qū)塊中聲明伯复,一律都是全局變量。
if (true) {
var x = 5;
}
console.log(x); // 5
上面代碼中邢笙,變量x
在條件判斷區(qū)塊之中聲明啸如,結(jié)果就是一個(gè)全局變量,可以在區(qū)塊之外讀取氮惯。
函數(shù)內(nèi)部的變量提升
與全局作用域一樣叮雳,函數(shù)作用域內(nèi)部也會(huì)產(chǎn)生“變量提升”現(xiàn)象。var
命令聲明的變量妇汗,不管在什么位置帘不,變量聲明都會(huì)被提升到函數(shù)體的頭部。
function foo(x) {
if (x > 100) {
var tmp = x - 100;
}
}
// 等同于
function foo(x) {
var tmp;
if (x > 100) {
tmp = x - 100;
};
}
函數(shù)本身的作用域
函數(shù)本身也是一個(gè)值杨箭,也有自己的作用域寞焙。它的作用域與變量一樣,就是其聲明時(shí)所在的作用域,與其運(yùn)行時(shí)所在的作用域無關(guān)捣郊。
var a = 1;
var x = function () {
console.log(a);
};
function f() {
var a = 2;
x();
}
f() // 1
上面代碼中辽狈,函數(shù)x
是在函數(shù)f
的外部聲明的,所以它的作用域綁定外層呛牲,內(nèi)部變量a
不會(huì)到函數(shù)f
體內(nèi)取值刮萌,所以輸出1
,而不是2
娘扩。
總之着茸,函數(shù)執(zhí)行時(shí)所在的作用域,是定義時(shí)的作用域畜侦,而不是調(diào)用時(shí)所在的作用域元扔。
很容易犯錯(cuò)的一點(diǎn)是,如果函數(shù)A
調(diào)用函數(shù)B
旋膳,卻沒考慮到函數(shù)B
不會(huì)引用函數(shù)A
的內(nèi)部變量澎语。
var x = function () {
console.log(a);
};
function y(f) {
var a = 2;
f();
}
y(x) // ReferenceError: a is not defined
上面代碼將函數(shù)x
作為參數(shù),傳入函數(shù)y
验懊。但是擅羞,函數(shù)x
是在函數(shù)y
體外聲明的,作用域綁定外層义图,因此找不到函數(shù)y
的內(nèi)部變量a
减俏,導(dǎo)致報(bào)錯(cuò)。
同樣的碱工,函數(shù)體內(nèi)部聲明的函數(shù)娃承,作用域綁定函數(shù)體內(nèi)部。
function foo() {
var x = 1;
function bar() {
console.log(x);
}
return bar;
}
var x = 2;
var f = foo();
f() // 1
上面代碼中怕篷,函數(shù)foo
內(nèi)部聲明了一個(gè)函數(shù)bar
历筝,bar
的作用域綁定foo
。當(dāng)我們?cè)?code>foo外部取出bar
執(zhí)行時(shí)廊谓,變量x
指向的是foo
內(nèi)部的x
梳猪,而不是foo
外部的x
。正是這種機(jī)制蒸痹,構(gòu)成了“閉包”現(xiàn)象春弥。
參數(shù)
概述
函數(shù)運(yùn)行的時(shí)候,有時(shí)需要提供外部數(shù)據(jù)叠荠,不同的外部數(shù)據(jù)會(huì)得到不同的結(jié)果匿沛,這種外部數(shù)據(jù)就叫參數(shù)。
function square(x) {
return x * x;
}
square(2) // 4
square(3) // 9
上式的x
就是square
函數(shù)的參數(shù)榛鼎。每次運(yùn)行的時(shí)候俺祠,需要提供這個(gè)值公给,否則得不到結(jié)果。
參數(shù)的省略
函數(shù)參數(shù)不是必需的蜘渣,JavaScript允許省略參數(shù)淌铐。
function f(a, b) {
return a;
}
f(1, 2, 3) // 1
f(1) // 1
f() // undefined
f.length // 2
上面代碼的函數(shù)f
定義了兩個(gè)參數(shù),但是運(yùn)行時(shí)無論提供多少個(gè)參數(shù)(或者不提供參數(shù))蔫缸,JavaScript 都不會(huì)報(bào)錯(cuò)腿准。省略的參數(shù)的值就變?yōu)?code>undefined。需要注意的是拾碌,函數(shù)的length
屬性與實(shí)際傳入的參數(shù)個(gè)數(shù)無關(guān)吐葱,只反映函數(shù)預(yù)期傳入的參數(shù)個(gè)數(shù)。
但是校翔,沒有辦法只省略靠前的參數(shù)弟跑,而保留靠后的參數(shù)。如果一定要省略靠前的參數(shù)防症,只有顯式傳入undefined
孟辑。
function f(a, b) {
return a;
}
f( , 1) // SyntaxError: Unexpected token ,(…)
f(undefined, 1) // undefined
上面代碼中,如果省略第一個(gè)參數(shù)蔫敲,就會(huì)報(bào)錯(cuò)饲嗽。
傳遞方式
函數(shù)參數(shù)如果是原始類型的值(數(shù)值、字符串奈嘿、布爾值)貌虾,傳遞方式是傳值傳遞(passes by value
)。這意味著裙犹,在函數(shù)體內(nèi)修改參數(shù)值尽狠,不會(huì)影響到函數(shù)外部。
var p = 2;
function f(p) {
p = 3;
}
f(p);
p // 2
上面代碼中叶圃,變量p
是一個(gè)原始類型的值袄膏,傳入函數(shù)f
的方式是傳值傳遞。因此盗似,在函數(shù)內(nèi)部,p
的值是原始值的拷貝平项,無論怎么修改赫舒,都不會(huì)影響到原始值。
但是闽瓢,如果函數(shù)參數(shù)是復(fù)合類型的值(數(shù)組接癌、對(duì)象、其他函數(shù))扣讼,傳遞方式是傳址傳遞(pass by reference
)缺猛。也就是說,傳入函數(shù)的原始值的地址,因此在函數(shù)內(nèi)部修改參數(shù)荔燎,將會(huì)影響到原始值耻姥。
var obj = { p: 1 };
function f(o) {
o.p = 2;
}
f(obj);
obj.p // 2
上面代碼中,傳入函數(shù)f
的是參數(shù)對(duì)象obj
的地址有咨。因此琐簇,在函數(shù)內(nèi)部修改obj
的屬性p
,會(huì)影響到原始值座享。
注意婉商,如果函數(shù)內(nèi)部修改的,不是參數(shù)對(duì)象的某個(gè)屬性渣叛,而是替換掉整個(gè)參數(shù)丈秩,這時(shí)不會(huì)影響到原始值。
var obj = [1, 2, 3];
function f(o) {
o = [2, 3, 4];
}
f(obj);
obj // [1, 2, 3]
上面代碼中淳衙,在函數(shù)f
內(nèi)部蘑秽,參數(shù)對(duì)象obj
被整個(gè)替換成另一個(gè)值。這時(shí)不會(huì)影響到原始值滤祖。這是因?yàn)榭昀牵问絽?shù)(o
)的值實(shí)際是參數(shù)obj
的地址,重新對(duì)o
賦值導(dǎo)致o
指向另一個(gè)地址匠童,保存在原地址上的值當(dāng)然不受影響埂材。
同名參數(shù)
如果有同名的參數(shù),則取最后出現(xiàn)的那個(gè)值汤求。
function f(a, a) {
console.log(a);
}
f(1, 2) // 2
上面代碼中俏险,函數(shù)f
有兩個(gè)參數(shù),且參數(shù)名都是a
扬绪。取值的時(shí)候竖独,以后面的a
為準(zhǔn),即使后面的a
沒有值或被省略挤牛,也是以其為準(zhǔn)莹痢。
function f(a, a) {
console.log(a);
}
f(1) // undefined
調(diào)用函數(shù)f
的時(shí)候,沒有提供第二個(gè)參數(shù)墓赴,a
的取值就變成了undefined
竞膳。這時(shí),如果要獲得第一個(gè)a
的值诫硕,可以使用arguments
對(duì)象坦辟。
function f(a, a) {
console.log(arguments[0]);
}
f(1) // 1
arguments 對(duì)象
1.定義
由于 JavaScript 允許函數(shù)有不定數(shù)目的參數(shù),所以需要一種機(jī)制章办,可以在函數(shù)體內(nèi)部讀取所有參數(shù)锉走。這就是arguments
對(duì)象的由來滨彻。
arguments
對(duì)象包含了函數(shù)運(yùn)行時(shí)的所有參數(shù),arguments[0]
就是第一個(gè)參數(shù)挪蹭,arguments[1]
就是第二個(gè)參數(shù)亭饵,以此類推。這個(gè)對(duì)象只有在函數(shù)體內(nèi)部嚣潜,才可以使用冬骚。
var f = function (one) {
console.log(arguments[0]);
console.log(arguments[1]);
console.log(arguments[2]);
}
f(1, 2, 3)
// 1
// 2
// 3
正常模式下,arguments
對(duì)象可以在運(yùn)行時(shí)修改懂算。
var f = function(a, b) {
arguments[0] = 3;
arguments[1] = 2;
return a + b;
}
f(1, 1) // 5
上面代碼中只冻,函數(shù)f
調(diào)用時(shí)傳入的參數(shù),在函數(shù)內(nèi)部被修改成3
和2
计技。
嚴(yán)格模式下喜德,arguments
對(duì)象與函數(shù)參數(shù)不具有聯(lián)動(dòng)關(guān)系。也就是說垮媒,修改arguments
對(duì)象不會(huì)影響到實(shí)際的函數(shù)參數(shù)舍悯。
var f = function(a, b) {
'use strict'; // 開啟嚴(yán)格模式
arguments[0] = 3;
arguments[1] = 2;
return a + b;
}
f(1, 1) // 2
上面代碼中,函數(shù)體內(nèi)是嚴(yán)格模式睡雇,這時(shí)修改arguments
對(duì)象萌衬,不會(huì)影響到真實(shí)參數(shù)a
和b
。
通過arguments
對(duì)象的length
屬性它抱,可以判斷函數(shù)調(diào)用時(shí)到底帶幾個(gè)參數(shù)秕豫。
function f() {
return arguments.length;
}
f(1, 2, 3) // 3
f(1) // 1
f() // 0
2.與數(shù)組的關(guān)系
需要注意的是,雖然arguments
很像數(shù)組观蓄,但它是一個(gè)對(duì)象混移。數(shù)組專有的方法(比如slice
和forEach
),不能在arguments
對(duì)象上直接使用侮穿。
如果要讓arguments
對(duì)象使用數(shù)組方法歌径,真正的解決方法是將arguments
轉(zhuǎn)為真正的數(shù)組。下面是兩種常用的轉(zhuǎn)換方法:slice
方法和逐一填入新數(shù)組亲茅。
var args = Array.prototype.slice.call(arguments);
// 或者
var args = [];
for (var i = 0; i < arguments.length; i++) {
args.push(arguments[i]);
}
3.callee屬性
arguments
對(duì)象帶有一個(gè)callee
屬性回铛,返回它所對(duì)應(yīng)的原函數(shù)。
var f = function () {
console.log(arguments.callee === f);
}
f() // true
可以通過arguments.callee
克锣,達(dá)到調(diào)用函數(shù)自身的目的茵肃。這個(gè)屬性在嚴(yán)格模式里面是禁用的,因此不建議使用娶耍。
函數(shù)的其他知識(shí)點(diǎn)
閉包
閉包(closure
)是JavaScript語言的一個(gè)難點(diǎn)免姿,也是它的特色饼酿,很多高級(jí)應(yīng)用都要依靠閉包實(shí)現(xiàn)榕酒。
理解閉包胚膊,首先必須理解變量作用域。前面提到想鹰,JavaScript 有兩種作用域:全局作用域和函數(shù)作用域紊婉。函數(shù)內(nèi)部可以直接讀取全局變量。
var n = 999;
function f1() {
console.log(n);
}
f1() // 999
上面代碼中辑舷,函數(shù)f1
可以讀取全局變量n
喻犁。
但是,函數(shù)外部無法讀取函數(shù)內(nèi)部聲明的變量何缓。
function f1() {
var n = 999;
}
console.log(n)
// Uncaught ReferenceError: n is not defined(
上面代碼中肢础,函數(shù)f1
內(nèi)部聲明的變量n
,函數(shù)外是無法讀取的碌廓。
如果出于種種原因传轰,需要得到函數(shù)內(nèi)的局部變量。正常情況下谷婆,這是辦不到的慨蛙,只有通過變通方法才能實(shí)現(xiàn)。那就是在函數(shù)的內(nèi)部纪挎,再定義一個(gè)函數(shù)期贫。
function f1() {
var n = 999;
function f2() {
console.log(n); // 999
}
}
上面代碼中,函數(shù)f2
就在函數(shù)f1
內(nèi)部异袄,這時(shí)f1
內(nèi)部的所有局部變量通砍,對(duì)f2
都是可見的。但是反過來就不行隙轻,f2
內(nèi)部的局部變量埠帕,對(duì)f1
就是不可見的。這就是JavaScript語言特有的"鏈?zhǔn)阶饔糜?結(jié)構(gòu)(chain scope
)玖绿,子對(duì)象會(huì)一級(jí)一級(jí)地向上尋找所有父對(duì)象的變量敛瓷。所以,父對(duì)象的所有變量斑匪,對(duì)子對(duì)象都是可見的呐籽,反之則不成立。
既然f2
可以讀取f1
的局部變量蚀瘸,那么只要把f2
作為返回值狡蝶,我們不就可以在f1
外部讀取它的內(nèi)部變量了嗎!
function f1() {
var n = 999;
function f2() {
console.log(n);
}
return f2;
}
var result = f1();
result(); // 999
上面代碼中贮勃,函數(shù)f1
的返回值就是函數(shù)f2
贪惹,由于f2
可以讀取f1
的內(nèi)部變量,所以就可以在外部獲得f1
的內(nèi)部變量了寂嘉。
閉包就是函數(shù)f2
奏瞬,即能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)枫绅。由于在JavaScript語言中,只有函數(shù)內(nèi)部的子函數(shù)才能讀取內(nèi)部變量硼端,因此可以把閉包簡(jiǎn)單理解成“定義在一個(gè)函數(shù)內(nèi)部的函數(shù)”并淋。閉包最大的特點(diǎn),就是它可以“記住”誕生的環(huán)境珍昨,比如f2
記住了它誕生的環(huán)境f1
县耽,所以從f2
可以得到f1
的內(nèi)部變量。在本質(zhì)上镣典,閉包就是將函數(shù)內(nèi)部和函數(shù)外部連接起來的一座橋梁兔毙。
閉包的最大用處有兩個(gè),一個(gè)是可以讀取函數(shù)內(nèi)部的變量兄春,另一個(gè)就是讓這些變量始終保持在內(nèi)存中瞒御,即閉包可以使得它誕生環(huán)境一直存在。請(qǐng)看下面的例子神郊,閉包使得內(nèi)部變量記住上一次調(diào)用時(shí)的運(yùn)算結(jié)果肴裙。
function createIncrementor(start) {
return function () {
return start++;
};
}
var inc = createIncrementor(5);
inc() // 5
inc() // 6
inc() // 7
上面代碼中舌厨,start
是函數(shù)createIncrementor
的內(nèi)部變量召嘶。通過閉包,start
的狀態(tài)被保留了纤垂,每一次調(diào)用都是在上一次調(diào)用的基礎(chǔ)上進(jìn)行計(jì)算夕晓。從中可以看到宛乃,閉包inc
使得函數(shù)createIncrementor
的內(nèi)部環(huán)境,一直存在蒸辆。所以征炼,閉包可以看作是函數(shù)內(nèi)部作用域的一個(gè)接口。
為什么會(huì)這樣呢躬贡?原因就在于inc
始終在內(nèi)存中谆奥,而inc
的存在依賴于createIncrementor
,因此也始終在內(nèi)存中拂玻,不會(huì)在調(diào)用結(jié)束后酸些,被垃圾回收機(jī)制回收。
閉包的另一個(gè)用處檐蚜,是封裝對(duì)象的私有屬性和私有方法魄懂。
function Person(name) {
var _age;
function setAge(n) {
_age = n;
}
function getAge() {
return _age;
}
return {
name: name,
getAge: getAge,
setAge: setAge
};
}
var p1 = Person('張三');
p1.setAge(25);
p1.getAge() // 25
上面代碼中,函數(shù)Person
的內(nèi)部變量_age
闯第,通過閉包getAge
和setAge
市栗,變成了返回對(duì)象p1
的私有變量。
注意,外層函數(shù)每次運(yùn)行填帽,都會(huì)生成一個(gè)新的閉包智厌,而這個(gè)閉包又會(huì)保留外層函數(shù)的內(nèi)部變量,所以內(nèi)存消耗很大盲赊。因此不能濫用閉包,否則會(huì)造成網(wǎng)頁的性能問題敷扫。
立即調(diào)用的函數(shù)表達(dá)式(IIFE)
在JavaScript中哀蘑,圓括號(hào)()
是一種運(yùn)算符,跟在函數(shù)名之后葵第,表示調(diào)用該函數(shù)绘迁。比如,print()
就表示調(diào)用print
函數(shù)卒密。
有時(shí)缀台,我們需要在定義函數(shù)之后,立即調(diào)用該函數(shù)哮奇。這時(shí)膛腐,你不能在函數(shù)的定義之后加上圓括號(hào),這會(huì)產(chǎn)生語法錯(cuò)誤鼎俘。
function(){ /* code */ }();
// SyntaxError: Unexpected token (
產(chǎn)生這個(gè)錯(cuò)誤的原因是哲身,function
這個(gè)關(guān)鍵字即可以當(dāng)作語句,也可以當(dāng)作表達(dá)式贸伐。
// 語句
function f() {}
// 表達(dá)式
var f = function f() {}
為了避免解析上的歧義勘天,JavaScript 引擎規(guī)定,如果function
關(guān)鍵字出現(xiàn)在行首捉邢,一律解釋成語句脯丝。因此,JavaScript 引擎看到行首是function
關(guān)鍵字之后伏伐,認(rèn)為這一段都是函數(shù)的定義宠进,不應(yīng)該以圓括號(hào)結(jié)尾,所以就報(bào)錯(cuò)了藐翎。
解決方法就是不要讓function
出現(xiàn)在行首砰苍,讓引擎將其理解成一個(gè)表達(dá)式。最簡(jiǎn)單的處理阱高,就是將其放在一個(gè)圓括號(hào)里面赚导。
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();
上面兩種寫法都是以圓括號(hào)開頭,引擎就會(huì)認(rèn)為后面跟的是一個(gè)表示式赤惊,而不是函數(shù)定義語句吼旧,所以就避免了錯(cuò)誤。這就叫做“立即調(diào)用的函數(shù)表達(dá)式”(Immediately-Invoked Function Expression
)未舟,簡(jiǎn)稱 IIFE圈暗。
注意掂为,上面兩種寫法最后的分號(hào)都是必須的。如果省略分號(hào)员串,遇到連著兩個(gè) IIFE勇哗,可能就會(huì)報(bào)錯(cuò)。
// 報(bào)錯(cuò)
(function(){ /* code */ }())
(function(){ /* code */ }())
上面代碼的兩行之間沒有分號(hào)寸齐,JavaScript 會(huì)將它們連在一起解釋欲诺,將第二行解釋為第一行的參數(shù)。
推而廣之渺鹦,任何讓解釋器以表達(dá)式來處理函數(shù)定義的方法扰法,都能產(chǎn)生同樣的效果,比如下面三種寫法毅厚。
var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();
甚至像下面這樣寫塞颁,也是可以的。
!function () { /* code */ }();
~function () { /* code */ }();
-function () { /* code */ }();
+function () { /* code */ }();
通常情況下吸耿,只對(duì)匿名函數(shù)使用這種“立即執(zhí)行的函數(shù)表達(dá)式”祠锣。它的目的有兩個(gè):一是不必為函數(shù)命名,避免了污染全局變量咽安;二是 IIFE 內(nèi)部形成了一個(gè)單獨(dú)的作用域锤岸,可以封裝一些外部無法讀取的私有變量。
// 寫法一
var tmp = newData;
processData(tmp);
storeData(tmp);
// 寫法二
(function () {
var tmp = newData;
processData(tmp);
storeData(tmp);
}());
上面代碼中板乙,寫法二比寫法一更好是偷,因?yàn)橥耆苊饬宋廴救肿兞俊?/p>
eval 命令
基本用法
eval
命令接受一個(gè)字符串作為參數(shù),并將這個(gè)字符串當(dāng)作語句執(zhí)行募逞。
eval('var a = 1;');
a // 1
上面代碼將字符串當(dāng)作語句運(yùn)行蛋铆,生成了變量a
。
如果參數(shù)字符串無法當(dāng)作語句運(yùn)行放接,那么就會(huì)報(bào)錯(cuò)刺啦。
eval('3x') // Uncaught SyntaxError: Invalid or unexpected token
放在eval
中的字符串,應(yīng)該有獨(dú)自存在的意義纠脾,不能用來與eval
以外的命令配合使用玛瘸。舉例來說,下面的代碼將會(huì)報(bào)錯(cuò)苟蹈。
eval('return;'); // Uncaught SyntaxError: Illegal return statement
上面代碼會(huì)報(bào)錯(cuò)糊渊,因?yàn)?code>return不能單獨(dú)使用,必須在函數(shù)中使用慧脱。
如果eval
的參數(shù)不是字符串渺绒,那么會(huì)原樣返回。
eval(123) // 123
eval
沒有自己的作用域,都在當(dāng)前作用域內(nèi)執(zhí)行宗兼,因此可能會(huì)修改當(dāng)前作用域的變量的值躏鱼,造成安全問題。
var a = 1;
eval('a = 2');
a // 2
上面代碼中殷绍,eval
命令修改了外部變量a
的值染苛。由于這個(gè)原因,eval
有安全風(fēng)險(xiǎn)主到。
為了防止這種風(fēng)險(xiǎn)茶行,JavaScript 規(guī)定,如果使用嚴(yán)格模式镰烧,eval
內(nèi)部聲明的變量,不會(huì)影響到外部作用域楞陷。
(function f() {
'use strict';
eval('var foo = 123');
console.log(foo); // ReferenceError: foo is not defined
})()
上面代碼中怔鳖,函數(shù)f
內(nèi)部是嚴(yán)格模式,這時(shí)eval
內(nèi)部聲明的foo
變量固蛾,就不會(huì)影響到外部结执。
不過,即使在嚴(yán)格模式下艾凯,eval
依然可以讀寫當(dāng)前作用域的變量献幔。
(function f() {
'use strict';
var foo = 1;
eval('foo = 2');
console.log(foo); // 2
})()
上面代碼中,嚴(yán)格模式下趾诗,eval
內(nèi)部還是改寫了外部變量蜡感,可見安全風(fēng)險(xiǎn)依然存在。
總之恃泪,eval
的本質(zhì)是在當(dāng)前作用域之中郑兴,注入代碼。由于安全風(fēng)險(xiǎn)和不利于 JavaScript 引擎優(yōu)化執(zhí)行速度贝乎,所以一般不推薦使用情连。通常情況下,eval
最常見的場(chǎng)合是解析 JSON 數(shù)據(jù)的字符串览效,不過正確的做法應(yīng)該是使用原生的JSON.parse
方法却舀。
eval 的別名調(diào)用
前面說過eval
不利于引擎優(yōu)化執(zhí)行速度。更麻煩的是锤灿,還有下面這種情況挽拔,引擎在靜態(tài)代碼分析的階段,根本無法分辨執(zhí)行的是eval
但校。
var m = eval;
m('var x = 1');
x // 1
上面代碼中篱昔,變量m
是eval
的別名。靜態(tài)代碼分析階段,引擎分辨不出m('var x = 1')
執(zhí)行的是eval
命令州刽。
為了保證eval
的別名不影響代碼優(yōu)化空执,JavaScript 的標(biāo)準(zhǔn)規(guī)定,凡是使用別名執(zhí)行eval
穗椅,eval
內(nèi)部一律是全局作用域辨绊。
var a = 1;
function f() {
var a = 2;
var e = eval;
e('console.log(a)');
}
f() // 1
上面代碼中,eval
是別名調(diào)用匹表,所以即使它是在函數(shù)中门坷,它的作用域還是全局作用域,因此輸出的a
為全局變量袍镀。這樣的話默蚌,引擎就能確認(rèn)e()
不會(huì)對(duì)當(dāng)前的函數(shù)作用域產(chǎn)生影響苇羡,優(yōu)化的時(shí)候就可以把這一行排除掉码俩。
eval
的別名調(diào)用的形式五花八門挠铲,只要不是直接調(diào)用,都屬于別名調(diào)用丘喻,因?yàn)橐嬷荒芊直?code>eval()這一種形式是直接調(diào)用跺撼。
eval.call(null, '...')
window.eval('...')
(1, eval)('...')
(eval, eval)('...')
上面這些形式都是eval
的別名調(diào)用躏嚎,作用域都是全局作用域。