函數(shù)聲明和函數(shù)表達式有什么區(qū)別
函數(shù)聲明語法:function functionName(arg0,arg1,arg2){ //函數(shù)體 }
函數(shù)表達式語法:var function = function(arg0,arg1,arg2){ //函數(shù)體 }
區(qū)別:
使用function關鍵字可以聲明一個函數(shù)棍弄,它的特征是函數(shù)聲明提升疟游,執(zhí)行代碼前會先讀取函數(shù)聲明,即函數(shù)聲明不必放在調用的前面蛮原,它可以放在當前作用域任何位置另绩;函數(shù)表達式在使用前必須先賦值,所以聲明必須放在調用前面蹦漠,不然瀏覽器解析代碼時會認為函數(shù)還不存在而拋出錯誤车海,理解函數(shù)提升的關鍵就是理解函數(shù)聲明與函數(shù)表達式之間的區(qū)別。
- 函數(shù)聲明示例
//函數(shù)聲明
function sayHi(){
console.log('hi');
}
//函數(shù)調用
sayHi();
或者
//函數(shù)調用
sayHi();
//函數(shù)聲明
function sayHi(){
console.log('hi');
}
- 函數(shù)表達式示例
//函數(shù)聲明
var sayHi = function(){
console.log('Hi!');
};
//函數(shù)調用
sayHi();
以下是錯誤做法
//函數(shù)調用
sayHi();
//函數(shù)聲明
var sayHi = function(){
console.log('Hi!');
};
會拋出錯誤:函數(shù)還不存在
什么是變量的聲明前置研铆?什么是函數(shù)的聲明前置
arguments 是什么棵红?
arguments是一個類數(shù)組對象,除了length屬性外沒有任何數(shù)組屬性嫂便,是所有函數(shù)中可用的局部變量忆绰,僅在函數(shù)內部有效。
- 可通過arguments對象來訪問函數(shù)的參數(shù)列表翰灾,使用方括號語法訪問參數(shù)列表的每一個元素稚茅,第一個條目的索引為0,即第一項為arguments[0]咽块,第二項為arguments[1]欺税,以此類推。
- 通過訪問arguments對象的length屬性可以確定有多少個參數(shù)傳遞給了函數(shù)亭罪。
- arguments對象中的值可以被重寫并會自動反映到對應的命名參數(shù)歼秽,所以值與對應命名參數(shù)的值保持同步。如果只傳入了一個參數(shù)箩祥,那么為arguments[1]設置的值不會反映到命名參數(shù)中肆氓,因為arguments對象的長度由傳入的參數(shù)個數(shù)決定,而不由定義函數(shù)時的命名參數(shù)的個數(shù)決定盲泛。
- 在嚴格模式下重寫arguments的值會導致語法錯誤,代碼不會執(zhí)行柑营。
使用場景:調用一個函數(shù)時村视,當這個函數(shù)的參數(shù)數(shù)量比它顯式聲明的參數(shù)數(shù)量更多時,就可以使用 arguments 對象奶赔。
函數(shù)的"重載"怎樣實現(xiàn)
- 概念:函數(shù)重載指同一函數(shù)名對應著多個函數(shù)的實現(xiàn)杠氢。即每種實現(xiàn)對應一個函數(shù)體,這些函數(shù)名字相同绞旅,但參數(shù)類型或個數(shù)或順序不同温艇。
- 函數(shù)重載主要是為了解決兩個問題:
- 可變參數(shù)類型
- 可變參數(shù)個數(shù)
- 可變參數(shù)順序
- 基本設計原則:當兩個函數(shù)除了參數(shù)類型和參數(shù)個數(shù)不同以外其他功能完全相同時勺爱,利用函數(shù)重載;兩個函數(shù)功能不同時不應使用重載卫旱,而應使用一個名字不同的函數(shù)绣否。
js是弱類型語言,參數(shù)不是固定的某個類型,所以在js中沒有重載跪呈,同名函數(shù)后面的會覆蓋前面的耗绿。但我們也可以實現(xiàn)重載所需要的功能。
實現(xiàn):
寫一個函數(shù)误阻,在函數(shù)體內針對不同的參數(shù)調用執(zhí)行不同的邏輯。
function printPeopleInfo(name,age,sex){
if(name){
console.log(name);
}
if(age){
console.log(age);
}
if(sex){
console.log(sex);
}
}
printPeopleInfo("dot",23); //dot 23
printPeopleInfo("dot","female",23); //dot female 23
function add(){
var num=0;
for(var i=0;i<arguments.length;i++){
num+=arguments[i];
}
console.log(num);
}
add(1); //1
add(1,2,3); //6
注意:始終記住函數(shù)名只是一個指向函數(shù)對象的指針儒洛,并不會與某個函數(shù)綁定狼速。
立即執(zhí)行函數(shù)表達式是什么?有什么作用
- 立即執(zhí)行函數(shù)表達式:
縮寫IIFE恼蓬,是一種利用javascript函數(shù)生成新作用域的編程方法僵芹,也叫自執(zhí)行函數(shù)。 - 作用:
- 令函數(shù)中聲明的變量繞過js的變量置頂聲明規(guī)則
- 避免新的變量被解釋成全局變量或函數(shù)名占用全局變量名的情況
- 在禁止訪問函數(shù)內變量聲明的情況下允許外部對函數(shù)的調用
- 實現(xiàn):因js里的()里不能包含語句荷辕,所以解析器會將()里的代碼解析成function表達式并立即執(zhí)行攀痊。
// 以下都能實現(xiàn)立即執(zhí)行
(function(){ /* code */ }());
(function(){ /* code */ })();
// function前加一元運算符也可實現(xiàn)苟径,advance-task2我有提到過
!function () { /* code */ } ();
~function () { /* code */ } ();
-function () { /* code */ } ();
+function () { /* code */ } ();
求n!,用遞歸來實現(xiàn)
- 方法一:
var factorial = (function f(n){
if (n <= 0){
return 1;
} else {
return n * f(n-1);
}
});
factorial(5); //120
- 方法二:
function factorial(n){
if(n === 1) {
return 1;
}
return n * factorial(n-1);
}
factorial(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, '男');
getInfo('小谷', 3);
getInfo('男');
輸出分別為:
name: 饑人谷
age: 2
sex: 男
["饑人谷",2,"男"]
name valley
name: 饑人谷
age: 3
sex: undefined
["小谷",3]
name valley
name: 男
age: undefined
sex: undefined
["男"]
name valley
寫一個函數(shù)遭殉,返回參數(shù)的平方和
function sumOfSquares() {
var sum = 0;
for (var i = 0; i < arguments.length; i++) {
sum += Math.pow(arguments[i], 2);
}
return sum;
}
var result = sumOfSquares(2, 3, 4);
var result2 = sumOfSquares(1, 3);
console.log(result); //29
console.log(result2); //10
如下代碼的輸出是险污?為什么
console.log(a); //undefined,因為變量a聲明提升并賦值為undefined拯腮,先讀取變量聲明
var a = 1;
console.log(b); //拋出ReferenceError:b is not defined蚁飒,因為b沒有聲明
如下代碼的輸出是?為什么
sayName('world');
sayAge(10);
function sayName(name){
console.log('hello ', name); //hello world琼懊,因為sayName函數(shù)聲明提升
}
var sayAge = function(age){
console.log(age); //拋出TypeError: sayAge is not a function,因為sayAge是函數(shù)表達式启妹,使用前必須賦值削祈,而聲明放在了調用的后面,此時函數(shù)還不存在咙崎,所以會報錯
};
擴充——執(zhí)行環(huán)境與作用域
理解了這兩個知識點才能做以下四題
執(zhí)行環(huán)境與環(huán)境對象:
為了保證指令的順序吨拍,在運行時需要一個容器,把一系列函數(shù)伊滋、表達式队秩、語句(統(tǒng)稱為腳本)包起來馍资。而腳本之間也具有相關性,實現(xiàn)一個功能所需要的腳本鸟蟹,又往往再構成一個集合建钥。
比如一個汽車工廠,有一個子工廠專門生產(chǎn)螺釘熊经。整個汽車工廠就是一個大容器镐依,螺釘工廠就是一個小容器,其中生產(chǎn)螺帽的車間構成一個腳本集合,生產(chǎn)螺栓的車間構成一個腳本集合秋秤。
這樣的一個集合就是腳本的執(zhí)行環(huán)境脚翘,而一個腳本執(zhí)行環(huán)境的直接容器就是其環(huán)境對象来农。
上例中崇堰,車間就是執(zhí)行環(huán)境,螺釘工廠就是車間的環(huán)境對象繁莹。
可見執(zhí)行環(huán)境特幔、環(huán)境對象的概念是相對、有精度和角度的薄风。
換個角度拍嵌,螺絲工廠也是一個執(zhí)行環(huán)境横辆,汽車工廠也是螺絲工廠的環(huán)境對象。
在JS中逆粹,通常以對象作為環(huán)境對象炫惩,具體運行腳本的函數(shù)為執(zhí)行環(huán)境。
如上所言蹋绽,注意以下兩點:
- 函數(shù)本身作為特殊的對象筋蓖,換個角度也可以充當環(huán)境對象:
function a(){};
a.color = "red";
a.getColor = function(){
console.log(this.color); //red
console.log(this); //function a(){};
};
a.getColor();
- 由于函數(shù)的打包性,就像一個車間可以搬到不同的工廠蚣抗,其環(huán)境對象就要看具體情況而言:
function a(){console.log(this.name)};
var object = {
name: 'Amily'
}
var name = 'Shaw';
a(); //Shaw
a.apply(object); //Amily
執(zhí)行環(huán)境通過棧來保存翰铡,所以也叫環(huán)境棧,每當調用一個函數(shù)就向棧中推入該函數(shù)的執(zhí)行環(huán)境锭魔,函數(shù)執(zhí)行之后棧將其環(huán)境彈出迷捧,把控制權返回給之前的執(zhí)行環(huán)境,棧底是全局執(zhí)行環(huán)境笙蒙。
作用域鏈:
腳本執(zhí)行時膛堤,肯定需要使用變量,但有些變量不是在該執(zhí)行環(huán)境內定義的绿渣。此時需要一個機制燕耿,使得一個執(zhí)行環(huán)境中的腳本能拿到另一個執(zhí)行環(huán)境中的變量誉帅。這個機制必須有規(guī)矩,明確哪些變量能跨環(huán)境拿档插,哪些不能亚再。這個機制就是作用域鏈。
作用域鏈由一連串的變量構成则剃,這些變量不是雜亂無章的如捅,而是根據(jù)自身所在的執(zhí)行環(huán)境,由內層環(huán)境到外層環(huán)境己肮,按順序鏈下去的〗呀#考慮到有同級的執(zhí)行環(huán)境戈稿,數(shù)據(jù)結構上更像一棵樹鞍盗。
內部環(huán)境可通過作用域鏈訪問所有外部環(huán)境跳昼,但外部環(huán)境不能訪問內部環(huán)境中的任何變量與函數(shù),同級執(zhí)行環(huán)境間沒法直接訪問對方的變量敷存,除非 return 到下一鏈中堪伍,例如:
var a = function(){
var name = 'Amily';
}
var b = function(){
alert(name); //undefined
}
通過try-catch語句和with語句可在作用域鏈前端臨時添加變量對象,此時涮俄,同名變量以給定的變量對象中的變量優(yōu)先尸闸。
注意:簡單理解棧和作用域鏈吮廉,棧就像一個桶,window是最先推到棧底的宙址,接著推入次外層變量對象踪旷,一直到最內層變量對象,即最內層在上舀患,從作用域的角度來看气破,查找作用域就是在棧里從上往下查找的過程聊浅,全局執(zhí)行環(huán)境的變量對象始終都是作用域鏈中的最后一個對象。
var color = "blue";
function changeColor(){
var anotherColor = "red";
function swapColors(){
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
// 這里可以訪問color、anotherColor低匙、tempColor
}
// 這里可以訪問color旷痕、anotherColor
}
changeColor();
// 這里只能訪問color
變量作用域
變量作用域相對于某個具體變量而言,指能夠通過作用域鏈拿到該變量的所有執(zhí)行環(huán)境顽冶。
垃圾收集
JS有自動垃圾收集機制欺抗,分別是標記清除和引用計數(shù)。
前者通過執(zhí)行環(huán)境來標記强重,后者通過地址的引用次數(shù)來標記,兩者都是在無法訪問后间景,被作為垃圾清除佃声。
在大型應用中,通常通過將無用的變量設置為 null 來進行手動垃圾清除倘要。
明確了以上概念圾亏,再來看以下四題會容易很多~
如下代碼輸出什么? 寫出作用域鏈查找過程偽代碼
var x = 10;
bar() ;
function foo() {
console.log(x);
}
function bar(){
var x = 30;
foo();
}
- 輸出結果:10 undefined
- 查找過程:不會寫查找過程偽代碼但能理解。
- 函數(shù)最終結果與在哪執(zhí)行無關封拧,只與初始所在環(huán)境有關志鹃,foo函數(shù)未執(zhí)行時要得到x的值,首先在自身作用域中查找有無x變量哮缺,沒有找到即向外層作用域弄跌,本例中為全局作用域中查找,注意同級函數(shù)間并不能訪問對方的作用域這一點尝苇。繼續(xù)剛才的步驟铛只,foo在全局中找到了x,所以即使foo函數(shù)身處bar函數(shù)中糠溜,取得的也是全局作用域中x的值淳玩,也就是10.
- undefined的出現(xiàn)可以理解為一個來自控制臺的調戲~
細心的話會發(fā)現(xiàn),寫一個函數(shù)return的時候非竿,控制臺會出現(xiàn)最后一行變量或表達式的值蜕着,return表示不背鍋,一切都是console.log自作主張打印的結果红柱,忽略它就好承匣,如果沒有return一個函數(shù),直接console.log一下锤悄,控制臺也會多打一個undefined韧骗,因為函數(shù)沒有return就會返回一個undefined,而這個undefined就會被控制臺打印出來零聚,注意這是個坑袍暴,所以以下都不寫出undefined些侍。
如下代碼輸出什么? 寫出作用域鏈查找過程偽代碼
var x = 10;
bar() ;
function bar(){
var x = 30;
function foo(){
console.log(x) ;
}
foo();
}
- 輸出結果:30
- 查找過程:調用foo函數(shù)時,foo函數(shù)先在自身作用域中查找變量x政模,沒有找到于是依次向外層查找岗宣,在bar函數(shù)中找到了x值為30,不再繼續(xù)向外查找淋样,控制臺打印30耗式。
以下代碼輸出什么? 寫出作用域鏈的查找過程偽代碼
var x = 10;
bar()
function bar(){
var x = 30;
(function (){
console.log(x)
})()
}
- 輸出結果:30
- 查找過程:函數(shù)bar里存在一個立即執(zhí)行函數(shù),立即執(zhí)行函數(shù)在自身作用域沒找到x變量于是繼續(xù)向外層查找习蓬,找到x=30纽什,打印30.
以下代碼輸出什么? 寫出作用域鏈查找過程偽代碼
var a = 1;
function fn(){
console.log(a) //(1)
var a = 5
console.log(a) //(2)
a++
var a
fn3() //(3)
fn2() //(4)
console.log(a) //(5)
function fn2(){
console.log(a)
a = 20
}
}
function fn3(){
console.log(a)
a = 200
}
fn()
console.log(a) //(6)
- 輸出結果:從上到下依次是undefined躲叼、5、1企巢、6枫慷、20、200
- 查找過程:
- 調用函數(shù)fn浪规,進入fn的執(zhí)行環(huán)境或听,var a變量聲明前置并初始化a為undefined,所以(1)處打印undefined笋婿;
- 給a賦值5誉裆,所以(2)處打印5;
- 繼續(xù)執(zhí)行a++缸濒,得到a為6留到下次使用
- 繼續(xù)執(zhí)行函數(shù)fn3()足丢,進入fn3作用域,fn3內沒有聲明變量a于是向外層查找庇配,而fn(3)所處作用域為全局作用域斩跌,無論它在哪執(zhí)行,永遠都是打印的全局變量1捞慌,所以(3)處打印1耀鸦,繼續(xù)向下讀取a=200,fn3不存在函數(shù)嵌套的情況啸澡,里面的a也未被聲明袖订,所以a是一個全局變量,全局中聲明了變量a嗅虏,于是替換全局變量a的值1為200洛姑,所以(6)處打印200;
- 接著調用fn2函數(shù)旋恼,進入fn2執(zhí)行環(huán)境吏口,沒有找到變量a奄容,于是向外層函數(shù)fn中查找,得到了上上一步中的值6产徊,所以(4)處打印6昂勒,繼續(xù)向下讀取a=20,a未被聲明于是會逐級向上查找變量a舟铜,在父函數(shù)fn中找到了變量a并賦值20戈盈,所以(5)處打印20。
分析:fn3改變了全局變量a的值谆刨,fn2改變了fn中局部變量a的值塘娶。
注意!H病刁岸!
mdn和阮一峰的教程中指出,所有未聲明的變量都是全局的她我。
這句話不太準確虹曙,因為沒有考慮到函數(shù)嵌套的情況。
在未聲明變量的情況下番舆,子函數(shù)是可以引用父函數(shù)內的變量的酝碳,它從子函數(shù)向父函數(shù)一層層向上查找,一直找到全局作用域恨狈,找到了變量疏哗,就使用那個作用域所在的變量,如果一直沒找到禾怠,就會在全局創(chuàng)建一個(非嚴格模式下)返奉。
參考資料: