## 2.2 全面解析this
### 2.2.1 調用位置
調用位置:調用位置就是函數在代碼中被調用的位置(而不是聲明的位置)。
最重要的是要分析調用棧(就是為了到達當前執行位置所調用的所有函數)。我們關心的調用位置就在當前正在執行的函數的前一個調用中。
~~~
function baz() {
// 當前調用棧是:baz
// 因此,當前調用位置是全局作用域
console.log("baz");
bar(); // <-- bar 的調用位置
}
function bar() {
// 當前調用棧是baz -> bar
// 因此,當前調用位置在baz 中
console.log("bar");
foo(); // <-- foo 的調用位置
}
function foo() {
// 當前調用棧是baz -> bar -> foo
// 因此,當前調用位置在bar 中
console.log("foo");
}
baz(); // <-- baz 的調用位置
~~~
### 2.2.2 綁定規則
**1. 默認綁定**
最常用的函數調用類型:**獨立函數調用。**
~~~
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
~~~
foo() 是直接使用不帶任何修飾的函數引用進行調用的,因此只能使用默認綁定,無法應用其他規則。函數調用時應用了this 的默認綁定,因此this 指向全局對象。
如果使用嚴格模式(strict mode),那么全局對象將無法使用默認綁定,因此this 會綁定到undefined:
~~~
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined
~~~
,雖然this 的綁定規則完全取決于調用位置,但是只有foo() 運行在非strict mode 下時,默認綁定才能綁定到全局對象;嚴格模式下與foo()的調用位置無關:
~~~
function foo() {
console.log( this.a );
}
var a = 2;
(function(){
"use strict";
foo(); // 2
})();
~~~
**2. 隱式綁定**
隱式綁定規則是**調用位置是否有上下文對象。**
~~~
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
~~~
當函數引用有上下文對象時,隱式綁定規則會把函數調用中的this 綁定到這個上下文對象。因為調用foo() 時this 被綁定到obj,因此this.a 和obj.a 是一樣的。
**對象屬性引用鏈中只有最頂層或者說最后一層會影響調用位置。**
~~~
function foo() {
console.log(this.a);
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
~~~
#### 隱式丟失
一個最常見的this 綁定問題就是被隱式綁定的函數會丟失綁定對象,也就是說它會應用默認綁定,從而把this 綁定到全局對象或者undefined 上,取決于是否是嚴格模式。
~~~
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函數別名!
var a = "oops, global"; // a 是全局對象的屬性
bar(); // "oops, global"
~~~
雖然bar 是obj.foo 的一個引用,但是實際上,它引用的是foo 函數本身,因此此時的bar() 其實是一個不帶任何修飾的函數調用,因此應用了默認綁定。
~~~
function foo() {
console.log(this.a);
}
function doFoo(fn) {
// fn 其實引用的是foo
fn(); // <-- 調用位置!
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局對象的屬性
doFoo(obj.foo); // "oops, global"
~~~
參數傳遞其實就是一種隱式賦值,因此我們傳入函數時也會被隱式賦值,所以結果和上一個例子一樣。
如果把函數傳入語言內置的函數而不是傳入你自己聲明的函數,結果是一樣的,沒有區別。
**3. 顯示綁定**
JavaScript可以使用函數的call(..) 和apply(..) 方法在某個對象上強制調用函數。
它們的第一個參數是一個對象,它們會把這個對象綁定到this,接著在調用函數時指定這個this。因為可以直接指定this 的綁定對象,所有稱之為顯式綁定。
~~~
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
foo.call( obj ); // 2
~~~
(1)硬綁定
~~~
function foo() {
console.log(this.a);
}
var obj = {
a: 2
};
var bar = function () {
foo.call(obj);
};
bar(); // 2
setTimeout(bar, 100); // 2
// 硬綁定的bar 不可能再修改它的this
bar.call(window); // 2
~~~
我們創建了函數bar(),并在它的內部手動調用了foo.call(obj),因此強制把foo 的this 綁定到了obj。無論之后如何調用函數bar,它總會手動在obj 上調用foo。這種綁定是一種顯式的強制綁定,因此稱之為**硬綁定**。
**硬綁定的典型應用場景**
* 創建一個包裹函數,傳入所有的參數并返回接收到的所有值:
~~~
function foo(something) {
console.log(this.a, something);
return this.a + something;
}
var obj = {
a: 2
};
var bar = function () {
return foo.apply(obj, arguments);
};
var b = bar(3); // 2 3
console.log(b); // 5
~~~
* 創建一個 i 可以重復使用的輔助函數:
~~~
function foo(something) {
console.log(this.a, something);
return this.a + something;
}
// 簡單的輔助綁定函數
function bind(fn, obj) {
return function () {
return fn.apply(obj, arguments);
};
}
var obj = {
a: 2
};
var bar = bind(foo, obj);
var b = bar(3); // 2 3
console.log( b ); // 5
~~~
由于硬綁定是一種非常常用的模式,所以在ES5 中提供了內置的方法`Function.prototype.bind`,它的用法如下:
~~~
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a:2
};
var bar = foo.bind( obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5
~~~
bind(..) 會返回一個硬編碼的新函數,它會把參數設置為this 的上下文并調用原始函數。
(2)API調用的“上下文”
第三方庫的許多函數,以及JavaScript 語言和宿主環境中許多新的內置函數,都提供了一個可選的參數,通常被稱為“上下文”(context),其作用和bind(..) 一樣,確保你的回調函數使用指定的this。
~~~
function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
};
// 調用foo(..) 時把this 綁定到obj
[1, 2, 3].forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome
~~~
**4. new綁定**
在JavaScript 中,構造函數只是一些使用new 操作符時被調用的函數。它們并不會屬于某個類,也不會實例化一個類。實際上,它們甚至都不能說是一種特殊的函數類型,它們只是被new 操作符調用的普通函數而已。
舉例來說,思考一下Number(..) 作為構造函數時的行為,ES5.1 中這樣描述它:
~~~
15.7.2 Number 構造函數
當Number 在new 表達式中被調用時,它是一個構造函數:它會初始化新創建的對象。
~~~
所以,包括內置對象函數(比如Number(..))在內的所有函數都可以用new 來調用,這種函數調用被稱為**構造函數調用**。這里有一個重要但是非常細微的區別:**實際上并不存在所謂的“構造函數”,只有對于函數的“構造調用”**。
使用new 來調用函數,或者說發生構造函數調用時,會自動執行下面的操作。
* 創建(或者說構造)一個全新的對象。
* 這個新對象會被執行[[ 原型]] 連接。
* 這個新對象會綁定到函數調用的this。
* 如果函數沒有返回其他對象,那么new 表達式中的函數調用會自動返回這個新對象。
~~~
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
~~~
使用new 來調用foo(..) 時,我們會構造一個新對象并把它綁定到foo(..) 調用中的this上。new 是最后一種可以影響函數調用時this 綁定行為的方法,稱之為new 綁定。
### 2.2.3 優先級
**隱式綁定和顯式綁定**哪個優先級更高?我們來測試一下:
~~~
function foo() {
console.log(this.a);
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call(obj2); // 3
obj2.foo.call(obj1); // 2
~~~
顯式綁定優先級更高,也就是說在判斷時應當先考慮是否可以應用顯式綁定。
現在我們需要搞清楚**new 綁定和隱式綁定**的優先級誰高誰低:
~~~
function foo(something) {
this.a = something;
}
var obj1 = {
foo: foo
};
var obj2 = {};
obj1.foo( 2 );
console.log( obj1.a ); // 2
obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4
~~~
可以看到new 綁定比隱式綁定優先級高。
~~~
function foo(something) {
this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar(3);
console.log( obj1.a ); // 2
console.log( baz.a ); // 3
~~~
bar 被硬綁定到obj1 上,但是new bar(3) 并沒有像我們預計的那樣把obj1.a修改為3。相反,new 修改了硬綁定(到obj1 的)調用bar(..) 中的this。因為使用了new 綁定,我們得到了一個名字為baz 的新對象,并且baz.a 的值是3。
* **為什么要在new 中使用硬綁定函數呢?**
之所以要在new 中使用硬綁定函數,主要目的是預先設置函數的一些參數,這樣在使用new 進行初始化時就可以只傳入其余的參數。bind(..) 的功能之一就是可以把除了第一個參數(第一個參數用于綁定this)之外的其他參數都傳給下層的函數(這種技術稱為“部分應用”,是“柯里化”的一種)。舉例來說:
~~~
function foo(p1,p2) {
this.val = p1 + p2;
}
// 之所以使用null 是因為在本例中我們并不關心硬綁定的this 是什么
// 反正使用new 時this 會被修改
var bar = foo.bind( null, "p1" );
var baz = new bar( "p2" );
baz.val; // p1p2
~~~
**判斷this**
現在我們可以根據優先級來判斷函數在某個調用位置應用的是哪條規則。可以按照下面的順序來進行判斷:
* 函數是否在new 中調用(new 綁定)?如果是的話this 綁定的是新創建的對象。
~~~
var bar = new foo()
~~~
* 函數是否通過call、apply(顯式綁定)或者硬綁定調用?如果是的話,this 綁定的是指定的對象。
~~~
var bar = foo.call(obj2)
~~~
* 函數是否在某個上下文對象中調用(隱式綁定)?如果是的話,this 綁定的是那個上下文對象。
~~~
var bar = obj1.foo()
~~~
* 如果都不是的話,使用默認綁定。如果在嚴格模式下,就綁定到undefined,否則綁定到全局對象。
~~~
var bar = foo()
~~~
### 2.2.4 綁定例外
**1. 被忽略的this**
如果你把null 或者undefined 作為this 的綁定對象傳入call、apply 或者bind,這些值在調用時會被忽略,實際應用的是默認綁定規則:
~~~
function foo() {
console.log( this.a );
}
var a = 2;
foo.call( null ); // 2
~~~
一種非常常見的做法是使用apply(..) 來“展開”一個數組,并當作參數傳入一個函數。類似地,bind(..) 可以對參數進行柯里化(預先設置一些參數),這種方法有時非常有用:
~~~
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 把數組“展開”成參數
foo.apply( null, [2, 3] ); // a:2, b:3
// 使用 bind(..) 進行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3
~~~
這兩種方法都需要傳入一個參數當作this 的綁定對象。如果函數并不關心this 的話,你仍然需要傳入一個占位值,這時null 可能是一個不錯的選擇,就像代碼所示的那樣。
~~~
在ES6 中,可以用... 操作符代替apply(..) 來“展開”數組,foo(...[1,2]) 和foo(1,2) 是一樣的,
這樣可以避免不必要的this 綁定。可惜,在ES6 中沒有柯里化的相關語法,因此還是需要使用bind(..)。
~~~
總是使用null 來忽略this 綁定可能產生一些副作用。如果某個函數確實使用了this(比如第三方庫中的一個函數),那默認綁定規則會把this 綁定到全局對象(在瀏覽器中這個對象是window),這將導致不可預計的后果(比如修改全局對象)。
#### 更安全的this
一種“更安全”的做法是傳入一個特殊的對象,把this 綁定到這個對象不會對你的程序產生任何副作用。我們可以創建一個“DMZ”(demilitarizedzone,非軍事區)對象——它就是一個空的非委托的對象。
無論你叫它什么,在JavaScript 中創建一個空對象最簡單的方法都是Object.create(null)。Object.create(null) 和{} 很像, 但是并不會創建Object.prototype 這個委托,所以它比{}“更空”:
~~~
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 我們的DMZ 空對象
var ? = Object.create( null );
// 把數組展開成參數
foo.apply( ?, [2, 3] ); // a:2, b:3
// 使用bind(..) 進行柯里化
var bar = foo.bind( ?, 2 );
bar( 3 ); // a:2, b:3
~~~
使用變量名? 不僅讓函數變得更加“安全”,而且可以提高代碼的可讀性,因為? 表示“我希望this 是空”,這比null 的含義更清楚。(不過你可以用任何喜歡的名字來命名DMZ 對象)
**2. 間接引用**
你有可能(有意或者無意地)創建一個函數的“間接引用”,在這種情況下,調用這個函數會應用默認綁定規則。
間接引用最容易在賦值時發生:
~~~
function foo() {
console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2
~~~
賦值表達式p.foo = o.foo 的返回值是目標函數的引用,因此調用位置是foo() 而不是p.foo() 或者o.foo(),應用默認綁定。
注意:對于默認綁定來說,決定this 綁定對象的并不是調用位置是否處于嚴格模式,而是函數體是否處于嚴格模式。如果函數體處于嚴格模式,this 會被綁定到undefined,否則 this 會被綁定到全局對象。
**3. 軟綁定**
硬綁定這種方式可以把this 強制綁定到指定的對象(除了使用new時),防止函數調用應用默認綁定規則。問題在于,硬綁定會大大降低函數的靈活性,使用硬綁定之后就無法使用隱式綁定或者顯式綁定來修改this
如果可以給默認綁定指定一個全局對象和undefined 以外的值,那就可以實現和硬綁定相同的效果,同時保留隱式綁定或者顯式綁定修改this 的能力。
~~~
if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this;
// 捕獲所有 curried 參數
var curried = [].slice.call( arguments, 1 );
var bound = function() {
return fn.apply(
(!this || this === (window || global)) ?
obj : this
curried.concat.apply( curried, arguments )
);
};
bound.prototype = Object.create( fn.prototype );
return bound;
};
}
~~~
除了軟綁定之外,softBind(..) 的其他原理和ES5 內置的bind(..) 類似。它會對指定的函數進行封裝,首先檢查調用時的this,如果this 綁定到全局對象或者undefined,那就把指定的默認對象obj 綁定到this,否則不會修改this。此外,這段代碼還支持可選的柯里化。
~~~
function foo() {
console.log("name: " + this.name);
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj
obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2
fooOBJ.call( obj3 ); // name: obj3
setTimeout( obj2.foo, 10 );
// name: obj <---- 應用了軟綁定
~~~
可以看到,軟綁定版本的foo() 可以手動將this 綁定到obj2 或者obj3 上,但如果應用默認綁定,則會將this 綁定到obj。
### 2.2.5 this詞法
ES6 中介紹了一種無法使用this規則的特殊函數類型:箭頭函數。
箭頭函數并不是使用function 關鍵字定義的,而是使用被稱為“胖箭頭”的操作符`=>` 定義的。箭頭函數不使用this 的四種標準規則,而是會繼承外層函數調用的this 綁定(無論this 綁定到什么)
~~~
function foo() {
// 返回一個箭頭函數
return (a) => {
//this 繼承自foo()
console.log( this.a );
};
}
var obj1 = {
a:2
};
var obj2 = {
a:3
};
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是3 !
~~~
foo() 內部創建的箭頭函數會捕獲調用時foo() 的this。由于foo() 的this 綁定到obj1,bar(引用箭頭函數)的this 也會綁定到obj1,箭頭函數的綁定無法被修改。(new 也不行!)
箭頭函數最常用于回調函數中,例如事件處理器或者定時器:
~~~
function foo() {
setTimeout(() => {
// 這里的this 在此法上繼承自foo()
console.log( this.a );
},100);
}
var obj = {
a:2
};
foo.call( obj ); // 2
~~~
箭頭函數可以像bind(..) 一樣確保函數的this 被綁定到指定對象,此外,其重要性還體現在它用更常見的詞法作用域取代了傳統的this 機制。實際上,在ES6 之前我們就已經在使用一種幾乎和箭頭函數完全一樣的模式。
~~~
function foo() {
var self = this; // lexical capture of this
setTimeout( function(){
console.log( self.a );
}, 100 );
}
var obj = {
a: 2
};
foo.call( obj ); // 2
~~~
雖然self = this 和箭頭函數看起來都可以取代bind(..),但是從本質上來說,它們想替代的是this 機制。
- 前言
- 第一章 JavaScript簡介
- 第三章 基本概念
- 3.1-3.3 語法、關鍵字和變量
- 3.4 數據類型
- 3.5-3.6 操作符、流控制語句(暫略)
- 3.7函數
- 第四章 變量的值、作用域與內存問題
- 第五章 引用類型
- 5.1 Object類型
- 5.2 Array類型
- 5.3 Date類型
- 5.4 基本包裝類型
- 5.5 單體內置對象
- 第六章 面向對象的程序設計
- 6.1 理解對象
- 6.2 創建對象
- 6.3 繼承
- 第七章 函數
- 7.1 函數概述
- 7.2 閉包
- 7.3 私有變量
- 第八章 BOM
- 8.1 window對象
- 8.2 location對象
- 8.3 navigator、screen與history對象
- 第九章 DOM
- 9.1 節點層次
- 9.2 DOM操作技術
- 9.3 DOM擴展
- 9.4 DOM2和DOM3
- 第十章 事件
- 10.1 事件流
- 10.2 事件處理程序
- 10.3 事件對象
- 10.4 事件類型
- 第十一章 JSON
- 11.1-11.2 語法與序列化選項
- 第十二章 正則表達式
- 12.1 創建正則表達式
- 12.2-12.3 模式匹配與RegExp對象
- 第十三章 Ajax
- 13.1 XMLHttpRequest對象
- 你不知道的JavaScript
- 一、作用域與閉包
- 1.1 作用域
- 1.2 詞法作用域
- 1.3 函數作用域與塊作用域
- 1.4 提升
- 1.5 作用域閉包
- 二、this與對象原型
- 2.1 關于this
- 2.2 全面解析this
- 2.3 對象
- 2.4 混合對象“類”
- 2.5 原型
- 2.6 行為委托
- 三、類型與語法
- 3.1 類型
- 3.2 值
- 3.3 原生函數