奇舞學院及高程學習筆記-函數篇

函數

函數聲明與函數表達式

函數聲明提升與變量聲明提升

函數聲明:以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:已失敗

    1. 對象的狀態不受外界影響。只有異步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。
    2. 一旦狀態改變,就不會再變,任何時候都可以得到這個結果。
    3. 無法取消Promise
    4. Promise內部拋出的錯誤,不會反應到外部。
    5. 當處於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傳的參數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章