一名合格前端人員必須知道的 this 用法和陷阱(JS系列之三)

JefferyXZF發表於2020-03-22

歡迎大家關注,接下來我會寫一個關於 JavaScirpt系列文章,希望我們一起進步。

前言

this 關鍵字是 JavaScript 中最複雜的機制之一。它是一個很特別的關鍵字,被自動定義在所有函式的作用域中。作為一名前端攻城獅對它再熟悉不過了,然而正是因為熟悉它所以很容易忽略它,以至於用它時踩了不少的坑,甚至在面試時還因為它掛了。所以學習和掌握 this 的用法和一些陷阱對於進階成名一名合格前端攻城獅很有必要。

this 誤解

正所謂先破而後立,我們首先解除一下長時間對 this 的誤解,再開始 this 學習之旅。

一直以來我們可能以為 this 是指向函式自身或者函式的詞法作用域,這在某種情況下是可行的,但是還是不夠。this 在未執行時我們誰也不知道它的指向到底是誰,因為只有函式被呼叫時才會對 this 進行賦值,所以要知道 this 指向誰,首先知道它是在什麼位置被呼叫的。

呼叫位置

呼叫位置是函式在程式碼中呼叫的位置(而不是宣告的位置)。這句話好像看起來像是廢話,不過在它面前踩過坑的人覺得這句話說的太精闢了(這就是我想說的)。

呼叫位置分為以下幾種情況:

  • 普通函式呼叫:在全域性環境中呼叫函式
  • 物件方法呼叫:通過物件的方法呼叫
  • 構造器呼叫:使用 new 運算子例項化呼叫
  • 顯式呼叫:通過 callapplybind 呼叫,修正 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 上,但 letconst 宣告的變數,不會掛載在 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 指向

callapply 呼叫函式和其他函式呼叫相比,它會改變傳入函式的 this, 指向第一個傳入的引數。callapply 兩者實現功能相同, 不同的地方在於接收引數形式不一樣,前者接收的是引數個數,後者接收的是一個陣列

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

複製程式碼

陷阱

callapply 第一個引數除了可以是物件引用型別,也可以是基本型別:

  • nullundefinedthis 指向 window;不過,在嚴格模式下,this 還是指向 undefined

  • numberstringbooleanthis 會指向其內建建構函式 NumberStringBoolean

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 指向

bindcallapply 不同的地方在於改變了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指向的問題

引用連結

推薦閱讀

相關文章