前言
在this All Makes Sense Now! [上]中我们介绍了什么是call-site与4种绑定的规则,我们需要做的就是观察一个程式找到他的call-site并了解他适用于哪个规则,这样就可以找到this所指向的是什么了。
Everything In Order
透过找到call-site与适用的规则可以找到this所指向的地方,但是如果一个call-site符合多个规则那怎么办?这些规则具有优先顺序
,接下来我们将介绍这些规则的顺序,首先要先了解default binding是所有规则中优先级最低的,所以先不讨论。
我们先讨论一下是implicit binding还是explicit binding?
function foo() {console.log( this.a );}var obj1 = {a: 2,foo: foo};var obj2 = {a: 3,foo: foo};obj1.foo(); // 2obj2.foo(); // 3obj1.foo.call( obj2 ); // 3obj2.foo.call( obj1 ); // 2
上面的程式码中可以看到当对function使用显性绑定后,显性绑定的规则会高于隐性绑定
,这意味着当你需要使用隐性绑定的时候需要检查funciton是否有被显性绑定住。
接下来要看看new binding
的优先级是如何。
function foo(something) {this.a = something;}var obj1 = {foo: foo};var obj2 = {};obj1.foo( 2 );console.log( obj1.a ); // 2obj1.foo.call( obj2, 3 );console.log( obj2.a ); // 3var bar = new obj1.foo( 4 );console.log( obj1.a ); // 2console.log( bar.a ); // 4
new binding的优先级是比隐性绑定高的,但是要如何确定new binding与显性绑定谁的优先极高呢?
function foo(something) {this.a = something;}var obj1 = {};var bar = foo.bind( obj1 );bar( 2 );console.log( obj1.a ); // 2var baz = new bar( 3 );console.log( obj1.a ); // 2console.log( baz.a ); // 3
bar强制绑定着obj1,之后使用new binding将bar中函数的this指向这个物件,但是结果却是用new创出来的baz没有改变到obj1.a的值,因为使用new binding后回传的会是一个新的物件并将函数的this指向这个新物件,所以他与bar中绑定的obj1是没关係的。
Currying
Currying(柯里化),又称为 parital application 或 partial evaluation,是个「将一个接受 n 个参数的 function,转变成 n 个只接受一个参数的 function」的过程。
bind(...)可以将输入的参数(需绑定物件后面的参数)默认的当作前函数的标準参数
function foo(p1,p2) {this.val = p1 + p2;}var bar = foo.bind( null, "p1" ); // 将"p1"默认的当作每次呼叫foo的参数var baz = new bar( "p2" );var bax = new bar( "p3" );baz.val; // p1p2bax.val; // p1p3
Determining this
了解了绑定的优先级,我们可以总结一下判断this的规则。
如果是函数使用new binding那么this所指向的便是被创建出来的新物件
。var bar = new foo(); // this指向bar
函数通过显性绑定(call或apply)或是bind的硬性绑定,则this指向被绑定的物件。var bar = foo.call(obj2); //this指向obj2
函数通过环境物件而被调用(隐性绑定),则this指向呼叫function reference的环境物件。const = obj1 = { foo: foo}obj1.foo();
不符合以上条件则属于default binding,this在非严格模式下指向全域物件,否则是undefined。Binding Exceptions
凡事均有特例,对于this的判定也不例外。
Ignored this
如果传递null
或undefined
给call,apply或bind的参数,那么这些值就会被忽略绑定规则会自动回到default binding。
function foo() {console.log( this.a );}var a = 2;foo.call( null ); // 2
既然传递非法的参数(null,undefined)会造成binding的忽略那么为什么会需要使用?
在ES6中我们可以使用扩展运算符(...)
来将阵列中的元素拆开,但是在ES6之前要达到同样的效果需要使用apply
来来达到。
// ES5function foo(a,b) {console.log( "a:" + a + ", b:" + b );}// spreading out array as parametersfoo.apply( null, [2, 3] ); // a:2, b:3// ES6foo(...[2, 3]); // a:2, b:3
将null当作参数传递给bind(...)可以达到currying的功能。
function foo(a,b) {console.log( "a:" + a + ", b:" + b );}var bar = foo.bind( null, 2 );bar( 3 ); // a:2, b:3
Indirection
若是创建了函数的间接引用(indirect reference),这种情况下会失去原本的bind而导致变回适用default binding。
function foo() {console.log( this.a );}var a = 2; // declaration in globalvar o = { a: 3, foo: foo };var p = { a: 4 };o.foo(); // 3(p.foo = o.foo)(); // 2
对于p.foo = o.foo来说,虽然看起来像是将o物件中的foo(...)赋予给p物件,但实际上p所拿到的是foo(...)本体的reference,代表着他的位置与o物件中的foo是不一样的,所以只适用default binding。
Lexical this
对于普通函数来说我们可以遵守4条绑定规则中找到this所指向的位置,但是ES6引入了一个不适用于这些规则的函数箭头函数
。
箭头函数不採用4个标準的绑定规则而是从封闭的(函数或全域)範围採用this绑定。
function foo() {// return an arrow functionreturn (a) => {// `this` here is lexically adopted from `foo()`console.log( this.a );};}var obj1 = {a: 2};var obj2 = {a: 3};var bar = foo.call( obj1 );bar.call( obj2 ); // 2, not 3!
在foo(...)中创建一个箭头函数,这个箭头函数会自动绑定foo()被调用时的this,以上面的例子来说当foo(...)被呼叫并且this被绑定为obj1,那么箭头函数中的this也会绑定obj1,而且箭头函数的绑定是不能被覆盖的
。
最常见的用法式将箭头函数应用在callback function中
function foo() {setTimeout(() => {// `this` here is lexically adopted from `foo()`console.log( this.a );},100);}var obj = {a: 2};var a = 4; // declaration in globalfoo.call( obj ); // 2
使用箭头函数当作callback function的参数传递给setTimeout并不会像我们在this All Makes Sense Now! [上]提到的,因为隐性赋值而产生binding遗失,箭头函数的this会在foo()被呼叫的时候就指定给foo()所绑定的物件。
其实这种绑定的方式在ES6的箭头函数出来之前就有类似的方式了
function foo() {var self = this; // lexical capture of `this`setTimeout( function(){console.log( self.a );}, 100 );}var obj = {a: 2};var a = 4; // declaration in globalfoo.call( obj ); // 2
透过在一进function后就先捕获foo中this所指向的物件,这样就不会因为隐性赋值的问题导致binding遗失。
结论
当我们需要找到函数中this所指向的位置,我们需要找到这个函数的call-site
与看这个函数符合4种绑定规则的哪一种。
new
调用funciton则this绑定新创建的物件
。通过call
、apply
或bind
调用则this绑定指定的物件
。通过环境物件调用函数reference则this绑定呼叫函数的环境物件
。default,严格模式下是undefined
否则this指向全域物件
。ES6提供的箭头函数不遵守上面的4个绑定规则,箭头函数的this绑定取决于他被创建的当下所绑定的物件。
参考文献:You Don't Know JavaScript