1. 程式人生 > >Web前端學習筆記——JavaScript 之函式進階

Web前端學習筆記——JavaScript 之函式進階

函式進階

函式的定義方式

  • 函式宣告
  • 函式表示式
  • new Function

函式宣告

function foo () {

}

函式表示式

var foo = function () {

}

函式宣告與函式表示式的區別

  • 函式宣告必須有名字
  • 函式宣告會函式提升,在預解析階段就已建立,宣告前後都可以呼叫
  • 函式表示式類似於變數賦值
  • 函式表示式可以沒有名字,例如匿名函式
  • 函式表示式沒有變數提升,在執行階段建立,必須在表示式執行之後才可以呼叫

下面是一個根據條件定義函式的例子:

if (true) {
  function f () {
    console.log(1)
  }
} else
{ function f () { console.log(2) } }

以上程式碼執行結果在不同瀏覽器中結果不一致。

不過我們可以使用函式表示式解決上面的問題:

var f

if (true) {
  f = function () {
    console.log(1)
  }
} else {
  f = function () {
    console.log(2)
  }
}

函式的呼叫方式

  • 普通函式
  • 建構函式
  • 物件方法

函式內 this 指向的不同場景

函式的呼叫方式決定了 this 指向的不同:

呼叫方式 非嚴格模式 備註
普通函式呼叫 window 嚴格模式下是 undefined
建構函式呼叫 例項物件 原型方法中 this 也是例項物件
物件方法呼叫 該方法所屬物件 緊挨著的物件
事件繫結方法 繫結事件物件
定時器函式 window

這就是對函式內部 this 指向的基本整理,寫程式碼寫多了自然而然就熟悉了。

函式也是物件

  • 所有函式都是 Function 的例項

call、apply、bind

那瞭解了函式 this 指向的不同場景之後,我們知道有些情況下我們為了使用某種特定環境的 this 引用, 這時候時候我們就需要採用一些特殊手段來處理了,例如我們經常在定時器外部備份 this 引用,然後在定時器函式內部使用外部 this 的引用。 然而實際上對於這種做法我們的 JavaScript 為我們專門提供了一些函式方法用來幫我們更優雅的處理函式內部 this 指向問題。 這就是接下來我們要學習的 call、apply、bind 三個函式方法。

call

call() 方法呼叫一個函式, 其具有一個指定的 this 值和分別地提供的引數(引數的列表)。

注意:該方法的作用和 `apply()` 方法類似,只有一個區別,就是 `call()` 方法接受的是若干個引數的列表,而 `apply()` 方法接受的是一個包含多個引數的陣列。

語法:

fun.call(thisArg[, arg1[, arg2[, ...]]])

引數:

  • thisArg

    • 在 fun 函式執行時指定的 this 值
    • 如果指定了 null 或者 undefined 則內部 this 指向 window
  • arg1, arg2, ...

    • 指定的引數列表

apply

apply() 方法呼叫一個函式, 其具有一個指定的 this 值,以及作為一個數組(或類似陣列的物件)提供的引數。

注意:該方法的作用和 `call()` 方法類似,只有一個區別,就是 `call()` 方法接受的是若干個引數的列表,而 `apply()` 方法接受的是一個包含多個引數的陣列。

語法:

fun.apply(thisArg, [argsArray])

引數:

  • thisArg
  • argsArray

apply()call() 非常相似,不同之處在於提供引數的方式。 apply() 使用引數陣列而不是一組引數列表。例如:

fun.apply(this, ['eat', 'bananas'])

bind

bind() 函式會建立一個新函式(稱為繫結函式),新函式與被調函式(繫結函式的目標函式)具有相同的函式體(在 ECMAScript 5 規範中內建的call屬性)。 當目標函式被呼叫時 this 值繫結到 bind() 的第一個引數,該引數不能被重寫。繫結函式被呼叫時,bind() 也接受預設的引數提供給原函式。 一個繫結函式也能使用new操作符建立物件:這種行為就像把原函式當成構造器。提供的 this 值被忽略,同時呼叫時的引數被提供給模擬函式。

語法:

fun.bind(thisArg[, arg1[, arg2[, ...]]])

引數:

  • thisArg

    • 當繫結函式被呼叫時,該引數會作為原函式執行時的 this 指向。當使用new 操作符呼叫繫結函式時,該引數無效。
  • arg1, arg2, …

    • 當繫結函式被呼叫時,這些引數將置於實參之前傳遞給被繫結的方法。

返回值:

返回由指定的this值和初始化引數改造的原函式拷貝。

示例1:

this.x = 9; 
var module = {
  x: 81,
  getX: function() { return this.x; }
};

module.getX(); // 返回 81

var retrieveX = module.getX;
retrieveX(); // 返回 9, 在這種情況下,"this"指向全域性作用域

// 建立一個新函式,將"this"繫結到module物件
// 新手可能會被全域性的x變數和module裡的屬性x所迷惑
var boundGetX = retrieveX.bind(module);
boundGetX(); // 返回 81

示例2:

function LateBloomer() {
  this.petalCount = Math.ceil(Math.random() * 12) + 1;
}

// Declare bloom after a delay of 1 second
LateBloomer.prototype.bloom = function() {
  window.setTimeout(this.declare.bind(this), 1000);
};

LateBloomer.prototype.declare = function() {
  console.log('I am a beautiful flower with ' +
    this.petalCount + ' petals!');
};

var flower = new LateBloomer();
flower.bloom();  // 一秒鐘後, 呼叫'declare'方法

小結

  • call 和 apply 特性一樣

    • 都是用來呼叫函式,而且是立即呼叫
    • 但是可以在呼叫函式的同時,通過第一個引數指定函式內部 this 的指向
    • call 呼叫的時候,引數必須以引數列表的形式進行傳遞,也就是以逗號分隔的方式依次傳遞即可
    • apply 呼叫的時候,引數必須是一個數組,然後在執行的時候,會將陣列內部的元素一個一個拿出來,與形參一一對應進行傳遞
    • 如果第一個引數指定了 null 或者 undefined 則內部 this 指向 window
  • bind

    • 可以用來指定內部 this 的指向,然後生成一個改變了 this 指向的新的函式
    • 它和 call、apply 最大的區別是:bind 不會呼叫
    • bind 支援傳遞引數,它的傳參方式比較特殊,一共有兩個位置可以傳遞
        1. 在 bind 的同時,以引數列表的形式進行傳遞
        1. 在呼叫的時候,以引數列表的形式進行傳遞
      • 那到底以誰 bind 的時候傳遞的引數為準呢還是以呼叫的時候傳遞的引數為準
      • 兩者合併:bind 的時候傳遞的引數和呼叫的時候傳遞的引數會合併到一起,傳遞到函式內部

函式的其它成員

  • arguments
    • 實參集合
  • caller
    • 函式的呼叫者
  • length
    • 形參的個數
  • name
    • 函式的名稱
function fn(x, y, z) {
  console.log(fn.length) // => 形參的個數
  console.log(arguments) // 偽陣列實參引數集合
  console.log(arguments.callee === fn) // 函式本身
  console.log(fn.caller) // 函式的呼叫者
  console.log(fn.name) // => 函式的名字
}

function f() {
  fn(10, 20, 30)
}

f()

高階函式

  • 函式可以作為引數
  • 函式可以作為返回值

作為引數

function eat (callback) {
  setTimeout(function () {
    console.log('吃完了')
    callback()
  }, 1000)
}

eat(function () {
  console.log('去唱歌')
})

作為返回值

function genFun (type) {
  return function (obj) {
    return Object.prototype.toString.call(obj) === type
  }
}

var isArray = genFun('[object Array]')
var isObject = genFun('[object Object]')

console.log(isArray([])) // => true
console.log(isArray({})) // => true

函式閉包

function fn () {
  var count = 0
  return {
    getCount: function () {
      console.log(count)
    },
    setCount: function () {
      count++
    }
  }
}

var fns = fn()

fns.getCount() // => 0
fns.setCount()
fns.getCount() // => 1

作用域、作用域鏈、預解析

  • 全域性作用域
  • 函式作用域
  • 沒有塊級作用域
{
  var foo = 'bar'
}

console.log(foo)

if (true) {
  var a = 123
}
console.log(a)

作用域鏈示例程式碼:

var a = 10

function fn () {
  var b = 20

  function fn1 () {
    var c = 30
    console.log(a + b + c)
  }

  function fn2 () {
    var d = 40
    console.log(c + d)
  }

  fn1()
  fn2()
}
  • 內層作用域可以訪問外層作用域,反之不行

什麼是閉包

閉包就是能夠讀取其他函式內部變數的函式, 由於在 Javascript 語言中,只有函式內部的子函式才能讀取區域性變數, 因此可以把閉包簡單理解成 “定義在一個函式內部的函式”。 所以,在本質上,閉包就是將函式內部和函式外部連線起來的一座橋樑。

閉包的用途:

  • 可以在函式外部讀取函式內部成員
  • 讓函式內成員始終存活在記憶體中

一些關於閉包的例子

示例1:

var arr = [10, 20, 30]
for(var i = 0; i < arr.length; i++) {
  arr[i] = function () {
    console.log(i)
  }
}

示例2:

console.log(111)

for(var i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log(i)
  }, 0)
}
console.log(222)

示例3:投票

示例4:判斷型別

示例5:沙箱模式

閉包的思考題

思考題 1:

var name = "The Window";
var object = {
  name: "My Object",
  getNameFunc: function () {
    return function () {
      return this.name;
    };
  }
};

console.log(object.getNameFunc()())

思考題 2:

var name = "The Window";  
var object = {    
  name: "My Object",
  getNameFunc: function () {
    var that = this;
    return function () {
      return that.name;
    };
  }
};
console.log(object.getNameFunc()())

小結

函式遞迴

遞迴執行模型

function fn1 () {
  console.log(111)
  fn2()
  console.log('fn1')
}

function fn2 () {
  console.log(222)
  fn3()
  console.log('fn2')
}

function fn3 () {
  console.log(333)
  fn4()
  console.log('fn3')
}

function fn4 () {
  console.log(444)
  console.log('fn4')
}

fn1()

舉個栗子:計算階乘的遞迴函式

function factorial (num) {
  if (num <= 1) {
    return 1
  } else {
    return num * factorial(num - 1)
  }
}

遞迴應用場景

  • 深拷貝
  • 選單樹
  • 遍歷 DOM 樹