JavaScript進階之(一) this指標

火柴盒zhang發表於2020-11-06

學習整理用,地址:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this

函式的 this 關鍵字在 JavaScript 中的表現略有不同,此外,在嚴格模式和非嚴格模式之間也會有一些差別。

(1)在絕大多數情況下,函式的呼叫方式決定了 this 的值(執行時繫結)。this 不能在執行期間被賦值,並且在每次函式被呼叫時 this 的值也可能會不同。

(2)ES5 引入了 bind 方法來設定函式的 this 值,而不用考慮函式如何被呼叫的。

(3)ES2015 引入了箭頭函式,箭頭函式不提供自身的this 繫結(this 的值將保持為閉合詞法上下文的值)。

一、語法

當前執行上下文(global、function 或 eval)的一個屬性,在非嚴格模式下,總是指向一個物件,在嚴格模式下可以是任意值。

(1)全域性上下文

無論是否在嚴格模式下,在全域性執行環境中(在任何函式體外部)this 都指向全域性物件。

// 在瀏覽器中, window 物件同時也是全域性物件:
console.log(this === window); // true

a = 37;
console.log(window.a); // 37

this.b = "MDN";
console.log(window.b) // "MDN"
console.log(b)         // "MDN"

Note: 你可以使用 globalThis 獲取全域性物件,無論你的程式碼是否在當前上下文執行。

(2)函式上下文

在函式內部,this的值取決於函式被呼叫的方式。
因為下面的程式碼不在嚴格模式下,且 this 的值不是由該呼叫設定的,所以 this 的值預設指向全域性物件,瀏覽器中就是 window。

function f1(){
  return this;
}
//在瀏覽器中:
f1() === window; //在瀏覽器中,全域性物件是window

//在Node中:
f1() === globalThis;

然而,在嚴格模式下,如果進入執行環境時沒有設定 this 的值,this 會保持為 undefined,如下:

function f2(){
  "use strict"; // 這裡是嚴格模式
  return this;
}

f2() === undefined;

在第二個例子中,this 應是 undefined,因為 f2 是被直接呼叫的,而不是作為物件的屬性或方法呼叫的(如 window.f2())。有一些瀏覽器最初在支援嚴格模式時沒有正確實現這個功能,於是它們錯誤地返回了window物件。

(3)類上下文

this 在 類 中的表現與在函式中類似,因為類本質上也是函式,但也有一些區別和注意事項。
(1)在類的建構函式中,this 是一個常規物件。類中所有非靜態的方法都會被新增到 this 的原型中

class Example {
  constructor() {
    const proto = Object.getPrototypeOf(this);
    console.log(Object.getOwnPropertyNames(proto));
  }
  first(){}
  second(){}
  static third(){}
}

new Example();

注意:靜態方法不是 this 的屬性,它們只是類自身的屬性

2)派生類

不像基類的建構函式,派生類的建構函式沒有初始的 this 繫結。在建構函式中呼叫 super()會生成一個 this 繫結,並相當於執行如下程式碼,Base為基類: this=new Base();
警告:在呼叫 super() 之前引用 this 會丟擲錯誤。

class Polygon {
  constructor(height, width) {
    this.name = 'Rectangle';
    this.height = height;
    this.width = width;
  }
  sayName() {
    console.log('Hi, I am a ', this.name + '.');
  }
  get area() {
    return this.height * this.width;
  }
  set area(value) {
    this._area = value;
  }
}

class Square extends Polygon {
  constructor(length) {
    this.height; // ReferenceError,super 需要先被呼叫!
    
    // 這裡,它呼叫父類的建構函式的,
    // 作為Polygon 的 height, width
    super(length, length);
    
    // 注意: 在派生的類中, 在你可以使用'this'之前, 必須先呼叫super()。
    // 忽略這, 這將導致引用錯誤。
    this.name = 'Square';
  }
}

派生類不能在呼叫 super() 之前返回,除非其建構函式返回的是一個物件,或者根本沒有建構函式。

class Base {}
class Good extends Base {}  //沒有建構函式 
class AlsoGood extends Base {
  constructor() {
    return {a: 5};  //返回了一個物件
  }
}
class Bad extends Base {
  constructor() {}  // 沒有返回物件,有建構函式  出錯:Must call super constructor in derived class before accessing 'this' or returning from derived constructor
}

new Good();
new AlsoGood();
new Bad();

二、示例

(1) 函式上下文中的this

       ==函式的呼叫方式決定了 this 的值(執行時繫結)==
// An object can be passed as the first argument to call or apply and this will be bound to it.
var obj = {a: 'Custom'}; //當一個物件作為第一個引數傳遞給call or apply時  this=obj 

// We declare a variable and the variable is assigned to the global window as its property.
var a = 'Global';  //全域性的變數,分配給windows

function whatsThis() {
  return this.a;      // The value of this is dependent on how the function is called //取決於呼叫方式
}

whatsThis();        // 'Global' as this in the function isn't set, so it defaults to the global/window object
                    // 全域性的windows==this,訪問windows上的a屬性
whatsThis.call(obj);  // 'Custom' as this in the function is set to obj
                      //this=obj,obj作為this作用與wahtsThis函式內 
whatsThis.apply(obj);  // 'Custom' as this in the function is set to obj

(2) this 和物件轉換

function add(c, d) {
  return this.a + this.b + c + d;
}

var o = {a: 1, b: 3};

// 第一個引數是用作“this”的物件
// 其餘引數用作函式的引數
add.call(o, 5, 7); // 16

// 第一個引數是用作“this”的物件
// 第二個引數是一個陣列,陣列中的兩個成員用作函式引數
add.apply(o, [10, 20]);  //34 

在非嚴格模式下使用 call 和 apply 時,如果用作 this 的值不是物件,則會被嘗試轉換為物件。null 和 undefined 被轉換為全域性物件。
原始值如 7 或 ‘foo’ 會使用相應建構函式轉換為物件。因此 7 會被轉換為 new Number(7) 生成的物件,字串 ‘foo’ 會轉換為 newString(‘foo’) 生成的物件。

function bar() {
  console.log(Object.prototype.toString.call(this));
}

bar.call(7); // [object Number]
bar.call('foo'); // [object String]
bar.call(undefined);  [object global]

(3)bind方法

ECMAScript 5 引入了 Function.prototype.bind()。呼叫f.bind(someObject)會建立一個與f具有相同函式體和作用域的函式,但是在這個新函式中,this將永久地被繫結到了bind的第一個引數,無論這個函式是如何被呼叫的。

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.a, o.f(), o.g(), o.h()); // 37, 37, azerty, azerty

(4)箭頭函式(較難理解)

為什麼叫Arrow Function?因為它的定義用的就是一個箭頭:

(param1, param2,, paramN) => { statements }
(param1, param2,, paramN) => expression
// equivalent to: (param1, param2, …, paramN) => { return expression; }

// Parentheses are optional when there's only one parameter name:
(singleParam) => { statements }
singleParam => { statements }

// A function with no parameters should be written with a pair of parentheses.
() => { statements }

在箭頭函式中,this與封閉詞法環境的this保持一致。在全域性程式碼中,它將被設定為全域性物件:

var globalObject = this;
var foo = (() => this); //一個匿名函式,返回this
console.log(foo() === globalObject); //true 

注意:如果將this傳遞給call、bind、或者apply來呼叫箭頭函式,它將被忽略。不過你仍然可以為呼叫新增引數,不過第一個引數(thisArg)應該設定為null。

// 接著上面的程式碼
// 作為物件的一個方法呼叫
var obj = {foo: foo};
console.log(obj.foo() === globalObject); // true

// 嘗試使用call來設定this
console.log(foo.call(obj) === globalObject); // true 

// 嘗試使用bind來設定this
foo = foo.bind(obj);
console.log(foo() === globalObject);    // true

無論如何,foo 的 this 被設定為他被建立時的環境(在上面的例子中,就是全域性物件)。
這同樣適用於在其他函式內建立的箭頭函式:這些箭頭函式的this被設定為封閉的詞法環境的。

// 建立一個含有bar方法的obj物件,
// bar返回一個函式,
// 這個函式返回this,
// 這個返回的函式是以箭頭函式建立的,
// 所以它的this被永久繫結到了它外層函式的this。
// bar的值可以在呼叫中設定,這反過來又設定了返回函式的值。
var obj = {
  bar: function() {
    var x = (() => this); //永久繫結到了它外層函式的this 即 obj
    return x;
  }
};

// 作為obj物件的一個方法來呼叫bar,把它的this繫結到obj。
// 將返回的函式的引用賦值給fn。
var fn = obj.bar();

// 直接呼叫fn而不設定this,
// 通常(即不使用箭頭函式的情況)預設為全域性物件
// 若在嚴格模式則為undefined
console.log(fn() === obj); // true

// 但是注意,如果你只是引用obj的方法,
// 而沒有呼叫它
var fn2 = obj.bar;
// 那麼呼叫箭頭函式後,this指向window,因為它從 bar 繼承了this。
console.log(fn2()() == window); //true 

在上面的例子中,一個賦值給了 obj.bar的函式(稱為匿名函式 A),返回了另一個箭頭函式(稱為匿名函式 B)。因此,在 A 呼叫時,函式B的this被永久設定為obj.bar(函式A)的this。當返回的函式(函式B)被呼叫時,它this始終是最初設定的。在上面的程式碼示例中,函式B的this被設定為函式A的this,即obj,所以即使被呼叫的方式通常將其設定為 undefined 或全域性物件(或者如前面示例中的其他全域性執行環境中的方法),它的 this 也仍然是 obj 。

(5)作為物件的方法

當函式作為物件裡的方法被呼叫時,this 被設定為呼叫該函式的物件。
下面的例子中,當 o.f() 被呼叫時,函式內的 this 將繫結到 o 物件。

var o = {
  prop: 37,
  f: function() {
    return this.prop;
  }
};

console.log(o.f()); //37

請注意,這樣的行為完全不會受函式定義方式或位置的影響。在前面的例子中,我們在定義物件o的同時,將其中的函式定義為成員 f 。但是,我們也可以先定義函式,然後再將其附屬到o.f。這樣做的結果是一樣的:

var o = {prop: 37};

function independent() {
  return this.prop;
}

o.f = independent;

console.log(o.f());// 37

這表明函式是從 o 的 f 成員呼叫的才是重點。
同樣,this 的繫結只受最接近的成員引用的影響。在下面的這個例子中,我們把一個方法g當作物件o.b的函式呼叫。在這次執行期間,函式中的this將指向o.b。事實證明,這與他是物件 o 的成員沒有多大關係,最近的引用才是最重要的。

o.b = {g: independent, prop: 42};
console.log(o.b.g());//42

原型鏈中的 this
對於在物件原型鏈上某處定義的方法,同樣的概念也適用。如果該方法存在於一個物件的原型鏈上,那麼 this 指向的是呼叫這個方法的物件,就像該方法就在這個物件上一樣。

var o = {
  f: function() {
    return this.a + this.b;
  }
};
var p = Object.create(o);
p.a = 1;
p.b = 4;

console.log(p.f());

在這個例子中,物件 p 沒有屬於它自己的 f 屬性,它的 f 屬性繼承自它的原型。雖然最終是在 o 中找到 f 屬性的,這並沒有關係;查詢過程首先從 p.f 的引用開始,所以函式中的 this 指向p。也就是說,因為f是作為p的方法呼叫的,所以它的this指向了p。這是 JavaScript 的原型繼承中的一個有趣的特性。

getter 與 setter 中的 this
再次,相同的概念也適用於當函式在一個 getter 或者 setter 中被呼叫。用作 getter 或 setter 的函式都會把 this 繫結到設定或獲取屬性的物件。

function sum() {
  return this.a + this.b + this.c;
}

var o = {
  a: 1,
  b: 2,
  c: 3,
  get average() {
    return (this.a + this.b + this.c) / 3;
  }
};

Object.defineProperty(o, 'sum', {
    get: sum, enumerable: true, configurable: true});

console.log(o.average, o.sum); //2 .6

(6) 作為建構函式

當一個函式用作建構函式時(使用new關鍵字),它的this被繫結到正在構造的新物件。
雖然建構函式返回的預設值是 this 所指的那個物件,但它仍可以手動返回其他的物件(如果返回值不是一個物件,則返回 this 物件)。

/*
 * 建構函式這樣工作:
 *
 * function MyConstructor(){
 * // 函式實體寫在這裡
 * // 根據需要在this上建立屬性,然後賦值給它們,比如:
 * this.fum = "nom";
 * // 等等...
 *
 * // 如果函式具有返回物件的return語句,
 * // 則該物件將是 new 表示式的結果。
 * // 否則,表示式的結果是當前繫結到 this 的物件。
 * //(即通常看到的常見情況)。
 * }
 */

function C(){
  this.a = 37;
}

var o = new C();
console.log(o.a); // logs 37


function C2(){
  this.a = 37;
  return {a:38};//手動返回其他的物件
}

o = new C2();
console.log(o.a); // logs 378

在剛剛的例子中(C2),因為在呼叫建構函式的過程中,手動的設定了返回物件,與this繫結的預設物件被丟棄了。(這基本上使得語句 “this.a = 37;”成了“殭屍”程式碼,實際上並不是真正的“殭屍”,這條語句執行了,但是對於外部沒有任何影響,因此完全可以忽略它)。

(7) 作為一個DOM事件處理函式

當函式被用作事件處理函式時,它的 this 指向觸發事件的元素(一些瀏覽器在使用非 addEventListener 的函式動態地新增監聽函式時不遵守這個約定)。

// 被呼叫時,將關聯的元素變成藍色
function bluify(e){
  console.log(this === e.currentTarget); // 總是 true

  // 當 currentTarget 和 target 是同一個物件時為 true
  console.log(this === e.target);
  this.style.backgroundColor = '#A5D9F3';
}

// 獲取文件中的所有元素的列表
var elements = document.getElementsByTagName('*');

// 將bluify作為元素的點選監聽函式,當元素被點選時,就會變成藍色
for(var i=0 ; i<elements.length ; i++){
  elements[i].addEventListener('click', bluify, false);
}

(8) 作為一個內聯事件處理函式

當程式碼被內聯 on-event 處理函式 呼叫時,它的this指向監聽器所在的DOM元素:

<button onclick="alert(this.tagName.toLowerCase());">
  Show this
</button>

上面的 alert 會顯示 button。注意只有外層程式碼中的 this 是這樣設定的:

<button onclick="alert((function(){return this})());">
  Show inner this
</button>

在這種情況下,沒有設定內部函式的 this,所以它指向 global/window 物件(即非嚴格模式下呼叫的函式未設定 this 時指向的預設物件)。

(9)類中的this

和其他普通函式一樣,方法中的 this 值取決於它們如何被呼叫。有時,改寫這個行為,讓類中的 this 值總是指向這個類例項會很有用。為了做到這一點,可在建構函式中繫結類方法:

class Car {
  constructor() {
    // Bind sayBye but not sayHi to show the difference
    this.sayBye = this.sayBye.bind(this);
    //sayBye的this繫結繫結為建立的car變數
    //sayHi的this 沒有
  }
  sayHi() {
    console.log(`Hello from ${this.name}`);
  }
  sayBye() {
    console.log(`Bye from ${this.name}`);
  }
  get name() {
    return 'Ferrari';
  }
}

class Bird {
  get name() {
    return 'Tweety';
  }
}

const car = new Car();
const bird = new Bird();

// The value of 'this' in methods depends on their caller
car.sayHi(); // Hello from Ferrari
bird.sayHi = car.sayHi;
bird.sayHi(); // Hello from Tweety

// For bound methods, 'this' doesn't depend on the caller
bird.sayBye = car.sayBye;
bird.sayBye();  // Bye from Ferrari

注意:類內部總是嚴格模式。呼叫一個 this 值為 undefined 的方法會丟擲錯誤。

相關文章