BACK
Featured image of post 【Javascript】整理關於 this 的指向

【Javascript】整理關於 this 的指向

在 JavaScript 中,「this 是什麼」絕對是讓人頭痛難題前三名。This 和物件方法息息相關,因此這篇文章會先介紹在物件方法、物件方法中的 this 是如何被決定的,把握一個簡單原則就可以知道 this 到底是誰,最後統整各個 this 的指向 😁

參考網站
參考網站
參考網站
參考網站
參考網站

在 JavaScript 中,「this 是什麼」絕對是讓人頭痛難題前三名。This 和物件方法息息相關,因此這篇文章會先介紹在物件方法、物件方法中的 this 是如何被決定的,把握一個簡單原則就可以知道 this 到底是誰,最後統整各個 this 的指向 😁

講解 this 的文章已經超級無敵多了,而且每一篇都寫得很不錯,之前看完 What’s THIS in JavaScript? 系列之後覺得講解的很完整,若是沒有把握自己能夠講得更清楚或是以不同的角度切入,似乎就沒必要再寫一篇文章;

若是想要「完全」搞懂 this,要付出的成本可能比你想像中要大得多。

這裡所說的「完全」指的是無論在任何情況下,你都有辦法講出為什麼 this 的值是這樣,直接給大家一個範例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
var value = 1;

var foo = {
  value: 2,
  bar: function () {
    return this.value;
  }
}

//範例1
console.log(foo.bar());
//範例2
console.log((foo.bar)());
//範例3
console.log((foo.bar = foo.bar)());
//範例4
console.log((false || foo.bar)());
//範例5
console.log((foo.bar, foo.bar)());

你能答的出來嗎?如果不行的話,代表你沒有「完全」懂 this。要完全懂 this 之所以要付出的成本很大,就是因為「完全懂 this」指的就是「熟記 ECAMScript 規範」。this 的值是什麼不是我們憑空想像的,其實背後都有完整的定義,而那個定義就是所謂的 ECMAScript 規範,你必須先搞懂這個規範,才有可能完全理解 this 在每個情況下所指涉的對象。

若是你真的很想完全搞懂,推薦你這一篇:JavaScript 深入之从 ECMAScript 规范解读 this,我上面的範例就是從這一篇拿來的,想看解答、想理解為什麼的話可以去看這一篇。

本篇教的方法,不會讓你把 this 完全搞懂,上面那五個範例你可能會答錯,但基本的題目你依舊可以解的出來,此文的目的是希望提供一個不同的角度來看 this,從為什麼會有 this 下手,再用一套規則來解釋 this 的值,至少讓你不再對 this 有誤解,也會知道一些常見的情境底下 this 到底是什麼。


從物件導向開始談 this

若是你對 JavaScript 的物件導向完全沒概念,你可以先補完相關基礎,並且看完這篇:該來理解 JavaScript 的原型鍊了

如果你有寫過其他程式語言,你就知道 this 從來都不是一件什麼困難的事。它代表的就是在物件導向裡面,那個 instance 本身。

舉個例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Car {
  setName(name) {
    this.name = name
  }

  getName() {
    return this.name
  }
}

const myCar = new Car()
myCar.setName('hello')
console.log(myCar.getName()) // hello

在上面我們宣告了一個 class Car,寫了 setName 跟 getName 兩個方法,在裡面用 this.name 來取放這個 instance 的屬性。

為什麼要這樣寫?因為這是唯一的方法,不然你要把 name 這個屬性存在哪裡?沒有其他地方讓你存了。所以 this 的作用在這裡是顯而易見的,所指到的對象就是那個 instance 本身

以上面的範例來說,myCar.setName('hello'),所以 this 就會是 myCar。在物件導向的世界裡面,this 的作用就是這麼單純。

或者換句話說:

一但脫離了物件導向,其實 this 就沒有什麼太大的意義

假設今天 this 只能在 class 裡面使用,那應該就不會有任何問題對吧?你有看過其他程式語言像是寫 Java 或是 C++ 的人在抱怨說 this 很難懂嗎?沒有,因為 this 的作用很單純。

那問題是什麼?問題就是在 JavaScript 裡面,你在任何地方都可以存取到 this。所以在 JavaScript 裡的 this 跟其他程式語言慣用的那個 this 有了差異,這就是為什麼 this 難懂的原因。

儘管 this 的定義不太一樣,但我認為本質上還是很類似的。要理解 this 的第一步就是告訴自己:「一但脫離了物件,就不太需要關注 this 的值,因為沒什麼意義


沒什麼太大意義的 this

1
2
3
4
5
function hello(){
  console.log(this)
}

hello()

this 的值會是什麼?

延續我們前面所講的,在這種情況下我會跟你說 this 沒有任何意義,而且你千萬不要想成 this 會指到 hello 這個 function,沒有這種事。

只要記得我前面跟你說的:「脫離了物件,this 的值就沒什麼意義」。

在這種很沒意義的情況下,this 的值在瀏覽器底下就會是 window,在 node.js 底下會是 global,如果是在嚴格模式(strict),this 的值就會是 undefined

這個規則應該滿好記的,幫大家重新整理一下:

  1. 嚴格模式(strict)底下就都是 undefined
  2. 非嚴格模式,瀏覽器底下是 window
  3. 非嚴格模式,node.js 底下是 global

這個就是你在其他文章看到的「預設綁定」,但我在這篇不打算用任何專有名詞去談 this。我認為不用這些名詞也不會妨礙你的理解,甚至還有可能讓你更好理解。我也不是說專有名詞不重要,是說可以先把概念學起來,再回過頭來補專有名詞。

一但脫離了物件,this 的值就沒什麼意義,在沒意義的情況底下就會有個預設值,而預設值也很好記,嚴格模式就是 undefined,非嚴格模式底下就是全域物件


更改 this 的值

僅管 this 可能有預設的值,但我們可以透過一些方法來改它。這改的方法也很簡單,一共有三種。

前兩種超級類似,叫做 callapply,這兩種都是能夠呼叫 fucntion 的函式,我舉一個例子給你看比較好懂:

1
2
3
4
5
6
7
8
'use strict';
function hello(a, b){
  console.log(this, a, b)
}

hello(1, 2) // undefined 1 2
hello.call(undefined, 1, 2) // undefined 1 2
hello.apply(undefined, [1, 2]) // undefined 1 2

我們有一個叫做 hello 的函式,會 console.log 出 this 的值以及兩個參數。在我們呼叫 hello(1, 2) 的時候,因為是嚴格模式所以 this 是 undefined,而 a 跟 b 就是 1 跟 2。

當我們呼叫 hello.call(undefined, 1, 2) 的時候,我們先忽略第一個參數不談,你可以發現他其實跟 hello(1, 2) 是一樣的。

而 apply 的差別只在於他要傳進去的參數是一個 array,所以上面這三種呼叫 function 的方式是等價的,一模一樣。除了直接呼叫 function 以外,你也可以用 call 或是 apply 去呼叫,差別在於傳參數的方式不同。

call 跟 apply 的差別就是這麼簡單,一個跟平常呼叫 function 一樣,一個用 array 包起來。

那我們剛剛忽略的第一個參數到底是什麼呢?

你可能已經猜到了,就是 this 的值!

1
2
3
4
5
6
7
'use strict';
function hello(a, b){
  console.log(this, a, b)
}

hello.call('yo', 1, 2) // yo 1 2
hello.apply('hihihi', [1, 2]) // hihihi 1 2

就是如此簡單,你第一個參數傳什麼,裡面 this 的值就會是什麼。儘管原本已經有 this,也依然會被這種方法給覆蓋掉:

1
2
3
4
5
6
7
8
9
class Car {
  hello() {
    console.log(this)
  }
}

const myCar = new Car()
myCar.hello() // myCar instance
myCar.hello.call('yaaaa') // yaaaa

原本 this 的值應該要是 myCar 這個 instance,可是卻被我們在使用 call 時傳進去的參數給覆蓋掉了。

除了以上兩種以外,還有最後一種可以改變 this 的方法:bind

1
2
3
4
5
6
7
'use strict';
function hello() {
  console.log(this)
}

const myHello = hello.bind('my')
myHello() // my

bind 會回傳一個新的 function,在這邊我們把 hello 這個 function 用 my 來綁定,所以最後呼叫 myHello() 時會輸出 my

以上就是三種可以改變 this 的值的方法。你可能會好奇如果我們把 call 跟 bind 同時用會怎樣:

1
2
3
4
5
6
7
'use strict';
function hello() {
  console.log(this)
}

const myHello = hello.bind('my')
myHello.call('call') // my

答案是不會改變,一但 bind 了以後值就不會改變了

這邊還要特別提醒的一點是在非嚴格模式底下,無論是用 call、apply 還是 bind,你傳進去的如果是 primitive 都會被轉成 object,舉例來說:

1
2
3
4
5
6
7
function hello() {
  console.log(this)
}

hello.call(123) // [Number: 123]
const myHello = hello.bind('my')
myHello() // [String: 'my']

詳細可以參考我另一篇文章 - 「使用 bind、call、apply 改變 this 指向的對象」,幫大家做個中場總結:

  1. 在物件以外的 this 基本上沒有任何意義,硬要輸出的話會給個預設值
  2. 可以用 call、apply 與 bind 改變 this 的值

物件中的 this

最前面我們示範了在物件導向 class 裡面的 this,但在 JavaScript 裡面還有另外一種方式也是物件:

1
2
3
4
5
6
7
8
const obj = {
  value: 1,
  hello: function() {
    console.log(this.value)
  }
}

obj.hello() // 1

這種跟一開始的物件導向範例不太一樣,這個範例是直接創造了一個物件而沒有透過 class,所以你也不會看到 new 這個關鍵字的存在。

再繼續往下講之前,要大家先記住一件事情:

this 的值跟作用域跟程式碼的位置在哪裡完全無關,只跟「你如何呼叫」有關

這個機制恰巧跟作用域相反,不確定我在說什麼的可以先看這篇:所有的函式都是閉包:談 JS 中的作用域與 Closure

舉個簡單的例子來幫大家複習一下作用域:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
var a = 10
function test(){
  console.log(a)
}

const obj = {
  a: 'ojb',
  hello: function() {
    test() // 10
  },
  hello2: function() {
    var a = 200
    test() // 10
  }
}

test() // 10
obj.hello() // 10
obj.hello2() // 10

無論我在哪裡,無論我怎麼呼叫 test 這個 function,他印出來的 a 永遠都會是全域變數的那個 a,因為作用域就是這樣運作,test 在自己的作用域裡面找不到 a 於是往上一層找,而上一層就是 global scope,這跟你在哪裡呼叫 test 一點關係都沒有。test 這個 function 在「定義」的時候就把 scope 給決定好了。

但 this 卻是完全相反,this 的值會根據你怎麼呼叫它而變得不一樣,還記得我們剛講過的 call、apply 跟 bind 嗎?這就是其中一個範例,你可以用不同的方式去呼叫 function,讓 this 的值變得不同。

所以你要很清楚知道這是兩種完全不同的運行模式,一個是靜態(作用域)、一個是動態(this)。要看作用域,就看這個函式在程式碼的「哪裡」;要看 this,就看這個函式「怎麽」被呼叫。

舉一個最常見的範例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const obj = {
  value: 1,
  hello: function() {
    console.log(this.value)
  }
}

obj.hello() // 1
const hey = obj.hello
hey() // undefined

明明就是同一個函式,怎麼第一次呼叫時 this.value 是 1,第二次呼叫時就變成 undefined 了?

記住我剛說的話:「要看 this,就看這個函式『怎麽』被呼叫」。

再繼續往下講之前,先教大家一個最重要的小撇步,是我從 this 的值到底是什么?一次说清楚學來的,是一個很方便的方法。

其實我們可以把所有的 function call,都轉成利用 call 的形式來看,以上面那個例子來說,會是這樣:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const obj = {
  value: 1,
  hello: function() {
    console.log(this.value)
  }
}

obj.hello() // 1
obj.hello.call(obj) // 轉成 call -> 1
const hey = obj.hello
hey() // undefined
hey.call() // 轉成 call -> undefined

而規則就是你在呼叫 function 以前是什麼東西,你就把它放到後面去。所以 obj.hello() 就變成了 obj.hello.call(obj)hey() 前面沒有東西,所以就變成了hey.call()

轉成這樣子的形式之後,還記得 call 的第一個參數就是 this 嗎?所以你就能立刻知道 this 的值是什麼了!

舉一個更複雜的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const obj = {
  value: 1,
  hello: function() {
    console.log(this.value)
  },
  inner: {
    value: 2,
    hello: function() {
      console.log(this.value)
    }
  }
}

const obj2 = obj.inner
const hello = obj.inner.hello
obj.inner.hello(obj.inner)
obj2.hello(obj2)
hello()

你可以不要往下拉,先想一下那三個 function 會各自印出什麼值。

接著我要公布解答了,只要轉成我們上面講的那種形式就好:

. . . . . . . . . .

1
2
3
obj.inner.hello() // obj.inner.hello.call(obj.inner) => 2
obj2.hello() // obj2.hello.call(obj2) => 2
hello() // hello.call() => undefined

特別講一下最後一個 hello 因為沒有傳東西進去,所以是預設綁定,在非嚴格模式底下是 window,所以會 log 出 window.value 也就是 undefined。

只要你把 function 的呼叫轉成用 call 的這種形式,就很容易看出來 this 的值是什麼。

這也是我前面一直在提的:「要看 this,就看這個函式『怎麽』被呼叫」,而你要看怎麼被呼叫的話,就轉成 call 的形式就行了。

學到這邊,其實你看見九成與 this 相關的題目你都會解了,不信的話我們來試試看(為了可讀性沒有防雷空行,所以請自行拉到程式碼就好,再往下拉就會是解答了):

1
2
3
4
5
6
7
8
9
function hello() {
  console.log(this)
}

var a = { value: 1, hello }
var b = { value: 2, hello }
hello()
a.hello()
b.hello.apply(a)

只要按照我們之前說的,用 call 來轉換一下形式就好:

1
2
3
hello() // hello.call() => window(瀏覽器非嚴格模式)
a.hello() // a.hello.call(a) => a
b.hello.apply(a) => 直接用 apply所以就是 a

再來一題比較不一樣的,要看仔細囉(假設在瀏覽器底下跑,非嚴格模式):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var x = 10
var obj = {
  x: 20,
  fn: function() {
    var test = function() {
      console.log(this.x)
    }
    test()
  }
}

obj.fn()

這題的話如果你搞錯,一定是你忘記了我們最重要的一句話:

要看 this,就看這個函式「怎麽」被呼叫

我們怎麼呼叫 test 的? test()

所以就是 test.call() 就是預設綁定,this 的值就會是 window,所以 this.x 會是 10,因為在第一行宣告了一個全域變數 x = 10。

寫到這裡,再來幫大家做個回顧,避免大家忘記前面在講什麼:

  1. 脫離物件的 this 基本上沒有任何意義
  2. 沒有意義的 this 會根據嚴格模式以及環境給一個預設值
  3. 嚴格模式底下預設就是 undefined,非嚴格模式在瀏覽器底下預設值是 window
  4. 可以用 call、apply 與 bind 改變 this 的值
  5. 要看 this,就看這個函式「怎麽」被呼叫
  6. 可以把 a.b.c.hello() 看成 a.b.c.hello.call(a.b.c),以此類推,就能輕鬆找出 this 的值

不合群的箭頭函式

原本有關 this 的部分應該講到上面就要結束了,但 ES6 新增的箭頭函式卻有不太一樣的運作方式。它本身並沒有 this,所以「在宣告它的地方的 this 是什麼,它的 this 就是什麼」,好,我知道這聽起來超難懂,我們來看個範例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const obj = {
  x: 1,
  hello: function(){
    // 這邊印出來的 this 是什麼,test 的 this 就是什麼
    // 就是我說的: 在宣告它的地方的 this 是什麼,test 的 this 就是什麼
    console.log(this)     
    const test = () => {
      console.log(this.x)
    }
    test()
  }
}

obj.hello() // 1
const hello = obj.hello
hello() // undefined

我們在 hello 這個 function 裡面宣告了 test 這個箭頭函式,所以 hello 的 this 是什麼,test 的 this 就是什麼

所以當我們呼叫 obj.hello() 時,test 的 this 就會是 obj;hello() 的時候 test 的 this 就會是全域物件。這規則其實都跟之前一樣,差別只有在於說箭頭函式的 this 不是自己決定的,而是取決於在宣告時那個地方的 this

如果你想看更複雜的範例,可以參考這篇:鐵人賽:箭頭函式 (Arrow functions)


實際應用:React

你有寫過 React 的話,就會知道裡面其實有些概念今天的教學可以派上用場,舉例來說,我們必須在 constructor 裡面先把一些 method 給 bind 好,你有想過是為什麼嗎?

先來看看如果沒有 bind 的話會發生什麼事:

1
2
3
4
5
6
7
8
class App extends React.Component {
  onClick() {
    console.log(this, 'click')
  }
  render() {
    return <button onClick={this.onClick}>click</button>
  }
}

最後 log 出來的值會是 undefined,為什麼?這細節就要看 React 的原始碼了,只有 React 知道實際上在 call 我們傳下去的 onClick 函式時是怎麼呼叫的。

所以為什麼要 bind?為了確保我們在 onClick 裡面拿到的 this 永遠都是這個 instance 本身。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class App extends React.Component {
  constructor() {
    super()

    // 所以當你把 this.onClick 傳下去時,就已經綁定好了 this
    // 而這邊的 this 就是這個 component
    this.onClick = this.onClick.bind(this)
  }
  onClick() {
    console.log(this, 'click')
  }
  render() {
    return <button onClick={this.onClick}>click</button>
  }
}

還有另外一種方式是用箭頭函式:

1
2
3
4
5
6
7
class App extends React.Component {
  render() {
    return <button onClick={() => {
     console.log(this)
    }}>click</button>
  }
}

為什麼箭頭函式也可以?因為我們前面提過,「在宣告它的地方的 this 是什麼,它的 this 就是什麼」,所以這邊 log 出來的 this 就會是 render 這個 function 的 this,而 render 的 this 就是這個 component。

如果你有點忘記了,可以把文章拉到最上面去,因為最上面我們就已經提過這些了。


總結

默認綁定的規則

1
2
3
4
5
console.log(this) // window, strict 模式下返回 undefined
function test() {
  console.log(this); // window
}
test(); // 純粹的調用 (Simple call) ==> window.test() 指向 window

隱式綁定規則 (誰呼叫就指向誰)

1
2
3
4
5
6
7
8
9
let a = 0;
let obj = {
  a:2,
  foo: function(){
    console.log(this); // obj  =>  obj 呼叫了 foo,因此 foo 裡的 this 指向為 obj
  }
}

obj.foo()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
let a = 0;
let obj = {
  a:2,
  foo: function(){
    console.log(this); // 返回 obj

    function test(){
      console.log(this) // window
    }
    test() // obj 呼叫了 foo, 但 test 並不是由 obj呼叫的,而是純粹調用,因此 this 指向 window
  }
}

obj.foo()

IIFE(立即執行函式)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let a = 0;
let obj = {
  a:2,
  foo: function(){
    (function(){
      console.log(this); // window
    })(); // 立即執行函式(IIFE)都屬於純粹調用,所以 this 的指向都是 window
  }
}
obj.foo()

閉包

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let a = 0;
let obj = {
  a: 2,
  foo: function () {
    function test() {
      console.log(this) // window
    }
    return test
  }
}

obj.foo()() // obj.foo() 的返回值為 test,因此該表達式 == test() 屬於純粹調用,因此 this 指向 window

變數賦值的情況

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
let a = 0;
function foo(){
  console.log(this);
}

let obj = {
  a:2,
  foo // ES6簡寫
}
obj.foo() // obj

let bar = obj.foo // obj.foo == foo,該表達式的結果是把 foo 的引用地址賦值給了 bar
bar() // 因此 bar == foo ,屬於純粹調用,this 的指向為 window

參數賦值的情況

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
let a = 0;
function foo(){
  console.log(this);
}

function bar(fn){
  // 將傳進來的 obj.foo == foo 賦值給形參 fn
  fn() // == foo()  屬於純粹調用,因此 this 的指向為 window
  // 因此 this 的指向只需要關注函式最終是如何執行的即可
}

let obj = {
  a:2,
  foo
}
bar(obj.foo) // obj.foo == foo 作為實參

阿里巴巴面試題

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
var name = 222;
var a = {
  name: 111,
  say: function () {
    console.log(this.name);
  },
};
var fun = a.say;
fun(); // ?
a.say(); // ?

var b = {
  name: 333,
  say: function (fun) {
    fun();
  },
};
b.say(a.say); // ?
b.say = a.say;
b.say(); // ?

答案:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
var name = 222;
var a = {
  name: 111,
  say: function () {
    console.log(this.name);
  },
};
var fun = a.say;
fun(); // 222
a.say(); // 111

var b = {
  name: 333,
  say: function (fun) {
    fun();
  },
};
b.say(a.say); // 222
b.say = a.say;
b.say(); // 333

forEach 內的 this 指向

1
2
3
4
5
6
7
let arr = [1,2,3,4,5]

let obj = { name : "obj" }
// 在對應的 api 接口文檔中指明
arr.forEach(function(item, index, arr){
  console.log(this); // 每次都輸出 obj
}, obj) // 這裡的 obj 就是 arr.forEach 遍歷時的 this 指向
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
arr.forEach(function callback(currentValue[, index[, array]]) {
  // your iterator
}[, thisArg]);

// 參數: 
// callback
// 這個 callback 函式將會把 Array 中的每一個元素作為參數,帶進本 callback 函式裡,每個元素各執行一次,接收三個參數:

// currentValue
// 代表目前被處理中的 Array 之中的那個元素。

// index (選擇性)
// 代表目前被處理中的 Array 之中的那個元素的 index。

// array (選擇性)
// 呼叫 forEach() 方法的那個 Array 本身,也就是上面語法中的 arr。

// thisArg (選擇性)
// 執行 callback 回呼函式的 this(即參考之 Object)值。

// source: https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach

顯式綁定: call、apply、bind

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function foo() {
  console.log(this); // 打印
}
let obj = {
  a: 2,
  foo
}

let bar = obj.foo

obj.foo() // obj

// 以下三種方式都能改變 this 的指向,將 bar 函式 的 this 指向改為 obj
bar.call(obj) // obj
bar.apply(obj) // obj
bar.bind(obj)() // obj
1
2
3
4
5
6
7
function foo(){
  console.log(this)
}

foo.call(1) // Number {1}
foo.call(null) // this 指向 window
foo.apply(undefined) // this 指向 window

建構式的顯式綁定實例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function Product(name, price) {
  this.name = name;
  this.price = price;

  if (price < 0)
    throw RangeError('Cannot create product "' + name + '" with a negative price');
  return this;
}

function Food(name, price) {
  // new 後 this 的指向就是這個實例物件
  console.log(this);
  Product.call(this, name, price);
  this.category = 'food';
}
Food.prototype = new Product();

function Toy(name, price) {
  Product.call(this, name, price);
  this.category = 'toy';
}
Toy.prototype = new Product();

let cheese = new Food('feta', 5);
console.log(cheese); // Food {name: 'feta', price: 5, category: 'food'}
let fun = new Toy('robot', 40);
console.log(fun); // Toy {name: 'robot', price: 40, category: 'toy'}

new 綁定

1
2
3
4
5
6
7
8
function Person(name='haewon', age=6){
  this.name = name
  this.age = age
  console.log(this) // 這裡會打印 this
}
let me = new Person('pupu', 12) // 打印 Person {name: 'pupu', age: 12},this 的指向為實例化的物件

Person() // 打印 window; 同時 window.name 的值為 'haewon'; window.age 的值為 6

優先級

顯式綁定 > 隱式綁定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function foo(){
  console.log(this.a);
}
let obj1 ={
  a:'obj1',
  foo
}
let obj2 ={
  a:'obj2',
  foo
}
obj1.foo() // obj1
obj2.foo() // obj2

obj1.foo.call(obj2) // obj2
obj2.foo.call(obj1) // obj1

new 綁定 > 顯式綁定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function foo(b){
  this.a = b
}
let obj1 = {}
let bar = foo.bind(obj1)
bar(2)

console.log(obj1.a) // 2

let bar_child = new bar(99) // new 將 this 的指向更改為 bar_child
console.log(obj1.a); // 2

console.log(bar_child.a); // {a:99}

Arrow function 的 this 指向

物件中的巢狀函式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
let a = 0
let obj = {
  a : 'obj',
  foo() {
    let that = this;
    console.log(this);
    function test() {
      console.log(this); // window
      console.log(that); // obj
    }
    test() 

    // 也可以通過這種方式使 this 指向 obj 👇🏻
    test.call(this)
  }
}
obj.foo()

箭頭函式中的 this

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
let a = 0
let obj = {
  a: 'obj',
  foo() {
    console.log(this); // obj

    let test = ()=>{
      console.log(this); // obj
    }
    test()
  }
}
obj.foo() // => 箭頭函式內部沒有 this 指向,箭頭函式的 this 指向是由外層函式的作用域決定的

更改箭頭函式的 this 指向

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function foo(){
  let test = ()=>{
    console.log(this);
  }
  return test
}

let obj1 = {
  a: 'obj1',
  foo
}

let obj2 = {
  a: 'obj2',
  foo
}

obj1.foo()() // 打印obj1
obj2.foo()() // 打印obj2

// => 默認綁定規則(純粹調用) 對箭頭函式無效,因為箭頭函式里面沒有 this

嘗試隱式綁定 this 指向

1
2
3
4
5
6
7
8
9
let obj1 = {
  a: 'obj1',
  foo: ()=>{
    console.log(this)
  }
}
obj1.foo() // window

// => 隱式綁定 對箭頭函式無效

嘗試顯式綁定 this 指向

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function foo(){
  let test = ()=>{
    console.log(this);
  }
  return test
}
let obj1 = {
  a: 'obj1',
  foo
}

foo()() // window
let bar = foo().call(obj1) // window

// => 顯式綁定 對箭頭函式無效

new 運算子能否改變箭頭函式的 this 指向?

1
2
3
4
5
6
7
let foo = ()=>{
  console.log(this);
}
let test = new foo()
// 報錯 Uncaught TypeError: foo is not a constructor
// => 箭頭函式不能作為建構式
// => 所有綁定規則對箭頭函式都不適用; 箭頭函式的 this 取決於父環境中的 this 指向; => 箭頭函式不存在 this

案例一

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
var name = '我是 window 裡的 name'
let obj1 = {
  name: 'obj1 的 name',
  fn1(){
    console.log(this.name);
  },
  fn2: ()=>{
    console.log(this.name);
  },
  fn3(){
    return function(){
      console.log(this.name);
    }
  },
  fn4(){
    return () => {
      console.log(this.name);
    }
  }
}
let obj2 = {
  name : 'obj2 的 name'
}

obj1.fn1(); // ?
obj1.fn1.call(obj2); // ?

obj1.fn2(); // ?
obj1.fn2.call(obj2); // ?

obj1.fn3()(); // ?
obj1.fn3().call(obj2); // ?
obj1.fn3.call(obj2)(); // ?

obj1.fn4()() // ?
obj1.fn4().call(obj2); // ?
obj1.fn4.call(obj2)(); // ?

答案:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
obj1.fn1(); // 'obj1 的 name'
obj1.fn1.call(obj2); // 'obj2 的 name'

obj1.fn2(); // '我是 window 裡的 name'
obj1.fn2.call(obj2); // '我是 window 裡的 name'

obj1.fn3()(); // '我是 window 裡的 name'
obj1.fn3().call(obj2); // 'obj2 的 name'
obj1.fn3.call(obj2)(); // '我是 window 裡的 name'

obj1.fn4()() // 'obj1 的 name'
obj1.fn4().call(obj2); // obj1 的 name
obj1.fn4.call(obj2)(); // obj2 的 name

案例二

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function Foo(){
  getName = function(){
    console.log(1)
  }
  return this
}
Foo.getName = function(){
  console.log(2);
}
Foo.prototype.getName = function(){
  console.log(3);
}
let getName = function(){
  console.log(4)
}
function getName(){
  console.log(5)
}

// 請寫出輸出結果: 
Foo.getName(); // ?
getName(); // ?
Foo().getName(); // ?
getName(); // ?

new Foo.getName(); // ?
new Foo().getName(); // ?
new new Foo().getName(); // ?

答案:

1
2
3
4
5
6
7
8
Foo.getName(); // 2
getName(); // 4
Foo().getName(); // 1
getName(); // 1

new Foo.getName(); // 2
new Foo().getName(); // 3
new new Foo().getName(); // 3

comments powered by Disqus