Javascript 中的上下文,我的認識的三個階段
來自: https://yq.aliyun.com/articles/4238
js 中的 上下文 Context ,可以說是一個,你即使不知道,沒聽過,也不影響你繼續寫 js 代碼的一個概念。但是,它又確確實實是無所不在的一個東西,是的,無所不在。
從我自己的經驗來看,對上下文的認識,算是分成了三個階段,每一個階段,都讓我從外在的表現中,理解了一些更本質上的東西。
第一階段,不知
我最開始接觸 js 的時候,看到了它的 new ,看到了它的 this ,很自然地會把 js 和其它的一些 OOP 語言等同起來看待,并且,好像,也是這么回事。比如:
var Class = function(a){
this.a = a;
this.add = function(){
this.a++;
}
}
var ins = new Class(1);
ins.add();
console.log(ins.a); //2</pre>
上面的代碼,可以按預期的那樣,最后得到 2 的輸出。
但是,如果僅僅是 類,實例 這種層面的認識,我無法解釋下面的問題:
var ins = new Class(1);
var func = ins.add;
func();
console.log(ins.a); //1
甚至解釋不清楚下面的代碼:
var obj = {
a: 1,
add: function(){
this.a++;
}
}
obj.add();
console.log(obj.a); //2
這里可沒有 類 ,也沒有 實例 。
我上面的最開始對 js 的認識當中,局限就在于,把 this 理解成了 實例 。也許在其它語言中(比如 Python 常用的實例方法第一個參數 self ),是這樣。但是在 js 中, this 跟 實例 完全沒有關系。
第二階段,this
當我明白問題出在 this 上,或者說,當我終于理解了 this 這個東西之后,上面的代碼,再也不會困擾我了。
我知道了, js 中有一個東西叫 上下文 ,可惜的是,這時,我對上下文的概念,僅僅停留在 this 上。
這時我的理解是: this 表示的是,函數調用時的 上下文 。
說得詳細一點,就是 this 不是表示的 實例 ,而是函數調用時的 上下文 。 上下文 這個東西,默認是 window ,即 全局 。但是,你可以明確地為函數指定一個 上下文 。回到 this 上,就是在定義時你根本不知道 this 是什么,因為在調用時,它可以是任何東西(因為 上下文 是可以人為指定的)。
回到剛開始的代碼:
var Class = function(a){
this.a = a;
this.add = function(){
this.a++;
}
}
var ins = new Class(1);
ins.add();
console.log(ins.a); //2</pre>
這段代碼的結構之所以是 2 ,不是因為 實例 ,而是因為 上下文 。
首先說一下 new 。 new 在 js 中,不考慮原型鏈它的作用,相當于是先創建了一個空的對象,然后把這個空的對象,作為 構造函數 的 上下文 ,再去執行 構造函數 ,最后再返回這個當初的空對象。即:
var what_new = function(func, a){
var context = {};
func.apply(context, [a]);
return context;
}
var Class = function(a){
this.a = a;
this.add = function(){
this.a++;
}
}
var ins = what_new(Class, 1);
ins.add();
console.log(ins.a);</pre>
當然, new 除了上面的 func.apply 的作用之外, 它還會處理原型鏈 ,這里就不介紹了。上面的代碼僅是為了說明 new 對于所謂的構造函數做了什么事。
有了上下文,就不難解釋 ins 這個東西了。所謂的構造函數,只是在指定了 this 到底是哪一個對象之后,作了相應的賦值操作而已,最后得到這個對象的返回,經過了一些賦值操作,對象中就有了新的東西了。
同樣,對于一個在定義時包含了 this 的函數,比如前面的例子:
var obj = {
a: 1,
add: function(){
this.a++;
}
}
如果來一句:
var func = obj.add; func(); //這里會作用到 window.a ,是一個未定義量
var other = {a: 0};
func.apply(other);
console.log(other.a); //1
這些都很容易明白了。 js 中的函數,都是一些很單純的函數,所有的函數跟它在哪里定義完全沒有關系(考慮閉包的情況除外)。所以上面的代碼,雖然 add 函數是寫在 obj 中的,但是,它跟你在 window 中寫一個函數是 完全一樣 的:
var add = function(){this.a++}
var obj = {
a: 1,
add: add
}
既然 add 函數中有 this ,那么這個函數執行時的行為,就要小心一點了。所以上面明確地指定了一個上下文給它 func.apply({a: 0}) 。
還是回到開始的代碼:
var obj = {
a: 1,
add: function(){
this.a++;
}
}
對于上面的代碼,我知道了:
obj.add();
和:
var func = obj.add();
func();
會得到不一樣的結果。并且知道,這個不一樣的結果是上下文引起的,還知道,后者 func() 執行時,上下文是全局的 window 了。
我雖然知道是這樣的一個情況,但是,為什么?執行同一個函數結果怎么就不一樣了呢?
我在很長時間里,都沒有去細細考慮過這個問題。不過,因為知道了“上下文是一個在定義時無意義,其具體值完全由執行時決定”這點之后,我都盡量避免去使用 this ,實在要用,在調用時,我都會通過 apply 或 call 明確指定上下文,這樣,至少不會踩坑里。
第三階段,一切都是上下文
某天,我在網上看到了這樣一段代碼(原始出處不知道):
var bind = Function.prototype.call.bind(Function.prototype.bind)
這個新定義的 bind 函數具體做什么事先不管它,我好奇的是 call.bind() 這個調用。因為 call 這個函數,之前一直以為它是 Function 對象的一個方法(它本身也是一個函數),但是,如果按“對象的方法”這個角度去想的話,那對它綁定一個上下文( bind() 的調用 )不就完全沒有意義了么?(因為對象的方法應該是跟上下文無關的)
后來看到了這篇文章, http://www.html-js.com/article/JavaScript-functional-programming-in-Javascript-Bind-Call-and-Apply
其中以 slice 函數舉的例子讓我恍然大悟:
- 上下文控制不僅僅是 apply / call ,所有的點 . ,都是在指定上下文。
- js 中的函數比我想像的還要純,根本沒有“對象中的方法”這個東西,即使是“原生對象”中。(它僅僅起一個名字空間的作用)
</ul>
所有的函數調用,都有兩層意義,比如 c.f() :
- f 這個函數,它在 c 中。(名字空間的問題)
- 把 c 作為 f 的上下文,去調用 f 。(前提是 f 沒有綁定過上下文)
</ul>
如果 c 沒有,則默認是 window 。
所有的,js 中所有的函數調用,都是如此。即使是 f.call(context, x) ,我之前只看到了第一層意義( f 中有一個 call 方法可以使用),則忽略了第二層意義 —— 把 f 作為 call 的上下文。
簡單來說,我們可以相像 call 這個函數,它的代碼大概是這樣的(可變參數的問題先不管):
var call = function(context, a){
var new_func = this.bind(context);
retur new_func(a);
}
它的作用,就是把 指定的上下文(context) 作為 自己的上下文(this) 的 上下文 ,然后再調用 自己的上下文(綁定上下文之后的 this) 。
上面一句話有些糾結哈,主要搞明白多種上下文的關系, f.call(context, x) 當中, 自己的上下文 上面是 f 。 指定的上下文 上面是 context 。
再看 f.call(context, x) 這個代碼,結合“函數是單純”這點,我想到,即使是原生對象的那些方法, 也不過是把一些單純的函數放到了 prototype 中而已 ,比如把 call 函數放到了 Function.prototype 當中。
至此,再看 c.f() , a.b.c() 這些,不要去想是調用 c 對象中的 f 方法(這么說沒錯,但是名字空間的問題是顯而易見的嘛),而是想成,調用時把 c 作為 f 的上下文。
好了,回到開始的那行例子:
var new_bind = Function.prototype.call.bind(Function.prototype.bind)
這個就非常好理解了(為了描述方便,我改成 new_bind 了),把 bind 作為上下文綁定到 call 中。
這里注意一下,綁定了上下文的 call 函數,還是 call 函數,但是 “此 call 已經非彼 call ” 了。
所以:
new_bind != Function.prototype.call
雖然調用形式上, new_bind 和 call 完全一樣,但是他們的上下文行為不一樣:
- call 是未綁定狀態,所以 f.call() 會在執行時把 f 作為上下文綁定到 call 函數中。
- new_bind 是已綁定狀態,所以 f.new_bind() 對 new_bind() 的執行完全沒影響。
</ul>
我們可以以這樣的流程來幫助我們理解:
new_bind => call => bind.call => bind.call(f, context) => f.bind(context)
一步一步解釋:
: new_bind => call
new_bind 在形式上就是 call 。
: call => bind.call
只是這個 call ,是指定了 bind 作為它的上下文的。既然是 bind 作為它的上下文,那我們可以寫成是 bind.call 的樣式。
: bind.call(f, context) => f.bind(context)
new_bind 的調用 new_bind(f, context) 就相當于是 bind.call(f, context) 。考慮 call 函數之前的行為: f.call(context, a) 是把 context 作為 f 的上下文,也就是 context.f(a) ,那么 bind.call(f, context) 對應的就是 f.bind(context) 。
: f.bind(context)
不用多說了吧,把 context 綁定到 f 上,返回一個綁定了上下文的新函數。
完全是最基本的代數推導嘛,形式上,上下文前置總是沒有問題的。
結語
我一直認同,要理解 js 的東西,從函數式語言入手,非常合適。硬要往面向對象的那套東西上套,太糾結了(我不管概念上到底什么樣才叫面向對象,原生沒有類定義,沒有繼承,沒有實例化,就別扯這些就完了。對了,我認為原型追溯那不叫繼承哈)。
當然,我不知道弄明白了最后那個“代數推導”到底有什么好處,也許沒有,因為就算不明白這些也不影響我寫了很多可以正常工作的 js 代碼嘛。只是,我以后再寫,思路上的可能會有一些不同了。比如代碼組織的形式上,可以嘗試把很多的小函數做到不同的“名字空間”中,然后再在業務層面,通過 Mixin 來拼出不同的業務對象。這些函數中可能到處充斥著 this ,我能控制好它們了。
</div>
</code></code></code></code></code></code></code></code></code></code></code></code></code></code></code></code>