函數
函數聲明與函數表達式
函數聲明提升與變量聲明提升
函數聲明:以function開頭的函數定義。
js在執行代碼之前會先讀取函數聲明和變量聲明,函數聲明整個提升,變量賦值在執行時再執行。
console.log([typeof add, typeof sub]); // ['function', 'undefined']
function add(x, y){
return x + y;
}
var sub = function(x, y){
return x - y;
}
console.log([add(1, 2), sub(1, 2)]);
函數聲明和var都是沒有塊級作用域的,所以,以下這種方式請不要使用函數聲明,應該使用函數表達式。
if (condition) {
function sayHi() {
alert('Hi!');
}
} else {
function sayHi() {
alert('Yo!')
}
}
除火狐瀏覽器外,一般都會返回第二個函數,因爲再執行之前,函數聲明均提升,所以第二個覆蓋了第一個。最好使用函數表達式。
函數表達式通常都是匿名函數。
var func = function() {}
console.log(func.name) // func
ES5之後,支持具名的函數表達式。
var func = function num() {}
console.log(func.name) // num
此時,func是存儲了一個名爲num的函數的變量。
箭頭函數
let abc = () => {}
同普通的函數表達式相比,這種形式的this綁定有些區別。
匿名函數(拉姆達函數 )
以爲匿名,所以name屬性爲空字符串。
在函數表達式、回調函數、IIFE中比較常見。
arguments.callee
指向正在執行的函數的指針,比如在遞歸的時候,使用arguments.callee更安全一些, 有可能函數名會改變。
不過,嚴格模式下,不能使用arguments.callee,此時,可以使用具名的函數表達式。
函數的參數
function.length(函數參數個數)
可以檢驗是否傳了足夠數量的參數,因爲有時少了參數,會造成異常。
function __matchArgs__(fn){
return function(...args){
if(args.length !== fn.length){
throw RangeError('Arguments not match!');
}
return fn.apply(this, args);
}
}
var add = __matchArgs__((a, b, c) => a + b + c);
console.log(add(1, 2, 3));
console.log(add(4, 5)); //error,如果沒有使用__matchArgs__檢查,那麼 4+5+undefined會報錯。
rest參數
在第零課中提到過,…args是rest參數(ES6),通過這種寫法,將參數轉化成數組形式傳到args變量中,同arguments參數不同,arguments參數是類數組,要用Array.from轉成真正的數組。
也可以直接用…args,然後調用數組的歸併方法。
注意:…args並不計算在length之中。這麼寫的話,length爲0。
[].slice.call(arguments)
ps:類數組轉數組
- Array.from(arguments)
- Array.prototype.slice.call(arguments)
- [].slice.call(arguments)
二三種方式實際上就是寫法不同,這兩種方式跟slice的底層實現有關,slice的底層實現摘錄如下:
if (size > 0) {
cloned = new Array(size);
if (this.charAt) {
for (i = 0; i < size; i++) {
cloned[i] = this.charAt(start + i);
}
} else {
for (i = 0; i < size; i++) {
cloned[i] = this[start + i];
}
}
}
如果不是字符串,即this.charAt爲false,那麼進入else,以這種方式淺拷貝到數組中,所以如果call(arguments),即可轉化爲數組。
由判斷了this.charAt可以看出,數組和字符串都有這個方法。
參數默認值
function add(x = (function(){throw new Error})(), y = 0) {
return x + y;
}
X默認值是拋出一個錯誤,則可以讓用戶至少傳入一個參數,y有一個0的默認值,避免只有一個參數時,返回的是NaN。
作用域、閉包、this
閉包
閉包是指有權訪問另一個函數作用域中變量的函數,注意與匿名函數區分開。
創建閉包
常見的創建方式,在一個函數內部創建另一個函數。
function createFunc(prop) {
return function(obj1, obj2) {
console.log(prop);
}
}
var newFunc = createFunc(1234);
newFunc(1,2); // 1234
console.log(newFunc.name) // ''
可以看出,即使內部函數被返回,在其他地方被調用了,也還是能訪問prop這個變量。這是因爲內部函數的作用域鏈中包含createFunc的作用域。
不過此時就是匿名函數,name也爲零,同匿名函數表達式不同。
我們來看看函數被調用的時候都發生了什麼。
函數調用時發生的事情
當函數被調用時,會創造一個執行環境及相應的作用域,會自動取得兩個特殊變量this和arguments,根據arguments和其他命名參數的值來初始化函數的活動對象,在作用域鏈中,按內部函數、外部函數、外部的外部等等順序初始化活動對象和創建作用域,一直到全局執行環境。
一般來說,當函數執行完畢後,局部活動對象就會被銷燬,內存中僅保存全局作用域,但是,閉包的情況會稍有不同。
如上面的例子中,函數調用完,將返回的匿名函數賦給了newFunc變量,其執行環境的作用域鏈會被銷燬,但是其活動對象仍會留在內存中,直到匿名函數被銷燬。
compareNames = null; // 解除對匿名函數的引用,釋放內存
調用時this是global,全局環境中調用的。
閉包的缺點
- 閉包會攜帶包含它的函數的作用域,因此會比其他函數佔用更多的內存,一般來說絕對必要的時候再使用。
- IE中可能會造成內存泄漏。如果閉包的作用域鏈中保存着一個HTML元素,那麼該元素無法被銷燬。
閉包作用域與變量
function createFunctions() {
var result = new Array();
for(var i = 0; i < 10; i++){
result[i] = function(num) {
return (function() {
return num;
})(i);
}
}
return result;
}
通過IIFE,創造了一層作用域,這樣setTimeout找i的時候,根據作用域鏈,就先查找到了這裏的i。
如果沒有這一層的話,返回時,讀到的都是同一個i,因爲var是不存在塊級作用域的。
此外也可以使用bind,先穿參,不立即執行的一個方法,
function createFunctions() {
var result = new Array();
for(var i = 0; i < 10; i++){
result[i] = function(num) {
return (function() {
return num;
}).bind(null, i);
}
}
return result;
}
閉包與私有數據
var MyClass = (function(){
var privateData = 'privateData';
function Class(){
this.publicData = 'publicData';
}
Class.prototype.getData = function(){
return privateData;
}
return Class;
})();
上面這個是使用IIFE的模式,形成一個函數級作用域。
也可以都改成let、const放在{}裏面,形成塊級作用域。
this
js是動態語言,this是由函數求值時的調用者決定的。
匿名函數的執行環境具有全局性,this通常指向window。
當然,通過call()、bind()、apply()改變函數執行環境的情況下,this會指向其它對象。
var b = 2;
var obj={
a: function (prop) {
return function(obj1, obj2) {
console.log(this.b);
}
},
b: 123
}
obj.a()() // 2
可以看到,即使是在obj這個環境中調用newFunc所存儲的匿名函數,匿名函數內的this仍然是指向全局的。
可以把外部作用域中的this保存在一個閉包能夠訪問到的變量內,來解決這個問題。
var obj={
a: function (prop) {
let that = this;
return function(obj1, obj2) {
console.log(that.b);
}
},
b: 123
}
obj.a()() // 123
注: this和arguments都存在這個問題,如果想訪問作用域中arguments參數,必須將該對象的引用保存到另一個閉包能訪問的變量中。
作用域
ES5可以使用IIFE來實現塊級作用域
(function() {
// 這裏是塊級作用域
})()
私有變量
任何函數中定義的變量,都可以認爲是私有變量,因爲不能在函數的外部訪問這些變量。
私有變量包括函數的參數、局部變量和在函數內部定義的其他函數。
可以通過閉包創建用於訪問私有變量的公有方法。
function MyObject() { // 構造函數
// 私有成員
var privateVariable = 10;
// 特權成員
this.public = function() { // 創建的閉包
console.log(privateVariable);
}
}
let obj = new MyObject();
除實例的public()外,沒有任何辦法可以直接訪問privateVariable。
優點:可以利用這個來隱藏不應該直接被修改的數據。
缺點:因爲這種方式必須使用構造函數,所以同構造函數模式相同,每一個實例都要創建一組新方法,可以通過靜態私有變量來實現特權方法避免這個問題。
靜態私有變量
將public方法放到原型上。
(function(){
...
MyObject.prototype.public = function() {
console.log(...);
}
...
})()
call、bind、apply
func.call(thisObj,arg1,arg2…)、func.apply(thisObj,[obj1,obj2…])
第一個參數改變當前的this,第二個參數爲函數傳入的參數。
call和apply的差別,僅在於後續參數是數組還是多個寫開。
func.bind(thisObj, arg1, arg2)
基本與call相同,差別在於bind返回的是綁定對象和參數後的函數,而非像call和apply那樣立即執行,bind之後要賦給一個新的變量,再執行。
執行是還可以接着傳參。
function foo(arg1, arg2) {
console.log(arg1, arg2); // 1, 2
}
var bar = foo.bind(null, 1);
bar(2,3); // 1, 2
bind第一個參數爲null,表示不改變函數this指向,這麼寫可以達到先傳參但不立即執行的效果。
而且通過bind也可以部分傳參,如上面的,可以先傳一個參數進去。
如果全部參數傳進去,則可以實現延時調用的效果,比如setTimeout就可以先將參數傳進去。
異步和回調函數
Promise
promise三種狀態
- pending:進行中
- fulfilled:已完成
rejected:已失敗
- 對象的狀態不受外界影響。只有異步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。
- 一旦狀態改變,就不會再變,任何時候都可以得到這個結果。
- 無法取消
Promise
。 Promise
內部拋出的錯誤,不會反應到外部。- 當處於
pending
狀態時,無法得知目前進展到哪一個階段。
聲明promise對象
// 方法1
let promise = new Promise ( (resolve, reject) => {
if ( /* 異步操作成功 */ ) {
resolve(a) // pending ——> resolved 參數將傳遞給對應的回調方法
} else {
reject(err) // pending ——> rejectd
}
} )
// 方法2
function promise () {
return new Promise ( function (resolve, reject) {
if ( /* 異步操作成功 */ ) {
resolve(value);
} else {
reject(error);
}
});
}
resolve:將Promise
對象的狀態從“未完成”變爲“成功”(即從 pending 變爲 resolved),成功是調用。
reject:將Promise
對象的狀態從“未完成”變爲“失敗”(即從 pending 變爲 rejected),失敗是調用。
方法一是直接實例化,就是立即執行了,方法二是調用promise時再執行。
Promise.prototype.then()和 Promise.prototype.catch()
實例生成後,可以用then
方法分別指定resolved
狀態和rejected
狀態的回調函數。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
then方法接受兩個回調函數作爲闡述。第一個是狀態變爲resolved的時候調用,第二個是變爲rejected時調用。第二個參數是可選的。
value和error分別是promise裏resolve和reject傳的參數。