1. 程式人生 > >【機制】js中的this指向

【機制】js中的this指向

## 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}。 --