在 JavaScript 中,函數其實是一個對象。前端
JavaScript 用 function 關鍵字來聲明一個函數:git
function fn () {
}
複製代碼
變體:函數表達式:es6
var fn = function () {
}
複製代碼
這種沒有函數名的函數被稱爲匿名函數表達式。github
函數能夠有返回值數組
function fn () {
return true
}
複製代碼
位於 return 以後的任何代碼都不會執行:瀏覽器
function fn () {
return true
console.log(false) // 永遠不會執行
}
fn() // true
複製代碼
沒有 return 或者只寫 return,函數將返回 undefined:bash
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)
}
複製代碼
沒有傳值的命名參數,會被自動設置爲 undefinedapp
// 參數有限
function fn (a, b) {
console.log(b) // undefined
}
fn(1)
複製代碼
函數能夠經過內部屬性 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 這個指針訪問擁有這個 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,我以爲仍是少用爲好。
具體查看總結:
引用《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 }
}
複製代碼
箭頭函數的特色: