this

this

  • 一句話說清楚:this 是 runtime binding

    this 沒有取得函式自身(function itself)的參考,this 與語彙範疇(lexial scope) 的 look-up 之間也沒有參考

    • this 的機制有點像動態範疇(dynamic scope),所以會和此前提到的 lexical scope 觀念不一樣

  • 以下內容幾乎全出自《你所不知道的 JS-範疇與Closures,this與物件原型》 https://github.com/getify/You-Dont-Know-JS/blob/master/this%20%26%20object%20prototypes/ch2.md,若時間充足可直接閱讀

  • 規則

  • /******************************************/
    // 規則1,預設繫結
    function foo() {
      console.log(this.a);
    }
    
    var a = 2;
    
    foo();
    
    // foo(); 的結果會是 2,因為 this 的預設繫結(default binding)指向全域物件(global object)
    // 順帶一提,var a = 2; 完全等同於 global object 的 property
    
    // 規則1 之 strict mode
    function foo() {
      "use strict";
      console.log(this.a);
    }
    
    var a = 2;
    
    foo();
    
    // foo(); 的結果會是 TypeError: Cannot read property 'a' of undefined
    
    // 規則1 之 呼叫地點使用 strict mode 並不會影響預設繫結
    function foo() {
      console.log(this.a);
    }
    
    var a = 2;
    
    (function() {
      "use strict";
      foo();
    }();
    
    // foo(); 的結果依然會是 2,不受呼叫地點使用 strict mode 的影響
    
    /******************************************/
    // 規則2,隱含繫結(implicit binding)
    function foo() {
      console.log(this.a);
    }
    
    var obj2 = {
      a: 42,
      foo: foo
    };
    
    var obj1 = {
      a: 2,
      obj2: obj2
    };
    
    obj1.obj2.foo();
    
    // obj1.obj2.foo(); 的結果會是 42
    
    // 規則2,隱含繫結常常失敗,請件以下三個範例
    // 失敗例子1
    function foo() {
      console.log(this.a);
    }
    
    var obj = {
      a: 2,
      foo: foo
    }
    
    var boo = obj.foo; // 這叫做函式參考(reference),或稱別名(alias)
    
    var a = "crycry, global"
    
    boo();
    
    // boo(); 的結果會是 crycry, global
    
    // 失敗例子2
    function foo() {
      console.log(this.a);
    }
    
    function doFoo(fn) {
      fn();
    }
    
    var obj = {
      a: 2,
      foo: foo
    }
    
    var a = "crycry, global"
    
    doFoo(obj.foo); // 改成傳遞 callback 到自訂函式裡,結果會是?
    // 結果會是 crycry, global
    
    // 失敗例子3
    function foo() {
      console.log(this.a);
    }
    
    var obj = {
      a: 2,
      foo: foo
    }
    
    var a = "crycry, global"
    
    setTimeout(obj.foo, 100);
    
    // 結果會是 crycry, global
  • 以上為規則1 與規則2 的示範,其中規則2 的三個錯誤例子是非常經典、常發生的錯誤寫法,務必熟悉

  • 規則

  • /******************************************/
    // 規則3,明確繫結(explicit binding)
    // 作法1,call(..) 和 apply(..)
    function foo() {
      console.log(this.a);
    }
    
    var obj = {
      a: 2
    };
    
    foo.call(obj);
    
    // 結果會是 2
    
    // 作法1 的變體,硬繫結(hard binding)
    function foo() {
      console.log(this.a);
    }
    
    var obj = {
      a: 2
    };
    
    var bar = function() {
      foo.call(obj);
    };
    
    bar();
    setTimeout(bar, 100);
    bar.call(window);
    
    // bar(); 和 setTimeout(bar, 100); 的結果都會是 2,不意外
    // bar.call(window); 的結果也是 2,超硬!
    
    // 作法2,由於作法1 的變體硬繫結很常見,所以 ES5 有提供,Function.prototype.bind
    function foo(something) {
      console.log(this.a, something);
      return this.a + something;
    }
    
    var obj = {
      a: 2
    };
    
    var bar = foo.bind(obj);
    
    var b = bar(3);
    console.log(b);
    
    // var b = bar(3); 的結果是 2 3
    // console.log(b); 的結果是 5
  • call(..) 與 apply(..)

    • 兩者的差別僅在可帶入的參數與型態,call 可接受一個一個的複數參數,apply 接受的則是一個 array(或是 array-like)參數

    • 實作上常用來「借 method」

    • Partial Application vs. Currying(Functional Programming)

    • // Source: https://gist.github.com/Integralist/054e34983e8680c506c3
      // Source: http://www.datchley.name/currying-vs-partial-application
      
      /******************************************/
      // Partial Application 例子
      
      // partial is a made up function
      fn = function (a, b, c) { return a + b + c }
      foo = partial(fn, 'x', 'y')
      foo('z') // => 'xyz'
      
      // 然後 it's possible to change the arguments we partially apply:
      fn = function (a, b, c) { return a + b + c }
      foo = partial(fn, 'x')
      foo('y', 'z') // => 'xyz'
      
      // Note how with Partial Application we make a function call twice (once when partially applying the arguments; and again when we fulfill the rest of the arguments).
      // But remember: we can choose how many arguments we partially apply on the first call.
      
      /******************************************/
      // Currying 例子
      
      // curry is a made up function
      fn = function (a, b, c) { return a + b + c }
      foo = curry(fn) foo('x')('y')('z') // => 'xyz'
      
      // 然後
      fn = function (a, b, c) { return a + b + c }
      foo = curry(fn)
      bar = foo('x') bar('y')('z') // => 'xyz'
      
      // Note that a function that has been curried wont return the value of the function until each argument has been provided (i.e. satisfied).
      // The arguments are manually partially applied one by one
      
      /******************************************/
      // 例子:functional programming 的實際應用場景
      
      // 1. 一個常見的 array,裡面都是物件
      var records = [
        { id: 1, name: 'Dave', age: 40, active: true },
        { id: 2, name: 'Kurt', age: 43, active: false },
        { id: 3, name: 'Beth', age: 28, active: true },
        { id: 4, name: 'Angie', age: 39, active: true },
        { id: 5, name: 'Adam', age: 34, active: false }
      ];
      
      // 2. 常見的處理,用 map 取出 id
      var ids = records.map(function(rec){ return rec.id; });
      
      // 2-1. 因為是很常見的處理,所以我們可以透過 currying 讓程式可重覆使用
      var getProp = curry(function(prop, obj) { return obj[prop]; });
      var ids = records.map(getProp('id'));
      
      // 3. 更進一步,我們想讓 Array.prototype.map function 更有用,讓它可以被 partially apply a predicate function to it before giving it any data
      var mapWith = curry(function(fn, list) { return list.map(fn); });
      var getNames = mapWith(getProp('name'));
      
      var names = getNames(records);
      
      // 4. 特殊技巧,composition,把它們都串起來
      // And if you bring in another functional programming technique called composition, along with the idea of building up smaller, reusable functions;
      // you can build complex chains from those pieces with ease:
      
      var filterBy = curry(function(fn, list){ return list.filter(fn); });
      var isActive = filterBy(function(o){ return o.active == true; });
      var isUnder40 = filterBy(function(o){ return o.age < 40; });
      var getNames = mapWith(getProp('name'));
      
      function compose() {
        var args = [].slice.call(arguments),
          fn = args.shift(),
          gn = args.shift(),
          fog = gn ? function() { return fn(gn.apply(this, arguments)); } : fn;
      
        return args.length ? compose.apply(this, [fog].concat(args)) : fog;
      }
      
      var getActiveUnder40 = compose(getNames, isActive, isUnder40);
      
      getActiveUnder40(records);
      // 上行的結果會是 ['Beth', 'Angie']

      http://raganwald.com/2013/03/07/currying-and-partial-application.html(文章日期:2013.03.07)

  • 規則

  • /******************************************/
    // 規則4,new binding
    function foo(a) {
      this.a = a;
    }
    
    var bar = new foo(2);
    console.log(bar.a); 
    
    // console.log(bar.a); 的結果會是 2
  • JavaScript 的 new 和你在類別導向(class-oriented)語言的運作方式不一樣

    • 其實 JavaScript 什麼都和別人不一樣...,見【物件(Object) 與原型(Prototype)】段落

  • 一些醜陋的事實:bind(..) 的 polyfill 與 ES5 的 bind(..),實際上有些不一樣,這會導致他們在與規則4,new binding 比較優先順序時,結果會不同。bind(..) 的 polyfill 不會被 new binding OVERRIDE

  • 規則優先順序:規則4,new binding > 規則3,明確繫結(explicit binding) > 規則2,隱含繫結(implicit binding) > 規則1,預設繫結

    • 一些例外,以及容易出錯的例子

    • /******************************************/
      // 下面兩個是實作時常見的手法
      function foo(a,b) {
        console.log("a:" + a + ", b:" + b);
      }
      
      // 把 array 的每個元素當作 parameter 灑進去
      // 順帶一提,ES6 已經有 spread syntax 這個功能了,MDN 的介紹:https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Operators/Spread_operator
      foo.apply(null, [2, 3]);
      // 這行的結果是,a:2, b:3
      
      // currying with bind(..)
      var bar = foo.bind(null, 2);
      bar(3);
      // bar(3); 的結果是,a:2, b:3

      上面這兩個傳 null 的作法,會讓 this 直接套用預設繫結,那麼就滿有可能直接參考到全域物件

    • 間接參考(indirect references)也會讓 this 直接套用預設繫結

    • /******************************************/
      // 間接參考也會讓 this 直接套用預設繫結
      function foo() {
        console.log(this.a);
      }
      
      var a = 2;
      var o = {a: 3, foo: foo};
      var p = {a: 4};
      
      o.foo();
      // 這行的結果是 3
      
      (p.foo = o.foo)();
      // 這行的結果是 2

      而 ES6 的箭號函式(arrow function),完全不管規則1、2、3、4,只走 lexical scope 的規則

      /******************************************/
      function foo() {
        return (a) => {
          console.log(this.a); // 只走 lexical scope,所以會連到 foo() 的 this
        };
      }
      
      var obj1 = {
        a: 2
      };
      
      var obj2 = {
        a: 3
      };
      
      var bar = foo.call(obj1);
      
      bar.call(obj2);
      // 這行的結果是 2。並不會被 obj2 改動
    • 而且就算用 new 也無法 OVERRIDE,上面 ES6 範例的作法,和下面的傳統作法是一樣的

    • /******************************************/
      function foo() {
        var self = this; // lexical capture of this
        setTimeout(function() {
          console.log(self.a);
        }, 100);
      }
      
      var obj = {
        a: 2
      };
      
      foo.call(obj);
      // 這行的結果是 2

      補充閱讀資訊

  • 總結,到底是要用 this 風格,還是要用 lexical scope,請決定好;兩種不同的機制混用只會讓人變成 bug 製造機

Last updated