深入學習js函數(shù)的隱式參數(shù) arguments 和 this
前言
在函數(shù)調用時,arguments和this會被靜默的傳遞給函數(shù),并可以在函數(shù)體內引用它們,借以訪問函數(shù)相關的一些信息。
其中arguments是一個類數(shù)組結構,它保存了調用時傳遞給函數(shù)的所有實參;this是函數(shù)執(zhí)行時的上下文對象, 這個對象有些讓人感到困惑的行為。 下面分別對他們進行討論。
1. arguments
1.1 背景
JavaScript 允許函數(shù)在調用時傳入的實參個數(shù)和函數(shù)定義時的形參個數(shù)不一致, 比如函數(shù)在定義時聲明了 n 個參數(shù), 在調用函數(shù)時不一定非要傳入 n 個參數(shù),例如:
// 1. 定義有一個形參的函數(shù)fn() function fn(arg){} // 2. 在調用時傳入 0 個或 多個參數(shù),并不會報錯 fn(); // 傳入 0 個參數(shù) fn(1,'a',3); // 傳入多個參數(shù)
1.2 arguments 與 形參的對應關系
arguments是個類數(shù)組結構,它存儲了函數(shù)在調用時傳入的所有實參, 通過訪問它的length屬性可以得到其中保存的實參的個數(shù),并可以通過arguments[n]按順序取出傳入的每個參數(shù)(n=1,2,..,arguments.length-1)。
參數(shù)在arguments中保存的順序和傳入的順序相同, 同時也和形參聲明的順序相同,例如:
function fn(arg1, arg2, arg3){ console.log(arg1 === arguments[0]); // true console.log(arg2 === arguments[1]); // true console.log(arg3 === arguments[2]); // true } fn(1,2,3); // 調用
當傳入的實參多于形參個數(shù)時,想要獲得多余出的實參,就可以用arguments[n]來獲取了, 例如:
// 定義只有一個形參的函數(shù) function fn(arg1){ console.log('length of arguments is:',arguments.length); console.log('arguments[0] is:', arguments[0]); // 獲取傳入的第一個實參, 也就是形參 arg1 的值 console.log('arguments[1] is:', arguments[1]); // 獲取第二個實參的值, 沒有形參與其對應 console.log('arguments[2] is:', arguments[2]); // 獲取第二個實參的值, 沒有形參與其對應 } fn(1,2,3); // 傳入 3 個實參 // 可以得到實際上傳入的實參的個數(shù)并取出所有實參 // length of arguments is: 3 // arguments[0] is: 1 // arguments[1] is: 2 // arguments[2] is: 3
1.3 arguments 與 形參的值相互對應
在非嚴格模式下, 修改arguments中的元素值會修改對應的形參值;同樣的,修改形參的值也會修改對應的arguments中保存的值。下面的實驗可以說明:
function fn(arg1, arg2){ // 1. 修改arguments元素,對應的形參也會被修改 arguments[0] = '修改了arguments'; console.log(arg1); // 2. 修改形參值,對應的arguments也會被修改 arg2 = '修改了形參值'; console.log(arguments[1]); } fn(1,2); // '修改了arguments' // '修改了形參值'
但是,在嚴格模式下不存在這種情況, 嚴格模式下的arguments和形參的值之間失去了對應的關系:
'use strict'; // 啟用嚴格模式 function fn(arg1, arg2){ // 修改arguments元素,對應的形參也會被修改 arguments[0] = '修改了arguments'; console.log(arg1); // 修改形參值,對應的arguments也會被修改 arg2 = '修改了形參值'; console.log(arguments[1]); } fn(1,2); // 1 // 2
注意: arguments 的行為和屬性雖然很像數(shù)組, 但它并不是數(shù)組,只是一種類數(shù)組結構:
function fn(){ console.log(typeof arguments); // object console.log(arguments instanceof Array); // false } fn();
1.4 為什么要了解 arguments
在ES6中, 可以用靈活性更強的解構的方式(...符號)獲得函數(shù)調用時傳入的實參,而且通過這種方式獲得的實參是保存在真正的數(shù)組中的,例如:
function fn(...args){ // 通過解構的方式得到實參 console.log(args instanceof Array); // args 是真正的數(shù)組 console.log(args); // 而且 args 中也保存了傳入的實參 } fn(1,2,3); // true // Array(3) [1, 2, 3]
那么在有了上面這種更加靈活的方式以后,為什么還要了解arguments呢? 原因是在維護老代碼的時候可能不得不用到它。
2. 函數(shù)上下文: this
在函數(shù)調用時, 函數(shù)體內也可以訪問到 this 參數(shù), 它代表了和函數(shù)調用相關聯(lián)的對象,被稱為函數(shù)上下文。
this的指向受到函數(shù)調用方式的影響, 而函數(shù)的調用方式可以分成以下4種:
- 直接調用, 例如: fn()
- 作為對象的方法被調用, 例如: obj.fn()
- 被當做一個構造函數(shù)來使用, 例如: new Fn()
- 通過函數(shù) call() 或者 apply() 調用, 例如: obj.apply(fn) / obj.call(fn)
下面分別討論以上 4 種調用方式下 this 的指向.
2.1 直接調用一個函數(shù)時 this 的指向
有些資料說在直接調用一個函數(shù)時, 這個函數(shù)的 this 指向 window, 這種說法是片面的, 只有在非嚴格模式下而且是瀏覽器環(huán)境下才成立, 更準確的說法是:在非嚴格模式下, this值會指向全局上下文(例如在瀏覽器中是window, Node.js環(huán)境下是global)。而在嚴格模式下, this 的值是 undefined。實驗代碼如下:
// 非嚴格模式 function fn(){ console.log(this); } fn(); // global || Window
嚴格模式下:
'use strict'; function fn(){ console.log(this); } fn(); // undefined
總結: 在直接調用一個函數(shù)時, 它的 this 指向分成兩種情況: 在非嚴格模式下指向全局上下文, 在嚴格模式下指向 undefined.
2.2 被一個對象當做方法調用
當函數(shù)被一個對象當成方法調用時, 這個函數(shù)的 this 會指向調用它的對象。代碼驗證如下:
// 定義一個對象 let xm = { getThis (){ // 定義一個函數(shù) return this; // 這個函數(shù)返回自己的 this 指向 } } let thisOfFunc = xm.getThis(); // 通過對象調用函數(shù)得到函數(shù)的 this 指向 console.log(thisOfFunc === xm); // true, 函數(shù)的this指向調用它的對象本身
因為這個原因, 對象的屬性可以通過this來訪問, 如果給 xm 加上一個 name 屬性, 則通過 xm.name可以得到這個屬性值, 也可以在函數(shù)中通過 this.name 得到屬性值, 即 this.name 就是 vm.name, 進一步, this===xm。 實驗如下:
let xm = { name: '小明', // 給 xm 加一個屬性, 可以通過 xm.name 訪問到 getName (){ return this.name; // 返回 this 的指向的 name 屬性 } } console.log(xm.name, xm.getName()); // 小明 小明
2.3 被作為構造函數(shù)來調用時
2.3.1 不要像使用普通函數(shù)一樣使用構造函數(shù)
構造函數(shù)本質上是函數(shù), 只是在被 new 操作符調用時一個函數(shù)才被稱為構造函數(shù)。然而話雖如此, 但是由于寫出一個構造函數(shù)的目的是用他來創(chuàng)建一個對象, 所以還要有一些約定俗成的東西來限制這個概念, 避免把構造函數(shù)當成普通函數(shù)來使用。例如, 構造函數(shù)雖然能被直接調用, 但是不要這樣做,因為這是一個普通函數(shù)就可以做到的事情,例如:
function Person(name){ this.name = name; return 1; // 不要這樣對待構造函數(shù) } let n = Person(); // 不要這樣使用構造函數(shù)
2.3.2 使用構造函數(shù)創(chuàng)建對象時發(fā)生了什么
當使用 new 關鍵字來調用構造函數(shù)的最終結果是產(chǎn)生了一個新對象, 而產(chǎn)生新對象的過程如下:
- 創(chuàng)建一個空對象 {}
- 將該對象的prototype鏈接到構造函數(shù)的prototype上
- 將這個新對象作為 this 的指向
- 如果這個構造函數(shù)沒有返回一個引用類型的值, 則將上面構造的新對象返回
上面的內容如果需要完全理解, 還需要了解原型相關的內容。這里只需要關注第3、4步就可以了,即:將this綁定到生成到的新對象上,并將這個新對象返回, 進一步下結論為:使用構造函數(shù)時, this 指向生成的對象, 實驗結果如下:
function Person(){ this.getThis = function(){ // 這個函數(shù)返回 this return this; } } let p1 = new Person(); // 調用了構造函數(shù)并返回了一個新的對象 console.log(p1.getThis() === p1); // true let p2 = new Person(); console.log(p2.getThis() === p2); // true
2.3.3 結論
從上面的內容可以得到如下的結論: 當函數(shù)作為構造函數(shù)使用時, this 指向返回的新對象
2.4 通過 call() 或者 apply() 調用時
使用函數(shù) call 和 apply 可以在調用一個函數(shù)時指定這個函數(shù)的 this 的指向, 語法是:
fn.call(targetThis, arg1, arg2,..., argN) fn.apply(targetThis, [arg1, arg2,.., argN]) fn: 要調用的函數(shù) targetThis: 要把 fn 的 this 設置到的目標 argument: 要給 fn 傳的實參
例如定義一個對象如下:
let xm = { name: '小明', sayName(){ console.log(this.name); } }; xm.sayName(); // 對象調用函數(shù)輸出 '小明'
上面定義了一個對象, 對象的 name 屬性為'小明'; sayName 屬性是個函數(shù), 功能是輸出對象的 name 屬性的值。根據(jù)2.2部分可知 sayName 這個函數(shù)的 this 指向 xm 對象, this.name 就是 xm.name。下面定義一個新對象, 并把 xm.sayName 這個函數(shù)的 this 指向新定義的對象。
新定義一個對象 xh:
let xh = { name: '小紅' };
對象 xh 只有 name 屬性, 沒有 sayName 屬性, 如果想讓 xh 也使用 sayName 函數(shù)來輸出自己的名字, 那么就要在調用 sayName 時讓它的 this 指向小紅, 以達到 this.name 等于 xh.name 的目的。 這個目的就可以通過 call 和 apply 兩個函數(shù)來實現(xiàn)。 以call 函數(shù)為例來實現(xiàn)這個需求, 只需要這樣寫就可以了:
xm.sayName.call(xh); // 小紅 xm.sayName.apply(xh); // 小紅
其中fn為xm.sayName; targetThis為xh, 這是因為targetThis的指向就是xh, 此結論可以由 2.2部分 的內容得到。
2.4.1 call 和 apply 的區(qū)別
call 和 apply 的區(qū)別僅僅是要傳給fn的參數(shù)的形式不同:對于apply,傳給fn的參數(shù)argument是個數(shù)組,數(shù)組由所有參數(shù)組成;對于call,傳給fn的參數(shù)argument直接是所有參數(shù)的排列, 直接一個個寫入就可以。
例如要傳給函數(shù)fn三個參數(shù): 1、2、3. 則對于 call和apply調用的方法分別是:
fn.call(targetThis, 1, 2, 3); // 把 1,2,3直接傳入 fn.apply(targetThis, [1,2,3]); // 把1,2,3合成數(shù)組后作為參數(shù)
2.5 箭頭函數(shù) 和 bind 函數(shù)
箭頭函數(shù)和bind函數(shù)對于this的處理與普通函數(shù)不同, 要單獨拿出來說。
2.5.1 箭頭函數(shù)
與傳統(tǒng)函數(shù)不同, 箭頭函數(shù)本身不包含this, 它的 this 繼承自它定義時的作用域鏈的上一層。而且箭頭函數(shù)不能作為構造函數(shù),它也沒有文章 第1部分 所說的arguments屬性。
下面用一個例子引出箭頭函數(shù)中this的來源:
function Person(){ this.age = 24; setTimeout(function(){ console.log(this.age); // undefined console.log(this === window); // true }, 1000); } var p = new Person(); // 創(chuàng)建一個實例的時候就立即執(zhí)行了定時器
可以看到, 在定時器內定義的普通匿名函數(shù)無法訪問到 Person 的 age 屬性, 這是因為setTimeout是個全局函數(shù), 它的內部的this指向的是window, 而 window 上沒有 age 這個屬性, 所以就得到了 undefined。 從下面this === window 為 true 也說明了匿名函數(shù)中this指向的是window。
將普通的函數(shù)換成箭頭函數(shù)之后可以看到如下結果:
function Person(){ this.age = 24; setTimeout(() => { console.log(this.age); // 24 console.log(this === p); // true }, 1000); } var p = new Person();
由上面的代碼可以看出箭頭函數(shù)內的 this 指向實例 p, 即它的 this 指向的是定義時候的作用域鏈的上一層。
說明: 這個例子僅用來引出箭頭函數(shù)的this指向的來源, 不要像這樣使用構造函數(shù)。
2.5.2 bind函數(shù)
bind函數(shù)的作用是根據(jù)一個舊函數(shù)而創(chuàng)建一個新函數(shù),語法為newFn = oldFn.bind(thisTarget)。它會將舊函數(shù)復制一份作為新函數(shù), 然后將新函數(shù)的this永遠綁定到thisTarget指向的上下文中, 然后返回這個新函數(shù), 以后每次調用這個新函數(shù)時, 無論用什么方法都無法改變這個新函數(shù)的 this 指向。例如:
// 創(chuàng)建一個對象有 name 和 sayName 屬性 let p1 = { name: 'P1', sayName(){ console.log(this.name); // 訪問函數(shù)指向的 this 的 name 屬性 } } p1.sayName(); // P1 // 創(chuàng)建一個對象 p2, 并把這個對象作為bind函數(shù)綁定的this let p2 = { name: 'P2' } // 將p1的 sayName 函數(shù)的 this 綁定到 p2 上, 生成新函數(shù) sayP2Name 并返回 let sayP2Name = p1.sayName.bind(p2); // 由于此時 sayP2Name 的內部 this 已經(jīng)綁定了 p2, // 所以即使是按 文章2.1部分 所說的直接調用 sayP2Name, 它的 this 也是指向 p2 的, 并不是指向全局上下文或者 undefined sayP2Name(); // P2 // 定義新對象, 嘗試將 sayP2Name 的 this 指向到 p3 上 let p3 = { name: 'P3' } // 嘗試使用 call和apply 函數(shù)來將 sayP2Name 函數(shù)的 this 指向p3, // 但是由于 sayP2Name 函數(shù)的this 已經(jīng)被bind函數(shù)永遠綁定到p2上了, 所以this.name仍然是p2.name sayP2Name.call(p3); // P2 sayP2Name.apply(p3); // P2
通過以上內容可知一旦通過 bind 函數(shù)綁定了 this, 就再也無法改變 this 的指向了.
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
express+mockjs實現(xiàn)模擬后臺數(shù)據(jù)發(fā)送功能
這篇文章主要介紹了express+mockjs實現(xiàn)模擬后臺數(shù)據(jù)發(fā)送功能,需要的朋友可以參考下2018-01-01JavaScript不使用prototype和new實現(xiàn)繼承機制
這篇文章主要介紹了JavaScript不使用prototype和new實現(xiàn)繼承機制的相關資料,需要的朋友可以參考下2014-12-12