Javascript 中的上下文

js 中的 上下文 Context,能夠說是一個,你即便不知道,沒聽過,也不影響你繼續寫 js 代碼的一個概念。可是,它又確確實實是無所不在的一個東西,是的,無所不在。 html

從我本身的經驗來看,對上下文的認識,算是分紅了三個階段,每個階段,都讓我從外在的表現中,理解了一些更本質上的東西。 閉包

1. 第一階段,不知

我最開始接觸 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

2. 第二階段,this

當我明白問題出在 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 明確指定上下文,這樣,至少不會踩坑裏。

3. 第三階段,一切都是上下文

某天,我在網上看到了這樣一段代碼(原始出處不知道):

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 中的函數比我想像的還要純,根本沒有「對象中的方法」這個東西,即便是「原生對象」中。(它僅僅起一個名字空間的做用)

全部的函數調用,都有兩層意義,好比 c.f():

  • f 這個函數,它在 c 中。(名字空間的問題)
  • 把 c 做爲 f 的上下文,去調用 f 。(前提是 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 徹底同樣,可是他們的上下文行爲不同:

  • call 是未綁定狀態,因此 f.call() 會在執行時把 f 做爲上下文綁定到 call 函數中。
  • new_bind 是已綁定狀態,因此 f.new_bind() 對 new_bind() 的執行徹底沒影響。

咱們能夠以這樣的流程來幫助咱們理解:

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 上,返回一個綁定了上下文的新函數。

徹底是最基本的代數推導嘛,形式上,上下文前置老是沒有問題的。

4. 結語

我一直認同,要理解 js 的東西,從函數式語言入手,很是合適。硬要往面向對象的那套東西上套,太糾結了(我無論概念上到底什麼樣才叫面向對象,原生沒有類定義,沒有繼承,沒有實例化,就別扯這些就完了。對了,我認爲原型追溯那不叫繼承哈)。

固然,我不知道弄明白了最後那個「代數推導」到底有什麼好處,也許沒有,由於就算不明白這些也不影響我寫了不少能夠正常工做的 js 代碼嘛。只是,我之後再寫,思路上的可能會有一些不一樣了。好比代碼組織的形式上,能夠嘗試把不少的小函數作到不一樣的「名字空間」中,而後再在業務層面,經過Mixin 來拼出不一樣的業務對象。這些函數中可能處處充斥着 this ,我能控制好它們了

相關文章
相關標籤/搜索