函數(shù)聲明和函數(shù)表達(dá)式有什么區(qū)別
-
JavaScript 中需要創(chuàng)建函數(shù)的話,有兩種方法:函數(shù)聲明、函數(shù)表達(dá)式,各自寫法如下:
// 方法一:函數(shù)聲明 function foo() {} // 方法二:函數(shù)表達(dá)式 var foo = function () {};
- 另外還有一種自執(zhí)行函數(shù)表達(dá)式,主要用于創(chuàng)建一個新的作用域,在此作用域內(nèi)聲明的變量不會和其它作用域內(nèi)的變量沖突或混淆,大多是以匿名函數(shù)方式存在,且立即自動執(zhí)行:
(function () {
// var x = ...
})();
此種自執(zhí)行函數(shù)表達(dá)式歸類于以上兩種方法的第二種,也算是函數(shù)表達(dá)式。
方法一和方法二都創(chuàng)建了一個函數(shù),且命名為 foo,但是二者還是有區(qū)別的。JavaScript 解釋器中存在一種變量聲明被 提升(hoisting) 的機(jī)制,也就 是說變量(函數(shù))的聲明會被提升到作用域的最前面,即使寫代碼的時候是寫在最后面,也還是會被 提升 至最前面。
** 例如以下代碼段:**
alert(foo); // function foo() {}
alert(bar); // undefined
function foo() {}
var bar = function bar_fn() {};
alert(foo); // function foo() {}
alert(bar); // function bar_fn() {}
輸出結(jié)果分別是function foo() {}
、undefined
、function foo() {}
和function bar_fn() {}
。
可以看到 foo
的聲明是寫在 alert
之后,仍然可以被正確調(diào)用,因?yàn)?JavaScript 解釋器會將其提升到 alert
前面,而以函數(shù)表達(dá)式創(chuàng)建的函數(shù) bar
則不享受此待遇。
那么bar
究竟有沒有被提升呢,其實(shí)用 var
聲明的變量都會被提升,只不過是被先賦值為 undefined
罷了,所以第二個 alert
彈出了 undefined
。
所以,JavaScript 引擎執(zhí)行以上代碼的順序可能是這樣的:
- 創(chuàng)建變量
foo
和bar
,并將它們都賦值為undefined
。 - 創(chuàng)建函數(shù)
foo
的函數(shù)體,并將其賦值給變量foo
。 - 執(zhí)行前面的兩個
alert
。 - 創(chuàng)建函數(shù)
bar_fn
,并將其賦值給bar
。 - 執(zhí)行后面的兩個
alert
。
注:
嚴(yán)格地說,再 JavaScript 中創(chuàng)建函數(shù)的話,還有另外一種方法,稱為“函數(shù)構(gòu)造法”:
var foo = Function('alert("hi!");');
var foo = new Function('alert("hi!");'); // 等同于上面一行
new function是可以傳參數(shù)的。
比如
var sum= new Function("a", "b", "c", "return a+b+c");
sum(1,2,3)//6
什么是變量的聲明前置?什么是函數(shù)的聲明前置
先來看一個例子
fn1(); // 輸出:我是函數(shù)聲明
fn2(); // 報錯
console.log(a); // 輸出:undefined
function fn1() {
console.log( "我是函數(shù)聲明" );
}
var fn2 = function() {
console.log( "我是函數(shù)表達(dá)式" );
}
var a = 20
因?yàn)镴S對使函數(shù)聲明前置,所以fn1()在函數(shù)聲明前執(zhí)行仍然可以得到正確答案,而函數(shù)表達(dá)式fn2則報錯,
為什么?我們先要弄清楚:
JS解釋器如何找到我們定義的函數(shù)和變量?
通過 變量對象(Variable Object, VO)來獲取。VO是一個抽象概念的“對象”,它用于存儲執(zhí)行上下文中的:1. 變量;2. 聲明;3. 函數(shù)參數(shù)。
函數(shù)的VO分為兩個階段——變量初始化和代碼執(zhí)行。在變量初始化階段,VO按照如下順序填充:
1. 函數(shù)參數(shù)(若未傳入,初始化該參數(shù)值為undefined)
2. 函數(shù)聲明(若發(fā)生命名沖突,會覆蓋)
3. 變量聲明(初始化變量值為undefined,若發(fā)生命名沖突,則忽略)
注意:函數(shù)表達(dá)式與變量初始化無關(guān)。
在變量初始化階段,需要先對arguments變量進(jìn)行初始化(激活對象,AO),再把函數(shù)體內(nèi)的變量聲明與函數(shù)聲明存儲在AO內(nèi),VO(functionContext) === AO。
根據(jù) VO數(shù)據(jù) 填充順序看以下例子
var x = 10;
bar();
function foo() {
console.log(x);
}
function bar() {
var x = 30;
foo(); //得到什么?
}
/*
1. globalContext = {
AO: {
x: 10
foo: function
bar: function
}
Scope: null
}
// 聲明 foo 時 得到下面
foo.[ [scope] ] = globalContext.AO
// 聲明 bar 時 得到下面
bar.[ [scope] ] = globalContext.AO
// 當(dāng)調(diào)用bar(),進(jìn)入bar 的執(zhí)行上下文
2. barContext = {
AO: {
x: 30
}
Scope = bar.[ [scope] ] // globalContext.AO
}
//當(dāng)調(diào)用 foo() 時,先從 foo 執(zhí)行上下文中的 AO里找,找不到再從 foo 的 [[scope]]里,找到后即調(diào)用
因?yàn)樵趂oo執(zhí)行上下文中找不到x,所以直接進(jìn)入foo的scope里面找,即globalContext.AO
3. fooContext = {
AO: {
}
Scope = foo.[ [scope] ] // globalContext.AO
}
*/
globalContext = {
AO: {
x: 10
foo: function
bar: function
}
Scope: null
所以console.log(x) 是 10
arguments 是什么
- arguments 是一個類似數(shù)組的對象, 對應(yīng)于傳遞給函數(shù)的參數(shù)。
- arguments.length表示的是實(shí)際上向函數(shù)傳入了多少個參數(shù)。
- arguments對象是所有函數(shù)中可用的局部變量。你可以使用arguments對象在函數(shù)中引用函數(shù)的參數(shù)。此對象包含傳遞給函數(shù)的每個參數(shù)的條目,第一個條目的索引從0開始。例如,如果一個函數(shù)傳遞了三個參數(shù),你可以參考它們?nèi)缦拢?/li>
arguments[0]
arguments[1]
arguments[2]
- 在函數(shù)內(nèi)部,你可以使用arguments對象獲取到該函數(shù)的所有傳入?yún)?shù)
函數(shù)的"重載"怎樣實(shí)現(xiàn)
首先想聲明下,什么是函數(shù)重載,javascript中不存在函數(shù)重載的概念,(其實(shí)是個偽命題)但一個函數(shù)通過不同參數(shù)列表(arguments)來實(shí)現(xiàn)各個功能,我們都叫函數(shù)重載,這就是牛逼閃閃的 JavaScript 函數(shù)重載
/*
* 傳統(tǒng)方法一
* */
var overrideMethod = function () {
switch (arguments.length) {
case 0 :
console.log("class no body");
break;
case 1 :
console.log("class has one student");
break;
default :
console.log("class has more students");
}
}
overrideMethod("test","blue"); //class has more students
overrideMethod("test-blue"); //class has one student
overrideMethod(); //class no body
/*
* 我們希望對象Company擁有一個find方法,當(dāng)不傳任何參數(shù)時,
* 就會把Company.names里面的所有元素返回來;
* 因?yàn)閒ind方法是根據(jù)參數(shù)的個數(shù)不同而執(zhí)行不同的操作的,
* 所以,需要有一個overrideCompanyFind方法,能夠如下的為Company添加find的重載:
* */
var company = {
names : ["baron" , "Andy" ,"Lily" , "Blures"],
find : function () {
return this.names.length
}
};
var overrideCompanyFind = function (object , method , cb) {
var oldMethod = object[method];
//給object 重新賦予新的方法
object[method] = function () {
if (cb.length == arguments.length) {
return cb.apply(this,arguments)
}else if(typeof oldMethod== 'function'){
return oldMethod.apply(this,arguments)
}
};
};
overrideCompanyFind(company,'find',function (name , name2) {
return this.names
});
overrideCompanyFind(company,'find',function (name) {
return name + '的位置是' + this.names.indexOf(name) + '排'
});
console.log(company.find()); //4
console.log(company.find('Lily')); // Lily的位置是2排
console.log(company.find('Lily','baron')); //["baron", "Andy", "Lily", "Blures"]
立即執(zhí)行函數(shù)表達(dá)式是什么?有什么作用
**立即調(diào)用函數(shù)表達(dá)式可以令其函數(shù)中聲明的變量繞過JavaScript的變量置頂聲明規(guī)則,還可以避免新的變量被解釋成全域變量或函數(shù)名占用全域變量名的情況。與此同時它能在禁止訪問函數(shù)內(nèi)聲明變量的情況下允許外部對函數(shù)的調(diào)用。有時,這種編程方法也被叫做“自執(zhí)行(匿名)函數(shù)”,但“立即調(diào)用函數(shù)表達(dá)式”是語義上最準(zhǔn)確的術(shù)語。 **
// 下面2個括弧()都會立即執(zhí)行
(function () { /* code */ } ()); // 推薦使用這個
(function () { /* code */ })(); // 但是這個也是可以用的
// 由于括弧()和JS的&&,異或,逗號等操作符是在函數(shù)表達(dá)式和函數(shù)聲明上消除歧義的
// 所以一旦解析器知道其中一個已經(jīng)是表達(dá)式了,其它的也都默認(rèn)為表達(dá)式了
// 不過,請注意下一章節(jié)的內(nèi)容解釋
var i = function () { return 10; } ();
true && function () { /* code */ } ();
0, function () { /* code */ } ();
// 如果你不在意返回值,或者不怕難以閱讀
// 你甚至可以在function前面加一元操作符號
!function () { /* code */ } ();
~function () { /* code */ } ();
-function () { /* code */ } ();
+function () { /* code */ } ();
// 還有一個情況,使用new關(guān)鍵字,也可以用,但我不確定它的效率
// http://twitter.com/kuvos/status/18209252090847232
new function () { /* code */ }
new function () { /* code */ } () // 如果需要傳遞參數(shù),只需要加上括弧()
有什么用?
javascript中沒用私有作用域的概念,如果在多人開發(fā)的項目上,你在全局或局部作用域中聲明了一些變量,可能會被其他人不小心用同名的變量給覆蓋掉,根據(jù)javascript函數(shù)作用域鏈的特性,可以使用這種技術(shù)可以模仿一個私有作用域,用匿名函數(shù)作為一個“容器”,“容器”內(nèi)部可以訪問外部的變量,而外部環(huán)境不能訪問“容器”內(nèi)部的變量,所以( function(){…} )()內(nèi)部定義的變量不會和外部的變量發(fā)生沖突,俗稱“匿名包裹器”或“命名空間”。
求n!,用遞歸來實(shí)現(xiàn)
function fn(n){
if(n===1){
return 1;
}
alert( n * fn(n-1));
}
fn(5);
以下代碼輸出什么?
function getInfo(name, age, sex){
console.log('name:',name);
console.log('age:', age);
console.log('sex:', sex);
console.log(arguments);
arguments[0] = 'valley';
console.log('name', name);
}
getInfo('饑人谷', 2, '男'); // name: 饑人谷 age:2 sex:男 ["饑人谷", 2, "男"] name valley
getInfo('小谷', 3); // name: 小谷 age:3 sex:undefined ["小谷", 3] name valley
getInfo('男'); // name: 男 age:undefined sex:undefined ["男"] name valley
寫一個函數(shù),返回參數(shù)的平方和?
function sumOfSquares(){
var sum = 0;
for (var i=0; i<arguments.length; i++) {
sum += arguments[i] * arguments[i];
}
console.log(sum);
}
var result = sumOfSquares(2,3,4)
var result2 = sumOfSquares(1,3)
console.log(result) //29
console.log(result) //10
如下代碼的輸出?為什么
console.log(a); // undefined
var a = 1;
console.log(b); // 報錯
JS變量聲明會被 提升 所以console.log(a)
var a
會提前,但是還沒被賦值,所以輸出undefined
console.log(b)
因?yàn)樽兞?code>b沒有被聲明,所以報錯
如下代碼的輸出?為什么
sayName('world'); // hello world
sayAge(10); // 報錯
function sayName(name){
console.log('hello ', name);
}
var sayAge = function(age){
console.log(age);
};
sayName('world');
根據(jù)函數(shù)聲明會被提升,所以輸出hello world。sayAge(10)
,報錯是因?yàn)?code>var sayAge = function(age){}是一個函數(shù)表達(dá)式,聲明var sayAge
時,還不是一個函數(shù), sayAge(10)
調(diào)用在聲明前,所以報錯。
如下代碼輸出什么? 寫出作用域鏈查找過程偽代碼
var x = 10
bar()
function foo() {
console.log(x)
}
function bar(){
var x = 30
foo()
} // 輸出 10
1. globalContext = {
AO: {
x: 10
foo: funciton
bar: funciton
}
Scope: null
}
//聲明 foo() 時 得到
foo.[ [scope] ] = globalContext.AO
//聲明 bar() 時 得到
bar.[ [scope] ] = globalContext.AO
// 當(dāng)調(diào)用bar(),進(jìn)入bar 的執(zhí)行上下文
2. barContext = {
AO: {
x: 30
}
Scope: bar.[ [scope] ]
}
//當(dāng)調(diào)用 foo() 時,先從 bar 執(zhí)行上下文中的 AO里找,找不到再從 bar 的 [[scope]]里,找到后即調(diào)用
fooContext = {
AO: {
}
Scope: foo.[ [scope] ]
}
所以console.log(x) 得到10
如下代碼輸出什么? 寫出作用域鏈查找過程偽代碼
var x = 10;
bar()
function bar(){
var x = 30;
function foo(){
console.log(x)
}
foo();
}
以下代碼輸出什么? 寫出作用域鏈的查找過程偽代碼
var x = 10;
bar()
function bar(){
var x = 30;
(function (){
console.log(x)
})()
}
1. globalContext = {
AO: {
x: 10
bar: funciton
}
Scope: null
}
bar.[ [scope] ] = globalContext.AO
2. barContext = {
AO: {
x: 30
}
Scope: bar.[ [scope] ]
}
聲明x,x賦值10 > 執(zhí)行bar() > 聲明x,x賦值30 > 立即執(zhí)行匿名函數(shù) > console.log(x) > 到bar上下文AO中找到x = 30 ,所以console.log(x) 得到30
//
以下代碼輸出什么? 寫出作用域鏈查找過程偽代碼
var a = 1;
function fn(){
console.log(a)
var a = 5
console.log(a)
a++
var a
fn3()
fn2()
console.log(a)
function fn2(){
console.log(a)
a = 20
}
}
function fn3(){
console.log(a)
a = 200
}
fn()
console.log(a)
1. globalContext = {
AO: {
a: 1
fn: funciton
fn3: funciton
}
Scope:null
}
// fn.[ [scope] ] = globalContext.AO
// fn3.[ [scope] ] = globalContext.AO
2. fnContext = {
AO: {
a: undefine
fn3: funciton
fn2: funciton
}
Scope: fn.[ [scope] ] // fn.[ [scope] ] = globalContext,AO
}
}
fn2Context {
AO: {
}
Scope: fn2.[ [scope] ] // fn2.[ [scope] ] = fnContext,AO
}
fn3Context {
AO: {
}
Scope: fn3.[ [scope] ] // fn3.[ [scope] ] = globalContext,AO
}
執(zhí)行fn() > console.log(a) 此時 a = undefined; > console.log(a) 此時 a = 5 > a++ 此時 a = 6 > 執(zhí)行fn3() , console.log(a) 找到globalContext.AO里 a = 1; a = 200 此時globalContext.AO里 a = 200 > 執(zhí)行fn2() , console.log(a) 找到fnContext.AO里 a = 6, 然后a = 20 此時 fnContext.AO里 a = 20;
> 執(zhí)行 console.log(a) 此時 a = 20 > 最后 執(zhí)行 console.log(a) 得到 a = 200
結(jié)果 undefined > 5 > 1 > 6 > 20 > 200