文章目录
  1. 1. 默认绑定
  2. 2. 隐式绑定
  3. 3. 隐式丢失
  4. 4. 显式绑定
    1. 4.1. 硬绑定
    2. 4.2. API调用的”上下文”
    3. 4.3. new绑定
  5. 5. 优先级
  6. 6. 绑定例外
    1. 6.0.1. 更安全的this
  7. 6.1. 间接引用

默认绑定

首先要介绍的是最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规
则时的默认规则。
思考一下下面的代码:

1
2
3
4
5
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2

你应该注意到的第一件事是,声明在全局作用域中的变量(比如var a = 2)就是全局对象的一个同
名属性。它们本质上就是同一个东西,并不是通过复制得到的,就像一个硬币的两面一样。
接下来我们可以看到当调用foo()时,this.a被解析成了全局变量a。为什么?因为在本例中,函数
调用时应用了this的默认绑定,因此this指向全局对象。
那么我们怎么知道这里应用了默认绑定呢?可以通过分析调用位置来看看foo()是如何调用的。在
代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应
用其他规则。
如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此this会绑定
到undefined:

1
2
3
4
5
6
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined

隐式绑定

另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含,不
过这种说法可能会造成一些误导。
思考下面的代码:

1
2
3
4
5
6
7
8
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2

当函数引用有上下文
对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因为调用foo()时this被绑
定到obj,因此this.a和obj.a是一样的。

对象属性引用链中只有最顶层或者说最后一层会影响调用位置。举例来说:

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42

隐式丢失

一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑
定,从而把this绑定到全局对象或者undefined上,取决于是否是严格模式。
思考下面的代码:

1
2
3
4
5
6
7
8
9
10
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a是全局对象的属性
bar(); // "oops, global"

虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时的bar()其实是一
个不带任何修饰的函数调用,因此应用了默认绑定。

一种更微妙、更常见并且更出乎意料的情况发生在传入回调函数时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo() {
console.log( this.a );
}
function doFoo(fn) {
// fn其实引用的是foo
fn(); // <-- 调用位置!
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a是全局对象的属性
doFoo( obj.foo ); // "oops, global"
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一个例子
一样。

如果把函数传入语言内置的函数而不是传入你自己声明的函数,会发生什么呢?结果是一样的,没
有区别:

1
2
3
4
5
6
7
8
9
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a是全局对象的属性
setTimeout( obj.foo, 100 ); // "oops, global"

JavaScript环境中内置的setTimeout()函数实现和下面的伪代码类似:

1
2
3
4
function setTimeout(fn,delay) {
// 等待delay毫秒
fn(); // <-- 调用位置!
}

就像我们看到的那样,回调函数丢失this绑定是非常常见的。除此之外,还有一种情况this的行为
会出乎我们意料:调用回调函数的函数可能会修改this。在一些流行的JavaScript库中事件处理器
常会把回调函数的this强制绑定到触发事件的DOM元素上。这在一些情况下可能很有用,但是有
时它可能会让你感到非常郁闷。遗憾的是,这些工具通常无法选择是否启用这个行为。
无论是哪种情况,this的改变都是意想不到的,实际上你无法控制回调函数的执行方式,因此就没
有办法控制会影响绑定的调用位置。

显式绑定

JavaScript中的“所有”函数都有一些有用的特性,可以用来解决这个问题。具体点说,可以使用函数的call(..)和apply(..)方法。JavaScript提供的绝大多数函数以及你自己创建的所有函数都可以使用call(..)
和apply(..)方法。

这两个方法是如何工作的呢?它们的第一个参数是一个对象,它们会把这个对象绑定到this,接着
在调用函数时指定这个this。因为你可以直接指定this的绑定对象,因此我们称之为显式绑定。
思考下面的代码:

1
2
3
4
5
6
7
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
foo.call( obj ); // 2

通过foo.call(..),我们可以在调用foo时强制把它的this绑定到obj上。

如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作this的绑定对象,这个原
始值会被转换成它的对象形式(也就是new String(..)、new Boolean(..)或者new Number(..))。这通
常被称为“装箱”。

可惜,显式绑定仍然无法解决我们之前提出的丢失绑定问题。

硬绑定

但是显式绑定的一个变种可以解决这个问题。
思考下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
var bar = function() {
foo.call( obj );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// 硬绑定的bar不可能再修改它的this
bar.call( window ); // 2

硬绑定的典型应用场景就是创建一个包裹函数,传入所有的参数并返回接收到的所有值:

1
2
3
4
5
6
7
8
9
10
11
12
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a:2
};
var bar = function() {
return foo.apply( obj, arguments );
};
var b = bar( 3 ); // 2 3
console.log( b ); // 5

另一种使用方法是创建一个bind可以重复使用的辅助函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
// 简单的辅助绑定函数
function bind(fn, obj) {
return function() {
return fn.apply( obj, arguments );
};
}
var obj = {
a:2
};
var bar = bind( foo, obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5

由于硬绑定是一种非常常用的模式,所以在ES5中提供了内置的方法Function.prototype.bind,它
的用法如下:

1
2
3
4
5
6
7
8
9
10
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 ); // 2 3
console.log( b ); // 5

bind(..)会返回一个硬编码的新函数,它会把参数设置为this的上下文并调用原始函数。

API调用的”上下文”

第三方库的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的
参数,通常被称为“上下文”(context),其作用和bind(..)一样,确保你的回调函数使用指定的this。
举例来说:

1
2
3
4
5
6
7
8
9
function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
};
// 调用foo(..)时把this绑定到obj
[1, 2, 3].forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome

这些函数实际上就是通过call(..)或者apply(..)实现了显式绑定,这样你可以少些一些代码。

new绑定

在传统的面向类的语言中,“构造函数”是类中的一些特殊方法,使用new初始化类时会调用类中的
构造函数。通常的形式是这样的:

1
something = new MyClass(..);

JavaScript也有一个new操作符,使用方法看起来也和那些面向类的语言一样,绝大多数开发者都
认为JavaScript中new的机制也和那些语言一样。然而,JavaScript中new的机制实际上和面向类的
语言完全不同。

首先我们重新定义一下JavaScript中的“构造函数”。JavaScript,构造函数只是一些使用new操作符
时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一
种特殊的函数类型,它们只是被new操作符调用的普通函数而已。
举例来说,思考一下Number(..)作为构造函数时的行为,ES5.1中这样描述它:

15.7.2 Number构造函数
当Number在new表达式中被调用时,它是一个构造函数:它会初始化新创建的对象。

所以,包括内置对象函数(比如Number(..),详情请查看第3章)在内的所有函数都可以用new来调
用,这种函数调用被称为构造函数调用。这里有一个重要但是非常细微的区别:实际上并不存在所
谓的“构造函数”,只有对于函数的“构造调用”。
使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行[[原型]]连接。
  3. 这个新对象会绑定到函数调用的this。
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
    思考下面的代码:
    1
    2
    3
    4
    5
    function foo(a) {
    this.a = a;
    }
    var bar = new foo(2);
    console.log( bar.a ); // 2

使用new来调用foo(..)时,我们会构造一个新对象并把它绑定到foo(..)调用中的this上。new是最
后一种可以影响函数调用时this绑定行为的方法,我们称之为new绑定。

优先级

  1. 函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。

    1
    var bar = new foo()
  2. 函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。

    1
    var bar = foo.call(obj2)
  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。

    1
    var bar = obj1.foo()
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。

    1
    var bar = foo()

绑定例外

####被忽略的this
如果你把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被
忽略,实际应用的是默认绑定规则:

1
2
3
4
5
function foo() {
console.log( this.a );
}
var a = 2;
foo.call( null ); // 2

那么什么情况下你会传入null呢?
一种非常常见的做法是使用apply(..)来“展开”一个数组,并当作参数传入一个函数。类似
地,bind(..)可以对参数进行柯里化(预先设置一些参数),这种方法有时非常有用:

1
2
3
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}

// 把数组“展开”成参数

1
2
3
4
foo.apply( null, [2, 3] ); // a:2, b:3
// 使用 bind(..) 进行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3

这两种方法都需要传入一个参数当作this的绑定对象。如果函数并不关心this的话,你仍然需要
传入一个占位值,这时null可能是一个不错的选择,就像代码所示的那样。

然而,总是使用null来忽略this绑定可能产生一些副作用。如果某个函数确实使用了this(比如第
三方库中的一个函数),那默认绑定规则会把this绑定到全局对象(在浏览器中这个对象
是window),这将导致不可预计的后果(比如修改全局对象)。
显而易见,这种方式可能会导致许多难以分析和追踪的bug。

更安全的this

一种“更安全”的做法是传入一个特殊的对象,把this绑定到这个对象不会对你的程序产生任何副
作用。就像网络(以及军队)一样,我们可以创建一个“DMZ”(demilitarized zone,非军事区)对象
——它就是一个空的非委托的对象(委托在第5章和第6章介绍)。
如果我们在忽略this绑定时总是传入一个DMZ对象,那就什么都不用担心了,因为任何对于this
的使用都会被限制在这个空对象中,不会对全局对象产生任何影响。

JavaScript中创建一个空对象最简单的方法都是Object.create(null)Object.create(null)和{}很像,但是并不会创建Object.prototype这个委托,所以它
比{}”更空”:

1
2
3
4
5
6
7
8
9
10
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 我们的DMZ空对象
var ø = Object.create( null );
// 把数组展开成参数
foo.apply( ø, [2, 3] ); // a:2, b:3
// 使用bind(..)进行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3

使用变量名ø不仅让函数变得更加“安全”,而且可以提高代码的可读性,因为ø表示“我希望this是
空”,这比null的含义更清楚。

间接引用

另一个需要注意的是,你有可能(有意或者无意地)创建一个函数的“间接引用”,在这种情况下,调
用这个函数会应用默认绑定规则。
间接引用最容易在赋值时发生:

1
2
3
4
5
6
7
8
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

赋值表达式p.foo = o.foo的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或
者o.foo()。根据我们之前说过的,这里会应用默认绑定。
注意:对于默认绑定来说,决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是
否处于严格模式。如果函数体处于严格模式,this会被绑定到undefined,否则this会被绑定到全局
对象。

文章目录
  1. 1. 默认绑定
  2. 2. 隐式绑定
  3. 3. 隐式丢失
  4. 4. 显式绑定
    1. 4.1. 硬绑定
    2. 4.2. API调用的”上下文”
    3. 4.3. new绑定
  5. 5. 优先级
  6. 6. 绑定例外
    1. 6.0.1. 更安全的this
  7. 6.1. 间接引用