2014
Mar
06

ECMAScript 5's Function.prototype.bind is a great tool that's implemented in all modern browser JavaScript engines. It allows you to modify the context, this, of a function when it is evaluated in the future.

Here's a common use case that developers need to watch for. Turns out that since we added the event listener to the window object, this in the event handler or callback refers to window.

this refers to Window object
  1. var UI = {
  2. element: document.getElementById("container"),
  3. bindEvent: function () {
  4. console.log("**** UI.bindEvent() ****");
  5. console.log(this); // this refers to Window object.
  6. },
  7. syncData: function(callback) {
  8. console.log("**** UI.syncData() ****");
  9. console.log(this);
  10.  
  11. if (callback) {
  12. callback();
  13. }
  14. },
  15. render: function () {
  16. console.log("**** UI.render() ****");
  17.  
  18. this.syncData(this.bindEvent);
  19. }
  20. };
  21.  
  22. UI.render();
this refers to Window object
  1. var UI = {
  2. element: document.getElementById("container"),
  3. bindEvent: function () {
  4. console.log("**** UI.bindEvent() ****");
  5. console.log(this);
  6. },
  7. syncData: function(callback) {
  8. console.log("**** UI.syncData() ****");
  9. console.log(this);
  10.  
  11. if (callback) {
  12. callback();
  13. }
  14. },
  15. remove: function (callback) {
  16. console.log("**** UI.remove() ****");
  17. console.log(this);
  18.  
  19. this.element.innerHTML = "";
  20. if (callback) {
  21. callback();
  22. }
  23. },
  24. reRender: function () {
  25. console.log("**** UI.reRender() ****");
  26.  
  27. this.remove(function () {
  28. console.log(this); // this refers to Window object.
  29. this.syncData(); // Uncaught TypeError: Object [object global] has no method 'syncData'
  30. this.bindEvent(); // Uncaught TypeError: Object [object global] has no method 'bindEvent'
  31. });
  32. }
  33. };
  34.  
  35. UI.reRender();

Another example below:

Function constructor
  1. function UI(element) {
  2. var privateMethod = function(element) {
  3. console.log("**** privateMethod ****");
  4. console.log(this); // this refers to Window object.
  5. console.log(element);
  6. };
  7.  
  8. this.publicMethod = function() {
  9. console.log("**** publicMethod ****");
  10. console.log(this); // this refers to UI constructor.
  11. };
  12.  
  13. element.onclick = function () {
  14. console.log("**** element.onclick ****");
  15. console.log(this); // this refers to element container.
  16. };
  17.  
  18. privateMethod(element);
  19.  
  20. setTimeout(function () {
  21. console.log("**** setTimeout ****");
  22. console.log(this); // this refers to Window object.
  23. }, 100);
  24. }
  25.  
  26. var elem = document.getElementById("container");
  27. var ui = new UI(elem);
  28. ui.publicMethod();

Solutions

The first time you hit upon the problem, you might be inclined to set this to a variable that you can reference when you change context. Many people opt for self, _this or sometimes context as a variable name.

Binding with var self = this

Set this to a variable
  1. var UI = {
  2. element: document.getElementById("container"),
  3. bindEvent: function () {
  4. console.log("**** UI.bindEvent() ****");
  5. console.log(this); // this refers to UI object.
  6. },
  7. syncData: function(callback) {
  8. console.log("**** UI.syncData() ****");
  9. console.log(this); // this refers to UI object.
  10.  
  11. if (callback) {
  12. callback();
  13. }
  14. },
  15. remove: function (callback) {
  16. console.log("**** UI.remove() ****");
  17. console.log(this);
  18.  
  19. this.element.innerHTML = "";
  20. if (callback) {
  21. callback();
  22. }
  23. },
  24. reRender: function () {
  25. console.log("**** UI.reRender() ****");
  26.  
  27. // Method One: store this to variable self.
  28. var self = this;
  29. this.remove(function () {
  30. self.syncData();
  31. self.bindEvent();
  32. });
  33. }
  34. };
  35.  
  36. UI.reRender();
Function constructor
  1. function UI(element) {
  2. var self = this;
  3.  
  4. var privateMethod = function(element) {
  5. console.log("**** privateMethod ****");
  6. console.log(this); // this refers to Window object.
  7. console.log(self); // this refers to UI constructor.
  8. console.log(element);
  9. };
  10.  
  11. this.publicMethod = function() {
  12. console.log("**** publicMethod ****");
  13. console.log(this); // this refers to UI constructor.
  14. };
  15.  
  16. element.onclick = function () {
  17. console.log("**** element.onclick ****");
  18. console.log(this); // this refers to element container.
  19. console.log(self); // this refers to UI constructor.
  20. };
  21.  
  22. privateMethod(element);
  23.  
  24. setTimeout(function () {
  25. console.log("**** setTimeout ****");
  26. console.log(this); // this refers to Window object.
  27. console.log(self); // this refers to UI constructor.
  28. }, 100);
  29. }
  30.  
  31. var elem = document.getElementById("container");
  32. var ui = new UI(elem);
  33. ui.publicMethod();

Function.prototype.bind

They're all usable and nothing is wrong with doing that, but there is a better, dedicated way. The fix we need is Function.prototype.bind().

Function.prototype.bind
  1. var UI = {
  2. element: document.getElementById("container"),
  3. bindEvent: function () {
  4. console.log("**** UI.bindEvent() ****");
  5. console.log(this); // this refers to UI object.
  6. },
  7. syncData: function(callback) {
  8. console.log("**** UI.syncData() ****");
  9. console.log(this); // this refers to UI object.
  10.  
  11. if (callback) {
  12. callback();
  13. }
  14. },
  15. remove: function (callback) {
  16. console.log("**** UI.remove() ****");
  17. console.log(this); // this refers to UI object.
  18.  
  19. this.element.innerHTML = "";
  20. if (callback) {
  21. callback();
  22. }
  23. },
  24. render: function () {
  25. console.log("**** UI.render() ****");
  26.  
  27. this.syncData(this.bindEvent.bind(this));
  28. },
  29. reRender: function () {
  30. console.log("**** UI.reRender() ****");
  31.  
  32. // Method Two
  33. this.remove(function () {
  34. this.syncData();
  35. this.bindEvent();
  36. }.bind(this));
  37. }
  38. };
  39.  
  40. UI.render();
  41. UI.reRender();
Event Handler
  1. var UI = {
  2. element: document.getElementById("container"),
  3. bindEvent: function () {
  4. console.log("**** UI.bindEvent() ****");
  5. console.log(this);
  6.  
  7. /*
  8. this.element.onclick = function (e) {
  9. console.log("**** this.element.onclick ****");
  10. console.log(this); // this refers to element container.
  11. };
  12. */
  13.  
  14. this.element.onclick = function (e) {
  15. console.log("**** this.element.onclick ****");
  16. console.log(this); // this refers to UI object.
  17. }.bind(this);
  18. },
  19. syncData: function(callback) {
  20. console.log("**** UI.syncData() ****");
  21. console.log(this); // this refers to UI object.
  22.  
  23. if (callback) {
  24. callback();
  25. }
  26. },
  27. remove: function (callback) {
  28. console.log("**** UI.remove() ****");
  29. console.log(this); // this refers to UI object.
  30.  
  31. this.element.innerHTML = "";
  32. if (callback) {
  33. callback();
  34. }
  35. },
  36. render: function () {
  37. console.log("**** UI.render() ****");
  38.  
  39. this.syncData(this.bindEvent.bind(this));
  40. },
  41. reRender: function () {
  42. console.log("**** UI.reRender() ****");
  43.  
  44. // Method Two
  45. this.remove(function () {
  46. this.syncData();
  47. this.bindEvent();
  48. }.bind(this));
  49. }
  50. };
  51.  
  52. UI.render();
  53. UI.reRender();
Function constructor
  1. Function.prototype.bind = Function.prototype.bind || function bind(func, fixThis) {
  2. console.log("**** bind ****");
  3.  
  4. return function () {
  5. return func.apply(fixThis, arguments);
  6. }
  7. };
  8.  
  9. function UI(element) {
  10. var privateMethod = function(element) {
  11. console.log("**** privateMethod ****");
  12. console.log(this); // this refers to UI constructor.
  13. console.log(element);
  14. }.bind(this);
  15.  
  16. this.publicMethod = function() {
  17. console.log("**** publicMethod ****");
  18. console.log(this); // this refers to UI constructor.
  19. };
  20.  
  21. element.onclick = function () {
  22. console.log("**** element.onclick ****");
  23. console.log(this); // this refers to UI constructor.
  24. }.bind(this);
  25.  
  26. privateMethod(element);
  27.  
  28. setTimeout(function () {
  29. console.log("**** setTimeout ****");
  30. console.log(this); // this refers to UI constructor.
  31. }.bind(this), 100);
  32. }
  33.  
  34. var elem = document.getElementById("container");
  35. var ui = new UI(elem);
  36. ui.publicMethod();

If you're interested to see what Function.prototype.bind() might look like and what its doing internally, here is a very simple example:

Example
  1. Function.prototype.bind = Function.prototype.bind || function bind(func, fixThis) {
  2. console.log("**** bind ****");
  3.  
  4. return function () {
  5. return func.apply(fixThis, arguments);
  6. }
  7. };

Browser Support

Desktop

Chrome Firefox (Gecko) Internet Explorer Opera Safari
7 4.0 (2) 9 11.60 5.14

Mobile

Android Chrome for Android Firefox Mobile (Gecko) IE Mobile Opera Mobile Safari Mobile
4.0 0.16 4.0 ? 11.50 6.0

View Demo

View Demo

Related Posts


回應 (Leave a comment)