this是我們在書寫代碼時最常用的關鍵詞之一,即使如此,它也是JavaScript最容易被最頭疼的關鍵詞,那么this到底是什么呢?
如果你了解執行背景關系,那么你就會知道,其實this是執行背景關系物件的一個屬性:
executionContext = {
scopeChain:[ ... ],
VO:{
...
},
this: ?
}
執行背景關系中有三個重要的屬性,作用域鏈(scopeChain)、變數物件(VO)和this,
this是在進入執行背景關系時確定的,也就是在函式執行時才確定,并且在運行期間不允許修改并且是永久不變的
在全域代碼中的this
在全域代碼中this 是不變的,this始終是全域物件本身,
var a = 10; this.b = 20; window.c = 30; console.log(this.a); console.log(b); console.log(this.c); console.log(this === window) // true // 由于this就是全域物件window,所以上述 a ,b ,c 都相當于在全域物件上添加相應的屬性
如果我們在代碼運行期嘗試修改this的值,就會拋出錯誤:
this = { a : 1 } ; // Uncaught SyntaxError: Invalid left-hand side in assignment
console.log(this === window) // true
函式代碼中的this
在函式代碼中使用this,才是令我們最容易困惑的,這里我們主要是對函式代碼中的this進行分析,
我們在上面說過this的值是,進入當前執行背景關系時確定的,也就是在函式執行時并且是執行前確定的,但是同一個函式,作用域中的this指向可能完全不同,但是不管怎樣,函式在運行時的this的指向是不變的,而且不能被賦值,
function foo() {
console.log(this);
}
foo(); // window
var obj={
a: 1,
bar: foo,
}
obj.bar(); // obj
函式中this的指向豐富的多,它可以是全域物件、當前物件、或者是任意物件,當然這取決于函式的呼叫方式,在JavaScript中函式的呼叫方式有一下幾種方式:作為函式呼叫、作為物件屬性呼叫、作為建構式呼叫、使用apply或call呼叫,下面我們將按照這幾種呼叫方式一一討論this的含義,
作為函式呼叫
什么是作為函式呼叫:就是獨立的函式呼叫,不加任何修飾符,
function foo(){
console.log(this === window); // true
this.a = 1;
console.log(b); // 2
}
var b = 2;
foo();
console.log(a); // 1
上述代碼中this系結到了全域物件window,this.a相當于在全域物件上添加一個屬性 a ,
在嚴格模式下,獨立函式呼叫,this的系結不再是window,而是undefined,
function foo() {
"use strict";
console.log(this===window); // false
console.log(this===undefined); // true
}
foo();
這里要注意,如果函式呼叫在嚴格模式下,而內部代碼執行在非嚴格模式下,this 還是會默認系結為 window,
function foo() {
console.log(this===window); // true
}
(function() {
"use strict";
foo();
})()
對于在函式內部的函式獨立呼叫 this 又指向了誰呢?
function foo() {
function bar() {
this.a=1;
console.log(this===window); // true
}
bar()
}
foo();
console.log(a); // 1
上述代碼中,在函式內部的函式獨立呼叫,此時this還是被系結到了window,
總結:當函式作為獨立函式被呼叫時,內部this被默認系結為(指向)全域物件window,但是在嚴格模式下會有區別,在嚴格模式下this被系結為undefined,
作為物件屬性呼叫
var a=1;
var obj={
a: 2,
foo: function() {
console.log(this===obj); // true
console.log(this.a); // 2
}
}
obj.foo();
上述代碼中 foo屬性的值為一個函式,這里稱 foo 為 物件obj 的方法,foo的呼叫方式為 物件 . 方法 呼叫,此時 this 被系結到當前呼叫方法的物件,在這里為 obj 物件,
再看一個例子:
var a=1;
var obj={
a: 2,
bar: {
a: 3,
foo: function() {
console.log(this===bar); // true
console.log(this.a); // 3
}
}
}
obj.bar.foo();
遵循上面說的規則 物件 . 屬性 ,這里的物件為 obj.bar ,此時 foo 內部this被系結到了 obj.bar , 因此 this.a 即為 obj.bar.a ,
再來看一個例子:
var a=1;
var obj={
a: 2,
foo: function() {
console.log(this===obj); // false
console.log(this===window); // true
console.log(this.a); // 1
}
}
var baz=obj.foo;
baz();
這里 foo 函式雖然作為物件obj 的方法,但是它被賦值給變數 baz ,當baz呼叫時,相當于 foo 函式獨立呼叫,因此內部 this被系結到 window,
使用apply或call呼叫
apply和call為函式原型上的方法,它可以更改函式內部this的指向,
var a=1;
function foo() {
console.log(this.a);
}
var obj1={
a: 2
}
var obj2={
a: 3
}
var obj3={
a: 4
}
var bar=foo.bind(obj1);
bar();// 2 this => obj1
foo(); // 1 this => window
foo.call(obj2); // 3 this => obj2
foo.call(obj3); // 4 this => obj3
當函式foo 作為獨立函式呼叫時,this被系結到了全域物件window,當使用bind、call或者apply方法呼叫時,this 被分別系結到了不同的物件,
作為建構式呼叫
var a=1;
function Person() {
this.a=2; // this => p;
}
var p=new Person();
console.log(p.a); // 2
上述代碼中,建構式 Person 內部的 this 被系結為 Person的一個實體,
總結:
當我們要判斷當前函式內部的this系結,可以依照下面的原則:
- 函式是否在是通過 new 運算子呼叫?如果是,this 系結為新創建的物件
var bar = new foo(); // this => bar;
- 函式是否通過call或者apply呼叫?如果是,this 系結為指定的物件
foo.call(obj1); // this => obj1; foo.apply(obj2); // this => obj2;
- 函式是否通過
物件 . 方法呼叫?如果是,this 系結為當前物件
obj.foo(); // this => obj;
- 函式是否獨立呼叫?如果是,this 系結為全域物件,
foo(); // this => window
DOM事件處理函式中的this
1). 事件系結
<button id="btn">點擊我</button>
// 事件系結
function handleClick(e) {
console.log(this); // <button id="btn">點擊我</button>
}
document.getElementById('btn').addEventListener('click',handleClick,false); // <button id="btn">點擊我</button>
document.getElementById('btn').onclick= handleClick; // <button id="btn">點擊我</button>
根據上述代碼我們可以得出:當通過事件系結來給DOM元素添加事件,事件將被系結為當前DOM物件,
2).行內事件
<button onclick="handleClick()" id="btn1">點擊我</button>
<button onclick="console.log(this)" id="btn2">點擊我</button>
function handleClick(e) {
console.log(this); // window
}
//第二個 button 列印的是 <button id="btn">點擊我</button>
我認為行內事件可以這樣理解:
//偽代碼
<button onclick=function(){ handleClick() } id="btn1">點擊我</button>
<button onclick=function() { console.log(this) } id="btn2">點擊我</button>
這樣我們就能理解上述代碼中為什么行內事件一個指向window,一個指向當前DOM元素,(當然瀏覽器處理行內事件時并不是這樣的)
定時器中的this
定時器中的 this 指向哪里呢?
function foo() {
setTimeout(function() {
console.log(this); // window
},1000)
}
foo();
再來看一個例子
var name="chen";
var obj={
name: "erdong",
foo: function() {
console.log(this.name); // erdong
setTimeout(function() {
console.log(this.name); // chen
},1000)
}
}
obj.foo();
到這里我們可以看到,函式 foo 內部this指向為呼叫它的物件,即:obj ,定時器中的this指向為 window,那么有什么辦法讓定時器中的this跟包裹它的函式系結為同一個物件呢?
1). 利用閉包:
var name="chen";
var obj={
name: "erdong",
foo: function() {
console.log(this.name)
var that=this;
setTimeout(function() {
console.log(that.name); // window
},1000)
}
}
obj.foo();
利用閉包的特性,函式內部的函式可以訪問含義訪問當前詞法作用域中的變數,此時定時器中的 that 即為包裹它的函式中的 this 系結的物件,在下面我們會介紹利用 ES6的箭頭函式實作這一功能,
當然這里也可以適用bind來實作:
var name="chen";
var obj={
name: "erdong",
foo: function() {
console.log(this.name);
setTimeout(function() {
console.log(this.name); // window
}.bind(this),1000)
}
}
obj.foo();
被忽略的this
如果你把 null 或者 undefined 作為 this 的系結物件傳入 call 、apply或者bind,這些值在呼叫時會被忽略,實體 this 被系結為對應上述規則,
var a=1;
function foo() {
console.log(this.a); // 1 this => window
}
var obj={
a: 2
}
foo.call(null);
var a=1;
function foo() {
console.log(this.a); // 1 this => window
}
var obj={
a: 2
}
foo.apply(null);
var a=1;
function foo() {
console.log(this.a); // 1 this => window
}
var obj={
a: 2
}
var bar = foo.bind(null);
bar();
bind 也可以實作函式柯里化:
function foo(a,b) {
console.log(a,b); // 2 3
}
var bar=foo.bind(null,2);
bar(3);
更復雜的例子:
var foo={
bar: function() {
console.log(this);
}
};
foo.bar(); // foo
(foo.bar)(); // foo
(foo.bar=foo.bar)(); // window
(false||foo.bar)(); // window
(foo.bar,foo.bar)(); // window
上述代碼中:
foo.bar()為物件的方法呼叫,因此 this 系結為 foo 物件,
(foo.bar)() 前一個() 中的內容不計算,因此還是 foo.bar()
(foo.bar=foo.bar)() 前一個 () 中的內容計算后為 function() { console.log(this); } 所以這里為匿名函式自執行,因此 this 系結為 全域物件 window
后面兩個實體同上,
這樣理解會比較好:
(foo.bar=foo.bar) 括號中的運算式執行為 先計算,再賦值,再回傳值, (false||foo.bar)() 括號中的運算式執行為 判斷前者是否為 true ,若為true,不計算后者,若為false,計算后者并回傳后者的值, (foo.bar,foo.bar) 括號中的運算式之行為分別計算 “,” 運算子兩邊,然后回傳 “,” 運算子后面的值,
箭頭函式中的this
箭頭函式時ES6新增的語法,
有兩個作用:
- 更簡潔的函式
- 本身不系結this
代碼格式為:
// 普通函式
function foo(a){
// ......
}
//箭頭函式
var foo = a => {
// ......
}
//如果沒有引數或者引數為多個
var foo = (a,b,c,d) => {
// ......
}
我們在使用普通函式之前對于函式的this系結,需要根據這個函式如何被呼叫來確定其內部this的系結物件,而且常常因為呼叫鏈的數量或者是找不到其真正的呼叫者對 this 的指向模糊不清,在箭頭函式出現后其內部的 this 指向不需要再依靠呼叫的方式來確定,
箭頭函式有幾個特點(與普通函式的區別)
- 箭頭函式不系結 this ,它只會從作用域鏈的上一層繼承 this,
- 箭頭函式不系結arguments,使用reset引數來獲取實參的數量,
- 箭頭函式是匿名函式,不能作為建構式,
- 箭頭函式沒有prototype屬性,
- 不能使用 yield 關鍵字,因此箭頭函式不能作為函式生成器,
這里我們只討論箭頭函式中的this系結,
用一個例子來對比普通函式與箭頭函式中的this系結:
var obj={
foo: function() {
console.log(this); // obj
},
bar: () => {
console.log(this); // window
}
}
obj.foo();
obj.bar();
上述代碼中,同樣是通過物件 . 方法呼叫一個函式,但是函式內部this系結確是不同,只因一個數普通函式一個是箭頭函式,
用一句話來總結箭頭函式中的this系結:
個人上面說的它會從作用域鏈的上一層繼承 this ,說法并不是很正確,作用域中存放的是這個函式當前執行背景關系與所有父級執行背景關系的變數物件的集合,因此在作用域鏈中并不存在 this ,應該說是作用域鏈上一層對應的執行背景關系中繼承 this ,
箭頭函式中的this繼承于作用域鏈上一層對應的執行背景關系中的this
var obj={
foo: function() {
console.log(this); // obj
},
bar: () => {
console.log(this); // window
}
}
obj.bar();
上述代碼中obj.bar執行時的作用域鏈為:
scopeChain = [
obj.bar.AO,
global.VO
]
根據上面的規則,此時bar函式中的this指向為全域執行背景關系中的this,即:window,
再來看一個例子:
var obj={
foo: function() {
console.log(this); // obj
var bar=() => {
console.log(this); // obj
}
bar();
}
}
obj.foo();
在普通函式中,bar 執行時內部this被系結為全域物件,因為它是作為獨立函式呼叫,但是在箭頭函式中呢,它卻系結為 obj ,跟父級函式中的 this 系結為同一物件,
此時它的作用域鏈為:
scopeChain = [
bar.AO,
obj.foo.AO,
global.VO
]
這個時候我們就差不多知道了箭頭函式中的this系結,
繼續看例子:
var obj={
foo: () => {
console.log(this); // window
var bar=() => {
console.log(this); // window
}
bar();
}
}
obj.foo();
這個時候怎么又指向了window了呢?
我們還看當 bar 執行時的作用域鏈:
scopeChain = [
bar.AO,
obj.foo.AO,
global.VO
]
當我們找bar函式中的this系結時,就會去找foo函式中的this系結,因為它是繼承于它的,這時 foo 函式也是箭頭函式,此時foo中的this系結為window而不是呼叫它的obj物件,因此 bar函式中的this系結也為全域物件window,
我們在回頭看上面關于定時器中的this的例子:
var name="chen";
var obj={
name: "erdong",
foo: function() {
console.log(this.name); // erdong
setTimeout(function() {
console.log(this); // chen
},1000)
}
}
obj.foo();
這時我們就可以很簡單的讓定時器中的this與foo中的this系結為同一物件:
var name="chen";
var obj={
name: "erdong",
foo: function() {
console.log(this.name); // erdong
setTimeout(() => {
console.log(this.name); // erdong
},1000)
}
}
obj.foo();
如需轉載請注明出處,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/75994.html
標籤:JavaScript
上一篇:蒲公英 · JELLY技術周刊 Vol.09 StackOverflow - 2020 開發者年度報告
下一篇:2020年的前端該怎么學?
