1. 程式人生 > >JavaScript學習筆記(2)——函式

JavaScript學習筆記(2)——函式

定義函式
函式的定義方式有兩種

function abs(x) {
	if (x >= 0) {
		return x;
	} else {
		return -x;
	}
}

上述函式定義為

  • function指出這是一個函式定義;
  • abs是函式名稱
  • (x)括號內列出函式的引數,多個引數以,分割
  • {…}之間的程式碼是函式體,可以包含若干語句,也可以沒有語句

請注意,函式體內部的語句在執行時,一旦執行到return時,函式就執行完畢,並將結果返回。

沒有return語句,函式執行完畢後也會返回結果,只是結果為undefined

JS的函式也是一個物件,上述abs()函式實際上也是一個函式物件,函式名abs可以視為指向該函式的變數。故而有以下第二種函式定義方式:

var abs = function (x) {
	if (x >= 0) {
		return x;
	} else {
		return -x;
	}
};

在這種方式下,funtion(x) {...}是一個匿名函式,它沒有函式名。但是,這個匿名函式賦值給變數abs,所以通過變數abs可以呼叫該元素。
兩種方式效果相同,注意第二種末尾要加一個;,因為是一個賦值語句。

函式呼叫
與一般的呼叫方式相同,但是JS中你可以傳入任意多個引數,傳入了函式也不會使用。
如果傳入的引數比定義少,也不會報錯,此時未被傳入的引數會收到undefined。
為了避免此問題,可以對引數進行檢查。

function abs(x) {
	if (typeof x !== 'number') {
		throw 'Not a number';
	}
	if (x >= 0) {
		return x;
	} else {
		return -1;
	}
}

arguments
JS還有一個免費贈送的關鍵字arguments,只在函式內部起作用,並且永遠指向當前函式的呼叫者傳入的所有引數。arguments類似於Array但本質不是array:

function foo(x) {
    console.log('x = ' + x); // 10
    for (var i=0; i<arguments.length; i++) {
        console.log('arg ' + i + ' = ' + arguments[i]); // 10, 20, 30
    }
}
foo(10, 20, 30);

利用arguments,你可以獲得呼叫者傳入的所有引數。也就是說,即使函式不定義任何引數,還是可以拿到引數的值:

function abs() {
    if (arguments.length === 0) {
        return 0;
    }
    var x = arguments[0];
    return x >= 0 ? x : -x;
}

abs(); // 0
abs(10); // 10
abs(-9); // 9

實際上arguments是常用於判斷傳入引數的個數。你可能會看到這樣的寫法:

// 目標是實現foo(a[, b], c)
// 接收2^3個引數,b是可選引數,如果只傳兩個引數,b預設為null:
function foo(a, b, c) {
	if (arguments.length === 2) {
		// 實際拿到的引數是a和b,c為undefined
		c = b;
		b = null;
	}
	// ...
}

要把中間引數b變為“可選”引數,就只能通過這種方式判斷,然後重新調整引數並賦值。

rest引數
為了獲取除了已定義引數以外的引數,ES6標準引入了rest引數。

function foo(a,b, ...rest) {
	console.log('a = ' + a);
	console.log('b = ' + b);
	console.log(rest);
}

foo( 1,2,3,4,5);
// 結果:
// a = 1
// b = 2
// (3) [ 3, 4, 5]
foo(1);
// 結果:
// a = 1
// b = undefined
// [ ]

JS有一個自動在行末新增分號的機制,因此在函式分行的時候一定要注意
比如:
return
{ name: ‘foo’ };
就可能被錯誤的執行為:
return;
{ name: ‘foo’ };
因此建議的多行寫法為:
function foo() {
return { // 這裡不會自動加分號
name:‘foo’
};
}

變數作用域
一般用var定義的函式,其變數作用域和C中的概念相同。
JS允許函式巢狀,內部函式允許訪問外部函式定義的變數,反之則不成立。
如果內部函式和外部函式定義了同名變數,JS函式在查詢變數時會從自身函式定義開始,從“內”向“外”查詢。如果內部函式定義了於外部函式重名的變數,則內部函式的變數將遮蔽外部函式的變數。

變數提升
JS的函式定義有個特點,它會先掃描整個函式體的語句,把所有以申明的變數“提升”到函式頂部。

'use strict';

function foo() {
    var x = 'Hello, ' + y;
    console.log(x);
    var y = 'Bob';
}

foo();

在JS引擎看到的程式碼相當於:

function foo() {
    var y; // 提升變數y的申明,此時y為undefined
    var x = 'Hello, ' + y;
    console.log(x);
    y = 'Bob';
}

注意y的宣告被提前了,但是y的賦值並不會提前。
由於JS這一詭異的特性,我們在函式內部定義變數時,請嚴格遵守“在函式內部首先宣告所有變數”這一原則。最方便的方法就是用一個var宣告函式內部用到的所有變數:

function foo() {
    var
        x = 1, // x初始化為1
        y = x + 1, // y初始化為2
        z, i; // z和i為undefined
    // 其他語句:
    for (i=0; i<100; i++) {
        ...
    }
}

全域性作用域
不在任何函式內定義的變數就具有全域性作用域。JS預設有一個全域性物件window,全域性作用域的變數實際上被繫結到window的一個屬性。

‘use strict’:
var course = 'Learn JavaScript';
 alert(course);
 alert(window.course);

直接訪問course和window.course是完全一樣的。

實際上函式的定義也是一樣的。以變數方式定義的的函式也是一個全域性變數,因此頂層函式的定義也被視為一個全域性變數,並繫結到window物件:

'use struct';

function foo() {
...
}

foo();
window.foo();

後面兩種呼叫的結果是相同的。
其實連我們直接呼叫的alert() 其實也是window的一個變數:

'use strict':
window.alert('呼叫window.alert()');
var old_alert = window.alert;
window.alert = function() {} 
alert('無法用alert()顯示了);
window.alert = old_alert;
alert('又可以用alert()了!‘);

這說明JS實際上只有一個全域性作用域。任何變數(函式也視為變數),如果沒有在當前函式作用域中找到,就會繼續往上查詢.如果最後在全域性作用域也沒有找到,則會報ReferrenceError錯誤。

名字空間
全域性變數會繫結到window上,因此不同的JS檔案如果使用了相同的全域性變數或定義了相同名字的頂層函式,都會造成命名衝突。
減少衝突的一個方法是,把自己的所有變數和函式全部繫結到一個全域性變數中。比如:

var MYAPP = {};

MYAPP.name = 'myapp';
MYAPP.version = 1.0;

MYAPP.foo = function () {
	return 'foo';
};

把自己的程式碼全部放入唯一的名字空間MYAPP中,會大大減少全域性變數衝突的可能。

區域性作用域
由於JS的變數作用域實際上是函式內部,我們在for迴圈都能語句塊中是無法定義具有區域性作用域的變數的:

'use strict':

function foo() {
	for (var i=0; i<100; i++) {
	//
	}
	i += 100;
}

為了解決塊級作用域,ES6引入了新的關鍵字let,用let替代var可以申明一個塊級作用域的變數:

'use strict':

function foo() {
	var sum = 0;
	for (let i=0; i<100; i++) {
		sum += i;
	}
       i += 1;
 }

常量
ES6引入了新的關鍵字const來定義常量,const 與 let 都具有塊級作用域:

'use strict';

const PI = 3.14;
PI =3; // 某些瀏覽器不會報錯,但是沒有效果
PI;

解構賦值
意思是把一個數組的元素分別賦值給幾個變數:

var array = ['hello', 'JS', 'ES6'];
var x = array[0];
var y = array[1];
var z = array[2];

ES6中,可以使用解構賦值,直接對多個變數同時賦值:

'use strict';

var [x, y, z] = ['hello', 'JS', 'ES6'];
console.log('x=' + x + ', y = '+ y + ', z = ' + z);

如果陣列本身還有巢狀,也可以通過下面的形式進行解構賦值,注意牽頭層次和位置要保持一致。

let [x, [y, z]] = ['hello', ['JavaScript', 'ES6']];
x; // 'hello'
y; // 'JavaScript'
z; // 'ES6'

解構賦值還可以忽略某些元素:

let [, , z] = ['hello','JS','ES6'];
z;

若需要從一個物件中取出若干屬性,也可以使用解構賦值,便於快速獲取物件的指定屬性:

'use strict';

var person = {
	name: '小明’,
	age: 20,
	gender: 'male',
	passport: 'G-12345678',
	school: 'NO.4 middle school'
};
var {name, age, passport} = person;
// name, age, passport分別被賦值為對應屬性
console.log('name = ' + name + ', age =  '+ age + ', passport = ' + passport);

對一個物件進行解構賦值時,同樣可以直接對巢狀的物件屬性進行賦值,只要保證對應的層次是一致的。

var person = {
    name: '小明',
    age: 20,
    gender: 'male',
    passport: 'G-12345678',
    school: 'No.4 middle school',
    address: {
        city: 'Beijing',
        street: 'No.1 Road',
        zipcode: '100001'
    }
};
var {name, address: {city, zip}} = person;
name; // '小明'
city; // 'Beijing'
zip; // undefined, 因為屬性名是zipcode而不是zip
// 注意: address不是變數,而是為了讓city和zip獲得巢狀的address物件的屬性:
address; // Uncaught ReferenceError: address is not defined

使用解構賦值對物件屬性進行賦值時,如果對應的屬性不存在,變數將被賦值為undefined,這和引用一個不存在的屬性獲得undefined是一致的。如果要使用的變數名和屬性名不一致,可以用下面的語法獲取:

var person = {
    name: '小明',
    age: 20,
    gender: 'male',
    passport: 'G-12345678',
    school: 'No.4 middle school'
};

// 把passport屬性賦值給變數id:
let {name, passport:id} = person;
name; // '小明'
id; // 'G-12345678'
// 注意: passport不是變數,而是為了讓變數id獲得passport屬性:
passport; // Uncaught ReferenceError: passport is not defined

解構賦值還可以使用預設值,這樣就避免了不存在的屬性返回undefined的問題:

var person = {
	name: '小明',
	age: 20,
	gender: 'male',
	passport: 'G-12345678'
};

// 如果person物件沒有single屬性, 預設賦值為true
var {name, single=true} = person;
name; // ‘小明’
single;// true

有些時候,如果變數已經被聲明瞭。再次賦值的時候,正確的寫法也會報語法錯誤:

//宣告變數:
var x, y;
// 解構賦值:
{x, y} = { name: '小明‘, x: 100, y: 200};
// 語法錯誤: Uncaught SyntaxError: Unexpected token =

這是因為JS引擎把{開頭的語句當做了塊處理,於是=不再合法。解決方法是用小括號括起來:

({x, y} = { name: '小明', x: 100, y: 200});

使用場景
解構賦值在很多時候可以大大簡化程式碼。比如,交換兩個變數xy的值,可以這麼寫,不再需要臨時變數:

var x=1, y=2;
[x, y] = [y, x]

快速獲取當前頁面的域名和路徑:
var {hostname:domain, pathname:path} = location;
如果一個函式解構一個物件作為引數,那麼 , 可以使用解構直接把物件的屬性繫結到變數中,例如,下面函式可以快速建立一個Date物件:

function buildDate({year, month, day, houir=0, minute=0, second=0}) {
	return new Date(yeat + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second);
}

它的方便之處在於傳入的物件只需要yearmonthday三個屬性:

buildDate({ year: 2017, month: 1, day: 1 });
// Sun Jan 01 2017 00:00:00 GMT+0800 (CST)

也可以傳入hour,minutesecond屬性:

buildDate({ year: 2017, month: 1, day: 1, hour: 20, minute: 15 });
// Sun Jan 01 2017 20:15:00 GMT+0800 (CST)