JavaScript基礎入門 02
條件語句
在js
中提供了if
和switch
語句,只有滿足了條件判斷,才能執行相應的語句。
if 語句
if
結構先判斷一個表達式的布爾值,然后根據布爾值的真偽,執行不同的語句。所謂布爾值,指的是 JavaScript 的兩個特殊值,true
表示真,false
表示偽
。
if(布爾值)
語句;
// 或者 是
if(布爾值) 語句;
上面是if
結構的基本形式。需要注意的是,“布爾值”往往由一個條件表達式產生的,必須放在圓括號中,表示對表達式求值。如果表達式的求值結果為true
,就執行緊跟在后面的語句;如果結果為false
,則跳過緊跟在后面的語句。
var name = "zhangsan";
if (name === "zhangsan") console.log("Welcome zhangsan");
上面的代碼會輸出Welcome zhangsan
,因為此時if
的條件語句里面條件為真,這是就會返回true
,if
語句條件為true
,就直接執行了后面的console.log()
語句,但是如果我們嘗試去更改這個變量,就會發現console.log()
語句將不會執行。
需要注意的是上面這種寫法if語句里面只能有一個語句,如果需要寫多個語句,那么需要在if條件之后使用{}
.
if (條件){
//code ... ;
//code ... ;
}
建議總是在if
語句中使用大括號,因為這樣方便插入語句。
注意,if
后面的表達式之中,不要混淆賦值表達式(=
)、嚴格相等運算符(===
)和相等運算符(==
)。尤其是賦值表達式不具有比較作用。
var x = 1;
var y = 2;
if (x = y) {
console.log(x);
}
// "2"
在上面的代碼中,原意是,當x
等於y
的時候,才執行相關語句。但是,不小心將嚴格相等運算符寫成賦值表達式,結果變成了將y
賦值給變量x
,再判斷變量x
的值(等於2)的布爾值(結果為true
)。
這種錯誤可以正常生成一個布爾值,因而不會報錯。為了避免這種情況,有些開發者習慣將常量寫在運算符的左邊,這樣的話,一旦不小心將相等運算符寫成賦值運算符,就會報錯,因為常量不能被賦值。
if (x = 2){
}// 不報錯
if (2 = x){
} // 報錯
if .. else 語句
if
代碼塊后面,還可以跟一個else
代碼塊,表示不滿足條件時,所要執行的代碼。
if (m === 3) {
// 滿足條件時,執行的語句
} else {
// 不滿足條件時,執行的語句
}
上面代碼判斷變量m
是否等於3,如果等於就執行if
代碼塊,否則執行else
代碼塊。
對同一個變量進行多次判斷時,多個if...else
語句可以連寫在一起。
if (m === 0) {
// ...
} else if (m === 1) {
// ...
} else if (m === 2) {
// ...
} else {
// ...
}
switch 結構
多個if...else
連在一起使用的時候,可以轉為使用更方便的switch
結構。
switch (fruit) {
case "banana":
// ...
break;
case "apple":
// ...
break;
default:
// ...
}
上面代碼根據變量fruit
的值,選擇執行相應的case
。如果所有case
都不符合,則執行最后的default
部分。需要注意的是,每個case
代碼塊內部的break
語句不能少,否則會接下去執行下一個case
代碼塊,而不是跳出switch
結構。
需要注意的是,switch
語句后面的表達式,與case
語句后面的表示式比較運行結果時,采用的是嚴格相等運算符(===
),而不是相等運算符(==
),這意味着比較時不會發生類型轉換。
循環語句
循環語句主要用於執行重復性的操作,在js
當中循環具有多種形式。
while 循環
While
語句包括一個循環條件和一段代碼塊,只要條件為真,就不斷循環執行代碼塊。
while (條件)
語句;
// 或者
while (條件) 語句;
while
語句的循環條件是一個表達式,必須放在圓括號中。代碼塊部分,如果只有一條語句,可以省略大括號,否則就必須加上大括號。
while (條件) {
語句;
}
下面是向指定的人打招呼的一個循環一句:
var name = "zhangsan";
var i = 0;
while(i<10){
console.log('Hello,' + name);
i = i + 1;
}
下面的例子是一個無限循環,因為循環條件總是為真。
while(true){
console.log("hello,world!");
}
continue 關鍵字
continue
語句用於立即終止本輪循環,返回循環結構的頭部,開始下一輪循環。
var i = 0;
while (i<100) {
i = i + 1;
if (i % 2 !==0) continue;
console.log(i);
}
例如上面的例子當中,輸出100以內的偶數,這是一種重復性較高的工作,那么就可以使用循環的形式。
上面的案例,讓i的初始值為0,並且隨着每一次的循環遞增,在遞增的過程中,判斷除以2是否會存在余數,如果存在
則使用continue
跳過。如果不存在則輸出。
do...while語句
do..while
循環與while
循環類似,唯一的區別是先運行依次循環體,然后判斷循環條件。
do
語句
while (條件);
// 或者
do {
語句
} while (條件);
需要注意的是,不管do ... while
條件是否為真,都會至少執行一次循環。同時,while語句后面的分號不能省略.
var x = 3;
var i = 0;
do {
console.log(i);
i++;
} while(i < x);
一定要注意,雖然
while
語句和do...while
語句很類似,但是while
語句必須在條件為真的情況下才能執行,而do..while
語句則
不論條件是否為真,都會執行至少一次,這是二者之前的區別。
for 循環
for
循環語句是循環的另外一種形式,可以指定循環的起點、終點和結束條件。
它的語法格式如下:
for (初始化表達式; 條件; 遞增表達式)
語句
// 或者
for (初始化表達式; 條件; 遞增表達式) {
語句
}
for
語句后面的括號里面,有三個表達式。
- 初始化表達式(initialize):確定循環變量的初始值,只在循環開始時執行一次。
- 條件表達式(test):每輪循環開始時,都要執行這個條件表達式,只有值為真,才繼續進行循環。
- 遞增表達式(increment):每輪循環的最后一個操作,通常用來遞增循環變量。
例如:
var x = 3;
for (var i = 0; i < x; i++) {
console.log(i);
}
下面是小練習:
1.入職薪水10K,每年漲幅5%,50年后工資多少?
demo:
//入職薪水10K,每年漲幅5%,50年后工資多少?
var base_m = 10000;
for (var i =1;i <= 50; i++){
base_m = base_m * 0.05 + base_m;
}
console.log('五十年后的薪水為:' + base_m);
2.打印100以內的偶數
demo:
for(var i=0;i<100;i++){
if (i % 2 ===0 ) {
console.log(i);
}
}
3.打印100以內所有偶數的和:
demo:
var num_val = 0;
for(var i=0;i<100;i++){
if (i % 2 ===0 ) {
num_val = num_val + i;
}
}
console.log(num_val);
for循環的嵌套
一般情況下,循環與循環之間是可以完成嵌套的。 例如,我們可以通過兩個for循環的嵌套打印出一個直角三角形。
demo:
for(var i =0;i<9;i++){
console.log("one" + i);
for(var j = 0; j < i -1 ;j++){
console.log("two - "+j)
document.writeln("*");
}
document.write("<br/>");
}
想要合理的使用嵌套循環,需要清楚的知道整個循環的執行順序。
在使用
for
循環的過程中,我們可以將for循環的條件省略,例如:
for(;;){
// 此時將會是一個死循環
}
break 關鍵字
break
關鍵字類似於continue
關鍵字,可以讓代碼不按既有順序執行。
而和continue
不同的是,break
會直接跳過整個循環流程,而continue
只會跳過那一個步驟。
例如:
for(var i=0;i<10;i++){
console.log('當前的i值是: ' + i);
if (i === 5) break;
}
代碼運行的結果就是當i
等於5時,整個循環都會被跳出。
而如果把上面的代碼中break
換成continue
,那么當i
等於5時,就會跳過當前這一步的循環開啟一個新的循環。
死循環
循環主要根據我們設定的條件來判斷是否要開啟下一次循環過程。如果我們設定的循環的條件不合理的話,就會進入死循環
,代碼進入死循環后,將一直陷入到死循環當中。
for(var i=0;i >=0 ;i++){
console.log('hello,world');
}
debug 調試工具
在編寫js
代碼的過程中,我們經常需要調試代碼,雖然可以使用console.log()
的形式來調試代碼,但是當業務
邏輯過於復雜時,將變得復雜起來。
所以我們在開發debug
的過程中,除了使用console.log()
以外,還應該更加靈活的使用關鍵字debugger
就可以讓代碼執行到
debugger
的位置停止。
當然,我們可以選擇使用不同瀏覽器當中內置的輔助工具幫助我們調試也是沒有問題的。無論用什么工具,只要能夠幫助我們准確的定位
錯誤就都可以。
1. 打印100–200之間所有能被3或者7整除的數
demo:
for(var i=100; i<= 200; i++){
if(i % 3 === 0 || i % 7 === 0) {
console.log(i);
}
}
2. 計算100的階乘
階乘指從1乘以2乘以3乘以4一直乘到所要求的數。例如所要求的數是4,則階乘式是1×2×3×4,得到的積是24,24就是4的階乘。 例如所要求的數是6,則階乘式是1×2×3×……×6,得到的積是720,720就是6的階乘。
例如所要求的數是n,則階乘式是1×2×3×……×n,設得到的積是x,x就是n的階乘。任何大於1的自然數n階乘表示方法: n!=1×2×3×……×n 或 n!=n×(n-1)! 5!=54321=120。
demo:
// 計算100 的階乘
var b_n = 1;
for(var i=1;i<=100;i++){
b_n = b_n * i;
}
console.log(b_n);
函數
函數是一段可以反復調用的代碼塊,在使用函數的過程中,也可以給函數傳入一定的參數,讓函數的使用更加的靈活。
同時,需要知道的是,不同的函數返回不同的值。
函數的聲明方式
在js
中有三種函數的聲明方式:
(1)function 命令
function命令聲明的代碼區塊,就是一個函數。function命令后面是函數名,函數名后面是一對圓括號,里面是傳入函數的參數。函數體放在大括號里面。
例如:
function fn1() {
// code ...
}
上面的代碼定義了一個函數fn1
,我們想要使用這個函數,只需要通過fn1()
這種形式即可執行其中相應的代碼段。
這就叫做函數的聲明(Function Declaration)。
(2) 函數表達式
除了用function命令聲明函數,還可以采用變量賦值的寫法。
var fn1 = function () {
console.log("hello,world!");
}
這種寫法將一個匿名函數賦值給變量。這時,這個匿名函數又稱函數表達式(Function Expression),因為賦值語句的等號右側只能放表達式。
采用函數表達式聲明函數時,function命令后面不帶有函數名。如果加上函數名,該函數名只在函數體內部有效,在函數體外部無效。
例如:
var fn1 = function test() {
console.log(typeof test);
};
fn1(); // 通過這種調用方式可以順利執行代碼
test(); // 通過這種調用方式則會出現not defined 錯誤。
在實際開發的過程中,上面的這種在使用函數表達式
寫法並且同時給函數設置名字的做法其實很常見。
這么做的目的有兩個:
- 可以非常方便的在函數體內部調用自身
- 方便除錯,除錯工具顯示函數調用棧時,將顯示函數名,而不再顯示這里是一個匿名函數
var f = function f() {};
需要注意的是,函數的表達式需要在語句的結尾加上分號,表示語句結束。而函數的聲明在結尾的大括號后面不用加分號。總的來說,這兩種聲明函數的方式,差別很細微,可以近似認為是等價的。
(3) Function 構造函數
第三種方式是通過Function
構造函數。
例如:
var add = new Function (
'x',
'y',
'return x + y'
);
console.log(add);
上面這個通過構造函數創建的代碼與下面的代碼大體是相等的:
function add(x,y) {
return x + y;
}
上面通過構造函數創建函數的代碼中,Function構造函數接受三個參數,除了最后一個參數是add函數的“函數體”,其他參數都是add函數的參數。
你可以傳遞任意數量的參數給Function構造函數,只有最后一個參數會被當做函數體,如果只有一個參數,該參數就是函數體。
var foo = new Function(
'return "hello world";'
);
// 等同於
function foo() {
return 'hello world';
}
總的來說,這種聲明函數的方式非常不直觀,幾乎無人使用。
函數重復聲明
當在js
代碼中出現函數重復聲明的情況(也就是函數名相同),那么后面的函數會把前面的函數給覆蓋掉。
function fn1() {
console.log(1111);
}
function fn1() {
console.log(2222);
}
fn1(); // 2222
函數提升
在js
當中,函數存在函數
提升的現象,類似於變量提升
。在宿主環境
執行代碼的前一刻,會預先解析一次代碼
,將代碼中的變量聲明
和函數
都提升到代碼的最頂端。
也就意味着我可以在函數定義之前的任何位置調用后面才定義的函數。
fn1();
function fn1() {
console.log("hello,world!");
}
圓括號運算符
在我們調用函數的時候,要使用圓括號運算符。圓括號之中,可以加入函數的參數。
例如:
function fn1() {
console.log("hello,world!");
}
fn1();
在上面的案例中,函數名后面緊跟一對圓括號,就會調用這個函數。
我們也可以通過在調用函數的時候傳遞參數從而實現數據的傳遞。
function sayHello(name) {
console.log("你好,"+name);
}
sayHello("張三");
一般情況下,我們將函數名后面的括號里面的參數稱之為
形參
,而在調用函數時使用的圓括號里面傳遞的參數,我們稱之為實參
。
return 語句
在js
函數體內的return
語句,表示返回。
JavaScript 引擎遇到return語句,就直接返回return后面的那個表達式的值,后面即使還有語句,也不會得到執行。也就是說,return語句所帶的那個表達式,就是函數的返回值。return語句不是必需的,如果沒有的話,該函數就不返回任何值,或者說返回undefined。
例如:
function fn1(){
var name = "zhangsan";
var age = 19;
return name + "今年" + age + "歲!";
}
上面的案例中我們在函數體內設置了一個返回值
。返回值會將表達式的結果從函數體內返回到函數體的外部,讓我們可以在函數外部訪問。
那么該如何訪問函數的返回值呢?
例如:
function fn1(){
var name = "zhangsan";
var age = 19;
return name + "今年" + age + "歲!";
}
// 可以直接打印調用函數的語句。就可以在調用函數的同時還打印出返回值
console.log(fn1());
// 也可以直接將返回值賦值給一個變量
var return_value = fn1(); // 函數在執行的同時也將返回值賦值給了變量return_value
console.log(return_value);
如果函數內沒有返回值,但是我們偏偏還來打印,那么結果將返回undefined
。
function fn1(){
var name = "zhangsan";
var age = 19;
}
console.log(fn1()); // undefined
var return_value = fn1();
console.log(return_value);//undefined
遞歸函數
當函數發生在函數體內自己調用自己這樣的情況時,這個函數我們就可以稱之為叫做遞歸函數
。
例如下面的demo
,就是一個遞歸函數
:
// 遞歸函數
function fib(num) {
if (num === 0) return 0;
if (num === 1) return 1;
return fib(num - 2) + fib(num - 1);
}
console.log(fib(6)); // 8
在上面的demo
中,通過遞歸函數
計算了一下斐波那契數列
。
上面代碼中,fib函數內部又調用了fib,計算得到斐波那契數列的第6個元素是8。
第一等公民的函數
在js
中,函數被稱為一等公民
。
什么意思呢,就是說函數
被看作是一種值,與其他的數據值(數值、字符串、布爾值等)地位相等。凡是可以使用值的地方都可以使用函數
。
例如,我們可以將一個值賦值給一個變量,對應的,同樣也可以將函數賦值給一個變量。
var fn = function () {};
同時,因為函數是一等公民
,所以也可以將函數當做參數傳遞給其他函數,或者作為函數的返回結果。
function fn1() {}
function fn2(parm) {
parm();
}
fn2(fn1);
到此,你需要明白,函數只是一個可以執行的值,此外並沒有什么特殊的地方。
函數的屬性和方法
- name 屬性
函數的name
屬性返回函數的名字。
function fn1() {}
fn1.name; // "fn1"
如果是通過變量賦值
定義的函數,那么name
屬性返回函數的名字。
var fn2 = function () {};
fn2.name; // fn2
上面的案例中,函數是一個匿名函數,我們使用name
屬性就可以獲得存儲函數的變量的名稱,但是一旦我們給匿名函數
也定義了一個函數名,那么我們通過name
屬性查看到的將會是函數的名字而不是變量名。
var fn2 = function test () {};
fn2.name; // test
需要注意的是,雖然打印出來的名稱是test
,但是真正的函數名還是fn2
,test
只是表達式的名字,並且test
這個名字只能夠在表達式內部使用。
我們可以通過name
屬性獲取參數函數的的名字。
function fn1() {}
function fn2(parm){
console.log(parm.name);
}
fn2(fn1); // fn1
在上面的代碼中,在函數的內部通過name
屬性就獲取了要傳入的函數的名字。
- length屬性
函數的length
屬性返回函數預期傳入的參數個數,即函數定義之中的參數個數也就是形參
的個數。
function fn1(a,b) {
}
console.log(fn1.length);//2
上面代碼定義了空函數f,它的length屬性就是定義時的參數個數。不管調用時輸入了多少個參數,length屬性始終等於2。
length屬性提供了一種機制,判斷定義時和調用時參數的差異,以便實現面向對象編程的“方法重載”(overload)。
- toString()
函數的toString
方法返回一個字符串,內容是函數的源碼。
function fn1(a,b) {
console.log("hello,world!");
}
console.log(fn1.toString());
通過toString
方法可以將函數全部的源碼返回。
而toString
方法可以返回function(){native code}
。
console.log(Math.abs.toString()); // function abs() { [native code] }
上面代碼中,Math.abs
是js
提供的原生的函數,toString
方法就會返回原生代碼的提示。
需要注意的是,當我們使用
toString
方法時,函數內部的注釋也會被返回
函數的作用域
- 定義
作用域(scope)
指的是變量可以生存的范圍或者說存在的范圍。
在老版本的ES5
的規范里,JS
只有兩種作用域:一種是全局作用域
,變量在整個程序中一直存在,在任何的地方都可以讀取;另外一種
是函數作用域
,變量只在函數內部存在,在函數的外部沒有辦法訪問到函數內部的變量。
ES6當中新增加了
塊級作用域
。
對於頂層函數來說,函數外部聲明的變量就是全局變量(global variable)
,可以在函數內部讀取。
var a = 10;
function fn1() {
console.log(a);
}
fn1(); // 10
上面的代碼中,我們在函數的外部聲明了一個變量,我們在函數的內部也讀取到了全局變量
的值。
在函數內部定義的變量,在函數的外部則無法讀取,我們將其稱為局部變量(local variable)
。
function t_fn1() {
var a = 10;
}
console.log(a); // ReferenceError: a is not defined
上面的案例中,我們嘗試從函數的外部訪問函數內部的局部變量(local variable)
,發現js提示我們變量沒有定義,訪問失敗。
函數內部定義的局部變量,會覆蓋掉函數外部定義的同名的全局變量
例如:
var x = 10;
function fn1(){
var x = 20;
console.log(x);
}
fn1(); // 20
在上面的代碼中,變量x
同時在函數外部和函數內部定義。結果顯而易見,函數內部的變量x
覆蓋了函數外部的變量x
。
但是需要注意的是,這種覆蓋的變量,生效范圍只能是在函數內部,如果出了函數,則變量x
恢復到本身的10
。
例如:
var x = 10;
function fn1() {
var x = 20;
console.log(x);
}
fn1(); // 20
console.log(x); // 10
在上面的代碼中,我們在函數內部訪問變量,x
的值輸出為函數內部變量的值20
,而當出了函數,我們再來訪問,你會發現變量x
的值仍然為10
。
所以說函數內部變量的覆蓋僅停留於函數內部,出了函數就會恢復過來。
注意,對於var命令來說,局部變量只能在函數內部聲明,在其他區塊中聲明,一律都是全局變量。
if (true) {
var x = 5;
}
console.log(x); // 5
函數內部的變量提升
與全局作用域一樣,函數作用域內部也會產生變量提升
現象。var
命令聲明的變量,不管在什么位置,變量聲明都會提升到函數體的頭部。
例如:
function fn1() {
var x = 2;
if (x > 3){
var tmp = x - 100;
x = x + 1;
}
}
上面的代碼並沒有什么實際的意義,但是這樣一段代碼在發生變量提升
之后等同於下面的代碼:
function fn1() {
var x,tmp;
x = 2;
if(x > 3) {
tmp = x - 100;
x = x + 1;
}
}
函數本身的作用域
上面我們說過,函數
在js
當中是一等公民
。本質上來講也就是一個能夠運行的值
。So,函數既然是一個值,那么函數也就有着自己的
作用域。
函數的作用域
與變量相同,就是其聲明時所在的作用域,一定要注意的是,函數的作用域與其運行時所在的作用域沒有關系。
例如:
var a = 1;
var x = function () {
console.log(a);
};
// 函數x在全局作用域環境下創建,所以說x函數的作用域就是全局,雖然x函數是在函數f中調用,但是作用域仍然是在全局。
function f() {
var a = 2;
x();
}
f() // 1
在上面的代碼中,函數x
是在全局作用域聲明的函數,所以此時函數的作用域被綁定到全局。
函數的參數
函數的運行時,有時需要提供外部數據,我們提供不同的數據會導致不同的結果。這種外部的數據就叫做參數。
// 定義一個函數,此時函數需要外部的數據a和b
function fn1(a,b) {
return a + b;
}
// 調用函數並且傳入數據
var x = 10;
var y = 20;
console.log(fn1(x,y));
在上面的案例中,a
和b
就是函數fn1
的參數。在調用的時候,我們需要從外部傳入數據來使用。
函數參數使用時需要注意的點:
1.函數參數不是必須的,JavaScript
允許省略參數。
function f1(a,b) {
// 如果函數在運行時沒有使用參數,那么參數也可以省略
console.log('hello,world!');
}
f1(); // hello,world
上面的代碼中,我們的函數在運行過程中並不需要參數a
和b
,所以我們在調用函數時候可以選擇省略。但是需要注意的是,我們
在函數運行的過程中如果定義了參數並且使用了參數而你卻沒有傳入實參。那么函數體內使用該參數會返回undefined
。
function f1(a,b) {
// console.log('hello,world!');
console.log(a,b);// undefined
}
// 調用函數的時候並沒有傳入實參
f1();
如果通過length
屬性查看參數,那么length
屬性返回的值其實是形參的長度而不是實參
的長度,也就意味着,只要你定義了函數
的形參,即使你在調用函數的時候沒有傳入實參,length
屬性也可以返回值。
2.函數的參數不能只省略個別
當我們調用函數的時候,我們不能省略只省略靠前的參數,而保留靠后的參數。如果一定要這么做,那么只有顯示的傳入undefined
才可以。
function f1(a,b,c,d) {
console.log(a,b,c,d);
}
// 調用函數的時候,如果想省略其中的個別參數,只能顯示的傳入undefined
// 而如果省略其中的一個參數就會報錯。
f1(undefined,undefined,10,20);
3.函數參數傳遞方式
函數參數如果是原始類型的值(數值、字符串、布爾值),傳遞方式是傳值傳遞(passes by value)。這意味着,在函數體內修改參數值,不會影響到函數外部。
例如:
var a = 10;
function fn1(param) {
param += 3;
console.log('在函數內部的變量值為:' + param);
}
fn1(a); // 在函數內部的變量值為:13
console.log(a);// 10
上面代碼中,變量p是一個原始類型的值,傳入函數f的方式是傳值傳遞。因此,在函數內部,p的值是原始值的拷貝,無論怎么修改,都不會影響到原始值。
但是,如果函數參數是復合類型的值(數組、對象、其他函數),傳遞方式是傳址傳遞(pass by reference)。也就是說,傳入函數的原始值的地址,因此在函數內部修改參數,將會影響到原始值。
var obj = { p: 1 };
function f(o) {
o.p = 2;
}
f(obj);
obj.p // 2
上面代碼中,傳入函數f的是參數對象obj的地址。因此,在函數內部修改obj的屬性p,會影響到原始值。
注意,如果函數內部修改的,不是參數對象的某個屬性,而是替換掉整個參數,這時不會影響到原始值。
var obj = [1, 2, 3];
function f(o) {
o = [2, 3, 4];
}
f(obj);
obj // [1, 2, 3]
上面代碼中,在函數f內部,參數對象obj被整個替換成另一個值。這時不會影響到原始值。這是因為,形式參數(o)的值實際是參數obj的地址,重新對o賦值導致o指向另一個地址,保存在原地址上的值當然不受影響。
4.同名參數
如果有同名的參數,則取最后出現的那個值。
例如:
function f1(a,a){
console.log(a);
}
f1(1,2); // 2
上面代碼中,函數f有兩個參數,且參數名都是a。取值的時候,以后面的a為准,即使后面的a沒有值或被省略,也是以其為准。
function f(a, a) {
console.log(a);
}
f(1) // undefined
調用函數f的時候,沒有提供第二個參數,a的取值就變成了undefined。這時,如果要獲得第一個a的值,可以使用arguments對象。
function f(a, a) {
console.log(arguments[0]);
}
f(1) // 1
arguments對象
定義
因為JavaScript
允許存在不定數目的參數,所以在開發的過程中就需要一種機制,通過這種機制,我們能夠很好的在函數內讀取所有的參數。
這就是arguments
對象的由來。
arguments
對象包含了函數運行時所有的參數(實參),arguments[0]
就是第一個參數,arguments[1]
就是第二個參數,以此類推。
需要注意的是,這個對象只有在函數體內部才能使用。
function f1() {
// 獲取傳入的實參
console.log(arguments[0]);
console.log(arguments[1]);
console.log(arguments[2]);
console.log(arguments[3]);
}
f1(1,2,3,4);// 1 2 3 4
在正常的模式下,arguments對象可以在運行的時候進行修改。
var f = function(a, b) {
arguments[0] = 3;
arguments[1] = 2;
return a + b;
}
f(1, 1) // 5
上面代碼中,函數f調用時傳入的參數,在函數內部被修改成3和2。
嚴格模式下,arguments對象與函數參數不具有聯動關系。也就是說,修改arguments對象不會影響到實際的函數參數。
var f = function(a, b) {
'use strict'; // 開啟嚴格模式
arguments[0] = 3;
arguments[1] = 2;
return a + b;
}
f(1, 1) // 2
上面代碼中,函數體內是嚴格模式,這時修改arguments對象,不會影響到真實參數a和b。
通過arguments對象的length屬性,可以判斷函數調用時到底帶幾個參數。
function f() {
return arguments.length;
}
f(1, 2, 3); // 3
f(1); // 1
f(); // 0
與數組的關系 -- 類數組對象
需要注意的是,雖然arguments
很像數組,但它是一個對象。數組專有的方法(比如slice
和forEach
),不能在arguments
對象上直接使用。
如果要讓arguments
對象使用數組方法,真正的解決方法是將arguments
轉為真正的數組。下面是兩種常用的轉換方法:slice
方法和逐一填入新數組。
var args = Array.prototype.slice.call(arguments);
// 或者
var args = [];
for (var i = 0; i < arguments.length; i++) {
args.push(arguments[i]);
}
callee屬性
arguments
對象帶有一個callee屬性,返回它所對應的原函數。
var f = function () {
console.log(arguments.callee === f);
}
f() // true
可以通過arguments.callee
,達到調用函數自身的目的。這個屬性在嚴格模式里面是禁用的,因此不建議使用。
閉包函數
閉包(closure)
是JavaScript
中的一個難點,在js當中非常多的高級應用都是通過閉包來實現的。
想要順利的理解和使用閉包函數,需要理解變量的作用域問題。在上面的博文中曾經講到,JavaScript
中有兩種作用域,
一種是全局作用域
,另外一種是函數作用域
。而且在函數內部可以直接讀取全局變量。
例如:
var x = "hello,world!";
function f() {
console.log(x);// 讀取函數外部的全局作用域
}
f(); // hello,world
在上面的案例當中,函數f
可以直接讀取函數外部的全局變量x。
但是我們需要知道的是,在函數的外部,我們沒有辦法直接讀取到函數內部的變量。
例如:
function f() {
var x = "hello,world!";
}
console.log(x);// Uncaught ReferenceError: x is not defined(
變量x
是函數f
內部的變量,所以在函數的外部我們是沒有辦法讀取的。原因是函數內部聲明的變量作用域僅限於函數的內部。
而如果在開發中存在需要調用函數內部變量的需求,我們只能通過一些特殊的手段才能夠得到函數內部的變量。
例如,我們在函數的內部在來創建一個函數,通過這個函數來調用函數內部的變量。
例如:
function f1() {
var n = 999;
function f2() {
console.log(n); // 999
}
}
上面代碼中,函數f2就在函數f1內部,這時f1內部的所有局部變量,對f2都是可見的。但是反過來就不行,f2內部的局部變量,對f1就是不可見的。這就是 JavaScript 語言特有的"鏈式作用域"結構(chain scope),子對象會一級一級地向上尋找所有父對象的變量。所以,父對象的所有變量,對子對象都是可見的,反之則不成立。
此時,函數f2
可以得到函數f1
的變量,那么我們只要將函數f2
作為返回值即可。
function f1() {
var n = 999;
function f2() {
console.log(n);
}
return f2;
}
var result = f1();
result(); // 999
上面代碼中,函數f1的返回值就是函數f2,由於f2可以讀取f1的內部變量,所以就可以在外部獲得f1的內部變量了。
閉包就是函數f2,即能夠讀取其他函數內部變量的函數。由於在 JavaScript 語言中,只有函數內部的子函數才能讀取內部變量,因此可以把閉包簡單理解成“定義在一個函數內部的函數”。閉包最大的特點,就是它可以“記住”誕生的環境,比如f2記住了它誕生的環境f1,所以從f2可以得到f1的內部變量。在本質上,閉包就是將函數內部和函數外部連接起來的一座橋梁。
閉包的最大用處有兩個,一個是可以讀取函數內部的變量,另一個就是讓這些變量始終保持在內存中,即閉包可以使得它誕生環境一直存在。請看下面的例子,閉包使得內部變量記住上一次調用時的運算結果。
例如:
function f1 (start) {
return function () {
return start ++;
}
}
var a = f1(1);
console.log(a()); // 1
console.log(a()); // 2
console.log(a()); // 3
上面代碼中,start
是函數f1
的內部變量。通過閉包,start的狀態被保留了,每一次調用都是在上一次調用的基礎上進行計算。從中可以看到,閉包a
使得函數f1
的內部環境,一直存在。所以,閉包可以看作是函數內部作用域的一個接口。
為什么會這樣呢?原因就在於a
始終在內存中,而a
的存在依賴於f1
,因此也始終在內存中,不會在調用結束后,被垃圾回收機制回收。
閉包的另一個用處,是封裝對象的私有屬性和私有方法。
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
上面代碼中,函數Person的內部變量_age,通過閉包getAge和setAge,變成了返回對象p1的私有變量。
注意,外層函數每次運行,都會生成一個新的閉包,而這個閉包又會保留外層函數的內部變量,所以內存消耗很大。因此不能濫用閉包,否則會造成網頁的性能問題。
立即調用的函數表達式 (IIFE)
在 JavaScript 中,圓括號()是一種運算符,跟在函數名之后,表示調用該函數。比如,print()就表示調用print函數。
有時,我們需要在定義函數之后,立即調用該函數。這時,你不能在函數的定義之后加上圓括號,這會產生語法錯誤。
例如:
function(){ /* code */ }();
// SyntaxError: Unexpected token (
產生這個錯誤的原因是,function這個關鍵字即可以當作語句,也可以當作表達式。
例如:
// 語句
function f() {}
// 表達式
var f = function f() {}
為了避免解析上的歧義,JavaScript 引擎規定,如果function關鍵字出現在行首,一律解釋成語句。因此,JavaScript 引擎看到行首是function關鍵字之后,認為這一段都是函數的定義,不應該以圓括號結尾,所以就報錯了。
解決方法就是不要讓function出現在行首,讓引擎將其理解成一個表達式。最簡單的處理,就是將其放在一個圓括號里面。
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();
上面兩種寫法都是以圓括號開頭,引擎就會認為后面跟的是一個表示式,而不是函數定義語句,所以就避免了錯誤。這就叫做“立即調用的函數表達式”(Immediately-Invoked Function Expression),簡稱 IIFE。
注意,上面兩種寫法最后的分號都是必須的。如果省略分號,遇到連着兩個 IIFE,可能就會報錯。
// 報錯
(function(){ /* code */ }())
(function(){ /* code */ }())
上面代碼的兩行之間沒有分號,JavaScript 會將它們連在一起解釋,將第二行解釋為第一行的參數。
推而廣之,任何讓解釋器以表達式來處理函數定義的方法,都能產生同樣的效果,比如下面三種寫法。
var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();
甚至像下面這樣寫,也是可以的。
!function () { /* code */ }();
~function () { /* code */ }();
-function () { /* code */ }();
+function () { /* code */ }();
通常情況下,只對匿名函數使用這種“立即執行的函數表達式”。它的目的有兩個:一是不必為函數命名,避免了污染全局變量;二是 IIFE 內部形成了一個單獨的作用域,可以封裝一些外部無法讀取的私有變量。
eval命令
eval
命令接受一個字符串作為參數,並將這個字符串當作語句執行。
eval('var a = 1;');
a // 1
上面代碼將字符串當作語句運行,生成了變量a。
如果參數字符串無法當作語句運行,那么就會報錯。
eval('3x') // Uncaught SyntaxError: Invalid or unexpected token
放在eval中的字符串,應該有獨自存在的意義,不能用來與eval以外的命令配合使用。舉例來說,下面的代碼將會報錯。
eval('return;'); // Uncaught SyntaxError: Illegal return statement
上面代碼會報錯,因為return不能單獨使用,必須在函數中使用。
如果eval的參數不是字符串,那么會原樣返回。
eval(123) // 123
eval沒有自己的作用域,都在當前作用域內執行,因此可能會修改當前作用域的變量的值,造成安全問題。
var a = 1;
eval('a = 2');
a // 2
上面代碼中,eval命令修改了外部變量a的值。由於這個原因,eval有安全風險。
為了防止這種風險,JavaScript 規定,如果使用嚴格模式,eval內部聲明的變量,不會影響到外部作用域。
(function f() {
'use strict';
eval('var foo = 123');
console.log(foo); // ReferenceError: foo is not defined
})()
上面代碼中,函數f內部是嚴格模式,這時eval內部聲明的foo變量,就不會影響到外部。
不過,即使在嚴格模式下,eval依然可以讀寫當前作用域的變量。
(function f() {
'use strict';
var foo = 1;
eval('foo = 2');
console.log(foo); // 2
})()
上面代碼中,嚴格模式下,eval內部還是改寫了外部變量,可見安全風險依然存在。
總之,eval的本質是在當前作用域之中,注入代碼。由於安全風險和不利於 JavaScript 引擎優化執行速度,所以一般不推薦使用。通常情況下,eval最常見的場合是解析 JSON 數據的字符串,不過正確的做法應該是使用原生的JSON.parse方法。
eval的別名調用
前面說過eval不利於引擎優化執行速度。更麻煩的是,還有下面這種情況,引擎在靜態代碼分析的階段,根本無法分辨執行的是eval。
例如:
var m = eval;
m('var x = 1');
x // 1
上面代碼中,變量m是eval的別名。靜態代碼分析階段,引擎分辨不出m('var x = 1')執行的是eval命令。
為了保證eval的別名不影響代碼優化,JavaScript 的標准規定,凡是使用別名執行eval,eval內部一律是全局作用域。
例如:
var a = 1;
function f() {
var a = 2;
var e = eval;
e('console.log(a)');
}
f() // 1
上面代碼中,eval是別名調用,所以即使它是在函數中,它的作用域還是全局作用域,因此輸出的a為全局變量。這樣的話,引擎就能確認e()不會對當前的函數作用域產生影響,優化的時候就可以把這一行排除掉。
eval的別名調用的形式五花八門,只要不是直接調用,都屬於別名調用,因為引擎只能分辨eval()這一種形式是直接調用。
eval.call(null, '...')
window.eval('...')
(1, eval)('...')
(eval, eval)('...')
上面這些形式都是eval的別名調用,作用域都是全局作用域。
遞歸函數
遞歸函數
指的是在函數執行的過程中,自己調用自己。通常情況下通過遞歸調用可以實現階乘等操作。
例如:
function f(n) {
if (n <= 1){
// console.log(1111);
return 1;
}else {
// console.log(n * f(n-1));
return n * f(n-1);
}
}
console.log(f(10)); // 3628800
小練習
1.編寫一個函數,計算兩個數字的和、差、積、商。
要求:使用傳參的形式
demo:
function count(num1,num2,sym) {
switch (sym) {
case "+" :
console.log(Number(num1) + Number(num2));
break;
case "-":
console.log(Number(num1) - Number(num2));
break;
case "*":
console.log(Number(num1) * Number(num2));
break;
case "/":
console.log(Number(num1) / Number(2));
break;
default:
console.log("對不起,輸入有誤");
}
}
count(10,20,"+");
count(10,20,"-");
count(10,20,"*");
count(10,20,"/");