让前端飞深入解读JavaScript

判断this绑定的4个规则

2018-07-07  本文已影响1人  悟C

this关键字是JavaScript中最复杂的机制之一,因为this既不指向函数自身也不指向函数的作用域,而是取决于函数在哪里被调用。

既然this取决于调用位置,那我们就先看看什么是调用栈和调用位置:

function baz() {
  // 当前调用栈是: baz
  // 因此, 当前调用位置是全局作用域
  console.log('baz');
  bar();
}

function bar() {
  // 当前调用栈是baz -> bar
  // 因此,当前调用位置在baz中
  console.log('bar');
  foo();
}

function foo() {
  // 当前调用栈是bza -> bar -> foo
  // 因此,当前调用位置在bar中
  console.log('foo');
}

baz(); // baz的调用位置

在明确了函数的直接调用位置后,我们就要学习四条判断this绑定对象的规则。

1. 默认绑定

默认绑定:直接使用不带任何修饰的函数引用进行调用函数,则this都指向全局对象(在浏览器中是window,在node中是global),下面来看一个例子:

function foo() {
  console.log(this.a);
}
var a = 2;
foo();

浏览器下: // 2
node下:  //undefined

以上foo中this.a就是使用默认绑定,this等于全局对象(window、global),由于var a = 2是在全局作用域中声明,在浏览器中这个声明相当于window.a = 2,所以结果得到了2。但在node中要声明全局对象要写成a=2 或者 global.a = 2,所以以上代码在node下运行是undefined

在严格模式下,this将保持他进入执行上下文时的值,所以下面的this将会默认为undefined:

function foo() {
  'use strict';
  console.log(this.a)
}
a = 2;
foo() // Cannot read property 'a' of undefined  (this 为 undefined)

在严格模式下,如果 this 没有被执行上下文(execution context)定义,那它将保持为 undefined。

不过这里有一个微妙的细节,只要foo()的执行上下文在非严格模式下时,即使在严格模式下调用,也不影响默认绑定规则。请看下面:

function foo() {
  console.log(this.a)
}

a = 2;

(function(){
  'use strict';
  foo();   //2
})();

以上就是this的默认绑定规则,下面我们来看下一条规则

2. 隐式绑定

什么是式隐式绑定,我们先来看一段代码:

function say() {
  console.log(this.name);
}

var man = {
  name: '内孤',
  say: say
}

man.say(); // 内孤

上面就是一个隐式绑定this的例子,通过man来调用say,使用了man的上下文引用了say()。所以在理解this上有句比较简单的话:谁去调用就指向谁,其实默认绑定this也可以这样理解,foo()其实就是window.foo(),所以this指向window

在这我们要注意,对象属性引用链中只有上一层起作用:

function say() {
  console.log(this.name);
}

var man = {
  name: '内孤',
  say: say
}

var he = {
  name: '他是内孤',
  say: man
}

he.say.say();   // 内孤

隐式绑定还是比较好理解,不过它会在我们不经意的时候失效,这就是隐式丢失现象。下面让跟着我一起来看看什么情况下会丢失:

function say() {
  console.log('我是' + this.color + '猫');
}

var whiteCat = {
  color: '白',
  say: say
}

var color = '黑';

var cat = whiteCat.say;

cat(); // 我是黑猫

虽然cat是whiteCat.say的一个引用,但是实际上,它引用的是say函数本身,因此此时的cat其实是一个不带任何修饰的函数调用,因此应用了默认绑定。下面再举两个常用并且容易造成隐式丢失的代码:

// 第一种
function foo() {
  console.log(this.a)
}

function doFoo(fn) {
  // fn 其实引用的是foo 
  fn();
}

var obj = {
  a: 2,
  foo: foo
}

var a = 'oops, global';
doFoo(obj.foo);  // 'oops,global'
// => 

// 第二种
function foo() {
  console.log(this.a)
}

var obj = {
  a: 2,
  foo: foo
}

var a = 'oops, global';
setTimeout(obj.foo, 100); // 'oops,global'
/**  setTimeout的伪代码
 * function setTimeout(fn, delay) {
 *    fn();
 * }
*/

参数传递其实就是一个隐式的赋值,因此我们传入函数时也是会被隐式赋值,所以现象和上面cat的例子是一样的结果。

3. 显式绑定

显式绑定里面有硬绑定API调用的"上下文"控制this�,下面我们来分别看一下:

1)硬绑定

硬绑定就是我们经常看到的callapplybind(es5中)三个方法,还是用一段代码来看一个究竟:

function foo() {
  console.log(this.a)
}

var obj = {
  a: 2
}

var bar = function() {
  foo.call(obj);
}

bar();  //2
setTimeout(bar, 100)  //2

global.a = 22;
// 硬绑定后就固定,不可修改
bar.apply(global)  // 2
bar.bind(global)  //2
2)API调用的"上下文"

在JavaScript中有几个内置函数,例如:filterforEach等都有一个可选参数,在执行 callback 时的用于的 this 值。下面以forEach为例子:

var girl = {
  name: '小郑'
}

function say(item) {
  console.log(this.name + ' ' + item)
}

[1,2,3,4].forEach(say, girl)
//小郑 1
//小郑 2
//小郑 3
//小郑 4

这写函数实际上就是用call、apply实现的显式绑定。

4. new绑定

new应该是比较常见的操作,经常用它new各种实例函数(情人节就new girlfriend())。那我们来看一下在new的时候,会发生一些什么事情:

function foo(name) {
  this.name = name;
}

var bar = new foo('内孤');
console.log(bar.name) // 内孤

总结

现在我们已经了解4个判断this绑定的规则,判断一个简单的情况肯定是没有什么问题。但面对复杂的情况,我们必须要了解他们之间的优先级。关于他们之间的优先级会在另外一个篇文章中介绍。
注意: es6中的箭头函数不会使用上面四条绑定规则,箭头函数是没有自己的this,它是通过继承外层函数调用的this绑定


参考资料

《你不知道的JavaScript》上卷

上一篇下一篇

猜你喜欢

热点阅读