文章首發於個人部落格
導讀
this
記得差不多在兩年多之前寫過一篇文章 兩句話理解js中的this,當時總結的兩句話原話是這樣的:
- 普通函式指向函式的呼叫者:有個簡便的方法就是看函式前面有沒有點,如果有點,那麼就指向點前面的那個值;
- 箭頭函式指向函式所在的所用域: 注意理解作用域,只有函式的
{}
構成作用域,物件的{}
以及if(){}
都不構成作用域; 當時對this的內部原理什麼的都理解的不是很深刻,就只能憑藉遇到很多坑之後,總結了出了那時候自己用來判斷的標準。這裡會再次略微深入的說一下。思路還是圍繞上面總結的那兩句話。
普通函式呼叫
- 預設繫結
var a = 'luckyStar';
function foo() {
console.log(this.a);
}
foo();
// luckyStar
複製程式碼
foo()直接呼叫非嚴格模式下是this是指向 window上的,嚴格模式 this 指向的是undefined;
- 隱式繫結
var a = 'luckyStar';
var obj = {
a: 'litterStar',
foo() {
console.log(this.a);
}
}
obj.foo(); // ①
// litterStar
var bar = obj.foo;
bar(); // ②
// luckyStar
setTimeout(obj.foo, 100); // ③
// luckyStar
複製程式碼
位置①,obj.foo(),是obj通過.
運算子呼叫了 foo(),所以指向的值 obj。
位置②,是把 obj.foo賦值給了 bar,實際上是把 foo函式賦值給了bar, bar() 呼叫的時候,沒有呼叫者,所以使用的是預設繫結規則。
位置③,是把 obj.foo賦值給了 setTimeout,實際上呼叫的還是 foo函式,呼叫的時候,沒有呼叫者,所以使用的是預設繫結規則。
位置②和位置 位置③ 的一定要注意。
- 顯式繫結
function foo() {
console.log(this.name);
}
const obj = {
name: 'litterStar'
}
const bar = function() {
foo.call(obj);
}
bar();
// litterStar
複製程式碼
使用 call,apply可以顯式修改 this的指向,下面會詳細介紹該部分。
- new 繫結
function Foo(name) {
this.name = name;
}
var luckyStar = new Foo('luckyStar');
luckyStar.name;
// luckyStar
複製程式碼
要解釋上面的結果就要從 new 的過程說起了
- 建立一個新的空物件 obj
- 將新物件的的原型指向當前函式的原型
- 新建立的物件繫結到當前this上
- 如果沒有返回其他物件,就返回 obj,否則返回其他物件
function _new(constructor, ...arg) {
// ① 建立一個新的空物件 obj
const obj = {};
// ② 將新物件的的原型指向當前函式的原型
obj.__proto__ = constructor.prototype;
// ③ 新建立的物件繫結到當前this上
const result = constructor.apply(obj, arg);
// ④ 如果沒有返回其他物件,就返回 obj,否則返回其他物件
return typeof result === 'object' ? result : obj;
}
function Foo(name) {
this.name = name;
}
var luckyStar = _new(Foo, 'luckyStar');
luckyStar.name; //luckyStar
複製程式碼
箭頭函式呼叫
箭頭函式中其實沒有 this 繫結,因為箭頭函式中this指向函式所在的所用域。箭頭函式不能作為建構函式
const obj = {
name: 'litterStar',
say() {
console.log(this.name);
},
read: () => {
console.log(this.name);
}
}
obj.say(); // litterStar
obj.read(); // undefined
複製程式碼
call,apply,bind
call,apply,bind 這三個函式是 Function原型上的方法 Function.prototype.call()
,Function.prototype.apply
,Function.prototype.bind()
,所有的函式都是 Funciton
的例項,因此所有的函式可以呼叫call,apply,bind 這三個方法。
call,apply,bind 在用法上的異同
相同點:
call,apply,bind 這三個方法的第一個引數,都是this。如果你使用的時候不關心 this是誰的話,可以直接設定為 null
不同點:
- 函式呼叫 call,apply方法時,返回的是呼叫函式的返回值。
- 而bind是返回一個新的函式,你需要再加一個小括號來呼叫。
- call和apply的區別就是,call接受的是一系列引數,而apply接受的是一個陣列。
但是有了 ES6引入的 ...
展開運算子,其實很多情況下使用 call和apply沒有什麼太大的區別。
舉個例子,找到陣列中最大的值
const arr = [1, 2, 3, 5];
Math.max.call(null, ...arr);
Math.max.apply(null, arr);
複製程式碼
Math.max
是數字的方法,陣列上並沒有,但是我們可以通過 call, apply 來使用 Math.max
方法來計算當前陣列的最大值。
手寫 call,apply,bind
實現一個call:
- 如果不指定this,則預設指向window
- 將函式設定為物件的屬性
- 指定this到函式並傳入給定引數執行函式
- 執行&刪除這個函式,返回函式執行結果
Function.prototype.myCall = function(thisArg = window) {
// thisArg.fn 指向當前函式 fn (fn.myCall)
thisArg.fn = this;
// 第一個引數為 this,所以要取剩下的引數
const args = [...arguments].slice(1);
// 執行函式
const result = thisArg.fn(...args);
// thisArg上並不存在fn,所以需要移除
delete thisArg.fn;
return result;
}
function foo() {
console.log(this.name);
}
const obj = {
name: 'litterStar'
}
const bar = function() {
foo.myCall(obj);
}
bar();
// litterStar
複製程式碼
實現一個apply 過程很call類似,只是引數不同,不再贅述
Function.prototype.myApply = function(thisArg = window) {
thisArg.fn = this;
let result;
// 判斷是否有第二個引數
if(arguments[1]) {
// apply方法呼叫的時候第二個引數是陣列,所以要展開arguments[1]之後再傳入函式
result = thisArg.fn(...arguments[1]);
} else {
result = thisArg.fn();
}
delete thisArg.fn;
return result;
}
function foo() {
console.log(this.name);
}
const obj = {
name: 'litterStar'
}
const bar = function() {
foo.myApply(obj);
}
bar();
// litterStar
複製程式碼
實現一個bind
MDN上的解釋:bind() 方法建立一個新的函式,在 bind() 被呼叫時,這個新函式的 this 被指定為 bind() 的第一個引數,而其餘引數將作為新函式的引數,供呼叫時使用。
Function.prototype.myBind = function(thisArg) {
// 儲存當前函式的this
const fn = this;
// 儲存原先的引數
const args = [...arguments].slice(1);
// 返回一個新的函式
return function() {
// 再次獲取新的引數
const newArgs = [...arguments];
/**
* 1.修改當前函式的this為thisArg
* 2.將多次傳入的引數一次性傳入函式中
*/
return fn.apply(thisArg, args.concat(newArgs))
}
}
const obj1 = {
name: 'litterStar',
getName() {
console.log(this.name)
}
}
const obj2 = {
name: 'luckyStar'
}
const fn = obj1.getName.myBind(obj2)
fn(); // luckyStar
複製程式碼
手寫部分的程式碼大部分參考了網上比較多的一些寫法。手寫程式碼的前提是一定要搞清楚這個函式是什麼,怎麼用,幹了什麼。
重要參考
- 你不知道的JavaScript(上卷)
- 不能使用call,apply,bind,如何用js實現call或者apply的功能?
- JavaScript深入之bind的模擬實現
- 「中高階前端面試」JavaScript手寫程式碼無敵祕籍
- 22 道高頻 JavaScript 手寫面試題及答案
- MDN上bind函式的Polyfill
其他
最近發起了一個100天前端進階計劃,主要是深挖每個知識點背後的原理,歡迎關注 微信公眾號「牧碼的星星」,我們一起學習,打卡100天。