相信每一個前端的朋友都會遇到過this.xxx is undefined或者this.xxx is not a function的錯誤,明明我們定義了這個xxx,但是還是要報錯?令人百思不得其解,其實就是因為this指針的引用對象中,沒有找到這個定義xxx導致的,因此今天來總結一下this指針的幾種常見的指向問題。
由於this的定義中提到了上下文,因此我們在這裏先簡單的梳理一下Js中的上下文。
一、執行上下文
上下文分為:
- 全局上下文:全局執行上下文是在代碼執行時就創建了,函數執行上下文是在函數調用的時候創建的。
-
函數上下文:同一個函數,在不同時刻調用,會創建不同的執行上下文。
變量和函數的上下文決定了它們可以訪問哪些數據以及他們的行為。
每個函數調用都有自己的上下文。當代碼執行流進入函數時,函數的上下文被推到一個上下文棧上。在函數執行完之後,上下文棧會彈出函數的上下文。---《JavaScript 高級程序設計》
無論是否在嚴格模式下,在全局執行環境中(在任何函數體外部)this 都指向全局對象。然而,在嚴格模式下,如果進入執行環境時沒有設置 this 的值,this 會保持為 undefined。如下代碼:
function f() {
"use strict"; // 這裏是嚴格模式
return this;
}
console.log(f() === undefined); // true
在瀏覽器中全局上下文也是window對象,node.js中的全局對象是global。
console.log(this === window); // true
a = 1;
console.log(window.a); // 1
this.b = "小白";
console.log(window.b) // "小白"
console.log(b) // "小白"
二、this指針的引用問題
this是函數中一個特殊的對象,它在標準函數和箭頭函數中的行為並不相同。
1.this在標準函數中的指向:
在標準函數的調用中,基本上可以分為以下三類:
①作為對象調用時,指針指向所屬對象。
②作為函數調用時,指針指向window。
③作為構造函數調用時,指針指向實例對象。
var obj = {
a: 1,
b: function () {
console.log(this);
},
c: function (a) {
this.a = a;
this.func = function () {
console.log(this.a)//2
console.log(this)//c {a: 2, func: ƒ}
}
}
}
//①作為對象調用時,指針指向所屬對象。
obj.b();//{a: 1, b: ƒ,c: ƒ}
//②作為函數調用時,指針指向window。
var fun = obj.b;
fun();//Window
//③作為構造函數調用時,指針指向實例對象。
var obj1 = new obj.c(2);
obj1.func();
解釋①:在①例子中,將b作為方法調用的對象是obj,因此this指針引用的是obj對象。
解釋②:在②例子中,相當於將obj.b的函數賦值給fun,因此fun是被作為函數調用,因此this指針引用的是window對象。
解釋③:在例子③中,使用new構造了一個obj.c的實例obj1,調用func()方法的對象是obj1,因此打印的是2和對象obj1。
擴展:
function Person(name,age){
this.name = name;
this.age = age;
}
var person1 = new Person("張三",18);
console.log(person1);//Person{name: '張三', age: 18}
console.log(person1.name,person1.age);//張三 18
var person2 = Person("李四",12);
console.log(person2);//undefined
console.log(window.name,window.age);//李四 12
在這裏補充一下new關鍵字的作用:
a.在構造函數代碼開始執行前,創建一個空的對象
b.修改this的指向:把this指向剛剛創建出來的空對象
c.執行函數的代碼
d.在函數完成之後,返回給this引用的對象,即創建出來的對象
1.此時Person相當於構造函數,使用new關鍵字後,創建一個空對象person1,並將this指向這個對象person1,所以打印時person1是一個對象,裏面有name和age屬性
2.為什麼person2是undefined呢?此時的Person(“李四”,12) 相當於函數執行,由於函數沒有返回值,所以person2為undefined。
3.為什麼window中有name和age屬性可以打印?因為此時Person(“李四”,12)是在window對象中調用的,因此相當於window.Person(“李四”,12),所以Person函數執行時,將name和age屬性添加到window對象中去了。
2.this在箭頭函數中的指向:
在箭頭函數中,this引用的是定義箭頭函數的上下文。
var obj = {
d: function () {
let a = () => {
console.log(this)
}
a();
}
}
//箭頭函數,指針始終指向定義箭頭函數的上下文。
obj.d();//{d: ƒ}
var funTest = obj.d;
funTest()//Window{window: Window, self: Window, document: document, name: '', location: Location,…}
解釋:第一次obj.b()是由對象調用b中定義的方法,因此this指針在匿名函數中指向對象obj,定義箭頭函數a的時候指針指向obj,因此打印的是{d: ƒ},而第二次是作為函數調用funTest,因此,匿名函數中this指向window,因此打印的是window對象。
var color = 'red';
let sayColor = () => {
console.log(this.color);
}
let obj = {
color: 'yellow',
sayColor: sayColor,
objSayColor: function () {
console.log(this.color)
},
objSayColor2: function () {
return () => {
console.log(this.color);
}
}
}
obj.sayColor();//red
obj.objSayColor();//yellow
obj.objSayColor2()();//yellow
3.this在閉包中的指向:
閉包指的是那些引用了另一個函數作用域中變量的函數,通常是在嵌套函數中實現的。---《JavaScript高級程序設計》
具體閉包從上下文、作用域鏈角度是如何實現的,我後續會寫一篇文章記錄詳解,在此就不多贅述了。
現在我們知道了this的引用,標準函數①當作為對象調用時,this指向調用的對象;②作為普通函數調用時,this指向window;③作為構造函數調用時,this指向生成的實例對象。箭頭函數中,this始終指向定義箭頭函數的上下文。
var identity = 'The Window';
let object = {
identity: 'The Object',
getIdentity() {
return function () {
console.log(this.identity);
}
}
}
object.getIdentity()();//The Window
大家看一下,這個結果和大家想的是否一樣呢?
在這個例子中,雖然getIdentity()函數是在對象中調用,所以,getIdentity()方法中的this指針指向object,但是返回的匿名函數,後面加上()相當於執行返回的匿名函數,如②作為普通函數調用時,this指針指向window;
可是,這樣就真的沒有辦法訪問object中的identity對象了嗎?其實不然,要知道每個函數在調用時都會自動創建兩個特殊變量:this和arguments。內部函數雖然永遠不可能直接訪問外部函數的這兩個變量,但是,如果把this保存到閉包可以訪問的其他變量中,則是行得通的,代碼如下:
var identity = 'The Window';
let object = {
identity: 'The Object',
getIdentity() {
let that = this;
return function () {
console.log(that.identity);
}
}
}
object.getIdentity()();//The Object
4.this在事件調用函數中的指向:
-
當函數作為dom事件的處理函數時:this 指向觸發事件的元素。
<button onclick="console.log(this)"> Show this </button> //<button onclick="console.log(this)">Show this</button> -
當函數作為一個內聯事件處理函數:它的this指向監聽器所在的DOM元素:
<button onclick="alert(this.tagName.toLowerCase());"> Show this </button>//button但需注意下面這種情況,當函數調用時,它是屬於被獨立調用的函數,所以函數裏面的this指向的是window。
<button onclick="alert((function(){return this})());"> Show inner this </button>//[object Window]三、改變this指針指向的方法(apply、bind、call)
-
call方法:
var a = 'The Window';
var obj={
a:'The Obj',
};
function sayA(){
console.log(this.a);
}
sayA()//The Window
sayA.call(obj)//The Obj
function func (a,b,c) {}
func.call(obj, 1,2,3)
// func 接收到的參數實際上是 1,2,3
func.call(obj, [1,2,3])
// func 接收到的參數實際上是 [1,2,3],undefined,undefined
需要注意以下幾點:
調用 call 的對象,必須是個函數 Function。
1)call 的第一個參數,是一個對象。 Function 的調用者,將會指向這個對象。 如果不傳,則默認為全局對象 window。
2)第二個參數開始,可以接收任意個參數。每個參數會映射到相應位置的 Function 的參數上。但是如果將所有的參數作為數組傳入,它們會作為一個整體映射到 Function 對應的第一個參數上,之後參數都為空。
-
apply方法:和call的作用是一樣的,需要注意的是:
1)它的調用者必須是函數 Function,並且只接收兩個參數,第一個參數的規則與 call 一致。
2)第二個參數,必須是數組或者類數組,它們會被轉換成類數組,傳入 Function 中,並且會被映射到 Function 對應的參數上。這也是 call 和 apply 之間,很重要的一個區別。
知識點總結:
apply、call的共同點:都能夠改變函數執行時的上下文(this指針指向),將一個對象的方法交給另一個對象來執行,並且是立即執行的。
apply、call的區別:傳入參數的方式,call接收單個參數,例如:func.call(obj, 1,2,3);而apply接受參數數組或者類數組,例如:func.apply(obj, [1,2,3])。
apply的巧用:
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
Array.prototype.push.apply(arr1, arr2);//實現兩個數組合並。
console.log(arr1); // [1, 2, 3, 4, 5, 6]
let max = Math.max.apply(null, arr1);//結合Math.max返回數組最大值
console.log(max)//6
let min = Math.min.apply(null, arr1);//結合Math.max返回數組最小值
console.log(min)//1
-
bind方法:
bind() 方法創建一個新的函數,在 bind() 被調用時,這個新函數的 this 被指定為 bind() 的第一個參數,而其餘參數將作為新函數的參數,供調用時使用。 ---MDN
其實,bind 方法 與 apply 和 call 方法功能類似,也能改變函數體內的 this 指向。不同的是,bind 方法的返回值是函數,並且需要稍後調用,才會執行,而 apply 和 call 則是立即調用。
var a = 'The Window';
var obj={
a:'The Obj',
};
function sayA(){
console.log(this);
}
let CopyFunction=sayA.bind(obj)//不打印,此處相當於返回方法。
CopyFunction();//{a: 'The Obj'}
sayA.bind(obj)()//{a: 'The Obj'}
sayA.bind()()//Window{window: Window, self: Window, document: document, name: '', location: Location,…}
參考資料:
- 「乾貨」細説 call、apply 以及 bind 的區別和用法
- MDN
- 徹底理解js中this的指向問題