-
函數
1.JavaScript允許傳入任意個參數而不影響調用,因此傳入的參數比定義的參數多也沒有問題,雖然函數內部并不需要這些參數。
2.arguments
JavaScript還有一個免費贈送的關鍵字arguments,
它只在函數內部起作用,并且永遠指向當前函數的調用者傳入的所有參數。
利用arguments,你可以獲得調用者傳入的所有參數。
arguments類似Array但它不是一個Array:
function foo(x) {
alert(x); // 10
for (var i=0; i<arguments.length; i++) {
alert(arguments[i]); // 10, 20, 30
}
}
foo(10, 20, 30);
3.rest參數
rest參數只能寫在最后,前面用...標識,
從運行結果可知,傳入的參數先綁定a、b,多余的參數以數組形式交給變量rest,
所以,不再需要arguments我們就獲取了全部參數。
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
// Array [ 3, 4, 5 ]
foo(1);
// 結果:
// a = 1
// b = undefined
// Array []
4.小心你的return語句
JavaScript引擎有一個在行末自動添加分號的機制,這可能讓你栽到return語句的一個大坑:
function foo() {
return
{ name: 'foo' };
}
foo(); // undefined
由于JavaScript引擎在行末自動添加分號的機制,上面的代碼實際上變成了:
function foo() {
return; // 自動添加了分號,相當于return undefined;
{ name: 'foo' }; // 這行語句已經沒法執行到了
}
5.全局作用域
JavaScript默認有一個全局對象window,全局作用域的變量實際上被綁定到window的一個屬性:
'use strict';
var course = 'Learn JavaScript';
alert(course); // 'Learn JavaScript'
alert(window.course); // 'Learn JavaScript'
進一步大膽地猜測,我們每次直接調用的alert()函數其實也是window的一個變量:
'use strict';
window.alert('調用window.alert()');
// 把alert保存到另一個變量:
var old_alert = window.alert;
// 給alert賦一個新函數:
window.alert = function () {}
alert('無法用alert()顯示了!');
// 恢復alert:
window.alert = old_alert;
alert('又可以用alert()了!');
6.名字空間
全局變量會綁定到window上,不同的JavaScript文件如果使用了相同的全局變量,
或者定義了相同名字的頂層函數,都會造成命名沖突,并且很難被發現。
減少沖突的一個方法是把自己的所有變量和函數全部綁定到一個全局變量中。例如:
// 唯一的全局變量MYAPP:
var MYAPP = {};
// 其他變量:
MYAPP.name = 'myapp';
MYAPP.version = 1.0;
// 其他函數:
MYAPP.foo = function () {
return 'foo';
};
把自己的代碼全部放入唯一的名字空間MYAPP中,會大大減少全局變量沖突的可能。
7.局部作用域
由于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
}
8.常量
由于var和let申明的是變量,如果要申明一個常量,
在ES6之前是不行的,我們通常用全部大寫的變量來表示“這是一個常量,不要修改它的值”:
var PI = 3.14;
ES6標準引入了新的關鍵字const來定義常量,const與let都具有塊級作用域:
'use strict';
const PI = 3.14;
PI = 3; // 某些瀏覽器不報錯,但是無效果!
PI; // 3.14
9.方法
在一個對象中綁定函數,稱為這個對象的方法。
var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
var y = new Date().getFullYear();
return y - this.birth;
}
};
xiaoming.age; // function xiaoming.age()
xiaoming.age(); // 今年調用是25,明年調用就變成26了
綁定到對象上的函數稱為方法,和普通函數也沒啥區別,但是它在內部使用了一個this關鍵字。
在一個方法內部,this是一個特殊變量,它始終指向當前對象,也就是xiaoming這個變量。
拆開寫:
function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}
var xiaoming = {
name: '小明',
birth: 1990,
age: getAge
};
xiaoming.age(); // 25, 正常結果
getAge(); // NaN
單獨調用函數getAge()怎么返回了NaN?請注意,我們已經進入到了JavaScript的一個大坑里。
JavaScript的函數內部如果調用了this,那么這個this到底指向誰?
答案是,視情況而定!
如果以對象的方法形式調用,比如xiaoming.age(),該函數的this指向被調用的對象,也就是xiaoming,這是符合我們預期的。
如果單獨調用函數,比如getAge(),此時,該函數的this指向全局對象,也就是window。
要保證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
10.apply
要指定函數的this指向哪個對象,可以用函數本身的apply方法,
它接收兩個參數,第一個參數就是需要綁定的this變量,第二個參數是Array,表示函數本身的參數。
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。
11.裝飾器
利用apply(),我們還可以動態改變函數的行為。
JavaScript的所有對象都是動態的,即使內置的函數,我們也可以重新指向新的函數。
現在假定我們想統計一下代碼一共調用了多少次parseInt(),可以把所有的調用都找出來,然后手動加上count += 1,
不過這樣做太傻了。最佳方案是用我們自己的函數替換掉默認的parseInt():
var count = 0;
var oldParseInt = parseInt; // 保存原函數
window.parseInt = function () {
count += 1;
return oldParseInt.apply(null, arguments); // 調用原函數
};
// 測試:
parseInt('10');
parseInt('20');
parseInt('30');
count; // 3
12.高階函數
一個函數就可以接收另一個函數作為參數,這種函數就稱之為高階函數。
一個最簡單的高階函數:
function add(x, y, f) {
return f(x) + f(y);
}
12.1.map
由于map()方法定義在JavaScript的Array中,我們調用Array的map()方法,
傳入我們自己的函數,就得到了一個新的Array作為結果:
function pow(x) {
return x * x;
}
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
小明希望利用map()把字符串變成整數,他寫的代碼很簡潔:
'use strict';
var arr = ['1', '2', '3'];
var r;
r = arr.map(parseInt);
alert('[' + r[0] + ', ' + r[1] + ', ' + r[2] + ']');
結果竟然是[1, NaN, NaN],
由于map()接收的回調函數可以有3個參數:callback(currentValue, index, array),
通常我們僅需要第一個參數,而忽略了傳入的后面兩個參數。
不幸的是,parseInt(string, radix)沒有忽略第二個參數,導致實際執行的函數分別是:
parseInt('0', 0); // 0, 按十進制轉換
parseInt('1', 1); // NaN, 沒有一進制
parseInt('2', 2); // NaN, 按二進制轉換不允許出現2
可以改為r = arr.map(Number);,因為Number(value)函數僅接收一個參數。
12.2.reduce
reduce的用法。Array的reduce()把一個函數作用在這個Array的[x1, x2, x3...]上,
這個函數必須接收兩個參數,reduce()把結果繼續和序列的下一個元素做累積計算,
其效果就是:[x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4)
比方說對一個Array求和,就可以用reduce實現:
var arr = [1, 3, 5, 7, 9];
arr.reduce(function (x, y) {
return x + y;
}); // 25
12.3.filter
和map()類似,Array的filter()也接收一個函數。
和map()不同的是,filter()把傳入的函數依次作用于每個元素,然后根據返回值是true還是false決定保留還是丟棄該元素。
例如,在一個Array中,刪掉偶數,只保留奇數,可以這么寫:
var arr = [1, 2, 4, 5, 6, 9, 10, 15];
var r = arr.filter(function (x) {
return x % 2 !== 0;
});
r; // [1, 5, 9, 15]
12.4.sort
JavaScript的Array的sort()方法就是用于排序的,但是排序結果可能讓你大吃一驚:
// 看上去正常的結果:
['Google', 'Apple', 'Microsoft'].sort(); // ['Apple', 'Google', 'Microsoft'];
// apple排在了最后:
['Google', 'apple', 'Microsoft'].sort(); // ['Google', 'Microsoft", 'apple']
// 無法理解的結果:
[10, 20, 1, 2].sort(); // [1, 10, 2, 20]
第二個排序把apple排在了最后,是因為字符串根據ASCII碼進行排序,而小寫字母a的ASCII碼在大寫字母之后。
第三個排序結果是什么鬼?簡單的數字排序都能錯?
這是因為Array的sort()方法默認把所有元素先轉換為String再排序,
結果'10'排在了'2'的前面,因為字符'1'比字符'2'的ASCII碼小。
如果不知道sort()方法的默認排序規則,直接對數字排序,絕對栽進坑里!
要按數字大小排序,我們可以這么寫:
var arr = [10, 20, 1, 2];
arr.sort(function (x, y) {
if (x < y) {
return -1;
}
if (x > y) {
return 1;
}
return 0;
}); // [1, 2, 10, 20]
默認情況下,對字符串排序,是按照ASCII的大小比較的,現在,我們提出排序應該忽略大小寫,按照字母序排序。要實現這個算法,不必對現有代碼大加改動,只要我們能定義出忽略大小寫的比較算法就可以:
var arr = ['Google', 'apple', 'Microsoft'];
arr.sort(function (s1, s2) {
x1 = s1.toUpperCase();
x2 = s2.toUpperCase();
if (x1 < x2) {
return -1;
}
if (x1 > x2) {
return 1;
}
return 0;
}); // ['apple', 'Google', 'Microsoft']
13.閉包
13.1.函數作為返回值
如果不需要立刻求和,而是在后面的代碼中,根據需要再計算怎么辦?
可以不返回求和的結果,而是返回求和的函數!
function lazy_sum(arr) {
var sum = function () {
return arr.reduce(function (x, y) {
return x + y;
});
}
return sum;
}
當我們調用lazy_sum()時,返回的并不是求和結果,而是求和函數:
var f = lazy_sum([1, 2, 3, 4, 5]); // function sum()
調用函數f時,才真正計算求和的結果:
f(); // 15
在這個例子中,我們在函數lazy_sum中又定義了函數sum,并且,
內部函數sum可以引用外部函數lazy_sum的參數和局部變量,
當lazy_sum返回函數sum時,相關參數和變量都保存在返回的函數中,
這種稱為“閉包(Closure)”的程序結構擁有極大的威力。
請再注意一點,當我們調用lazy_sum()時,每次調用都會返回一個新的函數,即使傳入相同的參數:
var f1 = lazy_sum([1, 2, 3, 4, 5]);
var f2 = lazy_sum([1, 2, 3, 4, 5]);
f1 === f2; // false
f1()和f2()的調用結果互不影響。
13.2.閉包
注意到返回的函數在其定義內部引用了局部變量arr,所以,
當一個函數返回了一個函數后,其內部的局部變量還被新函數引用,
所以,閉包用起來簡單,實現起來可不容易。
另一個需要注意的問題是,返回的函數并沒有立刻執行,而是直到調用了f()才執行。我們來看一個例子:
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push(function () {
return i * i;
});
}
return arr;
}
var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
在上面的例子中,每次循環,都創建了一個新的函數,然后,把創建的3個函數都添加到一個Array中返回了。
你可能認為調用f1(),f2()和f3()結果應該是1,4,9,但實際結果是:
f1(); // 16
f2(); // 16
f3(); // 16
全部都是16!原因就在于返回的函數引用了變量i,但它并非立刻執行。
等到3個函數都返回時,它們所引用的變量i已經變成了4,因此最終結果為16。
返回閉包時牢記的一點就是:***返回函數不要引用任何循環變量,或者后續會發生變化的變量。***
如果一定要引用循環變量怎么辦?方法是再創建一個函數,用該函數的參數綁定循環變量當前的值,
無論該循環變量后續如何更改,已綁定到函數參數的值不變:
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push((function (n) {
return function () {
return n * n;
}
})(i));
}
return arr;
}
var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
f1(); // 1
f2(); // 4
f3(); // 9
注意這里用了一個“創建一個匿名函數并立刻執行”的語法:
(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);
說了這么多,難道閉包就是為了返回一個函數然后延遲執行嗎?
當然不是!閉包有非常強大的功能。舉個栗子:
在面向對象的程序設計語言里,比如Java和C++,要在對象內部封裝一個私有變量,可以用private修飾一個成員變量。
在沒有class機制,只有函數的語言里,借助閉包,同樣可以封裝一個私有變量。我們用JavaScript創建一個計數器:
'use strict';
function create_counter(initial) {
var x = initial || 0;
return {
inc: function () {
x += 1;
return x;
}
}
}
它用起來像這樣:
var c1 = create_counter();
c1.inc(); // 1
c1.inc(); // 2
c1.inc(); // 3
var c2 = create_counter(10);
c2.inc(); // 11
c2.inc(); // 12
c2.inc(); // 13
在返回的對象中,實現了一個閉包,該閉包攜帶了局部變量x,并且,
從外部代碼根本無法訪問到變量x。
換句話說,閉包就是攜帶狀態的函數,并且它的狀態可以完全對外隱藏起來。
閉包還可以把多參數的函數變成單參數的函數。
例如,要計算xy可以用Math.pow(x, y)函數,
不過考慮到經常計算x2或x3,我們可以利用閉包創建新的函數pow2和pow3:
function make_pow(n) {
return function (x) {
return Math.pow(x, n);
}
}
// 創建兩個新函數:
var pow2 = make_pow(2);
var pow3 = make_pow(3);
pow2(5); // 25
pow3(7); // 343
14.箭頭函數
為什么叫Arrow Function?因為它的定義用的就是一個箭頭:
x => x * x
上面的箭頭函數相當于:
function (x) {
return 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是詞法作用域,由上下文確定。
回顧前面的例子,由于JavaScript函數對this綁定的錯誤處理,下面的例子無法得到預期結果:
var obj = {
birth: 1990,
getAge: function () {
var b = this.birth; // 1990
var fn = function () {
return new Date().getFullYear() - this.birth; // this指向window或undefined
};
return fn();
}
};
現在,箭頭函數完全修復了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
由于this在箭頭函數中已經按照詞法作用域綁定了,
所以,用call()或者apply()調用箭頭函數時,無法對this進行綁定,
即傳入的第一個參數被忽略:
var obj = {
birth: 1990,
getAge: function (year) {
var b = this.birth; // 1990
var fn = (y) => y - this.birth; // this.birth仍是1990
return fn.call({birth:2000}, year);
}
};
obj.getAge(2015); // 25
15.generator
ES6定義generator標準的哥們借鑒了Python的generator的概念和語法,
如果你對Python的generator很熟悉,那么ES6的generator就是小菜一碟了。
generator跟函數很像,定義如下:
function* foo(x) {
yield x + 1;
yield x + 2;
return x + 3;
}
generator和函數不同的是,generator由function*定義(注意多出的*號),并且,除了return語句,還可以用yield返回多次。
要編寫一個產生斐波那契數列的函數,可以這么寫:
function fib(max) {
var
t,
a = 0,
b = 1,
arr = [0, 1];
while (arr.length < max) {
t = a + b;
a = b;
b = t;
arr.push(t);
}
return arr;
}
// 測試:
fib(5); // [0, 1, 1, 2, 3]
fib(10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
函數只能返回一次,所以必須返回一個Array。但是,如果換成generator,
就可以一次返回一個數,不斷返回多次。用generator改寫如下:
function* fib(max) {
var
t,
a = 0,
b = 1,
n = 1;
while (n < max) {
yield a;
t = a + b;
a = b;
b = t;
n ++;
}
return a;
}
直接調用試試:
fib(5); // fib {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}
直接調用一個generator和調用函數不一樣,fib(5)僅僅是創建了一個generator對象,還沒有去執行它。
調用generator對象有兩個方法,一是不斷地調用generator對象的next()方法:
var f = fib(5);
f.next(); // {value: 0, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 2, done: false}
f.next(); // {value: 3, done: true}
next()方法會執行generator的代碼,然后,每次遇到yield x;就返回一個對象{value: x, done: true/false},
然后“暫停”。返回的value就是yield的返回值,done表示這個generator是否已經執行結束了。
如果done為true,則value就是return的返回值。
當執行到done為true時,這個generator對象就已經全部執行完畢,不要再繼續調用next()了。
第二個方法是直接用for ... of循環迭代generator對象,這種方式不需要我們自己判斷done:
for (var x of fib(5)) {
console.log(x); // 依次輸出0, 1, 1, 2, 3
}
generator和普通函數相比,有什么用?
因為generator可以在執行過程中多次返回,所以它看上去就像一個可以記住執行狀態的函數,
利用這一點,寫一個generator就可以實現需要用面向對象才能實現的功能。例如,用一個對象來保存狀態。