本篇目錄
定義函數的兩種方式
調用函數:arguments/rest參數/retun語句
全局作用域
名字空間
解構賦值(ES6)及實際使用場景
方法:apply和call/裝飾鼴
高階函數:map/reduce
擴展1:字符轉數字的方法
擴展2:最好別在map裏用parselnt
定義函數的兩種方式+
//方法一:直接定義
function abs(x) {
if (x >= 0) {
return x;
} else {
return -x;
}
}
//方法二:賦值定義
let abs = function (x) {
...
};
在第二種定義函數的方式下,function (x) { ... }是一個匿名函數,這個函數賦值給了變量abs,所以abs就可以調用該函數。兩種定義完全等價,注意賦值定義需要在函數體末尾加;表示賦值語句結束。
調用函數
JavaScript允許傳入任意個參數而不影響調用,因此傳入的參數與定義的參數不同不會報錯:
- 傳多時,多餘函數不會發生作用
- 傳少時,缺少的函數參數會被定義為
undefined加入函數操作
abs(10, 'blablabla'); // 返回10
abs(-9, 'haha', 'hehe', null); // 返回9
abs(); // x將收到undefined,計算結果為NaN
為了避免收到undefined,對參數進行傳入確認:
function abs(x) {
if (typeof x !== 'number') {
throw 'Not a number';
}
...
}
arguments
或者可以使用關鍵字arguments(實際上arguments就最常用於判斷傳入參數的個數),它用於函數內部,並且指向當前函數的調用者傳入的所有參數。arguments類似Array但它不是
function foo(x) {
console.log('x = ' + x); // 10
for (let i=0; i<arguments.length; i++) {
// 10, 20, 30
console.log('arg ' + i + ' = ' + arguments[i]);
}
}
foo(10, 20, 30);
//判斷傳入參數的個數用法
/*
foo(a[, b], c)
接收2~3個參數,b是可選參數,如果只傳2個參數,b默認為null:
*/
function foo(a, b, c) {
if (arguments.length === 2) {
// 實際拿到的參數是a和b,c為undefined
c = b; // 把b賦給c
b = null; // b變為默認值
}
// ...
}
因為找的教程裏講的不是很清楚,去找了一下arguments在上面的情況下里面長這樣:
arguments: {
// 數字索引屬性(存儲實際參數值)
0: 10, // 第一個參數,對應形參 x
1: 20, // 第二個參數
2: 30, // 第三個參數
// 核心屬性
length: 3, // 參數的總個數
// 內部屬性(雖然不常用,但確實存在)
callee: function foo(x) { ... }, // 指向當前正在執行的函數(即 foo 函數本身)
// 其他內部屬性和方法(在控制枱中可以查看)
__proto__: Object, // 原型鏈指向
Symbol(Symbol.iterator): ƒ values() // 使得 arguments 可以被迭代(用於 for...of)
}
利用arguments,你可以獲得調用者傳入的所有參數。也就是説,即使函數不定義任何參數,還是可以拿到參數的值:
function abs() {
if (arguments.length === 0) {
return 0;
}
let x = arguments[0];
return x >= 0 ? x : -x;
}
abs(); // 0
rest參數
ES6標準引入了rest參數,可以獲取獲取除了已定義參數之外的參數
//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
// Array [ 3, 4, 5 ] 多餘的參數以數組形式交給變量rest
foo(1);
// 結果: a = 1; b = undefined
// Array [] 傳入的參數沒填滿rest參數會接收一個空數組
//用rest寫的sum函數
function sum(...rest) {
let s = 0;
//console.log(rest.length);
if (rest.length === 1){
return rest[0];
}
if (rest.length > 1) {
for (i = rest.length-1; i>=0; i--) {
s = s + rest[i]
}
}
return s;
}
return語句
JavaScript引擎有一個在行末自動添加分號的機制,會導致:
function foo() {
return
{ name: 'foo' };
}
foo(); // undefined
//代碼實際邏輯 ↓
function foo() {
return; // 自動添加了分號,相當於return undefined;
{ name: 'foo' }; // 這行語句已經沒法執行到了
}
//想寫多行可以這樣寫:
function foo() {
return { // 這裏不會自動加分號,因為{表示語句尚未結束
name: 'foo'
};
}
- 對於函數,如果沒有
return語句,函數執行完畢後也會返回結果,只是結果為undefined
全局作用域
函數外定義的變量具有全局作用域,且頂層函數(非嵌套內函數)的定義也被視為一個全局變量,每次直接調用的alert()函數其實也是window的一個變量。
JavaScript默認有一個全局對象window,全局作用域的變量會被綁到window的一個屬性:
var course = 'Learn JavaScript';
console.log(course); // 'Learn JavaScript'
console.log(window.course); // 'Learn JavaScript'
function foo() {
alert('foo');
}
foo(); // 直接調用foo()
window.foo(); // 通過window.foo()調用
JavaScript只有一個全局作用域。任何變量(函數也視為變量)如果沒有在當前函數作用域中找到,就會繼續往上查找直到全局作用域中也沒有找到,則報ReferenceError錯誤。
名字空間
全局變量會綁定到window上,不同的JavaScript文件如果使用了相同的全局變量或相同名字的頂層函數,都會造成命名衝突
減少衝突的一個方法是把自己的所有變量和函數全部綁定到一個全局變量中。
例如:
// 唯一的全局變量MYAPP:
let MYAPP = {};
// 其他變量:
MYAPP.name = 'myapp';
MYAPP.version = 1.0;
// 其他函數:
MYAPP.foo = function () {
return 'foo';
};
把自己的代碼全部放入唯一的名字空間MYAPP中,會大大減少全局變量衝突的可能。
解構賦值(ES6)
對數組元素進行解構賦值時,多個變量要用[...]括起來。
let [x, y, z] = ['hello', 'JavaScript', 'ES6'];
// x, y, z分別被賦值為數組對應元素:
console.log(`x = ${x}, y = ${y}, z = ${z}`);
//x = hello, y = JavaScript, z = ES6
//數組本身有嵌套
let [x, [y, z]] = ['hello', ['JavaScript', 'ES6']];
//x = hello, y = JavaScript, z = ES6
// 忽略前兩個元素,只對z賦值第三個元素
let [, , z] = ['hello', 'JavaScript', 'ES6'];
z; // 'ES6'
對對象取數/解構賦值
let person = {
name: '小明',
age: 20,
gender: 'male',
passport: 'G-12345678',
school: 'No.4 middle school'
address: {
city: 'Beijing',
street: 'No.1 Road',
zipcode: '100001'
}
};
let {name, age, passport} = person;
// name, age, passport分別被賦值為對應屬性:
console.log(`name = ${name}, age = ${age}, passport = ${passport}`);
//對嵌套的對象屬性進行賦值
let {name, address: {city, zip}} = person;
name; // '小明'
city; // 'Beijing'
zip; // undefined, 因為屬性名是zipcode而不是zip
如果對應的屬性不存在,變量將被賦值為undefined
// address是嵌套的address對象的屬性
address; // Uncaught ReferenceError: address is not defined
// 如果要使用的變量名和屬性名不一致
// 把passport屬性賦值給變量id:
let {name, passport:id} = person;
id; // 'G-12345678'
passport; // Uncaught ReferenceError: passport is not defined
可以使用默認值,這樣就避免不存在的屬性返回undefined
// 如果person對象沒有single屬性,默認賦值為true:
let {name, single=true} = person;
name; // '小明'
single; // true
已經被聲明變量再次賦值:
// 聲明變量:
let x, y;
// 解構賦值:要小括號
({x, y} = { name: '小明', x: 100, y: 200});
//沒小括號會報錯
// 語法錯誤: Uncaught SyntaxError: Unexpected token =
{x, y} = { name: '小明', x: 100, y: 200};
實際使用場景
// 交換變量值
let x=1, y=2;
[x, y] = [y, x]
// 快速獲取當前頁面的域名和路徑
let {hostname:domain, pathname:path} = location;
// 快速創建一個Date對象
function buildDate({year, month, day, hour=0, minute=0, second=0}) {
return new Date(`${year}-${month}-${day} ${hour}:${minute}:${second}`);
}
buildDate({ year: 2017, month: 1, day: 1 });
// Sun Jan 01 2017 00:00:00 GMT+0800 (CST)
buildDate({ year: 2017, month: 1, day: 1, hour: 20, minute: 15 });
// Sun Jan 01 2017 20:15:00 GMT+0800 (CST)
方法
let xiaoming = {
name: '小明',
birth: 1990,
age: function () {
let y = new Date().getFullYear();
return y - this.birth;
}
};
xiaoming.age; // function xiaoming.age()
xiaoming.age(); // 今年調用是25,明年調用就變成26了
this
綁定到對象上的函數稱為方法,內部有個特殊變量this始終指向當前對象,也就是xiaoming這個變量。所以,this.birth可以拿到xiaoming的birth屬性。
function getAge() {
let y = new Date().getFullYear();
return y - this.birth;
//此時函數的this指向全局對象,也就是window
}
let xiaoming = {
name: '小明',
birth: 1990,
age: getAge
};
xiaoming.age(); // 25, 正常結果
getAge(); // 調用出錯所以NaN
//從外部重賦函數也不行
let fn = xiaoming.age; // 先拿到xiaoming的age函數
fn(); // NaN
注意:在strict模式下讓函數的this指向undefined。
例如這裏報錯是因為this指針只在age方法的函數內指向xiaoming,在函數內部定義的函數,this指向undefined(非strict模式下指向全局對象window)
'use strict';
let xiaoming = {
name: '小明',
birth: 1990,
age: function () {
function getAgeFromBirth() {
let y = new Date().getFullYear();
return y - this.birth;
}
return getAgeFromBirth();
}
};
xiaoming.age(); // Uncaught TypeError: Cannot read property 'birth' of undefined
修復方案:用that變量捕獲this:let that = this;
'use strict';
let xiaoming = {
name: '小明',
birth: 1990,
age: function () {
let that = this; // 在方法內部一開始就捕獲this
function getAgeFromBirth() {
let y = new Date().getFullYear();
// 用that而不是this
return y - that.birth;
}
return getAgeFromBirth();
}
};
xiaoming.age(); // 25
apply和call
apply可以指定函數的this指向哪個對象,接收兩個參數,第一個參數就是需要綁定的this變量,第二個參數是Array,表示函數本身的參數。
function getAge() {
let y = new Date().getFullYear();
return y - this.birth;
}
let xiaoming = {
name: '小明',
birth: 1990,
age: getAge
};
xiaoming.age(); // 25
getAge.apply(xiaoming, []); // 25, this指向xiaoming, 參數為空
或者用call(),apply()把參數打包成Array再傳入,call()把參數按順序傳入。
//使用區別
Math.max.apply(null, [3, 5, 4]); // 5
Math.max.call(null, 3, 5, 4); // 5
對普通函數調用,我們通常把this綁定為null
裝飾器
利用apply()動態改變函數的行為,JavaScript的所有對象都是動態的,內置的函數也可以重新指向新的函數。例如,統計一下代碼一共調用了多少次parseInt()
'use strict';
let count = 0;
let oldParseInt = parseInt; // 保存原函數
//將原裝函數parseInt換成我們自己設計的匿名函數
window.parseInt = function () {
count += 1; //增加計數步驟
return oldParseInt.apply(null, arguments); // 調用原函數
//arguments記錄所有傳入的函數
};
// 測試:
parseInt('10');
parseInt('20');
parseInt('30');
console.log('count = ' + count); // 3
高階函數
接收另一個函數作為參數的函數
function add(x, y, f) {
return f(x) + f(y);
}
add(-5, 6, Math.abs)
/*
x = -5;
y = 6;
f = Math.abs;
f(x) + f(y) ==> Math.abs(-5) + Math.abs(6) ==> 11;
return 11;
*/
map
map()方法定義在JavaScript的Array中,調用Array的map()方法,傳入自己的函數會得到一個新的Array作為結果。注意map()傳入的參數是函數對象本身。
function pow(x) {
return x * x;
}
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
// 傳入函數對象pow
let results = arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
console.log(results);
// 還可以這樣
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
arr.map(String); // ['1', '2', '3', '4', '5', '6', '7', '8', '9']
reduce
原博這麼説:
- Array的
reduce()把一個函數作用在這個Array的[x1, x2, x3...]上,這個函數必須接收兩個參數,reduce()把結果繼續和序列的下一個元素做累積計算。
大概意思是,reduce()函數起到一類似於“累加器”一樣的累計處理作用。假設Array = [a0, a1, a2, a3]結合代碼解釋是這樣:
arr = [a0, a1, a2, a3]
//定義函數add(x, y),函數接受x和y兩個參數
//(只能接受兩個參數的函數reduce才能用)
function add(x, y) {
return x + y;
}
//reduce使用函數add
let result = arr.reduce(add)
/*
內部過程:
取出arr[0] 與 arr[1]即 a0 與 a1
令x = a0, y = a1;add(x, y) = a1+a2
方便起見我們設r1 = a1 + a2
取出 arr[2] = a2
令x = r1, y = a2;add(x, y) = r1+a2 = a0+a1+a2
....(重複上面過程)
直到最後arr[4] 取出來沒東西了,reduce結束
result=a0 + a1 + a2 + a3
*/
如果Array只有一個元素的場合:
let arr = [123];
// 提供一個額外的初始數據0
// 順便這裏是在使用reduce時直接創建簡單的匿名函數f(x, y)
//map也可以這樣寫
arr.reduce(function (x, y) {return x + y;}, 0);
//這樣也可以(map同理arr.map(ch => ch - 0))
arr.reduce((x, y) => x + y, 0);
擴展1:字符轉數字的方法
//1. 使用parseInt、parseFloat和Number函數
parseInt("123.45")//123,保留為整數
parseFloat("123.45")//123.45
Number("123.45")//123.45嚴格轉換
//順便如果用parseInt有一個好處,能忽略一些結尾非數字元素
parseInt("123abc")//123,如果用number這裏會輸出NaN
//2. ASCII 碼差值法
let char = '7';
let num = char.charCodeAt(0) - '0'.charCodeAt(0);//7
//或者
let num = '5'.charCodeAt(0) - 48; // 因為 '0' 的 ASCII 碼是 48
//3. 減法隱式轉換
// JavaScript 在做減法時會嘗試將操作數轉為數字
let char = '3';
let num = char - 0; // 字符減 0 會被轉為數字
//4. 位運算
let char = '9';
let num1 = str | 0;
let num2 = ~~char; // 位運算會強制轉為整數
//5. 取整
let str = "123.7";
Math.floor(+str); // 123(向下取整)
Math.round(+str); // 124(四捨五入)
//6.一元加號運算符
let str = "123";
let num = +str; // 123
擴展2:最好別在map裏用parseInt
直接在map裏用parseInt很可能導致輸出NaN
const arr = ['1', '2', '3', '4'];
// 你期望的結果
arr.map(parseInt); // 返回 [1, 2, 3, 4]?
// 實際結果
console.log(arr.map(parseInt)); // 實際是 [1, NaN, NaN, NaN]
原因是map會給回調函數傳遞三個參數:當前元素、當前索引、整個數組。而parseInt接受兩個參數:要解析的字符串、基數(幾進制的意思)(要求為2-36 之間的整數,或者特殊情況用0)。導致parseInt接受map時會只接受map給它的前兩個參數,會運行成下面這個樣子:
//第一次調用:
parseInt('1', 0, ['1', '2', '3', '4'])
// parseInt('1', 0) -> 1 (基數為0時,如果字符串不以"0x"開頭,則基數為10)
// 第二次調用:
parseInt('2', 1, ['1', '2', '3', '4'])
// parseInt('2', 1) -> NaN (基數為1是無效的,必須在2-36之間)
// 第三次調用:
parseInt('3', 2, ['1', '2', '3', '4'])
// parseInt('3', 2) -> NaN (二進制中沒有3)
// 第四次調用:
parseInt('4', 3, ['1', '2', '3', '4'])
// parseInt('4', 3) -> NaN (三進制中沒有4)