1.this的使用場景
我們先把this
的使用場景分為兩大類:函式外和函式內:
函式外的this
就是在全域性程式碼裡,直接使用this:
"use strict";
let name = "window";
console.log(this);
console.log(this.name);
// Window
// 'window'
從結果看,在函式外的this指向很簡單,就直接指向的是全域性變數Window
物件,(瀏覽器環境,以下程式碼都是在瀏覽器環境)
而且嚴格模式或非嚴格模式都是。
函式內部的this
而在函式內部使用的時候,就可以分為以下幾類:
- 函式獨立呼叫
- 函式作為物件的方法被呼叫
- 函式作為建構函式呼叫
- 函式通過call,apply,bind呼叫
this指向確定的時間
在分析不同情況this
的指向之前,我們確認一個很重要的問題,就是this的指向是什麼時間確定的。
在說這個問題之前,需要簡單說一下執行上下文
,如果有看過: js的閉包、執行上下文、作用域鏈 這篇文章,我們就會知道執行上下文
包含三個部分:
- 變數物件
- 作用域鏈
- this指向
我們發現this
其實執行上下文
的一部分,當程式碼需要用到this的時候,也是從這裡取的。所以執行上下文
建立的時間,就是this確定的時間。
而執行上下文
的建立時間是:函式被呼叫,但是還沒執行具體程式碼之前。
所以this
的指向確定的時間也就明確了:當函式被呼叫的時候,才開始確定this指向。
2.分場景分析this指向
在瞭解了this
被確定的時間後,我們現在來按上面所列出的場景,來具體分析在函式裡面的this
:
2.1 函式獨立呼叫時
函式對立呼叫,其實就是我們最常見的以函式名直接呼叫函式:
// 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'
這裡有一點需要說明的是,這是在非嚴格模式
下,那如果是在嚴格模式
下呢?我們看下面的例子:
// 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 函式作為物件的方法呼叫
函式作為一個物件的方法呼叫,我們舉例來看:
//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 函式作為建構函式呼叫
函式作為建構函式的情況,可以分為兩種:
- 建構函式無返回
- 建構函式有返回值
a. 返回一個物件
b. 返回其他非物件的值
下面我們分別來看:
建構函式無返回
這是建構函式最常用的情況,直接來看程式碼:
//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的原型、原型鏈、繼承這篇可以知道操作符new實際上做了什麼事情。
建構函式有返回
如果返回的是非物件,則返回值會被忽略,情況等同於無返回。
下面就只討論返回值為一個物件的情況:
//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的值。
// 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.幾種特殊情況
在說明了上面常用情景後,我們來分析幾種特殊的情況:
陣列成員
當函式作為陣列的成員時:
// 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"], 類似於物件的成員方法。
事件繫結
函式作為繫結事件時:
// code-06 事件繫結
<button id="btn">點選</button>
document.getElementById("btn").addEventListener("click", function () {
console.log(this);
});
// <button id="btn">點選</button>
從結果看,我們知道當函式作為事件被繫結時,此函式內部的this指向的是繫結了該事件的dom元素。
非同步函式:promise,setTimeout
非同步執行函式的時候分為promise和setTimeout情況(關於非同步機制可以參看 【機制】 JavaScript的事件迴圈機制總結 eventLoop):
// 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:
// 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}。
-- 完 --