this & 作用域 & 閉包
this
的核心,在大多數情況下,可以簡單地理解為誰呼叫了函式,this
就指向誰。但請注意,這裡不包括透過call
、apply
、bind
、new
運算子或箭頭函式進行呼叫的特殊情況。在這些特殊情況下,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
你看明白了嗎?
那提問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())
// 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());
call & apply & bind
- 相同點:三者都是用來改變函式呼叫時的this指向
- 不同:
- call
functionName.call(thisArg, arg1, arg2, ...)
引數需要一個一個傳遞 - apply
functionName.apply(thisArg, [argsArray])
引數可以用陣列傳遞 - bind
functionName.bind(thisArg[, arg1[, arg2[, ...]]])
- 返回一個新函式,在
bind()
被呼叫時,這個新函式的this
被指定為bind()
的第一個引數,而其餘引數將作為新函式的引數供呼叫時使用。
- call
call
和apply
都會呼叫函式,並返回函式呼叫的結果(如果有的話)。bind
不會呼叫函式,而是返回一個新的函式,這個新函式在被呼叫時才會執行原始函式,並且具有指定的this
值和預置的引數。- 使用場景
- 如果知道要傳遞的引數,並且想要立即呼叫函式,那麼可以使用
call
或apply
。 - 如果想要建立一個新函式,這個函式在被呼叫時具有特定的
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
是被呼叫的函式,所以 this
在 myCall
內部指向 speak
函式。而 context
是我們傳遞給 myCall
的 obj1
物件,我們希望在 speak
函式內部使用 obj1
作為 this
上下文。因此,我們將 speak
函式作為 obj1
的一個方法(臨時)來呼叫它,實現了改變 this
上下文的效果。
中途打斷點可以看到context
的值如圖
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]
在這個例子中,listWithItems
是 list
函式的一個新版本,將 '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
時傳入的引數。
當我們使用 bind
方法(無論是原生的 Function.prototype.bind
還是手寫實現的 myBind
)時,我們實際上是在建立一個新的函式,這個函式被“繫結”到了特定的 this
上下文(在這個例子中是 obj
物件)以及一些預先設定的引數(在這個例子中是 'Hello'
)。
這個新函式(我們稱之為 boundHello
)現在可以獨立使用,並且每次呼叫它時,都會以我們指定的 this
上下文(obj
)和預先設定的引數('Hello'
)來呼叫原始的 hello
函式。