Javascript-this/作用域/閉包

_Bourbon發表於2024-06-28

this & 作用域 & 閉包

this的核心,在大多數情況下,可以簡單地理解為誰呼叫了函式,this就指向誰。但請注意,這裡不包括透過callapplybindnew運算子或箭頭函式進行呼叫的特殊情況。在這些特殊情況下,this的指向會有所不同。

this的值是在函式執行時根據呼叫方式和上下文來確定的。和作用域不同,作用域在程式碼寫出來的那一刻就已經決定好了。

const o1 = {
  text: "o1",
  fn: function(){
    console.log("o1 this",this);
    return this.text;
  }
};

const o2 = {
  text: "o2",
  fn: function(){
    console.log("o2 this",this);
    return o1.fn()
  }
};

const o3 = {
  text: "o3",
  fn: function(){
    // 當透過 let fn = o1.fn; 將 o1.fn 賦值給區域性變數 fn 時,已經丟失了與 o1 的任何關聯。
    let fn = o1.fn;
    return fn();
  }
}

console.log("o1fn",o1.fn());
// o1呼叫fn,所以先列印o1物件,再列印 "o1"
console.log("o2fn",o2.fn());
// o2呼叫fn,所以先列印o2物件,再列印 o1物件,最後列印 "o1"
console.log("o3fn",o3.fn());
// o3呼叫fn,此時fn沒有呼叫物件,所以this指向預設的window物件,window沒有text屬性,所以this.text返回undefined

你看明白了嗎?

image-20240628002235481

那提問o2.fn執行時怎麼讓最終的結果改成"o2"呢?

// 1. 可以直接借用函式,在o2本地去呼叫函式
const o1 = {
  text: "o1",
  fn: function(){
    console.log("o1 this",this);
    return this.text;
  }
};

const o2 = {
  text: "o2",
  fn: o1.fn
};
console.log("o2fn",o2.fn())

image-20240628003040224

// 2. 透過call apply bind去顯示指定this
const o1 = {
  text: "o1",
  fn: function(){
    console.log("o1 this",this);
    return this.text;
  }
};

const o2 = {
  text: "o2",
  fn: function(){
    console.log("o2 this",this);
    return o1.fn.call(o2) // o1.fn.call(this) o2呼叫fn時,this就是指向o2
  }
};
console.log("o2fn",o2.fn());

image-20240628003313572

call & apply & bind

  • 相同點:三者都是用來改變函式呼叫時的this指向
  • 不同:
    • call functionName.call(thisArg, arg1, arg2, ...) 引數需要一個一個傳遞
    • apply functionName.apply(thisArg, [argsArray]) 引數可以用陣列傳遞
    • bind
      • functionName.bind(thisArg[, arg1[, arg2[, ...]]])
      • 返回一個新函式,在bind()被呼叫時,這個新函式this被指定為bind()的第一個引數,而其餘引數將作為新函式的引數供呼叫時使用。
  • callapply 都會呼叫函式,並返回函式呼叫的結果(如果有的話)。
  • bind 不會呼叫函式,而是返回一個新的函式,這個新函式在被呼叫時才會執行原始函式,並且具有指定的this值和預置的引數。
  • 使用場景
    • 如果知道要傳遞的引數,並且想要立即呼叫函式,那麼可以使用callapply
    • 如果想要建立一個新函式,這個函式在被呼叫時具有特定的this值和預置的引數,那麼可以使用bind

有時我們會遇到要手寫這三個函式的情況,我們可以先寫一個大致的框架,列出函式的輸入輸出,然後再向框架裡填充內容。

call

function hello(start, end) {  
  return start + ', ' + this.name + end;  
}  
  
const person = { name: 'Alice' };  
  
// 使用 call 呼叫 hello 函式,並將 this 繫結到 person 物件  
const message = hello.call(person, 'Hello', '!');  
console.log(message); // 輸出 "Hello, Alice!"

手寫call

輸入:一個上下文,可選引數(一個一個傳遞)

輸出:函式執行的結果

/*
function speak() {
    console.log(this.name,'can speak!'); // Alice can speak
}

const obj1 = {
    name: 'Alice'
}
speak.myCall(obj1);
*/
Function.prototype.myCall = function (context,...args) {
    // 邊界檢測,如果context沒傳則將上下文替換成全域性物件window||global
    context = context || window;
    // 將呼叫myCall的函式(這裡指的是speak)作為新的上下文(呼叫myCall時傳入的obj1)的屬性值新增到上下文中
    // 注意:這裡我們用context.fn作為中間變數來呼叫函式
    context.fn = this; // // 將this賦值給context的一個屬性
    const result = context.fn(...args); // 使用context作為上下文呼叫函式
    delete context.fn; // 清理環境,避免記憶體洩漏
    return result;
}

在這個例子中,speak 是被呼叫的函式,所以 thismyCall 內部指向 speak 函式。而 context 是我們傳遞給 myCallobj1 物件,我們希望在 speak 函式內部使用 obj1 作為 this 上下文。因此,我們將 speak 函式作為 obj1 的一個方法(臨時)來呼叫它,實現了改變 this 上下文的效果。

中途打斷點可以看到context的值如圖

image-20240627234537458

apply

// Math.max() 函式不接受陣列作為引數。它接受任意數量的數字引數,並返回這些引數中的最大值。所以這是最適合用於演示apply的函式
function max(numbers) {  
  return Math.max.apply(null, numbers);  
}  
  
const maxNum = max([1, 2, 3, 4, 5]);  
console.log(maxNum); // 輸出 5

手寫apply

經過上面的例子手寫了call之後,手寫apply就沒什麼難的了,因為它倆就接受引數的方式不同而已。apply接受的是一個陣列

Function.prototype.myApply = function (context,argumentsArr) {
    context = context || window;
    context.fn = this;
    // 如果argumentsArr存在則將其內容作為引數傳遞
    const result = argumentsArr ? context.fn(...argumentsArr) : context.fn();
    delete context.fn;
    return result
}

// 將上面的Math.max.apply改成myApply是一樣的效果

bind

比如當我們想要確保某個函式總是以特定的上下文來執行時。例如,在事件處理器、回撥函式和定時器中,我們需要繫結 this 上下文,確保函式總是能夠正確地訪問和操作我們期望的物件。

1. 事件處理器中的 this 繫結

在事件處理器中,this 通常指向觸發事件的元素,而不是我們期望的物件。使用 bind 可以確保 this 指向正確的物件。

function Button() {
    this.name = 'My Button';
    this.handleClick = function(event) {
        console.log(this.name + ' was clicked!'); // 'this' 指向Button例項
    };
    const buttonElement = document.getElementById('btn');
    buttonElement.addEventListener('click', this.handleClick.bind(this));
}
const myButton = new Button();
// 輸出:My Button was clicked!

// 如果改成下面這個則不會輸出name,只會輸出 was clicked!
// buttonElement.addEventListener('click', this.handleClick);

2. 回撥函式中的 this 繫結

在非同步操作或回撥函式中,this 的值可能會變化。使用 bind 可以確保 this 的值在回撥函式執行時保持不變。

function User(firstName) {
    this.firstName = firstName;
    this.fetchData = function(url) {
        // 假設fetch是一個模擬的非同步函式
        fetch(url)
            .then(response => response.json())
            .then(data => {
                console.log(this.firstName + ' fetched data: ', data); // 'this' 指向User例項
            }.bind(this)) // 使用bind確保this指向User例項
            .catch(error => console.error('Error:', error));
    };
}

const user = new User('Alice');
user.fetchData('https://api.example.com/data');

注意:在現代JavaScript中,通常使用箭頭函式來自動繫結 this,因為箭頭函式不繫結自己的 this,而是捕獲其所在上下文的 this 值。

3. 預設引數

使用 bind 可以預設函式的引數。這在建立可複用的函式時非常有用。

function list() {
    return Array.prototype.slice.call(arguments);
}

const list1 = list(1, 2, 3); // [1, 2, 3]

// 建立一個新的函式,預設第一個引數為'boys'
const listWithItems = list.bind(null, 'boys');
const list2 = listWithItems(1, 2, 3); // ['boys', 1, 2, 3]

在這個例子中,listWithItemslist 函式的一個新版本,將 'boys' 作為第一個引數。當我們呼叫 listWithItems(1, 2, 3) 時,它實際上是在呼叫 list('items', 1, 2, 3)

4. 繫結到特定的上下文

有時我們可能希望將函式繫結到特定的物件,以便在其他地方呼叫它時,它總是以該物件為上下文。

const obj = {
    age: 10,
    getAge: function() {
        return this.age;
    }
};

const unboundGetAge = obj.getAge;
console.log(unboundGetAge()); // undefined,因為this沒有繫結到obj

const boundGetAge = obj.getAge.bind(obj);
console.log(boundGetAge()); // 10,因為this被繫結到obj

在這個例子中,unboundGetAge 在呼叫時沒有繫結 this,所以它的 this 值是 window。而 boundGetAge 則被繫結到 obj 物件,因此它總是返回 obj.age 的值。

手寫bind函式

// 手寫bind
// 輸入:一個新的this上下文,以及可選的引數列表
// 輸出:一個新的函式,這個新函式被呼叫時會將this設定為指定的值,並且將引數列表與bind呼叫時提供的引數合併
Function.prototype.myBind = function (context,...initialArgs){
  // 1.儲存呼叫 myBind 方法的原始函式。
  const self = this;
  // 2.返回一個新函式
  return function F(...boundArgs) {
    // 3.判斷函式是否以建構函式的方式呼叫 這一段我還沒理解
    if (this instanceof F) {
      // 如果是,那麼 this 會指向一個新建立的物件,而不是我們提供的 context。
      // 我們就使用new和原始函式self來呼叫
      return new self(...initialArgs,...boundArgs);
    }
    
    // 否則,直接呼叫原始函式self,並傳入context作為this,以及合併後的引數
    return self.apply(context,[...initialArgs,...boundArgs]);
  }
}


/*
function hello(start, end) {  
    return start + ', ' + this.name + end;  
}  
  
const obj = { name: 'Alice' };  
  
const boundHello = hello.myBind(obj, 'Hello');  
  
console.log(boundHello('!')); // 輸出 "Hello, Alice!"
*/

上面這段程式碼執行棧如下圖,context就是我們傳入的obj物件,initialArgs是我們在呼叫bind是傳入的'Hello'boundArgs是我們呼叫返回的新函式boundHello時傳入的引數。

image-20240628010304262

當我們使用 bind 方法(無論是原生的 Function.prototype.bind 還是手寫實現的 myBind)時,我們實際上是在建立一個新的函式,這個函式被“繫結”到了特定的 this 上下文(在這個例子中是 obj 物件)以及一些預先設定的引數(在這個例子中是 'Hello')。

這個新函式(我們稱之為 boundHello)現在可以獨立使用,並且每次呼叫它時,都會以我們指定的 this 上下文(obj)和預先設定的引數('Hello')來呼叫原始的 hello 函式。

作用域和閉包 後續補上

相關文章