【機制】js中的this指向
阿新 • • 發佈:2021-03-02
## 1.this的使用場景
我們先把`this`的使用場景分為兩大類:函式外和函式內:
**函式外的this**
就是在全域性程式碼裡,直接使用this:
``` javascript
"use strict";
let name = "window";
console.log(this);
console.log(this.name);
// Window
// 'window'
```
從結果看,在函式外的this指向很簡單,就直接指向的是全域性變數`Window`物件,(瀏覽器環境,以下程式碼都是在瀏覽器環境)
而且嚴格模式或非嚴格模式都是。
**函式內部的this**
而在函式內部使用的時候,就可以分為以下幾類:
1. 函式獨立呼叫
2. 函式作為物件的方法被呼叫
3. 函式作為建構函式呼叫
4. 函式通過call,apply,bind呼叫
**this指向確定的時間**
在分析不同情況`this`的指向之前,我們確認一個很重要的問題,就是this的指向是什麼時間確定的。
在說這個問題之前,需要簡單說一下`執行上下文`,如果有看過: [js的閉包、執行上下文、作用域鏈](https://www.cnblogs.com/zhuyutang/p/14134623.html) 這篇文章,我們就會知道`執行上下文`包含三個部分:
- 變數物件
- 作用域鏈
- this指向
我們發現`this`其實`執行上下文`的一部分,當代碼需要用到this的時候,也是從這裡取的。所以`執行上下文`建立的時間,就是this確定的時間。
而`執行上下文`的建立時間是:函式被呼叫,但是還沒執行具體程式碼之前。
所以`this`的指向確定的時間也就明確了:當函式被呼叫的時候,才開始確定this指向。
## 2.分場景分析this指向
在瞭解了`this`被確定的時間後,我們現在來按上面所列出的場景,來具體分析在函式裡面的`this`:
**2.1 函式獨立呼叫時**
函式對立呼叫,其實就是我們最常見的以函式名直接呼叫函式:
``` javascript
// code-01-非嚴格模式
var name = "window";
function fun() {
var name = "function";
console.log(this);
console.log(this.name);
}
fun()
// >> Window
// >> 'window'
```
我們看到,當這樣呼叫函式時,`this`指向的是全域性物件`Window`,所以`this.name`就相當於`Window.name`:'window',而不是函式的內部變數name='function'
這裡有一點需要說明的是,這是在`非嚴格模式`下,那如果是在`嚴格模式`下呢?我們看下面的例子:
``` javascript
// code-01-嚴格模式
"use strict"
var name = "window";
function fun() {
var name = "function";
console.log(this);
console.log(this.name);
}
fun()
// >> undefined
// >> 報錯
```
從結果來看,在`嚴格模式`下,獨立呼叫函式時,函式內部的this指向是 `undefined`
其實應該這麼說:不管是嚴格模式還是非嚴格模式,獨立呼叫函式時,函式內部的this指向都是 `undefined`,只不過在非嚴格模式下,js會自動把undefined的this預設指向全域性物件:Window
**2.2 函式作為物件的方法呼叫**
函式作為一個物件的方法呼叫,我們舉例來看:
``` javascript
//code-02 作為物件成員方法呼叫函式
var name = "window";
var obj = {
name: "obj",
fun: function () {
console.log(this.name);
},
child: {
name: "child",
fun: function () {
console.log(this.name);
},
},
};
// 作為成員方法呼叫
obj.fun();
// 'obj'
// 多級呼叫
obj.child.fun();
// 'child'
// 賦值後呼叫
let fun = obj.fun;
fun();
// 'window'
```
我們下面來分析下上面的程式碼結果:
- `obj.fun()`
首先我們從列印的結果來看,這裡的this等於obj物件。
所以當函式作為某個物件的方法來呼叫的時候,this指向這個方法所屬的物件。
- `obj.child.fun();`
從列印的結果來看,這裡this等於obj.child物件。
所以不管是多少級的呼叫,this指向最近的所屬物件。
- `var fun = obj.fun; fun();`
從列印的結果來看,這裡this等於全域性物件window。window.name = 'window'
從程式碼看,這裡先做了一個賦值操作,把函式obj.fun賦值給了變數fun, 上面我們有說到this的確定時間是在函式被呼叫的時候,這時候函式並沒有被呼叫,只是做了賦值操作,所以這一步的時候,this並沒有確定。
當執行到`fun()`的時候,函式被呼叫,this在這個時候要確定指向,這時候就相當於是作為獨立函式呼叫,應該指向的是undefined,但是在非嚴格模式下,undefined的this會預設指向全域性變數window。
所以this.name == window.name == 'window'。如果是嚴格模式,this.name == undefined.name,會報錯。
**2.3 函式作為建構函式呼叫**
函式作為建構函式的情況,可以分為兩種:
1. 建構函式無返回
2. 建構函式有返回值
a. 返回一個物件
b. 返回其他非物件的值
下面我們分別來看:
***建構函式無返回***
這是建構函式最常用的情況,直接來看程式碼:
``` javascript
//code-03 函式作為建構函式(無返回)
let _this;
function User(name, age) {
this.name = name;
this.age = age;
_this = this;
console.log(this);
// {name:"xiaoming",age:27}
}
let xiaoming = new User("xiaoming", 27);
console.log(_this === xiaoming);
// true
```
從結果來看,我們知道當函式作為建構函式的時候,該函式裡面的this等於這個建構函式new的例項物件,就是這裡的物件`xiaoming`。從[【機制】JavaScript的原型、原型鏈、繼承](https://www.cnblogs.com/zhuyutang/p/14145572.html)這篇可以知道操作符new實際上做了什麼事情。
***建構函式有返回***
如果返回的是非物件,則返回值會被忽略,情況等同於無返回。
下面就只討論返回值為一個物件的情況:
``` javascript
//code-03 函式作為建構函式(返回物件)
let _this;
function User(name, age) {
this.name = name;
this.age = age;
_this = this;
console.log(this);
// {name:'xiaoming',age:27}
let obj = {
name: "obj",
};
return obj;
}
let xiaoming = new User("xiaoming", 27);
console.log(xiaoming);
// {name:'obj'}
console.log(_this === xiaoming);
// false
```
從結果來看,當建構函式返回一個物件時,它new出來的例項就等於它返回的物件(xiaoming === obj),而建構函式的內部this並沒有起到任何作用。
**2.4 函式通過call,apply,bind呼叫**
call,apply,bind都是可以指定this的值。
``` javascript
// code-04 指定this
function fun(name, age) {
console.log(name, age, this);
}
let obj = {
name: "obj",
};
fun.call(obj, "obj", 27);
fun.apply(obj, ["obj", 27]);
let funBind = fun.bind(obj, "obj", 27);
funBind();
// 結果返回都一樣
// 'obj' 27 {name:obj}
```
call,apply,bind:
相同點:都可以指定函式內部的this值,引數的第一個即為this的值。
不同點:
- call:fun引數(name,age),由call函式的第2,3..引數依次賦值。
- apply:fun引數(name,age),由apply函式的第2個引數賦值,第二個引數是一個數組,所存的值依次賦值給fun引數。
- bind:fun引數(name,age)賦值方式同call,但bind返回的是一個函式,而不是直接執行fun。
## 3.幾種特殊情況
在說明了上面常用情景後,我們來分析幾種特殊的情況:
**陣列成員**
當函式作為陣列的成員時:
``` javascript
// code-05 函式作為陣列成員
function arrFun() {
console.log(this.length);
console.log(this === arr);
}
let arr = [1, 2, arrFun];
arr[2]();
// 3
// true
```
從結果看,我們知道當函式作為陣列的成員的時候,此函式內部的this指向的是當前陣列。
可以這樣理解:arr[2] == arr["2"], 類似於物件的成員方法。
**事件繫結**
函式作為繫結事件時:
``` javascript
// code-06 事件繫結
document.getElementById("btn").addEventListener("click", function () {
console.log(this);
});
//
```
從結果看,我們知道當函式作為事件被繫結時,此函式內部的this指向的是綁定了該事件的dom元素。
**非同步函式:promise,setTimeout**
非同步執行函式的時候分為promise和setTimeout情況(關於非同步機制可以參看 [【機制】 JavaScript的事件迴圈機制總結 eventLoop](https://www.cnblogs.com/zhuyutang/p/14116167.html)):
``` javascript
// code-07 非同步函式
"use strict";
setTimeout(function () {
console.log("setTimeout:", this);
});
new Promise(function (resolve) {
console.log("start");
resolve();
}).then(function () {
console.log("promise:", this);
});
// start
// promise: undefined
// setTimeout: Window
```
從結果來看,我們知道其實 setTimeout執行的函式下的this,相當於是在全域性環境下的this:執行全域性變數 Window物件,嚴格模式和非嚴格模式都一樣。
promise下執行的函式其實相當於函式獨立執行的情況:嚴格模式this等於undefined,非嚴格模式下會預設把undefined的this指向Window。
**箭頭函式**
其實箭頭函式本身沒有this,它裡面的this指向的是外部作用域中的this:
``` javascript
// code-08 箭頭函式
"use strict";
let Obj = {
name: "obj",
fun_1: () => {
console.log(this);
},
fun_2() {
let fun = () => {
console.log(this);
};
fun();
},
};
Obj.fun_1();
// Window
Obj.fun_2();
// Obj
function foo() {
setTimeout(() => {
console.log(this);
});
}
foo.call({ id: 42 });
// {id:42}
```
`Obj.fun_1()`
fun_1是箭頭函式,本身沒有this。它的外層作用域就是全域性作用域,所以箭頭函式的this指向的是全域性作用域下的this:Window
`Obj.fun_2()`
fun_2函式內部的fun是箭頭函式,本身沒有this。它的外層作用域就是fun_2,而fun_2的this是呼叫它的物件Obj,所以箭頭函式的this指向的也是Obj。
`foo.call({ id: 42 })`
foo函式用call呼叫,於是foo的this為{id:42}。本來setTimeout內部的函式this指向的是Widow,但是因為它是箭頭本身沒有this,箭頭函式的this指向的是外部作用域的this,在這裡就是foo的this:{id:42}。
--