this
說到this,需要明確三方面內容:
- this何時被賦值
- this被賦了什麼值
- 內置函數如何使用this的
this何時被賦值
進入函數代碼
當控制流根據一個函數對象 F、調用者提供的 thisArg 以及調用者提供的 argumentList,進入函數代碼的執行環境時,執行以下步驟:
- 如果函數代碼是嚴格模式下的代碼,設 this 綁定 爲 thisArg。
- 否則如果 thisArg 是 null 或 undefined ,則設 this 綁定爲全局對象。
- ……
以上信息來源:進入函數代碼
從上訴信息中可以知道:
- this 與調用者提供的 thisArg 密切相關
- this 在嚴格模式下爲 thisArg
- this 在非嚴格模式下爲 thisArg 或 全局對象
那麼 thisArg 又是怎麼來的呢?下面來看下函數調用過程:
函數調用
- 令 ref 爲解釋執行 MemberExpression 的結果。
- 令 func 爲 GetValue(ref)。
- 令 argList 爲解釋執行 Arguments 的結果,產生參數值們的內部列表(參見 11.2.4)。
- 如果 Type(func) 不是 Object,拋出一個 TypeError 異常。
- 如果 IsCallable(func) 爲 false,拋出一個 TypeError 異常。
-
如果 Type(ref) 爲 Reference,那麼
-
如果 IsPropertyReference(ref) 爲 true,
- 那麼令 thisValue 爲 GetBase(ref)。
-
否則,ref 的基值是一個環境記錄項。
- 令 thisValue 爲調用 GetBase(ref) 的 ImplicitThisValue 具體方法的結果。
-
-
否則,Type(ref) 不是 Reference。
- 令 thisValue 爲 undefined。
- 返回調用 func 的 [[Call]] 內置方法的結果,傳入 thisValue 作爲 this 值和列表 argList 作爲參數列表。
以上信息來源:函數調用
從上訴信息中可以知道:
- thisArg 即 thisValue
- thisValue 與 ref 的類型密切相關
-
如果 ref 的類型是 Reference(引用規範類型)
- 如果 ref 是屬性引用,通過 GetBase(ref)(返回引用值ref的基值部分) 獲取 thisValue
- 否則,通過 ImplicitThisValue 方法獲取 thisValue
- 否則,thisValue 爲 undefined
那麼,瞭解到這裏可能有許多新的疑問,比如:
- Reference 是怎樣的類型
- ref 是怎麼來的
- ref 什麼時候是 Reference,什麼時候不是。
- GetBase(ref) 和 ImplicitThisValue 是如何產生結果的
Reference 是怎樣的類型
首先先解釋下 Reference。
其實ES中的類型分爲ECMAScript語言類型和規範類型
ECMAScript語言類型對應的是程序員使用 ECMAScript 語言直接操作的值,如 Undefined、Null、Boolean、String、Number、Object等。
規範類型可用來描述 ECMAScript 表達式運算的中間結果,但這樣的值不能儲存爲對象的屬性或 ECMAScript 語言的變量值。引用尤雨溪的解釋:
這裏的 Reference 是一個 Specification Type,也就是 “只存在於規範裏的抽象類型”。它們是爲了更好地描述語言的底層行爲邏輯才存在的,但並不存在於實際的 js 代碼中。
ref 是怎麼來的
從上訴函數調用中可以知道,ref 是解釋執行 MemberExpression 的結果。
下面詳細看下 MemberExpression 的解析過程:
產生式 CallExpression : MemberExpression Arguments 按照下面的過程執行 :
- 令 baseReference 爲解釋執行 MemberExpression 的結果。
- 令 baseValue 爲 GetValue(baseReference)。
- 令 propertyNameReference 爲解釋執行 Expression 的結果。
- 令 propertyNameValue 爲 GetValue(propertyNameReference)。
- 調用 CheckObjectCoercible(baseValue)。
- 令 propertyNameString 爲 ToString(propertyNameValue)。
- 如果正在執行中的語法產生式包含在嚴格模式代碼當中,令 strict 爲 true,否則令 strict 爲 false。
- 返回一個值類型的引用,其基值爲 baseValue 且其引用名爲 propertyNameString,嚴格模式標記爲 strict。
從上訴信息中分析可以知道:
- 解釋執行 MemberExpression 的結果是一個引用規範類型(Reference)
-
這個引用規範類型包含三部分信息:
- baseValue
- propertyNameString
- strict
- thisValue 的值取決於 baseValue
- baseValue 是調用 GetValue 獲得的。
- GetValue 得到的基值是 undefined、Object、Boolean、String、Number、環境記錄項中的任意一個(詳見:引用規範類型),而不是引用規範類型。
GetValue 詳細過程見:GetValue
ref 什麼時候是 Reference,什麼時候不是 Reference
一般來說,ref 是MemberExpression解析的結果,都將是 Reference。
但是,如果 MemberExpression 是其函數表達式的一部分,則可能將改變最終解析結果的類型。
而改變解析結果類型的主要原因取決於是否調用了 GetValue 方法,如果調用了 GetValue ,函數中間值 ref 將是 Object 類型。
那麼哪些表達式不使用 GetValue 呢?
- 標識符引用 : 標識符執行的結果總是一個 Reference 類型的值。
- 羣組表達式:本算法不在執行 Expression 後使用 GetValue。這主要的目的是讓 delete 與 typeof 運算符可以作用在被括號括起來的表達式。
- 成員表達式
- 調用表達式
更多表達式詳見:表達式
GetBase(ref) 和 ImplicitThisValue 是如何產生結果的
- GetBase:返回引用值ref的基值部分
- ImplicitThisValue : 聲明式環境記錄項永遠將 undefined 作爲其 ImplicitThisValue 返回。
this被賦了什麼值
其實在 this何時被賦值 部分已經介紹了 this被賦了什麼值。下面總結三種賦值過程:
第一種this賦值過程:
var v = 1;
var obj = {
v: 2,
fn: function(){
console.log(this.v);
}
}
obj.fn(); // 2
(obj.fn)(); // 2
- 調用表達式解析 obj.fn ,返回引用規範類型,baseValue 爲 obj。
- 函數調用,由於 1 過程返回的爲引用規範類型,且爲屬性引用,調用 GetBase 將 baseValue (obj) 作爲返回值,返回給 thisValue。
- 進入函數代碼,thisArg 爲 obj,將其賦值給 this。
如果對步驟1中,baseValue 爲 obj 有疑問,詳見屬性訪問, 產生式 MemberExpression : MemberExpression [ Expression ] 執行過程。
相當於有兩個過程:
- 解析obj,baseValue 爲聲明式環境記錄項。
- 解析obj.fn,baseValue 爲 obj。
第二種this賦值過程:
function foo(){
console.log(this);
}
foo(); // Window
- 調用表達式解析 foo, 返回引用規範類型,baseValue 爲聲明式環境記錄項(函數聲明時綁定)。
- 函數調用,由於 1 過程返回的爲引用規範類型,且不爲屬性引用,調用 ImplicitThisValue 方法,返回 undefined 。
- 進入函數代碼,thisArg 爲 undefined,非嚴格模式下將 this 賦值爲全局對象。
第三種this賦值過程:
var v = 1;
var obj = {
v: 2,
fn: function(){
console.log(this.v);
}
}
var fn2 = obj.fn;
fn2(); // 1
(obj.fn, obj.fn)(); // 1
- 調用表達式解析 fn2,(obj.fn, obj.fn) ,由於賦值表達式、逗號表達式都使用了 GetValue 方法,返回函數(Object 類型)。
- 函數調用,由於 1 過程返回的不是引用規範類型,所以 thisValue 爲 undefined`。
- 進入函數代碼,thisArg 爲 undefined,非嚴格模式下將 this 賦值爲全局對象。
內置函數如何使用this的
內置函數修改 this 是通過給 func 的 [[Call]] 內置方法傳遞 thisArg 來實現的。
內置方法
- Function.prototype.apply (thisArg, argArray)
- Function.prototype.call (thisArg [ , arg1 [ , arg2, … ] ] )
- Function.prototype.bind (thisArg [, arg1 [, arg2, …] ] )
- Array.prototype.every ( callbackfn [ , thisArg ] )
- Array.prototype.some ( callbackfn [ , thisArg ] )
- Array.prototype.forEach ( callbackfn [ , thisArg ] )
- Array.prototype.map ( callbackfn [ , thisArg ] )
- Array.prototype.filter ( callbackfn [ , thisArg ] )
- new 運算符 使用內置方法 [[Construct]] 實現this指定
內置方法模擬實現
使用成員表達式模擬內置方法[[Call]]的效果:
apply
Function.prototype.apply_ = function (context, arr) {
var context = Object(context) || window;
var result;
// 臨時記錄需要調用的function
context.fn = this;
if (!arr) {
result = context.fn();
}
else {
var args = [];
for (var i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
// 使用成員表達式指定context.fn執行時this爲context
result = eval('context.fn(' + args + ')')
}
delete context.fn
return result;
}
call
Function.prototype.call_ = function (context) {
var context = context || window;
context.fn = this;
var args = [];
// 獲取參數列表
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
// 使用成員表達式指定context.fn執行時this爲context
var result = eval('context.fn(' + args +')');
delete context.fn
return result;
}
bind
Function.prototype.bind_ = function (context) {
// 記錄bind的函數
var self = this;
var args = [];
// 獲取綁定的參數列表
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
// 創建新函數
var fbound = function () {
// 獲取未綁定的參數列表
var bindArgs = Array.prototype.slice.call(arguments);
// fbound被當做構造函數使用,this指向實例。否則,指向 context
self.apply(this instanceof self ? this : context, args.concat(bindArgs));
}
// 維護原型關係
fbound.prototype = self.prototype || new Function().prototype ;
return fbound;
}