1.函數(shù)聲明和函數(shù)表達式有什么區(qū)別
函數(shù)就是一段可以反復(fù)調(diào)用的代碼塊。函數(shù)還能接受輸入的參數(shù)偷俭,不同的參數(shù)會返回不同的值浪讳。
JavaScript有三種方法,可以聲明一個函數(shù)涌萤。
1.function命令
function
命令聲明的代碼區(qū)塊淹遵,就是一個函數(shù)。function命令后面是函數(shù)名负溪,函數(shù)名后面是一對圓括號透揣,里面?zhèn)魅牒瘮?shù)的參數(shù)。函數(shù)體放在大括號里面川抡。
function add(s) {
console.log(s)
}
上面的代碼命名了一個print
函數(shù)辐真,以后使用print()
這種形式,就可以調(diào)用相應(yīng)的代碼崖堤。這叫做函數(shù)的聲明(Function Declaration)侍咱。
2.函數(shù)表達式
除了使用function
命令聲明函數(shù),還可以采用變量賦值的寫法密幔。
var add = function (s) {
console.log(typeof s);
};
這種寫法講一個匿名函數(shù)賦值給變量楔脯。這時,這個匿名函數(shù)又稱函數(shù)表達式(Function Declaration)胯甩,因為賦值語句的等號右側(cè)只能放表達式昧廷。
采用函數(shù)表達式聲明函數(shù)時,function
命令后面不帶有函數(shù)名偎箫。如果加上函數(shù)名木柬,該函數(shù)名只在函數(shù)體內(nèi)部有效,在函數(shù)體外部無效镜廉。
var add = function x() {
console.log(typeof x);
};
x //ReferenceError: s is not defined
add()
// function
上面代碼在函數(shù)表達式中弄诲,加入了函數(shù)名x
。這個x
只在函數(shù)體內(nèi)部可用娇唯,指代函數(shù)表達式本身齐遵,其他地方都不可用。這種寫法的用處有兩個塔插,一是可以在函數(shù)體內(nèi)部調(diào)用自身梗摇,二是方便除錯(除錯工具顯示函數(shù)調(diào)用棧時,將顯示函數(shù)名想许,而不再顯示這里是一個匿名函數(shù))伶授。
因此断序,下面的形式聲明函數(shù)也非常常見。
var f = function f() {};
函數(shù)聲明和函數(shù)表達式區(qū)別(一):
函數(shù)的表達式需要在語句的結(jié)尾加上分號糜烹,表示語句結(jié)束违诗。而函數(shù)的聲明在結(jié)尾的大括號后面不用加分號。
函數(shù)聲明和函數(shù)表達式區(qū)別(二):
function
命令聲明函數(shù)疮蹦,會聲明前置诸迟。
函數(shù)表達式聲明函數(shù),不會聲明前置愕乎。
fn()
function fn(){
console.log ('hello')
}
//"hello",不會報錯阵苇,因為function聲明會自動前置
fn()
var fn=function(){
console.log ('hello')
}
//TypeError: fn is not a function
// 之所以會報錯,是因為函數(shù)表達式聲明函數(shù)感论,函數(shù)聲明不會前置
// 所以當(dāng)調(diào)用函數(shù)fn()時绅项,沒有聲明所以就報錯了
3.Finction構(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ù)接受三個參數(shù)比肄,除了最后一個參數(shù)是add
函數(shù)的"函數(shù)體"快耿,其他參數(shù)都是add
函數(shù)的參數(shù)。
你可以傳遞任意數(shù)量的參數(shù)給Function
構(gòu)造函數(shù)薪前,只有最后一個參數(shù)會被當(dāng)作函數(shù)體润努,如果只有一個參數(shù),該參數(shù)就是函數(shù)體示括。
var foo = new Function(
' return "hello world" '
)
//等同于
function foo() {
return 'hello world';
}
Function
構(gòu)造函數(shù)可以不使用new
命令,返回結(jié)果完全一樣痢畜。
總的來說垛膝,這種聲明函數(shù)的方式非常不直觀,幾乎無人使用
函數(shù)的重復(fù)聲明
如果一個函數(shù)被多次聲明丁稀,后面的聲明就會覆蓋前面的聲明吼拥。
function f() {
console.log(1);
}
f() //1
function f() {
console.log(1);
}
f() //2
2.什么是變量的聲明前置?什么是函數(shù)的聲明前置
(1)變量的聲明前置
所謂的變量聲明前置就是在一個作用域塊中线衫,所有的變量都被放在塊的開始出聲明凿可。
用var
創(chuàng)建變量,聲明會前置
console.log(a)
var a=1
等同于
var a
console.log(a)
a=1
//輸出 undefined
如果上面沒用 var 聲明變量授账,直接使用就是會報錯
console.log(x)
x=1 // Uncaught ReferenceError: x is not defined
//因為變量 x 沒有聲明
(2)函數(shù)的聲明前置
和變量聲明前置一樣枯跑,執(zhí)行代碼之前會先讀取函數(shù)聲明,只要函數(shù)在代碼中進行了聲明白热,無論它在哪個位置上進行聲明敛助,js引擎都會將它的聲明放在范圍作用域的頂部。
js引擎將函數(shù)名視同變量名屋确,所以采用function
命令聲明函數(shù)時纳击,整個函數(shù)會像變量聲明一樣续扔,被提升到代碼頭部(用function
聲明函數(shù),函數(shù)聲明會前置)焕数。所以下面的代碼不會報錯纱昧。
f();
function f() {}
表面上,上面代碼好像在聲明之前就調(diào)用了函數(shù)f
堡赔。但是實際上识脆,由于"變量提升",函數(shù)f
被提升到了代碼頭部加匈,也就是在調(diào)用之前就已經(jīng)聲明了存璃。但是,如果采用賦值語句定義函數(shù)雕拼,JavaScript就會報錯纵东。
f();
var f = function () {};
// TypeError: f is not a function
等同于
var f;
f();
f = function () {};
// TypeError: f is not a function
上面的代碼調(diào)用f
的時候,f
只是被聲明了啥寇,還沒有被賦值偎球,等于undefined
,所以會報錯辑甜。因此衰絮,如果同時采用function
命令和賦值語句聲明同一個函數(shù),最后總是采用賦值語句的定義磷醋。
var f = function() {
console.log('1');
}
function f() {
console.log('2');
}
f() //1
等同于
var f
function f() {
console.log('2');
}
f = function() {
console.log('1');
}
f() //1
3.arguments 是什么?
(1)定義
由于JavaScript允許函數(shù)有不定數(shù)目的參數(shù)猫牡,所以我們需要一種機制,可以在函數(shù)體內(nèi)部讀取所有參數(shù)邓线。這就是arguments對象的由來淌友。
(2)用法
- 讀取參數(shù)
arguments對象包含了函數(shù)運行時的所有參數(shù),arguments[0]就是第一個參數(shù)骇陈,arguments[1]就是第二個參數(shù)震庭,以此類推。這個對象只有在函數(shù)體內(nèi)部你雌,才可以使用器联。
var f = function(one) {
console.log(arguments[0]);
console.log(arguments[1]);
console.log(arguments[2]);
console.log(arguments);
}
f(1, 2, 3)
// 1
// 2
// 3
// [1,2,3]
- 為參數(shù)賦值
arguments對象除了可以讀取參數(shù),還可以為參數(shù)賦值(嚴(yán)格模式不允許這種用法)婿崭。
var f = function(a, b) {
arguments[0] = 3;
arguments[1] = 2;
return a + b;
}
f(1, 1)
// 5
- 查詢參數(shù)個數(shù)
可以通過arguments對象的length屬性拨拓,判斷函數(shù)調(diào)用時到底帶幾個參數(shù)。
function f() {
return arguments.length;
}
f(1, 2, 3) // 3
f(1) // 1
f() // 0
(3)與數(shù)組的關(guān)系
需要注意的是逛球,雖然arguments很像數(shù)組千元,但它是一個對象。數(shù)組專有的方法(比如slice和forEach)颤绕,不能在arguments對象上直接使用幸海。
但是祟身,可以通過apply方法,把arguments作為參數(shù)傳進去物独,這樣就可以讓arguments使用數(shù)組方法了袜硫。
// 用于apply方法
myfunction.apply(obj, arguments).
// 使用與另一個數(shù)組合并
Array.prototype.concat.apply([1,2,3], arguments)
要讓arguments對象使用數(shù)組方法,真正的解決方法是將arguments轉(zhuǎn)為真正的數(shù)組挡篓。下面是兩種常用的轉(zhuǎn)換方法:slice方法和逐一填入新數(shù)組婉陷。
var args = Array.prototype.slice.call(arguments);
// or
var args = [];
for (var i = 0; i < arguments.length; i++) {
args.push(arguments[i]);
}
(4)callee屬性
arguments對象帶有一個callee屬性,返回它所對應(yīng)的原函數(shù)官研。
var f = function(one) {
console.log(arguments.callee === f);
}
f() // true
可以通過arguments.callee
秽澳,達到調(diào)用函數(shù)自身的目的。這個屬性在嚴(yán)格模式里面是禁用的戏羽,不建議使用担神。
4.函數(shù)的"重載"怎樣實現(xiàn)?
函數(shù)重載是指同一范圍內(nèi)聲明幾個同名函數(shù),它們的功能類似始花,但是形參不同(指參數(shù)的個數(shù)妄讯、類型或者順序必須不同)。
也就是說用同一個函數(shù)完成不同的功能酷宵。重載函數(shù)常用來實現(xiàn)功能類似而所處理的數(shù)據(jù)類型不同的問題亥贸,有了重載函數(shù),就不需要為功能相似浇垦、參數(shù)不同的函數(shù)而選用不同的函數(shù)名炕置。
// 其他語言重載范例
int sum(int num1, int num2){
return num1 + num2;
}
float sum(float num1, float num2){
return num1 + num2;
}
sum(1, 2);
sum(1.5, 2.4);
// 函數(shù)會根據(jù)形參的類型,這里是整數(shù)型和浮點型男韧,來選擇對應(yīng)的函數(shù)讹俊,這就是函數(shù)的“重載”
在JavaScript中,沒有重載煌抒,因為同名函數(shù)會發(fā)生覆蓋。但可以在函數(shù)體內(nèi)針對不同的參數(shù)調(diào)用執(zhí)行相應(yīng)的邏輯厕倍,來實現(xiàn)重載的功能:傳遞進來不同的參數(shù)寡壮,實現(xiàn)不同的功能。
// 用其他方法達到重載的效果
function printPeopleInfo(name, age, sex){
if(name){
console.log(name);
}
if(age){
console.log(age);
}
if(sex){
console.log(sex);
}
}
printPeopleInfo('Byron', 26);
printPeopleInfo('Byron', 26, 'male');
5.立即執(zhí)行函數(shù)表達式(IIFE)是什么讹弯?有什么作用
JavaScript 中况既,聲明一個函數(shù),要想它運行组民,就得調(diào)用它棒仍,在JavaScript中,一對圓括號()
是一種運算符臭胜,跟在函數(shù)名之后莫其,表示調(diào)用該函數(shù)癞尚。比如,print()
乱陡,表示調(diào)用print
函數(shù)浇揩。
有時,我們需要在定義函數(shù)之后憨颠,立即調(diào)用該函數(shù)胳徽。這時,你不能在函數(shù)的定義之后加上圓括號爽彤,這會產(chǎn)生語法錯誤养盗。
function() { /* code */ }();
//SyntaxError: Unexpected token (
在函數(shù)表達式后面加上圓括號調(diào)用就不會報錯
var f = function() { console.log(1)}();
//輸出1,不會報錯
產(chǎn)生這個錯誤的原因是适篙,function
這個關(guān)鍵字即可以當(dāng)做語句往核,也可以當(dāng)做表達式。
// 語句
function f() {}
// 表達式
var f = function f() {}
為了避免解析上的歧義匙瘪,JavaScript引擎規(guī)定铆铆,如果function
關(guān)鍵字出現(xiàn)在行首,一律解釋成語句丹喻。因此薄货,JavaScript引擎看到行首是function
關(guān)鍵字之后,認(rèn)為這一段都是函數(shù)的定義碍论,不應(yīng)該以圓括號結(jié)尾谅猾,所以就報錯了。
解決方法就是不要讓function
出現(xiàn)在行首鳍悠,讓引擎將其理解成一個表達式税娜。所以,如果想要聲明了這個函數(shù)藏研,并且立刻運行它敬矩,就可以把這個函數(shù)聲明變成表達式,最簡單的辦法蠢挡,就是將其放在一個括號里弧岳。
(function () { /* code */ } ());
//或者
(function () { /* code */ })();
上面兩種寫法都是以圓括號開頭,引擎就會認(rèn)為后面跟著的是一個表達式业踏,而不是函數(shù)語句禽炬,所以就避免了錯誤。這種就叫做"立即調(diào)用的函數(shù)表達式"(Immediately-Invoked Function Expression)勤家,簡稱IIFE腹尖。
注意,上面兩種寫法最后的分號都是必須的伐脖。如果省略分號热幔,遇到連著兩個IIFE乐设,可能就會報錯。
(function () { /* code */ } ())
(function () { /* code */ }())
//報錯
上面代碼的兩行之間沒有分號断凶,JavaScript會將它們連在一起解釋伤提,將第二行解釋為第一行的參數(shù)。
推而廣之认烁,任何讓解釋器以表達式來處理函數(shù)定義的方法肿男,都能產(chǎn)生同樣的效果,比如下面三種寫法却嗡。
var i = function() { console.log(1); }(); //1
true && function() { return 2; }(); //2
0,function() { return 3; }(); //3
立即執(zhí)行函數(shù)表達式的作用
通常情況下舶沛,只對匿名函數(shù)使用這種"立即執(zhí)行的函數(shù)表達式"。它的目的只有兩個:一是不必為函數(shù)命名窗价,避免了污染全局變量如庭;二是IIFE內(nèi)部形成了一個單獨的作用域,可以封裝一些外部無法讀取的私有變量撼港。
//寫法一
var tmp = newData;
processData(tmp);
storeData(tmp);
//寫法二
(function() {
var tmp = newData;
processData(tmp);
storeData(tmp);
}());
6.求n!坪它,用遞歸來實現(xiàn)
遞歸的特點:
- 自己調(diào)用自己
- 設(shè)定終止條件
優(yōu)點:算法簡單
缺點:效率低下
用遞歸實現(xiàn)階乘 n!
function f(n){
if (n<=1){
return 1
} //設(shè)定終止條件
return n * f(n-1)
} //自己調(diào)用自己
f(5) // 120
用 for 循環(huán)實現(xiàn)階乘 n!
function f(n) {
var y=1;
for (var i=1; i<n+1; i++){
y = y * i
}
return y
}
f(5) // 120
7.代碼練習(xí)
(1)以下代碼輸出什么?
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
(2)寫一個函數(shù)帝牡,返回參數(shù)的平方和往毡?
function sumOfSquares(){
}
var result = sumOfSquares(2,3,4)
var result2 = sumOfSquares(1,3)
console.log(result) //29
console.log(result2) //10
//實現(xiàn)功能函數(shù):
function sumOfSquares(){
var sum = 0;
for(var i=0; i<arguments.length; i++) {
sum = sum + arguments[i]*arguments[i];
}
return sum;
}
(3)如下代碼的輸出?為什么
console.log(a);
var a = 1;
console.log(b);
// undefined
// Uncaught ReferenceError: b is not defined
----------------------------------
/*解釋:
因為 var 的聲明會前置靶溜,所以代碼的順序是
var a
console.log(a) // undefined开瞭,因為 a 聲明了但沒有賦值
a=1
console.log(b) // 報錯,因為 b 沒有聲明
*/
(4)如下代碼的輸出罩息?為什么
sayName('world');
sayAge(10);
function sayName(name){
console.log('hello ', name);
}
var sayAge = function(age){
console.log(age);
};
// hello world
// 報錯嗤详,ncaught TypeError: sayAge is not a function
--------------------------------------
/*解釋:
采用function命令聲明函數(shù)時,整個函數(shù)會像變量聲明一樣瓷炮,被提升到代碼頭部(用function聲明函數(shù)葱色,函數(shù)聲明會前置)。而函數(shù)表達式聲明函數(shù)時娘香,函數(shù)聲明不會前置冬筒。
以上代碼順序為:
function sayName(name){
console.log('hello ', name);
}
var sayAge
sayName('world'); // hello world
sayAge(10); //報錯,因為sayAge() 函數(shù)沒有聲明
sayAge = function(age){
console.log(age)
}
*/
(5)如下代碼輸出什么? 寫出作用域鏈查找過程偽代碼
var x = 10
bar()
function foo() {
console.log(x)
}
function bar(){
var x = 30
foo()
}
// 輸出 10
--------------------------------
/*解釋:作用域鏈查找的偽代碼如下
進入全局執(zhí)行上下文:
globalContext = {
AO: {
x: 10,
foo: function,
bar: function,
};
Scope: null;
foo.[Scope] = globalContext.AO;
bar.[Scope] = globalContext.AO;
執(zhí)行 bar();
}
進入bar()的執(zhí)行上下文 // 從globalContext.AO進入
barContext = {
AO: {
x: 30,
};
Scope: globalContext.AO;
執(zhí)行 foo(); // 在這個作用域內(nèi)找不到茅主,就從Scope中去找
}
進入 foo()的執(zhí)行上下文 // 從globalContext.AO進入
fooContext = {
AO: {};
Scope: globalContext.AO;
執(zhí)行 console.log(x) // 輸出10土榴,因為 foo()的作用域是globalContext.AO
}
*/
(6)如下代碼輸出什么? 寫出作用域鏈查找過程偽代碼
var x = 10;
bar()
function bar(){
var x = 30;
function foo(){
console.log(x)
}
foo();
}
// 30
----------------------------------
/*解釋:作用域鏈查找偽代碼如下
進入全局的執(zhí)行上下文
globalContext = {
AO: {
x: 10,
bar: function,
};
Scope: null;
bar.[Scope] = globalContext.AO;
執(zhí)行 bar();
}
進入 bar()的執(zhí)行上下文 // 從 globalContext.AO 進入
barContext = {
AO: {
x: 30,
foo: function,
};
Scope: globalContext.AO;
foo.[Scope] = barContext.AO;
執(zhí)行 foo();
}
進入 foo()的執(zhí)行上下文 // 從 barContext.AO 進入
fooContext = {
AO: {};
Scope: barContext.AO;
執(zhí)行 console.log(x) // 輸出 30诀姚,因為在 AO 中找不到,就從 Scope 中找
}
*/
(7)如下代碼輸出什么? 寫出作用域鏈查找過程偽代碼
var x = 10;
bar()
function bar(){
var x = 30;
(function (){
console.log(x)
})()
}
// 30
-----------------------------------
/*解釋:作用域鏈查找偽代碼如下
進入全局的執(zhí)行上下文
globalContext = {
AO: {
X: 10,
bar: function,
};
Scope: null;
bar.[Scope] = globalContext.AO;
執(zhí)行 bar();
}
進入 bar() 的執(zhí)行上下文
barContext = {
AO: {
x: 30,
function: function,
};
Scope: globalContext.AO;
function.[Scope] = barContext.AO;
執(zhí)行 function ()
}
進入 function() 的執(zhí)行上下文
functionContext = {
AO: {};
Scope: barContext.AO
執(zhí)行 console.log(x) // 輸出30 因為 AO 中沒有玷禽,就從 Scope 中去找
}
*/
(8)如下代碼輸出什么? 寫出作用域鏈查找過程偽代碼
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)
// 最終輸出為 undefined 5 1 6 20 200
------------------------------------
/*解釋:作用域鏈查找偽代碼如下
進入全局執(zhí)行上下文
globalContext = {
AO: {
a: 1,
fn: function,
fn3: function,
};
Scope: null
fn.[Scope] = globalContext.AO;
fn3.[Scope] = globalContext.AO;
執(zhí)行 fn();
}
進入 fn() 的執(zhí)行上下文
fnContext = {
var a;
function fn2(){}
fn2.[Scope] = fnContext
console.log(a) // undefined [1] var 聲明前置未賦值
a = 5
console.log(a) // 5 [2] a 已經(jīng)賦值 5
a++
執(zhí)行 fn3(){
console.log(a) // 1 [3] fn3 的作用域為 globalContext
a = 200 //改變了 globalContext 中的 a
}
執(zhí)行 fn2(){
console.log(a) // 6 [4] fn2 的作用域為 fnContext
a = 20 // 改變了 fnContext 中的 a 為 20
}
console.log(a) // 20 [5] a 已經(jīng)賦值 20
}
console.log(a) // 200 [6] 它的作用域為globalContext
// 最終輸出為 undefined 5 1 6 20 200
*/