函數就是最基本的一種代碼抽象的方式。
- 定義函數
function abs(x) {
if (x >=0){
return x;
}else{
return-x;
}
}
function 指出這是一個函數的定義;
abs是函數的名稱;
(x)括號內列出函數的參數,多個參數以逗號分隔;
{...}之間的代碼是函數體,可以包含若干語句,也可以沒有任何語句.
函數體內部的語句在執行時,一旦執行到return時,函數就執行完畢,并將結果返回。因此,函數內部通過條件判斷和循環可以實現非常復雜的邏輯。
如果沒有return語句,函數執行完畢后也會返回結果,只是結果為undefined。
由于JavaScript的函數也是一個對象,上述定義的abs()函數實際上是一個函數對象,而函數名abs可以視為指向該函數的變量。
因此,第二種定義函數的方式如下:
var abs = function (x) {
if (x >=0){
return x;
}else{
return-x;
}
};
這種方式下:function(x){...}是個匿名函數,它沒有函數名,但是這個匿名函數賦值給了變量abs,所以通過變量abs就可以調用該函數.
上述兩種定義完全等價,注意第二種方式按照完整語法需要在函數體末尾加一個分號;表示賦值語句結束.
-
調用函數
調用函數時,按順序傳入參數即可:
由于JavaScript允許傳入任意個參數而不影響調用,因此傳入的參數比定義的參數多也沒有問題,雖然函數內部并不需要這些參數:
傳入的參數比定義的少也沒有問題:
abs(); // 返回NaN
此時abs(x)函數的參數x將收到undefined,計算結果為NaN。
要避免收到undefined,可以對參數進行檢查:
function abs(x) {
if (typeof x !== 'number') {
throw 'Not a number';
}if (x >= 0) {
return x;
} else {
return -x;
}
} arguments
JavaScript 有個免費贈送的關鍵字arguments,它只在函數內部起作用,并且永遠指向當前函數的調用者傳入的所有參數.
arguments類似數組,但它不是一個數組:
function foo(x){
alert(x);
for (var i = 0; i < arguments.length; i++) {
alert(arguments[i])
}
}
foo(10,20,30);
利用arguments,你可以獲得調用者傳入的所有參數。也就是說,即使函數不定義任何參數,還是可以拿到參數的值:
function abs() {
// body...
if (arguments.length === 0) {
return 0;
}
var x = arguments[0];
return x >= 0 ? x : -x;
}
實際上arguments最常用于判斷傳入參數的個數。你可能會看到這樣的寫法:
// foo(a[, b], c)
// 接收2~3個參數,b是可選參數,如果只傳2個參數,b默認為null:
function foo(a,b,c) {
// body...
if (arguments.length === 2) {
//實際拿到的參數是a和b c為undefined
c = b;//把b賦值給c
b = null;//b變為默認值
}
//...
}
要把中間的參數b變為“可選”參數,就只能通過arguments判斷,然后重新調整參數并賦值。-
rest
由于JavaScript函數允許接收任意個參數,于是我們就不得不用arguments來獲取所有參數:
function foo(a,b) {
var i, rest = [];
if (arguments.length > 2){
for (var i = 2; i < arguments.length; i++) {
rest.push(arguments[i])
}
}
console.log('a = ' + a)
console.log('b = ' + b)
console.log(rest)}
為了獲取除了已定義參數a、b之外的參數,我們不得不用arguments,并且循環要從索引2開始以便排除前兩個參數,這種寫法很別扭,只是為了獲得額外的rest參數,有沒有更好的方法?
ES6標準引入了rest參數,上面的函數可以改寫為:
function foo(a,b,...rest) {
console.log('a = ' + a)
console.log('b = ' + b)
console.log(rest)
}
rest參數只能寫在最后,前面用...標示,從運行結果可知,傳入的參數先綁定a,b,多余的參數以數組的形式交給變量rest,所以,不需要arguments我們就獲取了全部參數.
如果傳入的參數連正常定義的參數都沒有填滿,rest參數會接受一個空數組,注意不是undefined
5.小心return語句JavaScript引擎有一個在行末自動添加分號的機制,這可能讓你栽到return語句的一個大坑:
function foo() {
return { name: 'foo' };
}
如果把return語句拆成兩行:
function foo() {
return
{ name: 'foo' };
}
foo(); // undefined
要小心了,由于JavaScript引擎在行末自動添加分號的機制,上面的代碼實際上變成了:
function foo() {
return; // 自動添加了分號,相當于return undefined;
{ name: 'foo' }; // 這行語句已經沒法執行到了
}
所以正確的多行寫法是:
function foo() {
return{
name:'foo'
};
}
=================
變量
- 變量提升:
JavaScript的函數定義有個特點,它會先掃描整個函數體的語句,把所有申明的變量'提升'到函數的頂部,但不會提升變量賦值:
'use strict';
function foo() {
var x = 'Hello, ' + y;
alert(x);
var y = 'Bob';
}
foo();
雖然是strict模式,但語句var x = 'Hello, ' + y;并不報錯,原因是變量y在稍后申明了。但是alert顯示Hello, undefined,說明變量y的值為undefined。這正是因為JavaScript引擎自動提升了變量y的聲明,但不會提升變量y的賦值。
對于上述foo()函數,JavaScript引擎看到的代碼相當于:
function foo() {
var y; // 提升變量y的申明
var x = 'Hello, ' + y;
alert(x);
y = 'Bob';
}
由于JavaScript的這一怪異的“特性”,我們在函數內部定義變量時,請嚴格遵守“在函數內部首先申明所有變量”這一規則。最常見的做法是用一個var申明函數內部用到的所有變量:
function foo() {
var
x = 1, // x初始化為1
y = x + 1, // y初始化為2
z, i; // z和i為undefined
// 其他語句:
for (i=0; i<100; i++) {
...
}
}
2.全局作用域
不在任何函數內定義的變量就具有全局作用域。實際上,JavaScript默認有一個全局對象window,全局作用域的變量實際上被綁定到window的一個屬性:
'use strict';
var course = 'Learn JavaScript';
alert(course); // 'Learn JavaScript'
alert(window.course); // 'Learn JavaScript'
因此,直接訪問全局變量course和訪問window.course是完全一樣的。
由于函數定義有兩種方式,以變量方式var foo = function () {}定義的函數實際上也是一個全局變量,因此,頂層函數的定義也被視為一個全局變量,并綁定到window對象:
'use strict';
function foo() {
alert('foo');
}
foo(); // 直接調用foo()
window.foo(); // 通過window.foo()調用
我們每次直接調用的alert()函數其實也是window的一個變量:
這說明JavaScript實際上只有一個全局作用域。任何變量(函數也視為變量),如果沒有在當前函數作用域中找到,就會繼續往上查找,最后如果在全局作用域中也沒有找到,則報ReferenceError錯誤。
- 名字空間
全局變量會綁定到window上,不同的JavaScript文件如果使用了相同的全局變量,或者定義了相同名字的頂層函數,都會造成命名沖突,并且很難被發現。
減少沖突的一個方法是把自己的所有變量和函數全部綁定到一個全局變量中。例如:
// 唯一的全局變量MYAPP:
var MYAPP = {};
// 其他變量:
MYAPP.name = 'myapp';
MYAPP.version = 1.0;
// 其他函數:
MYAPP.foo = function () {
return 'foo';
};
把自己的代碼全部放入唯一的名字空間MYAPP中,會大大減少全局變量沖突的可能。
許多著名的JavaScript庫都是這么干的:jQuery,YUI,underscore等等。 - 局部作用域
由于JavaScript的變量作用域實際上是函數內部,我們在for循環等語句塊中是無法定義具有局部作用域的變量的:
'use strict';
function foo() {
for (var i=0; i<100; i++) {
//
}
i += 100; // 仍然可以引用變量i
}
為了解決塊級作用域,ES6引入了新的關鍵字let,用let替代var可以申明一個塊級作用域的變量:
'use strict';
function foo() {
var sum = 0;
for (let i=0; i<100; i++) {
sum += i;
}
i += 1; // SyntaxError
}
- 常量
由于var和let申明的是變量,如果要申明一個常量,在ES6之前是不行的,我們通常用全部大寫的變量來表示“這是一個常量,不要修改它的值”:
var PI = 3.14;
ES6標準引入了新的關鍵字const來定義常量,const與let都具有塊級作用域:
'use strict';
const PI = 3.14;
PI = 3; // 某些瀏覽器不報錯,但是無效果!
PI; // 3.14
6.方法:在一個對象中綁定函數,稱為這個對象的方法
var xioming = {
name:'小明',
birth:1990,
age:function () {
var y = new Data().getFullYear();
return y - this.birth;
}
};
xiaoming.age; // function xiaoming.age()
xiaoming.age(); // 今年調用是27,明年調用就變成28了
綁定到對象上的函數稱為方法,和普通函數也沒啥區別,但是它在內部使用了一個this關鍵字,在一個方法內部,this是一個特殊變量,它始終指向當前對象
JavaScript的函數內部如果調用了this,那么這個this到底指向誰?
答案是,視情況而定!
如果以對象的方法形式調用,比如xiaoming.age(),該函數的this指向被調用的對象,也就是xiaoming,這是符合我們預期的。
如果單獨調用函數,比如getAge(),此時,該函數的this指向全局對象,也就是window。
坑爹啊!
更坑爹的是,如果這么寫:
var fn = xiaoming.age; // 先拿到xiaoming的age函數
fn(); // NaN
要保證this指向正確,必須用obj.xxx()的形式調用!
有些時候,喜歡重構的你把方法重構了一下:
'use strict';
var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
function getAgeFromBirth() {
var y = new Date().getFullYear();
return y - this.birth;
}
return getAgeFromBirth();
}
};
xiaoming.age(); // Uncaught TypeError: Cannot read property 'birth' of undefined
結果又報錯了!原因是this指針只在age方法的函數內指向xiaoming,在函數內部定義的函數,this又指向undefined了!(在非strict模式下,它重新指向全局對象window!)
修復的辦法也不是沒有,我們用一個that變量首先捕獲this:
'use strict';
var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
var that = this; // 在方法內部一開始就捕獲this
function getAgeFromBirth() {
var y = new Date().getFullYear();
return y - that.birth; // 用that而不是this
}
return getAgeFromBirth();
}
};
xiaoming.age(); // 25
用var that = this;,你就可以放心地在方法內部定義其他函數,而不是把所有語句都堆到一個方法中。
- apply
雖然在一個獨立的函數調用中,根據是否是strict模式,this指向undefined或window,不過,我們還是可以控制this的指向的!
要指定函數的this指向哪個對象,可以用函數本身的apply方法,它接收兩個參數,第一個參數就是需要綁定的this變量,第二個參數是Array,表示函數本身的參數。
用apply修復getAge()調用:
function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}
var xiaoming = {
name: '小明',
birth: 1990,
age: getAge
};
xiaoming.age(); // 25
getAge.apply(xiaoming, []); // 25, this指向xiaoming, 參數為空
另一個與apply()類似的方法是call(),唯一區別是:
apply()把參數打包成Array再傳入;
call()把參數按順序傳入。
比如調用Math.max(3, 5, 4),分別用apply()和call()實現如下:
Math.max.apply(null, [3, 5, 4]); // 5
Math.max.call(null, 3, 5, 4); // 5
對普通函數調用,我們通常把this綁定為null。
- 裝飾器
利用apply(),我們還可以動態改變函數的行為。
JavaScript的所有對象都是動態的,即使內置的函數,我們也可以重新指向新的函數。
現在假定我們想統計一下代碼一共調用了多少次parseInt(),可以把所有的調用都找出來,然后手動加上count += 1,不過這樣做太傻了。最佳方案是用我們自己的函數替換掉默認的parseInt():
var count = 0;
var oldParsetInt = parseInt;//保存原函數
window.parseInt = function () {
count += 1;
return oldParsetInt.apply(null,arguments);//調用原函數
}
===========================
高階函數:函數的參數可以是另一個函數.
1. map 由于map()方法定義在JavaScript的Array中,我們調用Array的map()方法,傳入我們自己的函數,就得到了一個新的Array作為結果:
2. reduce 再看reduce的用法。Array的reduce()把一個函數作用在這個Array的[x1, x2, x3...]上,這個函數必須接收兩個參數,reduce()把結果繼續和序列的下一個元素做累積計算,
由于map()接收的回調函數可以有3個參數:callback(currentValue, index, array)通常我們僅需要第一個參數,而忽略了傳入的后面兩個參數。
parseInt(string, radix)沒有忽略第二個參數
3.filter 過濾器
和map()類似,Array的filter()也接收一個函數。和map()不同的是,filter()把傳入的函數依次作用于每個元素,然后根據返回值是true還是false決定保留還是丟棄該元素。
filter()接收的回調函數,其實可以有多個參數。通常我們僅使用第一個參數,表示Array的某個元素。回調函數還可以接收另外兩個參數,表示元素的位置和數組本身:
var arr = ['A', 'B', 'C'];
var r = arr.filter(function (element, index, self) {
console.log(element); // 依次打印'A', 'B', 'C'
console.log(index); // 依次打印0, 1, 2
console.log(self); // self就是變量arr
return true;
});
3.sort Array的sort()方法默認把所有元素先轉換為String再排序 sort()方法也是一個高階函數,它還可以接收一個比較函數來實現自定義的排序。
比較的過程必須通過函數抽象出來。通常規定,對于兩個元素x和y,如果認為x < y,則返回-1,如果認為x == y,則返回0,如果認為x > y,則返回1,
4.閉包 就是返回函數
返回的函數在其定義內部引用了局部變量arr,所以,當一個函數返回了一個函數后,其內部的局部變量還被新函數引用,所以,閉包用起來簡單,實現起來可不容易。
返回的函數并沒有立刻執行,而是直到調用了f()才執行。我們來看一個例子:
返回閉包時牢記的一點就是:返回函數不要引用任何循環變量,或者后續會發生變化的變量。
如果一定要引用循環變量怎么辦?方法是再創建一個函數,用該函數的參數綁定循環變量當前的值,無論該循環變量后續如何更改,已綁定到函數參數的值不變:
注意這里用了一個“創建一個匿名函數并立刻執行”的語法:
(function (x) {
return x * x;
})(3); // 9
理論上講,創建一個匿名函數并立刻執行可以這么寫:
function (x) { return x * x } (3);
但是由于JavaScript語法解析的問題,會報SyntaxError錯誤,因此需要用括號把整個函數定義括起來:
(function (x) { return x * x }) (3);
通常,一個立即執行的匿名函數可以把函數體拆開,一般這么寫:
(function (x) {
return x * x;
})(3);
換句話說,閉包就是攜帶狀態的函數,并且它的狀態可以完全對外隱藏起來。
5.箭頭函數
ES6標準新增了一種新的函數:Arrow Function(箭頭函數)。
為什么叫Arrow Function?因為它的定義用的就是一個箭頭:
x => x * x
箭頭函數相當于匿名函數,并且簡化了函數定義。箭頭函數有兩種格式,一種像上面的,只包含一個表達式,連{ ... }和return都省略掉了。還有一種可以包含多條語句,這時候就不能省略{ ... }和return:
x => {
if (x > 0) {
return x * x;
}
else {
return - x * x;
}
}
如果參數不是一個,就需要用括號()括起來:
// 兩個參數:
(x, y) => x * x + y * y
// 無參數:
() => 3.14
// 可變參數:
(x, y, ...rest) => {
var i, sum = x + y;
for (i=0; i<rest.length; i++) {
sum += rest[i];
}
return sum;
}
如果要返回一個對象,就要注意,如果是單表達式,這么寫的話會報錯:
// SyntaxError:
x => { foo: x }
因為和函數體的{ ... }有語法沖突,所以要改為:
// ok:
x => ({ foo: x })
箭頭函數完全修復了this的指向,this總是指向詞法作用域,也就是外層調用者obj:
var obj = {
birth: 1990,
getAge: function () {
var b = this.birth; // 1990
var fn = () => new Date().getFullYear() - this.birth; // this指向obj對象
return fn();
}
};
obj.getAge(); // 25
如果使用箭頭函數,以前的那種hack寫法:
var that = this;
就不再需要了。
由于this在箭頭函數中已經按照詞法作用域綁定了,所以,用call()或者apply()調用箭頭函數時,無法對this進行綁定,即傳入的第一個參數被忽略: