this全面解析
默认绑定
首先要介绍的是最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规
则时的默认规则。
思考一下下面的代码:
你应该注意到的第一件事是,声明在全局作用域中的变量(比如var a = 2)就是全局对象的一个同
名属性。它们本质上就是同一个东西,并不是通过复制得到的,就像一个硬币的两面一样。
接下来我们可以看到当调用foo()时,this.a被解析成了全局变量a。为什么?因为在本例中,函数
调用时应用了this的默认绑定,因此this指向全局对象。
那么我们怎么知道这里应用了默认绑定呢?可以通过分析调用位置来看看foo()是如何调用的。在
代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应
用其他规则。
如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此this会绑定
到undefined:
隐式绑定
另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含,不
过这种说法可能会造成一些误导。
思考下面的代码:
当函数引用有上下文
对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因为调用foo()时this被绑
定到obj,因此this.a和obj.a是一样的。
对象属性引用链中只有最顶层或者说最后一层会影响调用位置。举例来说:
隐式丢失
一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑
定,从而把this绑定到全局对象或者undefined上,取决于是否是严格模式。
思考下面的代码:
虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时的bar()其实是一
个不带任何修饰的函数调用,因此应用了默认绑定。
一种更微妙、更常见并且更出乎意料的情况发生在传入回调函数时:
如果把函数传入语言内置的函数而不是传入你自己声明的函数,会发生什么呢?结果是一样的,没
有区别:
JavaScript环境中内置的setTimeout()函数实现和下面的伪代码类似:
就像我们看到的那样,回调函数丢失this绑定是非常常见的。除此之外,还有一种情况this的行为
会出乎我们意料:调用回调函数的函数可能会修改this。在一些流行的JavaScript库中事件处理器
常会把回调函数的this强制绑定到触发事件的DOM元素上。这在一些情况下可能很有用,但是有
时它可能会让你感到非常郁闷。遗憾的是,这些工具通常无法选择是否启用这个行为。
无论是哪种情况,this的改变都是意想不到的,实际上你无法控制回调函数的执行方式,因此就没
有办法控制会影响绑定的调用位置。
显式绑定
JavaScript中的“所有”函数都有一些有用的特性,可以用来解决这个问题。具体点说,可以使用函数的call(..)和apply(..)方法。JavaScript提供的绝大多数函数以及你自己创建的所有函数都可以使用call(..)
和apply(..)方法。
这两个方法是如何工作的呢?它们的第一个参数是一个对象,它们会把这个对象绑定到this,接着
在调用函数时指定这个this。因为你可以直接指定this的绑定对象,因此我们称之为显式绑定。
思考下面的代码:
通过foo.call(..),我们可以在调用foo时强制把它的this绑定到obj上。
如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作this的绑定对象,这个原
始值会被转换成它的对象形式(也就是new String(..)、new Boolean(..)或者new Number(..))。这通
常被称为“装箱”。
可惜,显式绑定仍然无法解决我们之前提出的丢失绑定问题。
硬绑定
但是显式绑定的一个变种可以解决这个问题。
思考下面的代码:
硬绑定的典型应用场景就是创建一个包裹函数,传入所有的参数并返回接收到的所有值:
另一种使用方法是创建一个bind可以重复使用的辅助函数:
由于硬绑定是一种非常常用的模式,所以在ES5中提供了内置的方法Function.prototype.bind,它
的用法如下:
bind(..)会返回一个硬编码的新函数,它会把参数设置为this的上下文并调用原始函数。
API调用的”上下文”
第三方库的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的
参数,通常被称为“上下文”(context),其作用和bind(..)一样,确保你的回调函数使用指定的this。
举例来说:
这些函数实际上就是通过call(..)或者apply(..)实现了显式绑定,这样你可以少些一些代码。
new绑定
在传统的面向类的语言中,“构造函数”是类中的一些特殊方法,使用new初始化类时会调用类中的
构造函数。通常的形式是这样的:
JavaScript也有一个new操作符,使用方法看起来也和那些面向类的语言一样,绝大多数开发者都
认为JavaScript中new的机制也和那些语言一样。然而,JavaScript中new的机制实际上和面向类的
语言完全不同。
首先我们重新定义一下JavaScript中的“构造函数”。JavaScript,构造函数只是一些使用new操作符
时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一
种特殊的函数类型,它们只是被new操作符调用的普通函数而已。
举例来说,思考一下Number(..)作为构造函数时的行为,ES5.1中这样描述它:
15.7.2 Number构造函数
当Number在new表达式中被调用时,它是一个构造函数:它会初始化新创建的对象。
所以,包括内置对象函数(比如Number(..),详情请查看第3章)在内的所有函数都可以用new来调
用,这种函数调用被称为构造函数调用。这里有一个重要但是非常细微的区别:实际上并不存在所
谓的“构造函数”,只有对于函数的“构造调用”。
使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
- 创建(或者说构造)一个全新的对象。
- 这个新对象会被执行[[原型]]连接。
- 这个新对象会绑定到函数调用的this。
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
思考下面的代码:12345function foo(a) {this.a = a;}var bar = new foo(2);console.log( bar.a ); // 2
使用new来调用foo(..)时,我们会构造一个新对象并把它绑定到foo(..)调用中的this上。new是最
后一种可以影响函数调用时this绑定行为的方法,我们称之为new绑定。
优先级
函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。
1var bar = new foo()函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。
1var bar = foo.call(obj2)函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。
1var bar = obj1.foo()如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。
1var bar = foo()
绑定例外
####被忽略的this
如果你把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被
忽略,实际应用的是默认绑定规则:
那么什么情况下你会传入null呢?
一种非常常见的做法是使用apply(..)来“展开”一个数组,并当作参数传入一个函数。类似
地,bind(..)可以对参数进行柯里化(预先设置一些参数),这种方法有时非常有用:
// 把数组“展开”成参数
这两种方法都需要传入一个参数当作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这个委托,所以它
比{}”更空”:
使用变量名ø不仅让函数变得更加“安全”,而且可以提高代码的可读性,因为ø表示“我希望this是
空”,这比null的含义更清楚。
间接引用
另一个需要注意的是,你有可能(有意或者无意地)创建一个函数的“间接引用”,在这种情况下,调
用这个函数会应用默认绑定规则。
间接引用最容易在赋值时发生:
赋值表达式p.foo = o.foo的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或
者o.foo()。根据我们之前说过的,这里会应用默认绑定。
注意:对于默认绑定来说,决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是
否处于严格模式。如果函数体处于严格模式,this会被绑定到undefined,否则this会被绑定到全局
对象。