JS this词法(三)

在理解this绑定之前,先要理解调用位置。
我们可以通过浏览器的调试工具来查看调用栈。
在第一行代码钱插入一条debugger;语句,运行代码,可以看到当前位置函数的调用列表(call stack),找到栈中的第二个元素,就是真正的调用位置。

这里写图片描述

声明:下面很多内容使用的是《You Don’t Kown JavaScript》中的思想。
this的绑定对象———先找到函数的执行过程中调用位置,然后判断应用了下面四条绑定规则的哪一条。

this的四种绑定规则:

1、默认绑定
2、隐式绑定
3、显示绑定
4、new绑定

默认绑定

首先从默认绑定开始,在无法应用其他规则时的默认规则,通常是独立函数调用。

这里写图片描述

在调用add()时,应用了默认绑定,this指向全局对象(非严格模式下),严格模式下,this指向undefined。
这里写图片描述

隐式绑定

调用位置是否有上下文对象/被某个对象“拥有”或“包含”(但不是真正的拥有)
这里写图片描述

add函数声明在外部,严格来说并不属于instance,但是在调用add时,调用位置会使用instance的上下文来引用函数,隐式绑定会把函数调用中的this(即此例add函数中的this)绑定到这个上下文对象(即此例中的instance)
需要注意的是:对象属性链中只有最后一层会影响到调用位置。
这里写图片描述

隐式绑定有一个问题,就是隐式丢失(在第一篇中提到过的绑定丢失的问题)
这里写图片描述

绑定丢失的另一种情况发生在回调函数中。
这里写图片描述

我的理解是:fun(instance.add),做了一个隐式的赋值操作,var fn = instance.add,然后运行fn();这样就跟上面的例子一样了。
本来看书上说做了一个隐式赋值,自己并未理解,但是想努力地说清楚这个问题,于是自己思考,反而与书中不谋而合,本来没明白他说得隐式赋值操作是什么。
到这个地方,就不得不提起setTimeout了。
这里写图片描述

setTimeout(person1.grow, 100); //undefined
实话说,我原本是一个似理解又不是很理解的一个状态,现在算是豁然开朗了。
setTimeout(fn,delay){
fn();
}
与上面的例子其实是一样的。
除了上面的两种情况以外,还有一种情况this的行为会出乎我们意料:调用回调函数的函数可能会修改this,在一些流行的JS库中事件处理器常会把毁掉函数的this强制绑定到触发事件的DOM元素上。(关于这个问题,暂未研究,因此也不予举例说明)

显式绑定

显式绑定就是通过call()或者apply()函数指定this的绑定对象。
这里写图片描述

不过显式绑定还是未能解决绑定丢失的问题,如下面的代码,结果依旧为50。

这里写图片描述

因此我们引入“硬绑定”这个概念。注意下面例子中划红色横线的部分,根据书中说硬绑定之后,无法再修改它的this值,应该是我红色写得部分,即(fn.call(instance))这种写法,无论fun.call()第一个参数传什么,都不能再改变它的this值,相反,我例子中的写法,是需要和外围配合的,即需要传什么this值,就去指定它。
这里写图片描述

硬绑定是能够解决绑定丢失的问题的,如下:

这里写图片描述

ES5中提供了内置的方法进行硬绑定,在第一篇this词法的博文中也提及过,即bind. Function.prototype.bind,用法如下:
这里写图片描述

上面的两句代码可以用那一句红色的代码代替,需要注意的是:
bind是function对象的方法,千万不能写成add().bind(instance);因为这个add()函数中返回值是undefined,而undefined显然是没有bind方法的。
如果是下面这样,那显然又是不一样的。
这里写图片描述

这儿使用的是add().bind,原因也很简单,因为add()返回的是一个函数。
API调用的“上下文”
这里写图片描述

顺便了解一下forEach的用法。forEach是一个JavaScript扩展到ECMA-262标准;因此它可能不存在在标准的其他实现。为了使它工作,可以增加下面的代码:

if(!Array.prototype.forEach)
{
 Array.prototype.forEach = function(fun)
 {
  varlen = this.length;
  if(typeoffun != "function"){
    thrownewTypeError(); 
  }
  varself= arguments[1];
  for(vari = 0; i < len; i++)
  {
   if(i inthis)
    fun.call(self, this[i], i, this);
  }
 };
}

对每个函数成员执行callback函数,并且可以指定this的值。

new绑定

javaScript和C++不一样,并没有类,在javaScript中,构造函数只是使用new操作符时被调用的函数,这些函数和普通的函数并没有什么不同,它不属于某个类,也不可能实例化出一个类。任何一个函数都可以使用new来调用,因此其实并不存在构造函数,而只有对于函数的“构造调用”。
使用new来调用函数,会自动执行下面的操作:
1、创建一个新对象
2、这个新对象会被执行[[原型]]连接
3、这个新对象会绑定到函数调用的this
4、如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
这里写图片描述

instance绑定到Add的this。

优先级
了解了函数调用中this绑定的四条规则之后,需要做的就是找到函数的调用位置并判断应当应用哪条规则。当某个调用位置可以应用多条规则时,就需要了解这些绑定规则的优先级。
关于优先级的问题,不再细说,只给出结果,只说下显式绑定和new绑定优先级。
默认绑定 < 隐式绑定 < 显式绑定 < new绑定
new和call/apply无法一起使用,因此无法通过测试new add.call(obj)的方式来直接进行测试,但是可以使用硬绑定来测试他们的优先级
这里写图片描述

当硬绑定函数被new调用时,会使用新创建的this替换硬绑定的this。当然再次使用硬绑定,我们还是可以将this绑定在其它的对象上的。
这里写图片描述

总结一下,判断this的步骤:
1.函数是否在new中调用,即是否是new绑定,如果是new绑定,那么this绑定的是新创建的对象。
2.函数是否是显示绑定(call,apply,bind),如果是,那么this绑定的是指定的对象
3.函数是否在某个上下文对象中调用,即隐式绑定,较多的是在某个对象中调用,如果是的话,this绑定的是那个上下文对象。
4.如果以上规则都不能应用,那么就是默认绑定,在严格模式下,this被绑定到undefined,在非严格模式下,绑定到全局对象。
最后,我们来看下

绑定例外


1、被忽略的this
如果我们将null或者是undefined作为this的绑定对象传入call、apply或者是bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
这里写图片描述

实际应用的是默认绑定规则,add中的this被绑定到了全局对象(非严格模式)
在使用apply()来“展开”一个数组,并当做一个参数传入一个函数,或使用bind()对参数进行柯里化(即预先设置好一些参数),bind和apply都需要传入一个参数作为this的绑定对象,即使函数不关心this的值,仍然需要传入一个占位值,这是null或者是undefined可能就是一个不错的选择。
这里写图片描述

上面说可能是一个不错的选择,那么也就是说这其实并不是一个比较好的做法,因为如果某个函数确实使用了this,那默认绑定规则会把this绑定到全局对象,这就可能会产生难以预料的问题。
一个更安全的做法是传入一个特殊的对象,把this绑定到这个对象则不会对程序产生副作用,即使某个函数中使用了this,也不会影响到全局对象。
首先创建一个空对象:Φ(数学中的空集符号,既能表达意思,而且不易重名)
这里写图片描述

2、间接引用
可能会无意间创建一个函数的“间接引用”,在这种情况下,调用这个函数会引用默认绑定规则。间接引用最容易在赋值时发生。
这里写图片描述

这种情况下调用位置是add(),而不是example.add()或者是instance.add(),应用默认绑定。
想加个debugger看看,然后引发出了一个新问题,如下:期望得到解答。
这里写图片描述

3、软绑定
硬绑定可以将this的值强制绑定到指定的对象,防止函数调用应用默认绑定规则,但是,硬绑定同样会带来一个灵活性的问题,用硬绑定之后就无法使用隐式绑定或者显式绑定去修改this的值,只能通过new或者重写硬绑定。
如果给默认绑定指定一个全局对象和undefined/null以外的值,那就可以实现和硬绑定相同的效果,同时可以保留隐式绑定或者显式绑定修改this的能力。
softBind()的原理和bind()类似,会对指定的函数进行封装,首先检查调用时的this,如果this绑定到全局对象或者是undefined,那么就把指定的默认对象的obj绑定到this,否则不会修改this.

 if(!Function.prototype.softBind){
  Function.prototype.softBind = function(obj){
   var fn = this;
   var curried = [].slice.call( arguments,1);
   var bound = function(){
    return fn.apply(
     (!this || this === (window || global)) ? obj : this,
     curried.concat.apply( curried, arguments )
    );
   };
   bound.prototype = Object.create( fn.prototype );
   return bound;
  };
 }

这里写图片描述

最后简单说下ES6中的箭头函数,其实在第一篇关于this词法的博文中已经提及过了,再巩固一下吧。
前面所说的四种规则,只适用于普通的函数,对于ES6中的箭头函数是不起作用的。箭头函数不适用以上的规则,它根据外层的作用域来决定this.
这里写图片描述

这里写图片描述

对比这两个,应该能明显得看出问题吧,箭头函数的绑定是无法修改的,它被绑定到instance之后,即使我们通过call再去指定this的值,它也不会搭理我们,即使适用等级最高的new也是不行的,这里,适用new操作符,最终它只会华丽丽得返回给你一个undefined。

箭头函数事实上和增加一句self = this效果基本一致。无论是self = this或者是箭头函数,都想用替代this机制。
建议:
在编写代码时,应当:
1、只采用词法作用域并完全抛弃this风格的代码;
2、完全采用this风格,在必要的时候使用bind(),尽量避免使用self=this和箭头函数(出自Kyle Simpson)

到此,this词法就暂且告一段落,后续只会在有什么顿悟的时候再补充了。
本来已经看得有点崩溃,但是为了对读到此文的读者负责,还是看完了所有的关于这部分的内容,此前已经看了两遍,希望本文对您理解JS的this词法能有一点帮助。

文章导航