前言
當一個函式呼叫時,會創建一個執行背景關系,這個背景關系包括函式呼叫的一些資訊(呼叫堆疊,傳入引數,呼叫方式),this就指向這個執行背景關系,
this不是靜態的,也并不是在撰寫的時候系結的,而是在運行時系結的,它的系結和函式宣告的位置沒有關系,只取決于函式呼叫的方式,
本篇文章有點長,涉及到很多道面試題,有難有簡單,如果能耐心的通讀一編,我相信以后this都不成問題,
學習this之前,建議先學習以下知識:
- JavaScript之預編譯
- JavaScript之手撕new
- JavaScript之手撕call/apply
- JavaScript之靜態作用域與動態作用域
- JavaScript之手撕陣列高階函式
在文章的最開始,陳列一下本篇文章涉及的內容,保證讓大家不虛此行,
- 默認系結
- 隱式系結
- 隱式系結丟失
- 顯式系結
- 顯式系結應用
- new系結
- 箭頭函式系結
- 綜合題
- 總結
this指向哪里
在JavaScript中,要想完全理解this,首先要理解this的系結規則,this的系結規則一共有5種:
- 默認系結
- 隱式系結
- 顯式(硬)系結
new系結ES6新增箭頭函式系結
下面來一一介紹以下this的系結規則,
1.默認系結
默認系結通常是指函式獨立呼叫,不涉及其他系結規則,非嚴格模式下,this指向window,嚴格模式下,this指向undefined,
題目1.1:非嚴格模式
var foo = 123;
function print(){
this.foo = 234;
console.log(this); // window
console.log(foo); // 234
}
print();
非嚴格模式,print()為默認系結,this指向window,所以列印window和234,
這個foo值可以說道兩句:
如果學習過預編譯的知識,在預編譯程序中,foo和print函式會存放在全域GO中(即window物件上),所以上述代碼就類似下面這樣:
window.foo = 123
function print() {
this.foo = 234;
console.log(this);
console.log(window.foo);
}
window.print()
題目1.2:嚴格模式
把題目1.1稍作修改,看看嚴格模式下的執行結果,
"use strict"可以開啟嚴格模式
"use strict";
var foo = 123;
function print(){
console.log('print this is ', this);
console.log(window.foo)
console.log(this.foo);
}
console.log('global this is ', this);
print();
注意事項:開啟嚴格模式后,函式內部this指向undefined,但全域物件window不會受影響
答案
global this is Window{...}
print this is undefined
123
Uncaught TypeError: Cannot read property 'foo' of undefined
題目1.3:let/const
let a = 1;
const b = 2;
var c = 3;
function print() {
console.log(this.a);
console.log(this.b);
console.log(this.c);
}
print();
console.log(this.a);
let/const定義的變數存在暫時性死區,而且不會掛載到window物件上,因此print中是無法獲取到a和b的,
答案
undefined
undefined
3
undefined
題目1.4:物件內執行
a = 1;
function foo() {
console.log(this.a);
}
const obj = {
a: 10,
bar() {
foo(); // 1
}
}
obj.bar();
foo雖然在obj的bar函式中,但foo函式仍然是獨立運行的,foo中的this依舊指向window物件,
題目1.5:函式內執行
var a = 1
function outer () {
var a = 2
function inner () {
console.log(this.a) // 2
}
inner()
}
outer()
這個題與題目1.4類似,但要注意,不要把它看成閉包問題
題目1.6:自執行函式
a = 1;
(function(){
console.log(this);
console.log(this.a)
}())
function bar() {
b = 2;
(function(){
console.log(this);
console.log(this.b)
}())
}
bar();
默認情況下,自執行函式的
this指向window
自執行函式只要執行到就會運行,并且只會運行一次,this指向window,
答案
Window{...}
1
Window{...}
2 // b是imply global,會掛載到window上
2.隱式系結
函式的呼叫是在某個物件上觸發的,即呼叫位置存在背景關系物件,通俗點說就是**XXX.func()**這種呼叫模式,
此時func的this指向XXX,但如果存在鏈式呼叫,例如XXX.YYY.ZZZ.func,記住一個原則:this永遠指向最后呼叫它的那個物件,
題目2.1:隱式系結
var a = 1;
function foo() {
console.log(this.a);
}
// 物件簡寫,等同于 {a:2, foo: foo}
var obj = {a: 2, foo}
foo();
obj.foo();
foo(): 默認系結,列印1obj.foo(): 隱式系結,列印2
答案
1
2
obj是通過var定義的,obj會掛載到window之上的,obj.foo()就相當于window.obj.foo(),這也印證了this永遠指向最后呼叫它的那個物件規則,
題目2.2:物件鏈式呼叫
感覺上面總是空談鏈式呼叫的情況,下面直接來看一個例題:
var obj1 = {
a: 1,
obj2: {
a: 2,
foo(){
console.log(this.a)
}
}
}
obj1.obj2.foo() // 2
3.隱式系結的丟失
隱式系結可是個調皮的東西,一不小心它就會發生系結的丟失,一般會有兩種常見的丟失:
- 使用另一個變數作為函式別名,之后使用別名執行函式
- 將函式作為引數傳遞時會被隱式賦值
隱式系結丟失之后,this的指向會啟用默認系結,
具體來看題目:
題目3.1:取函式別名
a = 1
var obj = {
a: 2,
foo() {
console.log(this.a)
}
}
var foo = obj.foo;
obj.foo();
foo();
JavaScript對于參考型別,其地址指標存放在堆疊記憶體中,真正的本體是存放在堆記憶體中的,
上面將obj.foo賦值給foo,就是將foo也指向了obj.foo所指向的堆記憶體,此后再執行foo,相當于直接執行的堆記憶體的函式,與obj無關,foo為默認系結,籠統的記,只要fn前面什么都沒有,肯定不是隱式系結,
答案
2
1
不要把這里理解成
window.foo執行,如果foo為let/const定義,foo不會掛載到window上,但不會影響最后的列印結果
題目3.2:取函式別名
如果取函式別名沒有發生在全域,而是發生在物件之中,又會是怎樣的結果呢?
var obj = {
a: 1,
foo() {
console.log(this.a)
}
};
var a = 2;
var foo = obj.foo;
var obj2 = { a: 3, foo: obj.foo }
obj.foo();
foo();
obj2.foo();
obj2.foo指向了obj.foo的堆記憶體,此后執行與obj無關(除非使用call/apply改變this指向)
答案
1
2
3
題目3.3:函式作為引數傳遞
function foo() {
console.log(this.a)
}
function doFoo(fn) {
console.log(this)
fn()
}
var obj = { a: 1, foo }
var a = 2
doFoo(obj.foo)
用函式預編譯的知識來解答這個問題:函式預編譯四部曲前兩步分別是:
- 找形參和變數宣告,值賦予
undefined - 將形參與實參相統一,也就是將實參的值賦予形參,
obj.foo作為實參,在預編譯時將其值賦值給形參fn,是將obj.foo指向的地址賦給了fn,此后fn執行不會與obj產生任何關系,fn為默認系結,
答案
Window {…}
2
題目3.4:函式作為引數傳遞
將上面的題略作修改,doFoo不在window上執行,改為在obj2中執行
function foo() {
console.log(this.a)
}
function doFoo(fn) {
console.log(this)
fn()
}
var obj = { a: 1, foo }
var a = 2
var obj2 = { a: 3, doFoo }
obj2.doFoo(obj.foo)
console.log(this):obj2.doFoo符合xxx.fn格式,doFoo的為隱式系結,this為obj2,列印{a: 3, doFoo: ?}fn(): 沒有于obj2產生聯系,默認系結,列印2
答案
{a: 3, doFoo: ?}
2
題目3.5:回呼函式
下面這個題目我們寫代碼時會經常遇到:
var name='zcxiaobao';
function introduce(){
console.log('Hello,My name is ', this.name);
}
const Tom = {
name: 'TOM',
introduce: function(){
setTimeout(function(){
console.log(this)
console.log('Hello, My name is ',this.name);
})
}
}
const Mary = {
name: 'Mary',
introduce
}
const Lisa = {
name: 'Lisa',
introduce
}
Tom.introduce();
setTimeout(Mary.introduce, 100);
setTimeout(function(){
Lisa.introduce();
},200);
setTimeout是異步呼叫的,只有當滿足條件并且同步代碼執行完畢后,才會執行它的回呼函式,
Tom.introduce()執行:console位于setTimeout的回呼函式中,回呼函式的this指向windowMary.introduce直接作為setTimeout的函式引數(類似題目題目3.3),會發生隱式系結丟失,this為默認系結Lisa.introduce執行雖然位于setTimeout的回呼函式中,但保持xxx.fn模式,this為隱式系結,
答案
Window {…}
Hello, My name is zcxiaobao
Hello,My name is zcxiaobao
Hello,My name is Lisa
所以如果我們想在setTimeout或setInterval中使用外界的this,需要提前存盤一下,避免this的丟失,
const Tom = {
name: 'TOM',
introduce: function(){
_self = this
setTimeout(function(){
console.log('Hello, My name is ',_self.name);
})
}
}
Tom.introduce()
題目3.6:隱式系結丟失綜合題
name = 'javascript' ;
let obj = {
name: 'obj',
A (){
this.name += 'this';
console.log(this.name)
},
B(f){
this.name += 'this';
f();
},
C(){
setTimeout(function(){
console.log(this.name);
},1000);
}
}
let a = obj.A;
a();
obj.B(function(){
console.log(this.name);
});
obj.C();
console.log(name);
本題目不做決議,具體可以參照上面的題目,
答案
javascriptthis
javascriptthis
javascriptthis
undefined
4.顯式系結
顯式系結比較好理解,就是通過call()、apply()、bind()等方法,強行改變this指向,
上面的方法雖然都可以改變this指向,但使用起來略有差別:
call()和apply()函式會立即執行bind()函式會回傳新函式,不會立即執行函式call()和apply()的區別在于call接受若干個引數,apply接受陣列,
題目4.1:比較三種呼叫方式
function foo () {
console.log(this.a)
}
var obj = { a: 1 }
var a = 2
foo()
foo.call(obj)
foo.apply(obj)
foo.bind(obj)
foo(): 默認系結,foo.call(obj): 顯示系結,foo的this指向objfoo.apply(obj): 顯式系結foo.bind(obj): 顯式系結,但不會立即執行函式,沒有回傳值
答案
2
1
1
題目4.2:隱式系結丟失
題目3.4發生隱式系結的丟失,如下代碼:我們可不可以通過顯式系結來修正這個問題,
function foo() {
console.log(this.a)
}
function doFoo(fn) {
console.log(this)
fn()
}
var obj = { a: 1, foo }
var a = 2
doFoo(obj.foo)
- 首先先修正
doFoo()函式的this指向,
doFoo.call(obj, obj.foo)
- 然后修正
fn的this,
function foo() {
console.log(this.a)
}
function doFoo(fn) {
console.log(this)
fn.call(this)
}
var obj = { a: 1, foo }
var a = 2
doFoo(obj.foo)
大功告成,
題目4.3:回呼函式與call
接著上一個題目的風格,稍微變點花樣:
var obj1 = {
a: 1
}
var obj2 = {
a: 2,
bar: function () {
console.log(this.a)
},
foo: function () {
setTimeout(function () {
console.log(this)
console.log(this.a)
}.call(obj1), 0)
}
}
var a = 3
obj2.bar()
obj2.foo()
乍一看上去,這個題看起來有些莫名其妙,setTimeout那是傳了個什么東西?
做題之前,先了解一下setTimeout的內部機制:(關于異步的執行順序,可以參考JavaScript之EventLoop)
setTimeout(fn) {
if (回呼條件滿足) (
fn
)
}
這樣一看,本題就清楚多了,類似題目4.2,修正了回呼函式內fn的this指向,
答案
2
{a: 1}
1
題目4.4:注意call位置
function foo () {
console.log(this.a)
}
var obj = { a: 1 }
var a = 2
foo()
foo.call(obj)
foo().call(obj)
foo(): 默認系結foo.call(obj): 顯式系結foo().call(obj): 對foo()執行的回傳值執行call,foo回傳值為undefined,執行call()會報錯
答案
2
1
2
Uncaught TypeError: Cannot read property 'call' of undefined
題目4.5:注意call位置(2)
上面由于foo沒有回傳函式,無法執行call函式報錯,因此修改一下foo函式,讓它回傳一個函式,
function foo () {
console.log(this.a)
return function() {
console.log(this.a)
}
}
var obj = { a: 1 }
var a = 2
foo()
foo.call(obj)
foo().call(obj)
foo(): 默認系結foo.call(obj): 顯式系結foo().call(obj):foo()執行,列印2,回傳匿名函式通過call將this指向obj,列印1,
這里千萬注意:最后一個foo().call(obj)有兩個函式執行,會列印2個值,
答案
2
1
2
1
題目4.6:bind
將上面的call全部換做bind函式,又會怎樣那?
call是會立即執行函式,bind會回傳一個新函式,但不會執行函式
function foo () {
console.log(this.a)
return function() {
console.log(this.a)
}
}
var obj = { a: 1 }
var a = 2
foo()
foo.bind(obj)
foo().bind(obj)
首先我們要先確定,最后會輸出幾個值?bind不會執行函式,因此只有兩個foo()會列印a,
foo(): 默認系結,列印2foo.bind(obj): 回傳新函式,不會執行函式,無輸出foo().bind(obj): 第一層foo(),默認系結,列印2,后bind將foo()回傳的匿名函式this指向obj,不執行
答案
2
2
題目4.7:外層this與內層this
做到這里,不由產生了一些疑問:如果使用call、bind等修改了外層函式的this,那內層函式的this會受影響嗎?
(注意區別箭頭函式)
function foo () {
console.log(this.a)
return function() {
console.log(this.a)
}
}
var obj = { a: 1 }
var a = 2
foo.call(obj)()
foo.call(obj): 第一層函式foo通過call將this指向obj,列印1;第二層函式為匿名函式,默認系結,列印2,
答案
1
2
題目4.8:物件中的call
把上面的代碼移植到物件中,看看會發生怎樣的變化?
var obj = {
a: 'obj',
foo: function () {
console.log('foo:', this.a)
return function () {
console.log('inner:', this.a)
}
}
}
var a = 'window'
var obj2 = { a: 'obj2' }
obj.foo()()
obj.foo.call(obj2)()
obj.foo().call(obj2)
看著這么多括號,是不是感覺有幾分頭大,沒事,咱們來一層一層分析:
obj.foo()(): 第一層obj.foo()執行為隱式系結,列印出foo:obj;第二層匿名函式為默認系結,列印inner:windowobj.foo.call(obj2)(): 類似題目4.7,第一層obj.foo.call(obj2)使用call將obj.foo的this指向obj2,列印foo: obj2;第二層匿名函式默認系結,列印inner:windowobj.foo().call(obj2): 類似題目4.5,第一層隱式系結,列印:foo: obj,第二層匿名函式使用call將this指向obj2,列印inner: obj2
題目4.9:帶引數的call
顯式系結一開始講的時候,就談過call/apply存在傳參差異,那咱們就來傳一下引數,看看傳完引數的this會是怎樣的美妙,
var obj = {
a: 1,
foo: function (b) {
b = b || this.a
return function (c) {
console.log(this.a + b + c)
}
}
}
var a = 2
var obj2 = { a: 3 }
obj.foo(a).call(obj2, 1)
obj.foo.call(obj2)(1)
要注意call執行的位置:
obj.foo(a).call(obj2, 1):obj.foo(a): foo的AO中b值為傳入的a(形參與實參相統一),值為2,回傳匿名函式fn- 匿名函式
fn.call(obj2, 1): fn的this指向為obj2,c值為1 this.a + b + c = obj2.a + FooAO.b + c = 3 + 2 + 1 = 6
obj.foo.call(obj2)(1):obj.foo.call(obj2): obj.foo的this指向obj2,未傳入引數,b = this.a = obj2.a = 3;回傳匿名函式fn- 匿名函式
fn(1): c = 1,默認系結,this指向window this.a + b + c = window.a + obj2.a + c = 2 + 3 + 1 = 6
答案
6
6
麻了嗎,兄弟們,進度已經快過半了,休息一會,爭取把this一次性吃透,

5.顯式系結擴展
上面提了很多call/apply可以改變this指向,但都沒有太多實用性,下面來一起學幾個常用的call與apply使用,
題目5.1:apply求陣列最值
JavaScript中沒有給陣列提供類似max和min函式,只提供了Math.max/min,用于求多個數的最值,所以可以借助apply方法,直接傳遞陣列給Math.max/min
const arr = [1,10,11,33,4,52,17]
Math.max.apply(Math, arr)
Math.min.apply(Math, arr)
題目5.2:類陣列轉為陣列
ES6未發布之前,沒有Array.from方法可以將類陣列轉為陣列,采用Array.prototype.slice.call(arguments)或[].slice.call(arguments)將類陣列轉化為陣列,
題目5.3:陣列高階函式
日常編碼中,我們會經常用到forEach、map等,但這些陣列高階方法,它們還有第二個引數thisArg,每一個回呼函式都是顯式系結在thisArg上的,
例如下面這個例子
const obj = {a: 10}
const arr = [1, 2, 3, 4]
arr.forEach(function (val, key){
console.log(`${key}: ${val} --- ${this.a}`)
}, obj)
答案
0: 1 --- 10
1: 2 --- 10
2: 3 --- 10
3: 4 --- 10
關于陣列高階函式的知識可以參考: JavaScript之手撕高階陣列函式
6.new系結
使用new來構建函式,會執行如下四部操作:
- 創建一個空的簡單
JavaScript物件(即{}); - 為步驟1新創建的物件添加屬性
__proto__,將該屬性鏈接至建構式的原型物件 ; - 將步驟1新創建的物件作為
this的背景關系 ; - 如果該函式沒有回傳物件,則回傳
this,
關于new更詳細的知識,可以參考:JavaScript之手撕new
通過new來呼叫建構式,會生成一個新物件,并且把這個新物件系結為呼叫函式的this,
題目6.1:new系結
function User(name, age) {
this.name = name;
this.age = age;
}
var name = 'Tom';
var age = 18;
var zc = new User('zc', 24);
console.log(zc.name)
答案
zc
題目6.2:屬性加方法
function User (name, age) {
this.name = name;
this.age = age;
this.introduce = function () {
console.log(this.name)
}
this.howOld = function () {
return function () {
console.log(this.age)
}
}
}
var name = 'Tom';
var age = 18;
var zc = new User('zc', 24)
zc.introduce()
zc.howOld()()
這個題很難不讓人想到如下代碼,都是函式嵌套,具體解法是類似的,可以對比來看一下啊,
const User = {
name: 'zc';
age: 18;
introduce = function () {
console.log(this.name)
}
howOld = function () {
return function () {
console.log(this.age)
}
}
}
var name = 'Tom';
var age = 18;
User.introduce()
User.howOld()()
zc.introduce(): zc是new創建的實體,this指向zc,列印zczc.howOld()(): zc.howOld()回傳一個匿名函式,匿名函式為默認系結,因此列印18(阿包永遠18)
答案
zc
18
題目6.3:new界的天王山
new界的天王山,每次看懂后,沒過多久就會忘掉,但這次要從根本上弄清楚該題,
接下來一起來品味品味:
function Foo(){
getName = function(){ console.log(1); };
return this;
}
Foo.getName = function(){ console.log(2); };
Foo.prototype.getName = function(){ console.log(3); };
var getName = function(){ console.log(4); };
function getName(){ console.log(5) };
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
- 預編譯
GO = {
Foo: fn(Foo),
getName: function getName(){ console.log(5) };
}
- 分析后續執行
Foo.getName(): 執行Foo上的getName方法,列印2getName(): 執行GO中的getName方法,列印4Foo().getName()Foo()執行
// 修改全域GO的getName為function(){ console.log(1); } getName = function(){ console.log(1) } // Foo為默認系結,this -> window // return window return thisFoo().getName(): 執行window.getName(),列印1
getName(): 執行GO中的getName,列印1
-
分析后面三個列印結果之前,先補充一些運算子優先級方面的知識(圖源:MDN)

從上圖可以看到,部分優先級如下:new(帶引數串列) = 成員訪問 = 函式呼叫 > new(不帶引數串列)
-
new Foo.getName()
首先從左往右看:new Foo屬于不帶引數串列的new(優先級19),Foo.getName屬于成員訪問(優先級20),getName()屬于函式呼叫(優先級20),同樣優先級遵循從左往右執行,
Foo.getName執行,獲取到Foo上的getName屬性- 此時原運算式變為
new (Foo.getName)(),new (Foo.getName)()為帶引數串列(優先級20),(Foo.getName)()屬于函式呼叫(優先級20),從左往右執行 new (Foo.getName)()執行,列印2,并回傳一個以Foo.getName()為建構式的實體
這里有一個誤區:很多人認為這里的
new是沒做任何操作的的,執行的是函式呼叫,那么如果執行的是Foo.getName(),呼叫回傳值為undefined,new undefined會發生報錯,并且我們可以驗證一下該運算式的回傳結果,
console.log(new Foo.getName())
// 2
// Foo.getName {}
可見在成員訪問之后,執行的是帶引數串列格式的new操作,
new Foo().getName()- 同
步驟4一樣分析,先執行new Foo(),回傳一個以Foo為建構式的實體 Foo的實體物件上沒有getName方法,沿原型鏈查找到Foo.prototype.getName方法,列印2
- 同
new new Foo().getName()
從左往右分析: 第一個new不帶引數串列(優先級19),new Foo()帶引數串列(優先級20),剩下的成員訪問和函式呼叫優先級都是20
new Foo()執行,回傳一個以Foo為建構式的實體- 在執行成員訪問,
Foo實體物件在Foo.prototype查找到getName屬性 - 執行
new (new Foo().getName)(),回傳一個以Foo.prototype.getName()為建構式的實體,列印2
new Foo.getName()與new new Foo().getName()區別:
new Foo.getName()的建構式是Foo.getNamenew new Foo().getName()的建構式為Foo.prototype.getName
測驗結果如下:
foo1 = new Foo.getName()
foo2 = new new Foo().getName()
console.log(foo1.constructor)
console.log(foo2.constructor)
輸出結果:
2
3
? (){ console.log(2); }
? (){ console.log(3); }
通過這一步比較應該能更好的理解上面的執行順序,
答案
2
4
1
1
2
3
3
兄弟們,革命快要成功了,再努力一把,以后this都小問題啦,

7.箭頭函式
箭頭函式沒有自己的this,它的this指向外層作用域的this,且指向函式定義時的this而非執行時,
this指向外層作用域的this: 箭頭函式沒有this系結,但它可以通過作用域鏈查到外層作用域的this指向函式定義時的this而非執行時:JavaScript是靜態作用域,就是函式定義之后,作用域就定死了,跟它執行時的地方無關,更詳細的介紹見JavaScript之靜態作用域與動態作用域,
題目7.1:物件方法使用箭頭函式
name = 'tom'
const obj = {
name: 'zc',
intro: () => {
console.log('My name is ' + this.name)
}
}
obj.intro()
上文說到,箭頭函式的this通過作用域鏈查到,intro函式的上層作用域為window,
答案
My name is tom
題目7.2:箭頭函式與普通函式比較
name = 'tom'
const obj = {
name: 'zc',
intro:function () {
return () => {
console.log('My name is ' + this.name)
}
},
intro2:function () {
return function() {
console.log('My name is ' + this.name)
}
}
}
obj.intro2()()
obj.intro()()
obj.intro2()(): 不做贅述,列印My name is tomobj.intro()():obj.intro()回傳箭頭函式,箭頭函式的this取決于它的外層作用域,因此箭頭函式的this指向obj,列印My name is zc
題目7.3:箭頭函式與普通函式的嵌套
name = 'window'
const obj1 = {
name: 'obj1',
intro:function () {
console.log(this.name)
return () => {
console.log(this.name)
}
}
}
const obj2 = {
name: 'obj2',
intro: ()=> {
console.log(this.name)
return function() {
console.log(this.name)
}
}
}
const obj3 = {
name: 'obj3',
intro: ()=> {
console.log(this.name)
return () => {
console.log(this.name)
}
}
}
obj1.intro()()
obj2.intro()()
obj3.intro()()
obj1.intro()(): 類似題目7.2,列印obj1,obj1obj2.intro()():obj2.intro()為箭頭函式,this為外層作用域this,指向window,回傳匿名函式為默認系結,列印window,windowobj3.intro()():obj3.intro()與obj2.intro()相同,回傳值為箭頭函式,外層作用域intro的this指向window,列印window,window
答案
obj1
obj1
window
window
window
window
題目7.4:new碰上箭頭函式
function User(name, age) {
this.name = name;
this.age = age;
this.intro = function(){
console.log('My name is ' + this.name)
},
this.howOld = () => {
console.log('My age is ' + this.age)
}
}
var name = 'Tom', age = 18;
var zc = new User('zc', 24);
zc.intro();
zc.howOld();
zc是new User實體,因此建構式User的this指向zczc.intro(): 列印My name is zczc.howOld():howOld為箭頭函式,箭頭函式this由外層作用域決定,且指向函式定義時的this,外層作用域為User,this指向zc,列印My age is 24
題目7.5:call碰上箭頭函式
箭頭函式由于沒有this,不能通過call\apply\bind來修改this指向,但可以通過修改外層作用域的this來達成間接修改
var name = 'window'
var obj1 = {
name: 'obj1',
intro: function () {
console.log(this.name)
return () => {
console.log(this.name)
}
},
intro2: () => {
console.log(this.name)
return function () {
console.log(this.name)
}
}
}
var obj2 = {
name: 'obj2'
}
obj1.intro.call(obj2)()
obj1.intro().call(obj2)
obj1.intro2.call(obj2)()
obj1.intro2().call(obj2)
obj1.intro.call(obj2)(): 第一層函式為普通函式,通過call修改this為obj2,列印obj2,第二層函式為箭頭函式,它的this與外層this相同,同樣列印obj2,obj1.intro().call(obj2): 第一層函式列印obj1,第二次函式為箭頭函式,call無效,它的this與外層this相同,列印obj1obj1.intro2.call(obj2)(): 第一層為箭頭函式,call無效,外層作用域為window,列印window;第二次為普通匿名函式,默認系結,列印windowobj1.intro2().call(obj2): 與上同,列印window;第二層為匿名函式,call修改this為obj2,列印obj2
答案
obj2
obj2
obj1
obj1
window
window
window
obj2
8.箭頭函式擴展
總結
- 箭頭函式沒有
this,它的this是通過作用域鏈查到外層作用域的this,且指向函式定義時的this而非執行時, - 不可以用作建構式,不能使用
new命令,否則會報錯 - 箭頭函式沒有
arguments物件,如果要用,使用rest引數代替 - 不可以使用
yield命令,因此箭頭函式不能用作Generator函式, - 不能用
call/apply/bind修改this指向,但可以通過修改外層作用域的this來間接修改, - 箭頭函式沒有
prototype屬性,
避免使用場景
- 箭頭函式定義物件方法
const zc = {
name: 'zc',
intro: () => {
// this -> window
console.log(this.name)
}
}
zc.intro() // undefined
- 箭頭函式不能作為建構式
const User = (name, age) => {
this.name = name;
this.age = age;
}
// Uncaught TypeError: User is not a constructor
zc = new User('zc', 24);
- 事件的回呼函式
DOM中事件的回呼函式中this已經封裝指向了呼叫元素,如果使用建構式,其this會指向window物件
document.getElementById('btn')
.addEventListener('click', ()=> {
console.log(this === window); // true
})
9.綜合題
學完上面的知識,是不是感覺自己已經趨于化境了,現在就一起來華山之巔一決高下吧,
題目9.1: 物件綜合體
var name = 'window'
var user1 = {
name: 'user1',
foo1: function () {
console.log(this.name)
},
foo2: () => console.log(this.name),
foo3: function () {
return function () {
console.log(this.name)
}
},
foo4: function () {
return () => {
console.log(this.name)
}
}
}
var user2 = { name: 'user2' }
user1.foo1()
user1.foo1.call(user2)
user1.foo2()
user1.foo2.call(user2)
user1.foo3()()
user1.foo3.call(user2)()
user1.foo3().call(user2)
user1.foo4()()
user1.foo4.call(user2)()
user1.foo4().call(user2)
這個題目并不難,就是把上面很多題做了個整合,如果上面都學會了,此題問題不大,
user1.foo1()、user1.foo1.call(user2): 隱式系結與顯式系結user1.foo2()、user1.foo2.call(user2): 箭頭函式與calluser1.foo3()()、user1.foo3.call(user2)()、user1.foo3().call(user2): 見題目4.8user1.foo4()()、user1.foo4.call(user2)()、user1.foo4().call(user2): 見題目7.5
答案:
var name = 'window'
var user1 = {
name: 'user1',
foo1: function () {
console.log(this.name)
},
foo2: () => console.log(this.name),
foo3: function () {
return function () {
console.log(this.name)
}
},
foo4: function () {
return () => {
console.log(this.name)
}
}
}
var user2 = { name: 'user2' }
user1.foo1() // user1
user1.foo1.call(user2) // user2
user1.foo2() // window
user1.foo2.call(user2) // window
user1.foo3()() // window
user1.foo3.call(user2)() // window
user1.foo3().call(user2) // user2
user1.foo4()() // user1
user1.foo4.call(user2)() // user2
user1.foo4().call(user2) // user1
題目9.2:隱式系結丟失
var x = 10;
var foo = {
x : 20,
bar : function(){
var x = 30;
console.log(this.x)
}
};
foo.bar();
(foo.bar)();
(foo.bar = foo.bar)();
(foo.bar, foo.bar)();
突然出現了一個代碼很少的題目,還乍有些不習慣,
foo.bar(): 隱式系結,列印20(foo.bar)(): 上面提到過運算子優先級的知識,成員訪問與函式呼叫優先級相同,默認從左到右,因此括號可有可無,隱式系結,列印20(foo.bar = foo.bar)():隱式系結丟失,給foo.bar起別名,雖然名字沒變,但是foo.bar上已經跟foo無關了,默認系結,列印10(foo.bar, foo.bar)(): 隱式系結丟失,起函式別名,將逗號運算式的值(第二個foo.bar)賦值給新變數,之后執行新變數所指向的函式,默認系結,列印10,
上面那說法有可能有幾分難理解,隱式系結有個定性條件,就是要滿足
XXX.fn()格式,如果破壞了這種格式,一般隱式系結都會丟失,
題目9.3:arguments(推薦看)
var length = 10;
function fn() {
console.log(this.length);
}
var obj = {
length: 5,
method: function(fn) {
fn();
arguments[0]();
}
};
obj.method(fn, 1);
這個題要注意一下,有坑,
-
fn(): 默認系結,列印10 -
arguments[0](): 這種執行方式看起來就怪怪的,咱們把它展開來看看:arguments是一個類陣列,arguments展開,應該是下面這樣:
arguments: { 0: fn, 1: 1, length: 2 }arguments[0]: 這是訪問物件的屬性0?0不好理解,咱們把它稍微一換,方便一下理解:
arguments: { fn: fn, 1: 1, length: 2 }- 到這里大家應該就懂了,隱式系結,
fn函式this指向arguments,列印2
題目9.4:壓軸題(推薦看)
var number = 5;
var obj = {
number: 3,
fn: (function () {
var number;
this.number *= 2;
number = number * 2;
number = 3;
return function () {
var num = this.number;
this.number *= 2;
console.log(num);
number *= 3;
console.log(number);
}
})()
}
var myFun = obj.fn;
myFun.call(null);
obj.fn();
console.log(window.number);
fn.call(null)或者fn.call(undefined)都相當于fn()
-
obj.fn為立即執行函式: 默認系結,this指向window我們來一句一句的分析:
var number: 立即執行函式的AO中添加number屬性,值為undefinedthis.number *= 2:window.number = 10number = number * 2: 立即執行函式AO中number值為undefined,賦值后為NaNnumber = 3:AO中number值由NaN修改為3- 回傳匿名函式,形成閉包
此時的obj可以類似的看成以下代碼(注意存在閉包):
obj = { number: 3, fn: function () { var num = this.number; this.number *= 2; console.log(num); number *= 3; console.log(number); } } -
myFun.call(null): 相當于myFun(),隱式系結丟失,myFun的this指向window,依舊一句一句的分析:
var num = this.number:this指向window,num = window.num = 10this.number *= 2:window.number = 20console.log(num): 列印10number *= 3: 當前AO中沒有number屬性,沿作用域鏈可在立即執行函式的AO中查到number屬性,修改其值為9console.log(number): 列印立即執行函式AO中的number,列印9
-
obj.fn(): 隱式系結,fn的this指向obj繼續一步一步的分析:
var num = this.number:this->obj,num = obj.num = 3this.number *= 2:obj.number *= 2 = 6console.log(num): 列印num值,列印3number *= 3: 當前AO中不存在number,繼續修改立即執行函式AO中的number,number *= 3 = 27console.log(number): 列印27
-
console.log(window.number): 列印20
這里解釋一下,為什么
myFun.call(null)執行時,找不到number變數,是去找立即執行函式AO中的number,而不是找window.number: JavaScript采用的靜態作用域,當定義函式后,作用域鏈就已經定死,(更詳細的解釋文章最開始的推薦中有)
答案
10
9
3
27
20
總結
- 默認系結: 非嚴格模式下
this指向全域物件,嚴格模式下this會系結到undefined - 隱式系結: 滿足
XXX.fn()格式,fn的this指向XXX,如果存在鏈式呼叫,this永遠指向最后呼叫它的那個物件 - 隱式系結丟失:起函式別名,通過別名運行;函式作為引數會造成隱式系結丟失,
- 顯示系結: 通過
call/apply/bind修改this指向 new系結: 通過new來呼叫建構式,會生成一個新物件,并且把這個新物件系結為呼叫函式的this,- 箭頭函式系結: 箭頭函式沒有
this,它的this是通過作用域鏈查到外層作用域的this,且指向函式定義時的this而非執行時
后語
this到這里基本接近尾聲了,松了一口氣,
這篇文章寫了好久,找資源,修改博文,各種亂七八糟的雜事,導致遲遲寫不出滿意的博文,有可能天生理科男的緣故吧,怎么寫感覺文章都很生硬,但好在還是順利寫完了,
在文章的最后,感謝一下參考的博客和題目的來源
- 霖呆呆大佬
- 小夕大佬:嗨,你真的懂this嗎?
- 渡一教育的題源
最后按照阿包慣例,附贈一道面試題:
var num = 10
var obj = {num: 20}
obj.fn = (function (num) {
this.num = num * 3
num++
return function (n) {
this.num += n
num++
console.log(num)
}
})(obj.num)
var fn = obj.fn
fn(5)
obj.fn(10)
console.log(num, obj.num)
最后祝大家都能學好前端,步步登神,成為大佬,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/316668.html
標籤:其他
上一篇:跨域問題的解決方案(還在苦苦求后端小伙伴寫cors嗎? 看完站起來,翻身做主人!!)
下一篇:常用滑鼠 、鍵盤事件及事件物件
