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.
- var UI = {
- element: document.getElementById("container"),
- bindEvent: function () {
- console.log("**** UI.bindEvent() ****");
- console.log(this); // this refers to Window object.
- },
- syncData: function(callback) {
- console.log("**** UI.syncData() ****");
- console.log(this);
- if (callback) {
- callback();
- }
- },
- render: function () {
- console.log("**** UI.render() ****");
- this.syncData(this.bindEvent);
- }
- };
- UI.render();
- var UI = {
- element: document.getElementById("container"),
- bindEvent: function () {
- console.log("**** UI.bindEvent() ****");
- console.log(this);
- },
- syncData: function(callback) {
- console.log("**** UI.syncData() ****");
- console.log(this);
- if (callback) {
- callback();
- }
- },
- remove: function (callback) {
- console.log("**** UI.remove() ****");
- console.log(this);
- this.element.innerHTML = "";
- if (callback) {
- callback();
- }
- },
- reRender: function () {
- console.log("**** UI.reRender() ****");
- this.remove(function () {
- console.log(this); // this refers to Window object.
- this.syncData(); // Uncaught TypeError: Object [object global] has no method 'syncData'
- this.bindEvent(); // Uncaught TypeError: Object [object global] has no method 'bindEvent'
- });
- }
- };
- UI.reRender();
Another example below:
- function UI(element) {
- var privateMethod = function(element) {
- console.log("**** privateMethod ****");
- console.log(this); // this refers to Window object.
- console.log(element);
- };
- this.publicMethod = function() {
- console.log("**** publicMethod ****");
- console.log(this); // this refers to UI constructor.
- };
- element.onclick = function () {
- console.log("**** element.onclick ****");
- console.log(this); // this refers to element container.
- };
- privateMethod(element);
- setTimeout(function () {
- console.log("**** setTimeout ****");
- console.log(this); // this refers to Window object.
- }, 100);
- }
- var elem = document.getElementById("container");
- var ui = new UI(elem);
- 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
- var UI = {
- element: document.getElementById("container"),
- bindEvent: function () {
- console.log("**** UI.bindEvent() ****");
- console.log(this); // this refers to UI object.
- },
- syncData: function(callback) {
- console.log("**** UI.syncData() ****");
- console.log(this); // this refers to UI object.
- if (callback) {
- callback();
- }
- },
- remove: function (callback) {
- console.log("**** UI.remove() ****");
- console.log(this);
- this.element.innerHTML = "";
- if (callback) {
- callback();
- }
- },
- reRender: function () {
- console.log("**** UI.reRender() ****");
- // Method One: store this to variable self.
- var self = this;
- this.remove(function () {
- self.syncData();
- self.bindEvent();
- });
- }
- };
- UI.reRender();
- function UI(element) {
- var self = this;
- var privateMethod = function(element) {
- console.log("**** privateMethod ****");
- console.log(this); // this refers to Window object.
- console.log(self); // this refers to UI constructor.
- console.log(element);
- };
- this.publicMethod = function() {
- console.log("**** publicMethod ****");
- console.log(this); // this refers to UI constructor.
- };
- element.onclick = function () {
- console.log("**** element.onclick ****");
- console.log(this); // this refers to element container.
- console.log(self); // this refers to UI constructor.
- };
- privateMethod(element);
- setTimeout(function () {
- console.log("**** setTimeout ****");
- console.log(this); // this refers to Window object.
- console.log(self); // this refers to UI constructor.
- }, 100);
- }
- var elem = document.getElementById("container");
- var ui = new UI(elem);
- 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()
.
- var UI = {
- element: document.getElementById("container"),
- bindEvent: function () {
- console.log("**** UI.bindEvent() ****");
- console.log(this); // this refers to UI object.
- },
- syncData: function(callback) {
- console.log("**** UI.syncData() ****");
- console.log(this); // this refers to UI object.
- if (callback) {
- callback();
- }
- },
- remove: function (callback) {
- console.log("**** UI.remove() ****");
- console.log(this); // this refers to UI object.
- this.element.innerHTML = "";
- if (callback) {
- callback();
- }
- },
- render: function () {
- console.log("**** UI.render() ****");
- this.syncData(this.bindEvent.bind(this));
- },
- reRender: function () {
- console.log("**** UI.reRender() ****");
- // Method Two
- this.remove(function () {
- this.syncData();
- this.bindEvent();
- }.bind(this));
- }
- };
- UI.render();
- UI.reRender();
- var UI = {
- element: document.getElementById("container"),
- bindEvent: function () {
- console.log("**** UI.bindEvent() ****");
- console.log(this);
- /*
- this.element.onclick = function (e) {
- console.log("**** this.element.onclick ****");
- console.log(this); // this refers to element container.
- };
- */
- this.element.onclick = function (e) {
- console.log("**** this.element.onclick ****");
- console.log(this); // this refers to UI object.
- }.bind(this);
- },
- syncData: function(callback) {
- console.log("**** UI.syncData() ****");
- console.log(this); // this refers to UI object.
- if (callback) {
- callback();
- }
- },
- remove: function (callback) {
- console.log("**** UI.remove() ****");
- console.log(this); // this refers to UI object.
- this.element.innerHTML = "";
- if (callback) {
- callback();
- }
- },
- render: function () {
- console.log("**** UI.render() ****");
- this.syncData(this.bindEvent.bind(this));
- },
- reRender: function () {
- console.log("**** UI.reRender() ****");
- // Method Two
- this.remove(function () {
- this.syncData();
- this.bindEvent();
- }.bind(this));
- }
- };
- UI.render();
- UI.reRender();
- Function.prototype.bind = Function.prototype.bind || function bind(func, fixThis) {
- console.log("**** bind ****");
- return function () {
- return func.apply(fixThis, arguments);
- }
- };
- function UI(element) {
- var privateMethod = function(element) {
- console.log("**** privateMethod ****");
- console.log(this); // this refers to UI constructor.
- console.log(element);
- }.bind(this);
- this.publicMethod = function() {
- console.log("**** publicMethod ****");
- console.log(this); // this refers to UI constructor.
- };
- element.onclick = function () {
- console.log("**** element.onclick ****");
- console.log(this); // this refers to UI constructor.
- }.bind(this);
- privateMethod(element);
- setTimeout(function () {
- console.log("**** setTimeout ****");
- console.log(this); // this refers to UI constructor.
- }.bind(this), 100);
- }
- var elem = document.getElementById("container");
- var ui = new UI(elem);
- 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:
- Function.prototype.bind = Function.prototype.bind || function bind(func, fixThis) {
- console.log("**** bind ****");
- return function () {
- return func.apply(fixThis, arguments);
- }
- };
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 |
回應 (Leave a comment)