JavaScript函數的隱式參數: arguments 和 this

在函數調用時,arguments和this會被靜默的傳遞給函數,並能夠在函數體內引用它們,藉以訪問函數相關的一些信息。
其中arguments是一個類數組結構,它保存了調用時傳遞給函數的全部實參;this是函數執行時的上下文對象, 這個對象有些讓人感到困惑的行爲。 下面分別對他們進行討論。javascript

1. arguments

1.1 背景

JavaScript 容許函數在調用時傳入的實參個數和函數定義時的形參個數不一致, 好比函數在定義時聲明瞭 n 個參數, 在調用函數時不必定非要傳入 n 個參數,例如:java

// 1. 定義有一個形參的函數fn()
function fn(arg){}

// 2. 在調用時傳入 0 個或 多個參數,並不會報錯
fn(); // 傳入 0 個參數
fn(1,'a',3); // 傳入多個參數
複製代碼

1.2 arguments 與 形參的對應關係

arguments是個類數組結構,它存儲了函數在調用時傳入的全部實參, 經過訪問它的length屬性能夠獲得其中保存的實參的個數,並能夠經過arguments[n]按順序取出傳入的每一個參數(n=1,2,..,arguments.length-1)。數組

參數在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); // 調用
複製代碼

當傳入的實參多於形參個數時,想要得到多餘出的實參,就能夠用arguments[n]來獲取了, 例如:app

// 定義只有一個形參的函數
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 個實參
// 能夠獲得實際上傳入的實參的個數並取出全部實參
// 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和形參的值之間失去了對應的關係:ui

'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 的行爲和屬性雖然很像數組, 但它並非數組,只是一種類數組結構:this

function fn(){
    console.log(typeof arguments);  // object
    console.log(arguments instanceof Array);  // false
}

fn();
複製代碼

1.4 爲何要了解 arguments

在ES6中, 能夠用靈活性更強的解構的方式(...符號)得到函數調用時傳入的實參,並且經過這種方式得到的實參是保存在真正的數組中的,例如:spa

function fn(...args){ // 經過解構的方式獲得實參
    console.log(args instanceof Array);  // args 是真正的數組
    console.log(args);  // 並且 args 中也保存了傳入的實參
}

fn(1,2,3);
// true
// Array(3) [1, 2, 3]
複製代碼

那麼在有了上面這種更加靈活的方式之後,爲何還要了解arguments呢? 緣由是在維護老代碼的時候可能不得不用到它。prototype

2. 函數上下文: this

在函數調用時, 函數體內也能夠訪問到 this 參數, 它表明了和函數調用相關聯的對象,被稱爲函數上下文。

this的指向受到函數調用方式的影響, 而函數的調用方式能夠分紅如下4種:

  1. 直接調用, 例如: fn()
  2. 做爲對象的方法被調用, 例如: obj.fn()
  3. 被當作一個構造函數來使用, 例如: new Fn()
  4. 經過函數 call() 或者 apply() 調用, 例如: obj.apply(fn) / obj.call(fn)

下面分別討論以上 4 種調用方式下 this 的指向.

2.1 直接調用一個函數時 this 的指向

有些資料說在直接調用一個函數時, 這個函數的 this 指向 window, 這種說法是片面的, 只有在非嚴格模式下並且是瀏覽器環境下才成立, 更準確的說法是:在非嚴格模式下, this值會指向全局上下文(例如在瀏覽器中是window, Node.js環境下是global)。而在嚴格模式下, this 的值是 undefined。實驗代碼以下:

// 非嚴格模式
function fn(){
    console.log(this);
}

fn();  // global || Window
複製代碼

嚴格模式下:

'use strict';
function fn(){
    console.log(this);
}

fn(); // undefined
複製代碼

總結: 在直接調用一個函數時, 它的 this 指向分紅兩種狀況: 在非嚴格模式下指向全局上下文, 在嚴格模式下指向 undefined.

2.2 被一個對象當作方法調用

當函數被一個對象當成方法調用時, 這個函數的 this 會指向調用它的對象。代碼驗證以下:

// 定義一個對象
let xm = {
    getThis (){ // 定義一個函數
        return this;  // 這個函數返回本身的 this 指向
    }
}

let thisOfFunc = xm.getThis();  // 經過對象調用函數獲得函數的 this 指向
console.log(thisOfFunc === xm); // true, 函數的this指向調用它的對象自己
複製代碼

由於這個緣由, 對象的屬性能夠經過this來訪問, 若是給 xm 加上一個 name 屬性, 則經過 xm.name能夠獲得這個屬性值, 也能夠在函數中經過 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 被做爲構造函數來調用時

2.3.1 不要像使用普通函數同樣使用構造函數

構造函數本質上是函數, 只是在被 new 操做符調用時一個函數才被稱爲構造函數。然而話雖如此, 可是因爲寫出一個構造函數的目的是用他來建立一個對象, 因此還要有一些約定俗成的東西來限制這個概念, 避免把構造函數當成普通函數來使用。例如, 構造函數雖然能被直接調用, 可是不要這樣作,由於這是一個普通函數就能夠作到的事情,例如:

function Person(name){
    this.name = name;

    return 1; // 不要這樣對待構造函數
}

let n = Person();  // 不要這樣使用構造函數
複製代碼

2.3.2 使用構造函數建立對象時發生了什麼

當使用 new 關鍵字來調用構造函數的最終結果是產生了一個新對象, 而產生新對象的過程以下:

  1. 建立一個空對象 {}
  2. 將該對象的prototype連接到構造函數的prototype
  3. 將這個新對象做爲 this 的指向
  4. 若是這個構造函數沒有返回一個引用類型的值, 則將上面構造的新對象返回

上面的內容若是須要徹底理解, 還須要瞭解原型相關的內容。這裏只須要關注第三、4步就能夠了,即:將this綁定到生成到的新對象上,並將這個新對象返回, 進一步下結論爲:使用構造函數時, this 指向生成的對象, 實驗結果以下:

function Person(){
    this.getThis = function(){ // 這個函數返回 this
        return this;
    }
}

let p1 = new Person(); // 調用了構造函數並返回了一個新的對象
console.log(p1.getThis() === p1); // true

let p2 = new Person();
console.log(p2.getThis() === p2); // true
複製代碼

2.3.3 結論

從上面的內容能夠獲得以下的結論: 當函數做爲構造函數使用時, this 指向返回的新對象

2.4 經過 call() 或者 apply() 調用時

使用函數 callapply 能夠在調用一個函數時指定這個函數的 this 的指向, 語法是:

fn.call(targetThis, arg1, arg2,..., argN)
fn.apply(targetThis, [arg1, arg2,.., argN])

fn: 要調用的函數
targetThis: 要把 fn 的 this 設置到的目標
argument: 要給 fn 傳的實參
複製代碼

例如定義一個對象以下:

let xm = {
    name: '小明',
    sayName(){
        console.log(this.name);
    }
};

xm.sayName();  // 對象調用函數輸出 '小明'
複製代碼

上面定義了一個對象, 對象的 name 屬性爲'小明'; sayName 屬性是個函數, 功能是輸出對象的 name 屬性的值。根據2.2部分可知 sayName 這個函數的 this 指向 xm 對象, this.name 就是 xm.name。下面定義一個新對象, 並把 xm.sayName 這個函數的 this 指向新定義的對象。

新定義一個對象 xh:

let xh = {
    name: '小紅'
};
複製代碼

對象 xh 只有 name 屬性, 沒有 sayName 屬性, 若是想讓 xh 也使用 sayName 函數來輸出本身的名字, 那麼就要在調用 sayName 時讓它的 this 指向小紅, 以達到 this.name 等於 xh.name 的目的。 這個目的就能夠經過 call 和 apply 兩個函數來實現。 以call 函數爲例來實現這個需求, 只須要這樣寫就能夠了:

xm.sayName.call(xh);  // 小紅
xm.sayName.apply(xh);  // 小紅
複製代碼

其中fn爲xm.sayName; targetThis爲xh, 這是由於targetThis的指向就是xh, 此結論能夠由 2.2部分 的內容獲得。

2.4.1 call 和 apply 的區別

call 和 apply 的區別僅僅是要傳給fn的參數的形式不一樣:對於apply,傳給fn的參數argument是個數組,數組由全部參數組成;對於call,傳給fn的參數argument直接是全部參數的排列, 直接一個個寫入就能夠。 例如要傳給函數fn三個參數: 一、二、3. 則對於 call和apply調用的方法分別是:

fn.call(targetThis, 1, 2, 3); // 把 1,2,3直接傳入
fn.apply(targetThis, [1,2,3]); // 把1,2,3合成數組後做爲參數
複製代碼

2.5 箭頭函數 和 bind 函數

箭頭函數和bind函數對於this的處理與普通函數不一樣, 要單獨拿出來講。

2.5.1 箭頭函數

與傳統函數不一樣, 箭頭函數自己不包含this, 它的 this 繼承自它定義時的做用域鏈的上一層。並且箭頭函數不能做爲構造函數,它也沒有文章 第1部分 所說的arguments屬性。

下面用一個例子引出箭頭函數中this的來源:

function Person(){
    this.age = 24;
  
    setTimeout(function(){
      console.log(this.age); // undefined
      console.log(this === window); // true
    }, 1000);
  }
  
  var p = new Person(); // 建立一個實例的時候就當即執行了定時器
複製代碼

能夠看到, 在定時器內定義的普通匿名函數沒法訪問到 Person 的 age 屬性, 這是由於setTimeout是個全局函數, 它的內部的this指向的是window, 而 window 上沒有 age 這個屬性, 因此就獲得了 undefined。 從下面this === windowtrue 也說明了匿名函數中this指向的是window。

將普通的函數換成箭頭函數以後能夠看到以下結果:

function Person(){
this.age = 24;

setTimeout(() => {
    console.log(this.age); // 24
    console.log(this === p); // true
}, 1000);
}
  
var p = new Person();
複製代碼

由上面的代碼能夠看出箭頭函數內的 this 指向實例 p, 即它的 this 指向的是定義時候的做用域鏈的上一層。

說明: 這個例子僅用來引出箭頭函數的this指向的來源, 不要像這樣使用構造函數。

2.5.2 bind函數

bind函數的做用是根據一箇舊函數而建立一個新函數,語法爲newFn = oldFn.bind(thisTarget)。它會將舊函數複製一份做爲新函數, 而後將新函數的this永遠綁定到thisTarget指向的上下文中, 而後返回這個新函數, 之後每次調用這個新函數時, 不管用什麼方法都沒法改變這個新函數的 this 指向。例如:

// 建立一個對象有 name 和 sayName 屬性
let p1 = {
    name: 'P1',
    sayName(){ 
        console.log(this.name); // 訪問函數指向的 this 的 name 屬性
    }
}
p1.sayName(); // P1

// 建立一個對象 p2, 並把這個對象做爲bind函數綁定的this
let p2 = {
    name: 'P2'
}
// 將p1的 sayName 函數的 this 綁定到 p2 上, 生成新函數 sayP2Name 並返回
let sayP2Name = p1.sayName.bind(p2); 

// 因爲此時 sayP2Name 的內部 this 已經綁定了 p2, 
// 因此即便是按 文章2.1部分 所說的直接調用 sayP2Name, 它的 this 也是指向 p2 的, 並非指向全局上下文或者 undefined
sayP2Name();  // P2

// 定義新對象, 嘗試將 sayP2Name 的 this 指向到 p3 上
let p3 = {
    name: 'P3'
}
// 嘗試使用 call和apply 函數來將 sayP2Name 函數的 this 指向p3,
// 可是因爲 sayP2Name 函數的this 已經被bind函數永遠綁定到p2上了, 因此this.name仍然是p2.name
sayP2Name.call(p3); // P2
sayP2Name.apply(p3); // P2
複製代碼

經過以上內容可知一旦經過 bind 函數綁定了 this, 就再也沒法改變 this 的指向了.

若有錯誤, 多謝指出!
參考文章:
<< JavaScript 忍者祕籍 >>
JavaScript new Keyword
MDN

相關文章
相關標籤/搜索