前言
this
關鍵字是 JavaScript
中最複雜的機制之一。它是一個很特別的關鍵字,被自動定義在所有函數的作用域中。作爲一名前端攻城獅對它再熟悉不過了,然而正是因爲熟悉它所以很容易忽略它,以至於用它時踩了不少的坑,甚至在面試時還因爲它掛了。所以學習和掌握 this
的用法和一些陷阱對於進階成名一名合格前端攻城獅很有必要。
this 誤解
正所謂先破而後立,我們首先解除一下長時間對 this
的誤解,再開始 this
學習之旅。
一直以來我們可能以爲 this
是指向函數自身或者函數的詞法作用域,這在某種情況下是可行的,但是還是不夠。this
在未執行時我們誰也不知道它的指向到底是誰,因爲只有函數被調用時纔會對 this
進行賦值,所以要知道 this
指向誰,首先知道它是在什麼位置被調用的。
調用位置
調用位置是函數在代碼中調用的位置(而不是聲明的位置)。這句話好像看起來像是廢話,不過在它面前踩過坑的人覺得這句話說的太精闢了(這就是我想說的)。
調用位置分爲以下幾種情況:
- 普通函數調用:在全局環境中調用函數
- 對象方法調用:通過對象的方法調用
- 構造器調用:使用
new
運算符實例化調用 - 顯式調用:通過
call
、apply
、bind
調用,修正this
指向
普通函數調用
普通函數調用, this
指向全局對象。在瀏覽器 JS
引擎中this
指向 window
, Nodejs
環境 this
指向 global
function f1(){
return this;
}
// 在瀏覽器中,全局對象是 window
f1() === window // true
//在Node中,全局對象是 global
f1() === global // true
// 示例代碼
var name = 'globalName'
var getName = function () {
return this.name
}
getName() // globalName
// or
var obj = function () {
name: 'John',
getName: function () {
return this.name
}
}
var getName = obj.getName
getName() // globalName
值得注意的是在最後兩行代碼,對象方法賦值給了 getName
變量,調用 getName()
相當於 調用window.getName()
,此時 this
是指向 window
陷阱
- 在嚴格模式下,
this
指向undefined
- 在全局環境中,使用
var
聲明的變量會掛載在window
上,但let
、const
聲明的變量,不會掛載在window
上
function f1(){
'use strict'
return this;
}
f1() // 嚴格模式下,this 指向 undefined
var a = 111
window.a // 111
let b = 222
const c = 333
window.b // undefined let、const 聲明變量沒有掛載在 window 上
window.c // undefined
對象方法調用
當函數作爲對象的方法被調用時, this 指向該對象:
var obj = {
name: '張三',
getName: function () {
return this.name
}
}
obj.getName() // 張三
陷阱
使用對象方法調用時,this
有可能會丟失,看下面這段代碼
var name = 'globalName'
var obj = {
name: '張三',
getName: function () {
function fn () {
return this.name
}
return fn() // globalName
}
}
obj.getName() // globalName
上面代碼輸出 globalName
而不是 張三
·,因爲在 getName
函數內部調用 fn
, 此時 fn
函數執行上下文this
不是指向調用的對象 obj
,而是指向 window
構造器調用
除了宿主提供的一些內置函數,大部分 JavaScript
函數可以當作構造器使用。構造器表面和普通函數一模一樣,不同的地方在於被調用的方式。
使用 new
運算符調用函數時,該函數總會返回一個對象,通常情況下,構造器裏的 this
就指向這個對象
var MyName = function () {
this.name = 'jeffery'
}
var obj = new MyName()
console.log(obj.name) // jeffery
使用 new
運算符創建 MyName
構造器,此時this
指向 obj
陷阱
使用 new
調用構造器時,還要注意一個問題,如果構造器顯式返回一個 object
類型的對象,那麼此次運行結果最終是返回這個對象,而不是我們之前期待的 this
:
var MyName = function () {
this.name = 'jeffery'
return { // 顯示返回一個對象
name: 'this is myName'
}
}
var obj = new MyName()
// 輸出 this is myName,而不是上面的 jeffery
console.log(obj.name) // this is myName
如果構造器不顯式返回任何數據,或者返回一個非對象類型的數據,就不會存在上面這個問題
var MyName = function () {
this.name = 'jeffery'
return 'this is myName'
}
var obj = new MyName()
console.log(obj.name) // jeffery
call、apply、bind 顯式調用修正 this 指向
call、apply 修正 this 指向
call
和 apply
調用函數和其他函數調用相比,它會改變傳入函數的 this
, 指向第一個傳入的參數。call
和 apply
兩者實現功能相同, 不同的地方在於接收參數形式不一樣,前者接收的是參數個數,後者接收的是一個數組
var obj1 = {
name: 'obj1 name',
getName: function () {
return this.name
}
}
var obj2 = {
name: 'obj2 name'
}
// 對象方法調用,this 指向 obj1
obj1.getName() // obj1 name
// 使用 call 顯示調用,改變了原來 this 指向,指向了 obj2
obj1.getName.call(obj2) // obj2 name
陷阱
call
、apply
第一個參數除了可以是對象引用類型,也可以是基本類型:
-
null
或undefined
:this
指向window
;不過,在嚴格模式下,this
還是指向undefined
。 -
number
、string
、boolean
:this
會指向其內置構造函數Number
、String
、Boolean
var name = 'globalName'
var obj = {
name: 'obj name',
getName: function () {
// 'use strict' // 嚴格模式下,this 指向 undefined, null 會報錯
return this.name
},
getThis: function () {
return this
}
}
obj.getName.call(null) // globalName
obj.getName.apply(undefined) // globalName
// number boolean string
obj.getThis.call(111) // Number {111}
obj.getThis.call(true) // Boolean {true}
obj.getThis.call('str') // String {"str"}
bind 修正 this 指向
bind
和 call
、apply
不同的地方在於改變了this
指向同時會返回一個新的函數
function f(){
return this.a;
}
var g = f.bind({a:"azerty"});
console.log(g()); // azerty
var h = g.bind({a:'yoo'}); // bind只生效一次!
console.log(h()); // azerty
var o = {a:37, f:f, g:g, h:h};
console.log(o.f(), o.g(), o.h()); // 37, azerty, azerty
bind
綁定改變 this
只能生效一次,如果鏈式發生多次綁定以第一次爲準
兩道經典面試題
俗話說:“實踐是驗證真理的唯一標準”。很多時候我們以爲學會了也只是自己以爲學會了,是騾子還是馬牽出來溜溜就知道了。所以,檢驗自己的學習成果莫過於實踐。下面附上兩道面試題讓大家動腦實踐一下
求解答爲什麼x.x調用結果會是undefined
function fn(xx){
this.x = xx;
return this;
}
var x = fn(5);
var y = fn(6);
console.log(x.x);
console.log(y.x);
下面的代碼輸出什麼
let length = 10;
function fn() {
console.log(this.length);
}
var obj = {
length: 5,
method: function(fn) {
fn();
arguments[0]();
}
}
obj.method(fn, 1);
不知道答案的小夥伴可以戳這裏:前端面試題(八)關於this指向的問題