Javascript 中的上下文,我的認識的三個階段

uybv0087 8年前發布 | 9K 次閱讀 JavaScript開發 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>

 本文由用戶 uybv0087 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!