0%

【翻译】Understanding JavaScript Bind ()

原文:Understanding JavaScript Bind ()

前言

稍微写多点儿 JS 的人应该都见过var self = this这种写法,它是为了解决在不同函数调用时,this所指向的上下文参数变化的问题,你可以通过MDN this这篇文章先了解一下 JS 中的this。下文提供了将函数与其想要的this绑定的方法,以下是翻译正文。

理解 JS 中的 Bind()

当你初学 JavaScript 时你可能并不关心函数绑定的问题,但是当你需要一个在其他函数中保持this内容的解决方案时,你可能并没有意识到你真正需要的就是Function.prototype.bind()函数。

第一次遇到这个问题时,当你切换上下文时可能会将this赋值给一个可以引用的变量。大多数人会选择self_thiscontext作为变量名,这种方法是可用的,并不会出错,但是还有一种更好、更优雅的方式。

Jake Archibald发推讨论过捕获this的问题:

Ohhhh I would do anything for scope, but I won’t do that = this — Jake Archibald (@jaffathecake) February 20, 2013

Sindre Sorhus讨论这个问题时,答案已经很显然了:

@benhowdle $this for jQuery, for plain JS i don’t, use .bind() — Sindre Sorhus (@sindresorhus) February 22, 2013

但是我却忽略了好几个月。

我们想要解决什么问题?

在下面的代码中,将上下文对象赋值给一个变量是情有可原的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var myObj = {
specialFunction: function () {
},

anotherSpecialFunction: function () {
},

getAsyncData: function (cb) {
cb();
},

render: function () {
var that = this;
this.getAsyncData(function () {
that.specialFunction();
that.anotherSpecialFunction();
});
}
};

myObj.render();

如果我们在上面的代码中直接调用this.specialFunction(),那么就会看到如下的错误信息:

1
Uncaught TypeError: Object [object global] has no method 'specialFunction'

我们需要持有myObj对象的上下文用于回调函数的调用,调用that.specialFunction()函数使我们持有上下文并且正确的执行我们的函数。但是使用Function.prototype.bind()是更优雅的方法。

我们写个例子:

1
2
3
4
5
6
render: function () {
this.getAsyncData(function () {
this.specialFunction();
this.anotherSpecialFunction();
}.bind(this));
}

上例中做了什么?

.bing()只是创建了一个新的函数,当它被调用时会将this关键字设置为之前提供的值。这样我们就可以向.bind()函数传入期望的上下文参数即this(在这里就是myObj),然后当回调函数执行时,this就引用了myObj对象。

如果你想看看Function.prototype.bind()这个函数内部是如何运行的,可以看下面这个简单的例子:

1
2
3
4
5
6
Function.prototype.bind = function (scope) {
var fn = this;
return function () {
return fn.apply(scope);
};
}

下面是个简单的用例:

1
2
3
4
5
6
7
8
9
10
var foo = {
x: 3
}
var bar = function(){
console.log(this.x);
}

bar(); // undefined
var boundFunc = bar.bind(foo);
boundFunc(); // 3

我们创建了一个新的函数,其执行时会将this设置为foo对象,而不是像例子中直接调用bar()时,this默认指向的全局对象。

浏览器支持

浏览器 支持的版本
Chrome 7
Firefox(Gecko) 4.2(2)
Internet Explore0 9
Opera 11.60
Safari 5.1.4

如上所示,在 Internet Explorer 8 及以下版本中并不支持Function.prototype.bind()函数,所以你需要一个备用方案。

幸好,MDN提供了一个可靠的备选方案,用于没有在本地实现.bind()方法的浏览器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== "function") {
// closest thing possible to the ECMAScript 5 internal IsCallable function
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}

var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {},
fBound = function () {
return fToBind.apply(this instanceof fNOP && oThis
? this
: oThis,
aArgs.concat(Array.prototype.slice.call(arguments)));
};

fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();

return fBound;
};
}

用法范例

我发现当学习东西时,不仅要透彻的学习它的概念,还要将它用在实践中。幸好,下面的几个例子可以用于你的代码中或者解决你的问题。

点击事件处理

通常用于记录点击事件(或者点击后执行一个动作),这时就需要将信息保存在一个对象中,如下:

1
2
3
4
5
6
7
var logger = {
x: 0,
updateCount: function(){
this.x++;
console.log(this.x);
}
}

我们可能会像下面这样添加点击事件处理,在其中调用logger对象的updateCount()方法:

1
2
3
document.querySelector('button').addEventListener('click', function(){
logger.updateCount();
});

但是为使得updateCount()函数中的this关键字表示正确的值,我们需要创建一个并不必须的匿名函数。

可以像下面这样优化:

1
document.querySelector('button').addEventListener('click', logger.updateCount.bind(logger));

我们可以使用方便的.bind()函数来创建一个新的函数,然后将作用域设置的绑定到logger对象。

SetTimeout

如果你尝试过模板引擎(例如Handlebars)或者某种 MV* 框架(如 Backbone.js),你可能会碰到这样的问题:当你渲染模板时,在调用了渲染方法后,想要立即获取新的 DOM 节点可能就会出错。

假设我们再初始化一个 jQuery 插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
var myView = {
template: '/* a template string containing our <select /> */',
$el: $('#content'),
afterRender: function () {
this.$el.find('select').myPlugin();
},
render: function () {
this.$el.html(this.template());
this.afterRender();
}
}

myView.render();

你可能会发现这段代码可以正常运行,但并不总是这样。问题就在这里,它产生了一种竞争的情况:有时候渲染先执行完成,有时候插件的初始化先执行完成。

有些人可能不知道,我们可以通过使用setTimeout()函数来解决问题。

像下面这样简单的修改下代码,使得我们可以在 DOM 节点展示完成后立马安全的初始化我们的 jQuery 插件:

1
2
3
4
5
6
7
8
9
10
//
afterRender: function () {
this.$el.find('select').myPlugin();
},

render: function () {
this.$el.html(this.template());
setTimeout(this.afterRender, 0);
}
//

然而,我们会看到找不到.afterRender()函数的报错信息。

是时候祭出我们的.bind()方法了:

1
2
3
4
5
6
7
8
9
10
//
afterRender: function () {
this.$el.find('select').myPlugin();
},

render: function () {
this.$el.html(this.template());
setTimeout(this.afterRender.bind(this), 0);
}
//

现在,我们的afterRender()函数就会执行在正确的上下文环境中了。

整理通过 querySelectorAll 绑定的事件

自从添加了诸如querySelector,querySelectorAllclassList等有用的方法后,DOM API 提升了很多。

然而,到目前为止并没有一个原生的方法来对一个NodeList中的所有节点添加事件,因此我们还需要使用Array.prototype中的forEach方法来循环添加:

1
2
3
Array.prototype.forEach.call(document.querySelectorAll('.klasses'), function(el){
el.addEventListener('click', someFunction);
});

我们可以通过使用.bind()方法来优化一下:

1
2
3
4
5
6
var unboundForEach = Array.prototype.forEach,
forEach = Function.prototype.call.bind(unboundForEach);

forEach(document.querySelectorAll('.klasses'), function (el) {
el.addEventListener('click', someFunction);
});

现在我们有一个整齐的方法来循环我们的 DOM 节点了。

结论

如你所见,JS 的bind()函数可以巧妙的用于各种用途或者代码的整理。期望你能在需要时将.bind()添加进你的代码中来驾驭this值转换的能力。