文章首發于個人部落格
導讀

導圖
this
記得差不多在兩年多之前寫過一篇文章 兩句話了解js中的this,當時總結的兩句話原話是這樣的:
- 普通函數指向函數的調用者:有個簡便的方法就是看函數前面有沒有點,如果有點,那麼就指向點前面的那個值;
- 箭頭函數指向函數所在的所用域: 注意了解作用域,隻有函數的
構成作用域,對象的{}
以及{}
if(){}
都不構成作用域;
當時對this的内部原理什麼的都了解的不是很深刻,就隻能憑借遇到很多坑之後,總結了出了那時候自己用來判斷的标準。這裡會再次略微深入的說一下。思路還是圍繞上面總結的那兩句話。
普通函數調用
-
預設綁定var a = 'luckyStar';
function foo() {
console.log(this.a);
}
foo();
// luckyStarfoo()直接調用非嚴格模式下是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