前言
不知道看了多少篇關於this的文章,很難理解,每次看到的文基本都是說明了this
的幾個不同環境的指向。然後像記住歷史一樣,記在了腦子裡,但這種記憶隨著時間的變化會越來越分辨不清。
主要麼,也是我沒有真正深入的去了解this
。真的去深入了,才發現this
涉及的內容真的好多。
this
原理
在《高程3》中的p182的第9行寫到:在全域性函式中,this
等於window
;當函式作為某個物件的方法呼叫時,this
等於那個物件;匿名函式的執行環境具有全域性性,所以this
等於window
。
這個概念,應該是一個前端必備的知識點吧?那麼問題來了,為什麼會有這樣一個判定呢?
先了解一個概念:
執行上下文:也叫一個執行環境,有 全域性執行環境 和 函式執行環境 和 eval
。
每個執行環境中包含這三部分:變數物件/活動物件,作用域鏈,this
的值
下面這段程式碼大家肯定都不陌生
var obj = {
name: 'a',
say: function(){
console.log(this.name)
}
}
var say = obj.say;
obj.say(); //行1 a
say(); //行2 undefined
複製程式碼
以上那幾行程式碼,其實包含了兩個我曾經一度沒有深入理解的內容:
this
是在函式執行時基於的函式的執行上下文繫結的。這個時候需要了解一下 執行上下文的建立過程
- 物件的值都是存入記憶體的,而
this
跟記憶體裡面的資料結構有關係,瞭解一下阮大大的 javascript 的 this 原理
物件的記憶體結構
我這裡直接進入物件中的函式的宣告與呼叫,以上文中的obj
為例。
在建立執行上下文中,變數的宣告的步驟依次為:建立、初始化、賦值。物件是一種特殊的變數,所以它也存在這3個步驟。
在初始化之後賦值之前,obj
的值為undefined
,在賦值操作時,javascript
引擎會在記憶體裡建立一個下面的物件,然後將這個物件的記憶體地址賦值給 obj
這個變數。
{
name: 'a',
say: function(){
console.log(this.name)
}
}
複製程式碼
若要讀取 obj.name
,引擎會先拿到obj
的記憶體地址,然後到這個記憶體地址中找到name
這個屬性的value
屬性。
當給物件賦值的是一個函式時,如上面的obj.say
的地址,引擎會把函式單獨儲存在記憶體中,然後再將函式的地址賦值給say
屬性的value
屬性。
{
name: {
[[value]]: 'a'
[[writable]]: true
[[enumerable]]: true
[[configurable]]: true
},
say: {
[[value]]: 函式的地址
[[writable]]: true
[[enumerable]]: true
[[configurable]]: true
}
}
複製程式碼
大概的記憶體結構是這樣的
這張圖就可以很清晰的看出來了,因為函式是一個獨立的地址,那麼執行var say = obj.say;
時,其實是把函式的地址賦值給了say
,那麼say
就可以獨立執行了,當執行say
時,引擎可以直接拿到函式的地址而不再通過obj
的記憶體了。
大概的結構是這樣的
環境變數
js允許在函式體內部,引用當前環境的其他變數。
比如下面的程式碼,x代表著當前環境的變數。
var f = function(){
console.log(x);
}
複製程式碼
那麼問題來了,函式可以在不同的環境裡執行,那麼怎麼獲取到它的當前執行環境呢?所以,this就出現了,它的設計目的就是在函式體內部,指代函式當前的執行環境。
var x = 'a';
var f = function(){
console.log(this.x);
}
var obj = {
f:f,
x: 'b'
}
f(); //a 在全域性環境下單獨執行,this指代全域性環境,全域性的x為a
obj.f(); //b 在obj中執行,this指代obj環境,obj的x 為b
複製程式碼
this的繫結
常見的有兩種,一種是直接在全域性執行,函式內部的this直接繫結到全域性,一個是通過物件呼叫,函式內部的this繫結到呼叫他的物件上。
除了常見的兩種,還有兩種:
1.是從一個環境傳到另一個環境中,需要使用
call
和apply
,俗稱改變this的指向。
2.不繫結。箭頭函式獨有,此時的this繼承於作用域鏈的上一層,這裡不免要聊一下作用域鏈上的this了。
call and apply
還是用上面的粒子
var x = 'a';
var f = function(){
console.log(this.x);
}
var obj = {
f:f,
x: 'b'
}
f(); //a 在全域性環境下單獨執行,this指代全域性環境,全域性的x為a
f.call(obj, x); //b
f.apply(obj, [x]); //b
複製程式碼
call
和apply
都是將this繫結到它第一個引數上。若其第一個引數不是物件,則會使用js的型別強制將第一個引數轉為物件。
bind
bind
和call
、apply
不一樣的是,它會生成一個新的函式,且這個新生成的函式的this
將永久地被繫結到了bind
的第一個引數。具體原因還不清楚,若有知悉的望告知。
繼續上面的那個粒子
var g = f.bind(obj);
var obj2 = {
x: 'c'
}
var h = g.bind(obj2);
g(); //b
h(); //b g的this指向不會被bind改變
複製程式碼
箭頭函式不繫結this
箭頭函式不繫結this,此時的this繼承於作用域鏈的上一層
var x = 1;
function a(){ //函式作用域上沒有x,向上找,最後找到window
setTimeout(() => {
console.log(this.x);
})
}
function c(){ //函式作用域上沒有x,向上找,最後找到window
setTimeout(() => {
console.log(this.x);
})
}
var obj = {
x: 2, //物件的原型鏈上有x
c: c
}
var obj1 = {//物件的原型鏈上沒有x
c: c
}
a(); //1 執行環境是全域性
obj.c(); //2 執行環境是物件
obj1.c(); //undefined 執行環境是物件
複製程式碼
箭頭函式上的call和apply,bind
由於箭頭函式沒有自己的this指標,通過 call() 或 apply(),bind()方法呼叫一個函式時,只能傳遞引數,不能繫結this,他們的第一個引數會被忽略。
var x = 1;
var a = (b) =>{ //函式作用域上沒有x,向上找,最後找到window
console.log(this.x + b);
}
var obj = {
b: 2,
x: 3
}
a.call(obj, obj.b); //3
複製程式碼
文末
因為寫到這道題,發現自己並不清楚為什麼結果是window,然後翻了下this的原理,算是做一份總結吧。 回顧一下上面的內容,因為js引擎在生成物件的記憶體時,obj這個變數還未被賦值,所以它的值為undefined,在es5中以明確,當bind第一個引數為undefined時,在瀏覽器上將執行window。
var obj = {
say: function () {
function _say() {
console.log(this)
}
return _say.bind(obj)
}()
}
obj.say() //window
複製程式碼
若有理解不合理的地方,還望指正。