在 JavaScript 中 this
其實是一顆語法糖,但是這糖有毒。this
致命的地方在於它的指向往往不能直觀確定。希望下面可以一步步去掉有毒的糖衣。
1 用 f.call(thisVal, ...args)
指定 this
呼叫函式的方式有三種,用 Function.prototype.call
呼叫可以指定 this
:
定義 function f(...args){/*...*/}
呼叫 f.call(thisVal, ...args);
例一
function greet(){
console.log(`Hello, ` + this);
}
// 手動指定 `greet` 中的 `this`:
greet.call(`ngolin`); // Hello, ngolin
例二
function whoAreYou(){
console.log("I`m " + this.name);
}
whoAreYou.call({name: `Jane`}); // I`m Jane
2 使用語法糖,this
自動指定
先接受函式 f
的正確呼叫方式是 f.call(thisVal, ...args);
, 然後就可以把 f(...args);
理解成語法糖。
但是不用 f.call(thisVal, ...args)
, this
怎樣動態指定?
一、函式(function)
// 1. 在非嚴格模式下:window
f(); // 解糖為 f.call(window);
// 2. 但在嚴格模式下:undefined
f(1, 2, 3); // 解糖為 f.call(undefined, 1, 2, 3);
一、方法(method)
// 無論是在嚴格還是非嚴格模式:
obj.m(1, 2, 3); // 解糖為 obj.m.call(obj, 1, 2, 3);
obj1.obj2.m(...args); // obj1.obj2.m.call(obj1.obj2, ...args);
obj1.obj2....objn.m(); // obj1.obj2....objn.m.call(obj1.obj2....objn);
通過上面的例子,分別演示了函式 f(..args)
和方法 obj1.obj2....objn.m(..args)
怎樣自動指定 this
.
嚴格區分函式(function)和方法(method)這兩個概念有利於清晰思考,因為它們在繫結 this
時發生的行為完全不一樣。同時函式和方法可以相互賦值(轉換),在賦值前後,唯一發生變化的是繫結 this
的行為(當然這種變化在呼叫時才會體現)。下面先看函式轉方法,再看方法轉函式。
3 函式轉方法
函式宣告(function f(){}
)和函式表示式(var f = function(){};
)有一些微妙的區別,但是兩種方式在呼叫時繫結this
行為完全一樣,下面在嚴格模式下以函式表示式為例:
var f = function(){
console.log(this.name);
};
var obj1 = {
name: `obj 1`,
getName: f;
};
var obj2 = {
name: `obj 2`,
getName: f;
};
// 函式 `f` 轉方法 `obj1.getName`
obj1.getName();// `obj 1` => obj1.getName.call(obj1)
// 不認為函式轉方法
obj2.getName.call(obj1);// `obj 1`(不是 `obj 2`)
將函式轉成方法通常不太容易出錯,因為起碼在方法中 this
能夠有效地指向一個物件。函式轉成方法是一個模糊的說法,實際上可以這樣理解:
JavaScript 不能定義一個函式,也不能定義一個方法,是函式還是方法,要等到它執行才能確定;當把它當成函式執行,它就是函式,當把它當成方法執行,它就是方法。所以只能說執行一個函式和執行一個方法。
這樣理解可能有些極端,但是它可能有助於避免一些常見的錯誤。因為關係到
this
怎樣繫結,重要的是在哪裡呼叫(比如在obj1
,obj2
… 上呼叫)以及怎樣呼叫(比如以f()
,f.call()
… 的方式),而不是在哪裡定義。
但是,為了表達的方便,這裡仍然會使用定義函式和定義方法這兩種說法。
4 方法轉函式
將方法轉成函式比較容易出錯,比如:
var obj = {
name: `obj`,
show: function(){
console.log(this.name);
}
};
var _show = obj.show;
_show(); // error!! => _show.call(undefined)
button.onClick = obj.show;
button.onClick(); // error!! => button.onClick.call(button)
(function(cb){
cb(); // error!! =>cb.call(undefined)
})(obj.show);
當一個物件的方法使用了 this
時,如果這個方法最後不是由這個物件呼叫(比如由其他框架呼叫),這個方法就可能會出錯。但是有一種技術可以將一個方法(或函式)繫結(bind)在一個物件上,從而無論怎樣呼叫,它都能夠正常執行。
5 把方法繫結(bind)在物件上
先看這個obj.getName
的例子:
var obj = {
getName: function(){
return `ngolin`;
}
};
obj.getName(); // `ngolin`
obj.getName.call(undefined); // `ngolin`
obj.getName.call({name: `ngolin`}); // `ngolin`
var f = obj.getName;
f(); // `ngolin`
(function(cb){
cb(); // `ngolin`
})(obj.getName);
上面的例子之所以可以成功是因為 obj.getName
根本沒有用到 this
, 所以 this
指向什麼對 obj.getName
都沒有影響。
這裡有一種技術把使用 this
的方法轉成不使用 this
的方法,就是建立兩個閉包(即函式),第一個閉包將方法(method)和物件(obj)捕獲下來並返回第二個閉包,而第二個閉包用於呼叫並返回 obj.method.call(obj);
. 下面一步步實現這種技術:
第一步 最簡單的情況下:
function method(){
obj.method.call(obj);
}
method(); // correct, :))
存在的缺陷:
- 只適合沒有引數和返回的
obj.method
- 存在兩個安全隱患:
1 後續改變obj.method
,比如obj.method = null;
2 後續改變obj
,比如obj = null
第二步 在方法有引數有返回的情況下:
function method(a, b){
return obj.method.call(obj, a, b);
}
method(a, b); // correct, :))
存在的缺陷:
- 只適合兩個引數的
obj.method
- 存在兩個安全隱患,同上。
第三步 一個傳遞引數更好的辦法:
function method(){
return obj.method.apply(obj, arguments);
}
method(a, b); // correct, :))
仍存在兩個安全隱患。
第四步 更加安全的方式:
var method = (function(){
return function(){
return obj.method.apply(obj, arguments);
};
})(obj.method, obj);
method(a, b); // correct, :))
第五步 抽象出一個函式,用於將方法繫結到物件上:
function bind(method, obj){
return function(){
return method.apply(obj, arguments);
};
}
var obj = {
name: `ngolin`,
getName: function(){
return this.name;
}
};
var method = bind(obj.getName, obj);
method(); // `ngolin`
6 Function.prototype.bind
這種方法很常見,後來 ECMAScript 5 就增加了 Function.prototype.bind
, 比如:
var binded = function(){
return this.name;
}.bind({name: `ngolin`});
binded(); // `ngolin`
具體來說,Function.prototype.bind
這樣工作:
var bindedMethod = obj.method.bind(obj);
// 相當於:
var bindedMethod = (function(){
return function(){
return obj.method.apply(obj, arguments);
};
})(obj.method, obj);
更多使用 Function.prototype.bind
的例子:
var f = obj.method.bind(obj);
button.onClick = obj.method.bind(obj);
document.addEventListener(`click`, obj.method.bind(obj));
7 常見問題及容易出錯的地方
一 在定義物件時有沒有 this
?
obj = {
firstName: `First`,
lastName: `Last`,
// `fullName` 可以得到預期結果嗎?
fullName: this.firstName + this.lastName
}
// 或者:
function makePoint(article){
if(article.length <= 144) return article;
return article.substr(0, 141) + `...`;
}
obj = {
fulltext: `...a long article go here...`,
// `abstract` 呢?
abstract: makePoint(this.fulltext)
}
二 在方法內的 this
都是同一物件嗎?
obj = {
count: 3,
field: `field`,
method: function(){
function repeat(){
if(this.count > 100){
return this.field.repeat(this.count % 100);
}
this.field.repeat(this.count);
}.bind(this);
// 這個呢?
return repeat();
}
}