JS 總結之函式、作用域鏈

Karon_發表於2018-12-24

在 JavaScript 中,函式實際上是一個物件。

? 宣告

JavaScript 用 function 關鍵字來宣告一個函式:

function fn () {

}
複製程式碼

變體:函式表示式:

var fn = function () {

}
複製程式碼

這種沒有函式名的函式被稱為匿名函式表示式。

?‍ return

函式可以有返回值

function fn () {
  return true
}
複製程式碼

位於 return 之後的任何程式碼都不會執行:

function fn () {
  return true
  console.log(false) // 永遠不會執行
}
fn() // true
複製程式碼

沒有 return 或者只寫 return,函式將返回 undefined:

function fn () {
}
fn() // undefined
// 或者
function fn () {
  return
}
fn() // undefined
複製程式碼

⛹ 引數

函式可以帶有限個數或者不限個數的引數

// 引數有限
function fn (a, b) {
  console.log(a, b)
}
// 引數不限
function fn (a, b, ..., argN) {
  console.log(a, b, ..., argN)
}
複製程式碼

沒有傳值的命名引數,會被自動設定為 undefined

// 引數有限
function fn (a, b) {
  console.log(b) // undefined
}
fn(1)
複製程式碼

? arguments

函式可以通過內部屬性 arguments 這個類陣列的物件來訪問引數,即便沒有命名引數

// 有命名引數
function fn (a, b) {
  console.log(arguments.length) // 2
}
fn(1, 2)

// 無命名引數
function fn () {
  console.log(arguments[0], arguments[1], arguments[2]) // 1, 2, 3
}
fn(1, 2, 3)
複製程式碼

⛳️ 長度

arguments 的長度由傳入的引數決定,並不是定義函式時決定的。

function fn () {
  console.log(arguments.length) // 3
}
fn(1, 2, 3)
複製程式碼

如果按定義函式是決定個的,那麼此時的 arguments.length 應該為 0 而不為 3。

? 同步

arguments 物件中的值會自動反應到對應的命名引數,可以理解為同步,不過並不是因為它們讀取了相同的記憶體空間,而只是保持值同步而已

function fn (a) {
  console.log(arguments[0]) // 1
  a = 2
  console.log(arguments[0]) // 2
  arguments[0] = 3
  console.log(a) // 3
}
fn(1)
複製程式碼

嚴格模式下,重寫 arguments 的值會導致錯誤。

? callee

通過 callee 這個指標訪問擁有這個 arguments 物件的函式

function fn () {
  console.log(arguments.callee) // fn
}
fn()
複製程式碼

? 類陣列

長的跟陣列一樣,可以通過下標訪問,如 arguments[0],卻無法使用陣列的內建方法,如 forEach 等:

function fn () {
  console.log(arguments[0], arguments[1]) // 1, 2
  console.log(arguments.forEach) // undefined
}
fn(1, 2)
複製程式碼

通過物件那章知道,可以用 call 或者 apply 借用函式,所以 arguments 可以借用陣列的內建方法:

function fn () {
  Array.prototype.forEach.call(arguments, function (item) {
    console.log(item)
  })
}
fn(1, 2)
// 1
// 2
複製程式碼

對於如此詭異的 arguments,我覺得還是少用為好。

? this、 prototype

具體檢視總結:

? 按值傳遞

引用《JavaScript 高階程式設計》4.1.3 的一句話:

ECMAScript 中所有函式的引數都是按值傳遞的,也就是說,把函式外部的值複製給函式內部的引數,就和把一個變數複製到另一個變數一樣。

? 基本型別的引數傳遞

基本型別的傳遞很好理解,就是把變數複製給函式的引數,變數和引數是完全獨立的兩個個體:

var name = 'jon'
function fn (a) {
  a = 'karon'
  console.log('a: ', a) // a: karon
}
fn(name)
console.log('name: ', name) // name: jon
複製程式碼

用表格模擬過程:

棧記憶體 堆記憶體
name, a jon

將 a 複製為其他值後:

棧記憶體 堆記憶體
name jon
a karon

? 引用型別的引數傳遞

var obj = {
  name: 'jon'
}
function fn (a) {
  a.name = 'karon'
  console.log('a: ', a) // a: { name: 'karon' }
}
fn(obj)
console.log(obj) // name: { name: 'karon' }
複製程式碼

嗯?說好的按值傳遞呢?我們嘗試把 a 賦值為其他值,看看會不會改變了 obj 的值:

var obj = {
  name: 'jon'
}
function fn (a) {
  a = 'karon'
  console.log('a: ', a) // a: karon
}
fn(obj)
console.log(obj) // name: { name: 'jon' }
複製程式碼

? 真相浮出水面

引數 a 只是複製了 obj 的引用,所以 a 能找到物件 obj,自然能對其進行操作。一旦 a 賦值為其他屬性了,obj 也不會改變什麼。

用表格模擬過程:

棧記憶體 堆記憶體
obj, a 引用值 { name: 'jon' }

引數 a 只是 複製了 obj 的引用,所以 a 能找到存在堆記憶體中的物件,所以 a 能對堆記憶體中的物件進行修改後:

棧記憶體 堆記憶體
obj, a 引用值 { name: 'karon' }

將 a 複製為其他值後:

棧記憶體 堆記憶體
obj 引用值 { name: 'karon' }
a 'karon'

因此,基本型別和引用型別的引數傳遞也是按值傳遞的

? 作用域鏈

理解作用域鏈之前,我們需要理解執行環境變數物件

? 執行環境

執行環境定義了變數或者函式有權訪問的其它資料,可以把執行環境理解為一個大管家。

執行環境分為全域性執行環境函式執行環境,全域性執行環境被認為是 window 物件。而函式的執行環境則是由函式建立的。

每當一個函式被執行,就會被推入一個環境棧中,執行完就會被推出,環境棧最底下一直是全域性執行環境,只有當關閉網頁或者推出瀏覽器,全域性執行環境才會被摧毀。

? 變數物件

每個執行環境都有一個變數物件,存放著環境中定義的所有變數和函式,是作用域鍊形成的前置條件。但我們無法直接使用這個變數物件,該物件主要是給 JS 引擎使用的。具體可以檢視《JS 總結之變數物件》

? 作用域鏈的作用

作用域鏈屬於執行環境的一個變數,作用域鏈收集著所有有序的變數物件,函式執行環境中函式自身的變數物件(此時稱為活動物件)放置在作用域鏈的最前端,如:

scope: [函式自身的變數物件,變數物件1,變數物件2,..., 全域性執行環境的變數物件]
複製程式碼

作用域鏈保證了對執行環境有權訪問的所有變數和函式的有序訪問

var a = 1
function fn1 () {
  var b = 2
  console.log(a,b) // 1, 2
  function fn2 () {
    var c = 3
    console.log(a, b, c) // 1, 2, 3
  }
  fn2()
}
fn1()
複製程式碼

對於 fn2 來說,作用域鏈為: fn2 執行環境fn1 執行環境全域性執行環境 的變數物件(所有變數和函式)。

對於 fn1 來說,作用域鏈為: fn1 執行環境全域性執行環境 的變數物件(所有變數和函式)。

總結為一句:函式內部能訪問到函式外部的值,函式外部無法範圍到函式內部的值。引出了閉包的概念,檢視總結:《JS 總結之閉包》

? 箭頭函式

ES6 新語法,使用 => 定義一個函式:

let fn = () => {}
複製程式碼

當只有一個引數的時候,可以省略括號:

let fn = a => {}
複製程式碼

當只有一個返回值沒有其他語句時,可以省略大括號:

let fn = a => a

// 等同於
let fn = function (a) {
  return a
}
複製程式碼

返回物件並且沒有其他語句的時候,大括號需要括號包裹起來,因為 js 引擎認為大括號是程式碼塊

let fn = a => ({ name: a })

// 等同於
let fn = function (a) {
  return { name: a }
}
複製程式碼

箭頭函式的特點:

  1. 沒有 this,函式體內的 this 是定義時外部的 this
  2. 不能被 new,因為沒有 this
  3. 不可以使用 arguments,可以使用 rest 代替
  4. 不可以使用 yield 命令,因此箭頭函式不能用作 Generator 函式。

? 參考

相關文章