this的中文意思是這,在javascript中指的是當前執行代碼的環境對象.在非嚴格模式下,總是指向一個對象,在嚴格模式下可以是任意值.相信很多同學在看到這個this的時候,肯定是有點腦殼疼的.所以今天我就寫了一篇有關this的小文章,來梳理梳理有關this的幾種用法,希望對大家都能有所幫助
事件調用環境
誰觸發事件,函數裏面的this
就指向誰
<button id="btn1">click me</button>
let btn1 = document.querySelector('#btn1')
function fn(){
console.log(this)
}
btn1.onclick = fn
上面的代碼打印出button按鈕這個對象
全局環境
首先我們看下在全局環境下this指向誰?
console.log(this)
我們把上面的代碼放到瀏覽器環境下執行,結果是Window
對象
再打開終端Terminal,鍵入node
指令,進入node執行環境,結果是global
新建一個index.js
文件,使用node index.js
運行腳本,這裏的this
指的是node的默認導出對象,我們執行如下代碼
console.log(this) // {}
console.log(module.exports) // {}
console.log(module.exports === this) // true
函數環境
單純函數調用
function fn(){
console.log(this)
}
fn() // Window
但是在嚴格模式下,this指向的就是undefined
了
'use strict'
function fn(){
console.log(this)
}
fn() // undefined
可以看出單純的調用函數時,this指向的就是全局對象
對象方法調用
let obj = {
a:1,
fn:function(){
console.log(this)
}
}
obj.fn() // {a: 1, fn: ƒ}
直接通過對象.方法
的形式調用函數的時候,this
指向的是調用這個方法的對象,即上面的obj
對象
我們稍微改動一下上面的代碼,讓這個函數在對象中再深入一層,通過對象.屬性.方法
的形式去調用函數
let obj = {
a:1,
b:{
fn:function(){
console.log(this)
}
}
}
obj.b.fn() // {fn: ƒ}
可以看到輸出的結果是{fn: f}
,即b
,可以得出結論: this指向的是最終調用它的對象.當函數被多層對象所包含,且函數被最外層對象調用,this
指向的也只是它的上一級對象
我們再來修改下代碼,如下:
let obj = {
a:1,
fn:function(){
console.log(this)
}
}
let fn2 = obj.fn
fn2() // Window
可以看出執行結果是Window
對象,是不是覺得奇怪,這是爲什麼呢?
這是因爲當我們進行let fn2 = obj.fn
這步賦值操作的時候,我們將obj.fn
這個函數的內存地址賦值給了fn2
這個變量,而obj.fn
這個函數乾的事情就是console.log(this)
,現在我們執行的是fn2()
,調用這個fn2
函數的是Window對象.那你說,window調用了一個函數,這個函數的任務就是打印出誰調用了它,那答案不是顯而易見嘛
這次,我們還要來修改下代碼,如下
function fn2(){
console.log(this)
}
let obj = {
a:1,
fn:fn2
}
obj.fn() // {a: 1, fn: ƒ}
要解析這段代碼的思路,其實和上面那段代碼是一樣的.obj
調用fn
函數,而fn
函數指向的是fn2
,我們可以理解其實就是obj
調用fn2
函數,所以執行的結果就是打印出obj
這次,我們還要來修改下代碼,如下
let obj = {
a:1,
fn:function(){
setTimeout(function(){
console.log(this)
})
}
}
obj.fn() // Window
那麼,這又是爲何呢?我們知道setTimeout
是定時器,作用是在一段時間之後執行,setTimeout()
這個函數的()
中的是它的參數,也就是說function(){console.log(this)}
這個函數其實是被當作一個參數傳入setTimeout
當中的.這個其實有個隱式的操作就是將function(){console.log(this)}
賦值給一個假想函數f
,到這裏的時候,其實已經和obj
這個對象無關了.然後等待定時器的時間到了以後,執行的就是f
這個函數.這個f
是被當作普通函數直接調用的,所以this
指向了Window
對象.我們再這麼一想,function(){console.log(this)}
這是個匿名函數啊,它都沒有名字,是不能被普通對象調用的,但是Window
可以調用啊.
那我們現在要是想要用console.log(this)
打印出obj
這個對象該怎麼辦呢?有兩個方案:
- 保存this變量
- 箭頭函數
方法1:
let obj = {
a:1,
fn:function(){
let _self = this // 在這裏保存好this,免得它到時候跑了
setTimeout(function(){
console.log(_self)
})
}
}
obj.fn() // {a: 1, fn: ƒ}
方法2:
let obj = {
a:1,
fn:function(){
setTimeout(() => {
console.log(this)
})
}
}
obj.fn() // {a: 1, fn: ƒ}
我們使用箭頭函數也是可以達到同樣的效果的,至於原因我們後面會再提到,這裏就先跳過去了
但是setTimeout
其實還有一個點容易忽略,我們之前說過在嚴格模式下直接調用一個函數,它的this
指向undefined
.但是在setTimeout
方法中傳入函數的時候,如果這個函數沒有指定this
的話,它會有自動注入全局上下文,類似於xxx.call(window)
這樣的操作.看下面代碼
function fn(){
'use strict'
console.log(this)
}
setTimeout(fn) // Window
當然,如果我們在setTimeout
中傳入函數的時候綁定了this
的話,那就不會被注入全局對象
function fn(){
'use strict'
console.log(this)
}
setTimeout(fn.bind({})) // {}
構造函數調用
構造函數大家都應該比較熟悉了吧,我們new
一下,就new出一個新對象的那種.其實構造函數就是普通的函數,只不過在調用的時候前面加了new
運算符.關於new
運算符是幹嘛的,可以看我的另外一篇文章:JS new的時候幹了啥
function Person(){
console.log(this)
}
let p = new Person() // Person {}
當我們使用構造函數調用的時候,this
指向了這個實例化出來的對象,我們將上面的代碼和下面的代碼進行一個比對就能看出不同了
function Person(){
console.log(this)
}
let p = Person() // Window
兩段代碼的唯一區別就是後面的代碼沒有new
它,從而指向了Window
對象.由此可見,使用了new
之後,這個構造函數的this
被綁定到了正在構造的新對象上.這個在 JS new的時候幹了啥 也是有講到的,不太清楚的童鞋可以跳過去看一看
我們再來一個例子鞏固下
function Person(){
return this
}
let p = Person()
console.log(p) // Window
console.log(p === window) // true
不使用new
關鍵字,Person
普通函數返回this
,函數又是被Window
調用的,所以就是返回Window
對象,那麼p === window
返回的結果自然就是true
了
但是在構造函數中,如果顯式的返回了一個新的對象(非null
),那麼this
就會指向那個對象
function Person(name){
this.name = name
return {
name:'lisi'
}
}
let p = new Person('zhangsan')
console.log(p.name) // lisi
call,apply,bind 三兄弟
這三兄弟都是Function
這個對象原型上的方法.它們可以更改函數中的this
指向.
let obj = {}
function fn(){
console.log(this)
}
fn() // Window
fn.call(obj) // {}
fn.apply(obj) // {}
fn.bind(obj)() // {}
這裏可以看出它們三兄弟確實都可以改變this
的指向,那麼它們的區別在哪裏呢?
call
和apply
的作用基本一致,區別在於傳參的方式不太一樣
let obj = {}
function fn(){
console.log(arguments)
}
fn.call(obj,1,2) // Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
fn.apply(obj,[1,2]) // Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
call
的參數是一個個傳進去的,apply
的參數是直接傳了一個數組進去
那麼call
和bind
又有什麼區別呢?
let obj = {}
function fn(){
console.log(arguments)
}
fn.bind(obj)(1,2) // Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
fn.bind(obj,1,2)() // Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
可以看出來,綁定bind
的時候是不會直接調用函數的,它會返回一個新的函數,我們需要主動去()
一下,它纔會執行.相對比之下,call
和apply
都是會立即執行函數的.從第二個參數開始傳入的都是執行函數時需要傳入的參數,call
和bind
傳參的格式一致.另外,在構造函數調用的時候,內部其實也是有通過call
來變更this
指向的.這個前面我們也說到了,指向了創建出來的對象.
使用bind
方法創建的上下文爲永久性的上下文環境,沒法更改,call
和apply
也不行.
var a = 0
let obj = {
a:1
}
function fn(){
console.log(this.a)
}
fn() // 0
fn.call({}) // undefined # 通過call改變了this
fn.bind(obj)() // 1 # 通過bind改變了this
fn.bind(obj).call({}) // 1 # 通過bind改變了this, call再想要來更改就沒門了
上面代碼中,我們是給一個普通函數指定this
,下面我們來看看爲對象中的方法指定this
let obj = {
fn:function(){
console.log(this)
}
}
obj.fn.call({}) // {}
換成構造函數,再來看看
function Person(){
console.log(this)
}
let p = new Person.call({}) // Person.call is not a constructor
錯誤提示是Person.call不是一個構造函數,這是因爲此時我們去new
的是Person.call
而不是Person
,這當然不是一個構造函數了.我們換bind
再來試一試,看看結果
function Person(){
console.log(this)
}
let P = Person.bind({a:1})
let p = new P() // Person {}
發現結果是Person {}
,這說明我們的綁定沒有成功,否則結果就應該是{a:1}
了.因此,我們也可以得到結論,在構造函數中,我們去new
的時候,bind
綁定的this
是不會起效果的
箭頭函數
箭頭函數是在ES6的時候纔有的,它的語法比普通的函數表達式要簡潔,並且沒有自己的this
和arguments
.它並不創建自身的上下文,其上下文在定義的時候就已經確定了,即一次綁定,便不可更改.
let obj = {
fn:() => console.log(this)
}
obj.fn() //Window
這裏爲啥是Window
而不是obj
呢,箭頭函數的this
在定義的時候就已經確定了.在箭頭函數中引用this
,實際上調用的是定義時的上一層作用域的this
.那麼上面的代碼中調用的就是Window
對象了,因爲obj
對象是不能形成作用域的.
我們再來看下面的代碼,結果也是Window
,因爲即使fn
在函數中的位置深了一層,但是仍然沒有形成作用域,箭頭函數定義的時候還是指向全局對象了
let obj = {
a:{
fn:() => console.log(this)
}
}
obj.a.fn() //Window
我們先看如下代碼:
let obj = {
fn:function(){
return function(){
console.log(this)
}
}
}
obj.fn()() // Window
這裏函數作爲對象的一個方法使用,裏面有個閉包.當我們通過對象.方法
去調用的時候,實際上就相當於是函數直接調用.此時的this
指向Window
我們改動代碼如下,將閉包的形式變成箭頭函數
let obj = {
fn:function(){
return () => console.log(this)
}
}
obj.fn()() // {fn: ƒ}
此時的輸出結果變成obj
這個對象了.因爲箭頭函數在定義的時候是在fn
這個函數中,所以箭頭函數中的this
指向了fn
的this
,也就是obj
.大家是否還記得前面有個地方我們留了一個懸念給大家.對了,就是定時器那裏.我們說過,使用箭頭函數也可以達到同樣的效果,現在童鞋們是否都明白了.箭頭函數的this
在定義的時候就已經確定了,call
,apply
,bind
等都無法改變它.
由於箭頭函數的外部決定了上下文以及靜態上下文等特性,因此最好不要在全局環境下使用箭頭函數來定義方法,我們建議使用函數表達式來定義函數,可確保正確的上下文環境
總結: 有關
this
的學習到這裏基本就結束了.有時間的同學建議將我文章中的代碼都敲一遍,加深對this
的理解,達到事半功倍的效果