js中this指向的問題與聯繫
- 2021 年 2 月 23 日
- 筆記
- javascript
前言
JavaScript 中最大的一個安全問題,也是最令人困惑的一個問題,就是在某些情況下this
的值是如何確定的。有js基礎的同學面對這個問題基本可以想到:this
的指向和函數調用的方式相關。這當然是正確的,然而,這幾種方式有什麼聯繫嗎?這是我接下來要說明的問題。
this
從哪裡來
this
是js的一個關鍵字,和arguments
類似,它是函數運行時,在函數體內部自動生成的一個對象,只能在函數體內部使用。這句話似乎與認知不同,我們在函數體外部即全局作用域下也能使用this
。
// 直接在全局作用域下輸出this
console.log(this);
// 輸出window
但是不要忘記,即便是全局作用域,依舊是運行在window
下的,我們寫的代碼都在window
的某個函數中。而這也催生了一種理解this
指向的方法:this
永遠指向調用者(非箭頭函數中)。
作為普通函數調用
函數作為普通函數直接調用(也稱為自執行函數)的時候,無論函數在全局還是在另一個函數中,this
都是指向window
。
function fn() {
this.author = 'Wango';
}
fn();
console.log(author);
// Wango
這很好理解,但又不是很好理解,因為在代碼中省略了window
,補全後就好理解了:this
指向的是調用者。
function fn() {
this.author = 'Wango';
}
window.fn();
console.log(window.author);
// Wango
而在內部函數中,自執行函數中的this
依舊指向全局作用域,我們無法通過window.foo()
調用函數,但並不妨礙我們先這樣理解(具體參見本文最後一部分this
的強制轉型)。
function fn() {
function foo() {
console.log(this);
}
foo();
// Window
window.foo();
// TypeError
}
fn();
作為構造函數調用
在構造函數中,this
指向new
生成的新對象,即構造函數是通過new
調用的,構造函數內部的this
當然就應該指向new
出來的對象。
function Person(name, age) {
this.name = name;
this.age = age;
console.log(this);
// Person { name: 'Wango', age: 24 }
}
new Person('Wango', 24);
構造函數中的this
與構造函數的返回值類型無關,下列代碼中p
指向了構造函數返回的對象,而不是new
出來的對象。當然,這是構造函數的特性,與本主題關係不大。
function Person(name, age) {
console.log(this);
// Person {}
this.name = name;
this.age = age;
console.log(this);
// Person { name: 'Wango', age: 24 }
return {
name: 'Lily',
age: 25
}
}
Person.prototype.sayName = function() {
return this.name + ' ' + this.age
}
const p = new Person('Wango', 24);
console.log(p.sayName());
// TypeError: p.sayName is not a function
通過對象方法調用
通過對象方法調用時,this
指嚮應該是最明晰的了。與其他面向對象語言的this
行為相同,指向該方法的調用者。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayName = fn;
function fn() {
return this.name + ' ' + this.age
}
const p = new Person('Wango', 24);
console.log(p);
// Person { name: 'Wango', age: 24 }
console.log(p.sayName());
// Wango 24
通過[]
調用對象方法
通常,我們對於對象方法是通過.
語法調用,但通過[]
也可以調用對象方法,在這種情況下的this
指向常常會被我們混淆、忽略。
function fn() {
console.log(this);
}
const arr = [fn, 1];
arr[0]();
// [Function: fn, 1]
function fn2() {
arguments[0]();
}
fn2(fn, 1);
// [Arguments] { '0': [Function: fn], '1': 1 }
在上例中,無論是數組還是偽數組,其本質上都是對象,在通過[]
獲取函數元素並調用的時候,會改變函數中的this
指向,this
指向這個數組或偽數組,與對象調用函數的行為一致。
通過call、apply調用函數
function fn() {
console.log(this.name);
}
const author = {
name: 'Wango'
}
fn.call(author);
// Wango
這似乎與this
永遠指向調用者相違背,但一旦我們明白了call函數的實現機制就會明白,這不僅不是違背,反而是佐證。對call
、apply
、bind
實現機制不熟悉的同學可以參考我另一篇文章,下面截取call
簡要說明。
// 保存一個全局變量作為默認值
const root = this;
Function.prototype.myCall = function(context, ...args) {
if (typeof context === 'object') {
// 如果參數是null,使用全局變量
context = context || root;
} else {
// 參數不是對象的創建一個空對象
context = Object.create(null);
}
// 使用Symbol創建唯一值作為函數名
let fn = Symbol();
context[fn] = this;
context[fn](...args);
delete context[fn];
}
let person = {
name: 'Wango',
fn: function() {
console.log(this.name);
}
}
function sayHi(age, sex) {
console.log(this.name, age, sex);
}
sayHi.myCall(person, 24, 'male');
// Wango 24 male
sayHi.myCall(null, 24, 'male');
// undefined 24 male
sayHi.myCall(123, 24, 'male');
// undefined 24 male
// 原函數不受影響
person.fn();
// Wango
call
函數最核心的實現在於context[fn] = this;
和context[fn](...args);
這兩行。實際上就是將沒有函數調用者的普通函數掛載到指定的對象上,這時this
指向與對象調用方法的一致。而delete context[fn];
是在調用後立即解除對象與函數之間的關聯。
嚴格模式下的不同表現
this
強制轉型
使用函數的apply()
或call()
方法時,在非嚴格模式下null
或undefined
值會被強制轉型為全局對象。在嚴格模式下,則始終以指定值作為函數this
的值,無論指定的是什麼值。這也是為何在嚴格模式下,自執行函數的this
不再指向window
,而是指向undefined
的根本原因。
// 定義一個全局變量
color = "red";
function displayColor() {
console.log(this.color);
}
// 在非嚴格模式下使用call修改this指向,並指定null,或undefined,
displayColor.call(null);
displayColor.call();
// red
// 修改指向無效,傳入null或undefined被轉換為了window
實際上,我們也可以將自執行函數,如fn()
,看作是fn.call()
的語法糖,在普通模式下,第一個參數默認為undefined
,但被強制轉換為window
。這也就解釋了為何所有自執行函數中this
都指向window
但無法通過window
調用的問題(函數在call
函數中掛載到window
對象上,執行後被立即刪除,所以無法再次通過window
訪問)。
apply()
或call()
方法在嚴格模式下傳入簡單數據類型作為第一個參數時,該簡單數據類型會被轉換為相應的包裝類,而非嚴格模式不會如此轉換。
function foo() {
console.log(this);
}
foo.call(); // Window {}
foo.call(2); // Number {2}
function foo() {
console.log(this);
}
foo.call(); // undefined
foo.call(2); // 2
箭頭函數的this
指向
在箭頭函數中, this
引用的是定義箭頭函數的上下文。即箭頭函數中的this
不會隨着函數調用方式的改變而改變。
function Person(name) {
this.name = name;
this.getName = () => console.log(this.name);
}
const p = new Person('Wango');
p.getName();
// Wango
const getName = p.getName;
getName();
// Wango
getName.call({name: 'Lily'});
// Wango
參考資料:
Javascript高級程序設計(第四版)