在 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 }
}
複製程式碼
箭頭函式的特點:
- 沒有 this,函式體內的 this 是定義時外部的 this
- 不能被 new,因為沒有 this
- 不可以使用 arguments,可以使用 rest 代替
- 不可以使用 yield 命令,因此箭頭函式不能用作 Generator 函式。
? 參考
- 《JavaScript 深入之引數按值傳遞》 by 冴羽
- 《ECMAScript 6 入門》函式的擴充套件 - 箭頭函式 by 阮一峰
- 《JavaScript 高階程式設計》3 基本概念、4.1.3 傳遞引數、4.2 執行環境及作用域