日期:2019 年 9 月 5 日
this 指向問題
介紹
this 指向問題一直是 js 中一個令人頭疼的問題,這幾天得空,復習了一下以前的知識,順便整理了有關 js 中 this 指向的知識點。
this 綁定
很多人對 this 指向一直都存在一個誤區(qū):this 寫在誰里面就指向誰。但其實這是不對的,this 既不指向函數自身,也不指函數的詞法作用域,它實際是在函數被調用時才發(fā)生的綁定,也就是說this具體指向什么,取決于你是怎么調用的函數。
this 存在4種綁定規(guī)則:
- 默認綁定
- 隱式綁定
- 顯示綁定
- new綁定
這 4 種綁定規(guī)則的優(yōu)先級是從低到高的
1、默認綁定
function foo(){
console.log(this.a);
}
var a = 2;
foo(); // 打印結果?
打印結果:2
因為foo()是直接調用的(獨立函數調用),沒有應用其他的綁定規(guī)則,這里進行了默認綁定,將全局對象( window )綁定this上,所以this.a 就解析成了全局變量中的a,即 2
注意:在嚴格模式下(strict mode),全局對象將無法使用默認綁定( this 指向 undefined ),即執(zhí)行會報 undefined 的錯誤:
function foo(){
“use strict”;
console.log(this.a);
}
var a = 2;
foo(); // Uncaught TypeError : Cannot read property 'a' of undefined
另外,這幾天正在學 Typescript,剛好也遇到了一些 this 指向的問題,這里也說道說道,看一看下面的幾種情況:
例一:
let a = 233;
let obj1 = {
a: 666,
fn: function(){
console.log(this.a);
}
}
obj1.fn(); // 輸出: 666
例二:
let a = 233;
let obj2 = {
a: 888,
fn: function(){
console.log("外層:", this.a);
return function(){
console.log("內層:", this.a);
}
}
}
let ha = obj2.fn(); // 外層: 888
ha(); // 內層: undefined
例三:
var a = 233;
let obj2 = {
a: 888,
fn: function(){
console.log("外層:", this.a);
return function(){
console.log("內層:", this.a);
}
}
}
let ha = obj2.fn(); // 外層: 888
ha(); // 內層: 233
比較之下我們可以看到,對象的 fn 屬性是一個函數,對象調用這個函數,函數的執(zhí)行上下文就是這個對象,所以外層的 this 指向對象沒有問題;
然后 fn 還返回了一個函數,這個函數里面的 this (內層 this)指向的是 window 而不是對象 obj2;
有人就要問了既然指向 window,為什么例二中內層的輸出不是 233 呢?這個問題問得好,這就涉及到了 ES6 與 ES5 中變量聲明方面的區(qū)別了:
- ES5聲明變量只有兩種方式:var 和 function
- ES6有 let、const、import、class 再加上 ES5 的 var、function 共有六種聲明變量的方式
- 還需要了解頂層對象:瀏覽器環(huán)境中頂層對象是window,Node中是global對象
- ES5中,頂層對象的屬性等價于全局變量 (敲黑板了啊)
- ES6中,有所改變:var、function聲明的全局變量,依然是頂層對象的屬性;let、const、class聲明的全局變量不屬于頂層對象的屬性,也就是說ES6開始,全局變量和頂層對象的屬性開始分離、脫鉤
所以ES6非嚴格模式下,與var聲明的全局變量都會成為window的屬性:
a = 1;
console.log(window.a); // 1
var b = 2;
console.log(window.b); // 2
而使用let聲明的全局變量,不會成為window的屬性:
let c = 3;
console.log(window.c); // undefined
關于這方面問題,更多請移步原文:
關于let聲明的變量在window下無法獲取的問題
2、隱式綁定
除了直接對函數進行調用外,有些情況是,函數的調用是在某個對象上觸發(fā)的,即調用位置上存在上下文對象
function foo(){
console.log(this.a);
}
var a = 2;
var obj = {
a : 3,
foo : foo
}
obj.foo(); // ?
輸出結果: 3
這里foo函數被當做引用屬性,被添加到obj對象上。這里的調用過程是這樣的:
獲取obj.foo屬性 -> 根據引用關系找到foo函數,執(zhí)行調用,所以這里對foo的調用存在上下文對象obj,this進行了隱式綁定,即this綁定到了obj上,所以this.a被解析成了obj.a,即 3
當存在多層調用鏈時:
function foo(){
console.log(this.a);
}
var a = 2;
var obj1 = {
a : 4,
foo : foo
}
var obj2 = {
a : 3,
obj1 : obj1
}
obj2.obj1.foo(); // ?
輸出結果:4
同樣,我們看下函數的調用過程:
先獲取obj2.obj1 -> 通過引用獲取到obj1對象,再訪問 obj1.foo -> 最后執(zhí)行foo函數調用
這里調用鏈不只一層,存在obj1、obj2兩個對象,那么隱式綁定具體會綁哪個對象。這里原則是獲取最后一層調用的上下文對象,即obj1,所以結果顯然是 4
3、顯示綁定
對于隱式綁定,this值在調用過程中會動態(tài)變化,可是我們就想綁定指定的對象,這時就用到了顯示綁定
顯示綁定主要是通過改變對象的prototype關聯(lián)對象,這里不展開講。具體使用上,可以通過這兩個方法 call() 或 apply() 來實現(xiàn)(大多數函數及自己創(chuàng)建的函數默認都提供這兩個方法)
call 與apply 是同樣的作用,區(qū)別只是其他參數的設置上:
/*apply()方法*/
function.apply(thisObj[, argArray])
/*call()方法*/
function.call(thisObj[, arg1[, arg2[, [,...argN]]]]);
它們各自的定義:
apply:調用一個對象的一個方法,用另一個對象替換當前對象。例如:B.apply(A, arguments);即A對象應用B對象的方法。
call:調用一個對象的一個方法,用另一個對象替換當前對象。例如:B.call(A, args1,args2);即A對象調用B對象的方法。
它們的共同之處:
都“可以用來代替另一個對象調用一個方法,將一個函數的對象上下文從初始的上下文改變?yōu)橛蓆hisObj指定的新對象”。
它們的不同之處:
apply:最多只能有兩個參數——新this對象和一個數組argArray。如果給該方法傳遞多個參數,則把參數都寫進這個數組里面,當然,即使只有一個參數,也要寫進數組里。如果argArray不是一個有效的數組或arguments對象,那么將導致一個TypeError。如果沒有提供argArray和thisObj任何一個參數,那么Global對象將被用作thisObj,并且無法被傳遞任何參數。
call:它可以接受多個參數,第一個參數與apply一樣,后面則是一串參數列表。這個方法主要用在js對象各方法相互調用的時候,使當前this實例指針保持一致,或者在特殊情況下需要改變this指針。如果沒有提供thisObj參數,那么 Global 對象被用作thisObj。 實際上,apply和call的功能是一樣的,只是傳入的參數列表形式不同。
有關 apply 與 call 的比較,詳情請移步原文:
apply() 與 call() 的區(qū)別
OK,回歸正題,我們來看看顯示綁定:
function foo(){
console.log(this.a);
}
var a = 2;
var obj1 = {
a : 3,
foo : foo
}
var obj2 = {
a : 4,
obj1 : obj1
}
foo.call(obj1); // ?
foo.call(obj2); // ?
結果:
3
4
這里因為顯示的申明了要綁定的對象,所以this就被綁定到了 obj 上,打印的結果自然就是obj1.a 和 obj2.a
4、new 綁定
var a = 2;
function foo(a){
this.a = a;
}
let bar1 = new foo(3);
console.log(bar1.a); // ?
let bar2 = new foo(4);
console.log(bar2.a); // ?
結果:
3
4
因為每次調用生成的是全新的對象,該對象又會自動綁定到this上,所以答案顯而易見
綁定規(guī)則優(yōu)先級
上面也說過,這里在重復一下。優(yōu)先級是這樣的,以按照下面的順序來進行判斷:
函數是否在new中調用(new綁定)?如果是的話this綁定的是新創(chuàng)建的對象;
函數是否通過call、apply(顯式綁定)或者硬綁定調用?如果是的話,this綁定的是 指定的對象;
函數是否在某個上下文對象中調用(隱式綁定)?如果是的話,this綁定的是那個上下文對象;
如果都不是的話,使用默認綁定。如果在嚴格模式下,就綁定undefined,否則綁定到全局對象。
規(guī)則例外:
在顯示綁定中,對于 null 和 undefined 的綁定將不會生效
function foo(){
console.log(this.a);
}
var a = 2;
foo.call(null); // 2
foo.call(undefined); // 2
這種情況主要是用在不關心this的具體綁定對象(用來忽略this),而傳入null實際上會進行默認綁定,導致函數中可能會使用到全局變量,與預期不符
所以對于要忽略this的情況,可以傳入一個空對象?,該對象通過Object.create(null)創(chuàng)建。這里不用{}的原因是,?是真正意義上的空對象,它不創(chuàng)建Object.prototype委托,{}和普通對象一樣,有原型鏈委托關系
箭頭函數中的 this
箭頭函數的this指向:
- 箭頭函數不會創(chuàng)建自己的this,它只會從自己的作用域鏈的上一層繼承this;
- 箭頭函數里的this指向無法被call,apply,bind 改變;
var obj = {
foo(){
console.log(this);
},
bar : ()=>{
console.log(this);
}
}
obj.foo(); // { foo: foo(), bar: bar() }
obj.bar(); // window