js 中的 上下文 Context,能夠說是一個,你即便不知道,沒聽過,也不影響你繼續寫 js 代碼的一個概念。可是,它又確確實實是無所不在的一個東西,是的,無所不在。 html
從我本身的經驗來看,對上下文的認識,算是分紅了三個階段,每個階段,都讓我從外在的表現中,理解了一些更本質上的東西。 閉包
我最開始接觸 js 的時候,看到了它的 new ,看到了它的 this ,很天然地會把 js 和其它的一些 OOP 語言等同起來看待,而且,好像,也是這麼回事。好比: app
var Class = function(a){ this.a = a; this.add = function(){ this.a++; } } var ins = new Class(1); ins.add(); console.log(ins.a); //2
上面的代碼,能夠按預期的那樣,最後獲得 2 的輸出。 函數
可是,若是僅僅是 類,實例 這種層面的認識,我沒法解釋下面的問題: this
var ins = new Class(1); var func = ins.add; func(); console.log(ins.a); //1
甚至解釋不清楚下面的代碼: spa
var obj = { a: 1, add: function(){ this.a++; } } obj.add(); console.log(obj.a); //2
這裏可沒有 類 ,也沒有 實例 。 prototype
我上面的最開始對 js 的認識當中,侷限就在於,把 this 理解成了 實例 。也許在其它語言中(好比 Python 經常使用的實例方法第一個參數 self),是這樣。可是在 js 中, this 跟 實例 徹底沒有關係。 code
當我明白問題出在 this 上,或者說,當我終於理解了 this 這個東西以後,上面的代碼,不再會困擾我了。 htm
我知道了, 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
這段代碼的結構之因此是 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);
固然, new 除了上面的 func.apply 的做用以外, 它還會處理原型鏈 ,這裏就不介紹了。上面的代碼僅是爲了說明 new 對於所謂的構造函數作了什麼事。
有了上下文,就不難解釋 ins 這個東西了。所謂的構造函數,只是在指定了 this 究竟是哪個對象以後,做了相應的賦值操做而已,最後獲得這個對象的返回,通過了一些賦值操做,對象中就有了新的東西了。
一樣,對於一個在定義時包含了 this 的函數,好比前面的例子:
var obj = { a: 1, add: function(){ this.a++; } }
若是來一句:
var func = obj.add; func(); //undefined func.apply({a: 0}) //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 函數舉的例子讓我恍然大悟:
全部的函數調用,都有兩層意義,好比 c.f():
若是 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 徹底同樣,可是他們的上下文行爲不同:
咱們能夠以這樣的流程來幫助咱們理解:
new_bind => call => bind.call => bind.call(f, context) => f.bind(context)
一步一步解釋:
new_bind => callnew_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 ,我能控制好它們了