大前端学习笔记整理【六】this关键字详解

前言

在上一篇博客里我总结了下辨认this指向的四种方式,可是有师兄抛出一个问题来,我发现那些this的指向并不能说明更复杂的状况,先看下这段代码javascript

var a = {
    name: 'a',
    getName: function(){
        console.log(this.name)
    }
}

var c = a.getName.bind(a)
var b={

}

b.getName=a.getName;

c();//a

 那么为何最后执行c会获得a呢?this在其中的指向究竟是啥呢?我以为利用上篇的博文并不能很好的解释。因此,这里查找了不少资料,而后我以为还要再作一个二次总结。也但愿各位看官在看完博文以后可以思考出这个例子中的this最后的指向。java

 

 什么是this?

  this 是在运行时进行绑定的,并非在编写时绑定,它的上下文取决于函数调用时的各类条件。 this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。当一个函数被调用时,会建立一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this 就是记录的其中一个属性,会在函数执行的过程当中用到。app

因此,咱们要明白,this其实既不指向函数自身,也不指向函数的词法做用域。实际上,this是在函数被调用时绑定,它指向什么彻底取决于函数在哪里被调用。函数

 

调用位置

调用位置,也就是所谓的函数实际的调用位置,而不是函数的声明位置。这个决定了this最后的调用。其中最为重要的就是分析调用栈(为了到达当前执行位置所调用的全部函数)。咱们所关心的调用位置就在当前执行函数的前一个调用中。测试

function baz() {
// 当前调用栈是:baz
// 所以,当前调用位置是全局做用域
console.log( "baz" );
bar(); // <-- bar 的调用位置
}
function bar() {
// 当前调用栈是 baz -> bar
// 所以,当前调用位置在 baz 中
console.log( "bar" );
foo(); // <-- foo 的调用位置
}
function foo() {
// 当前调用栈是 baz -> bar -> foo
// 所以,当前调用位置在 bar 中
console.log( "foo" );
}
baz(); // <-- baz 的调用位置

绑定规则

  在函数的执行过程当中调用位置如何决定 this 的绑定对象。
  你必须找到调用位置,而后判断须要应用下面四条规则中的哪一条。咱们首先会分别解释这四条规则,而后解释多条规则均可用时它们的优先级如何排列。this

a.默认绑定

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 :
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined

 这里有一个微妙可是很是重要的细节,虽然 this 的绑定规则彻底取决于调用位置,可是只有 foo() 运行在非 strict mode 下时,默认绑定才能绑定到全局对象;严格模式下与 foo()的调用位置无关:编码

function foo() {
     console.log( this.a );
  }
  var a = 2;
  (function(){
    "use strict";
    foo(); // 2
  })();

b.隐式绑定

 首先,咱们来看一段代码:spa

function foo() {
      console.log( this.a );
  }
  var obj = {
      a: 2,
      foo: foo
  };
  obj.foo(); // 2

 咱们能够看出,foo()函数的声明方式和最后的obj中做为引用属性添加到obj里,但即便是这样,严格意义上来讲,foo()也不属于obj对象,就算咱们把声明放入obj中,亦是如此。然而,调用位置会使用 obj 上下文来引用函数,所以你能够说函数被调用时 obj 对象“拥
有”或者“包含”它。prototype

可是不管如何去称呼这个模式,当foo()被调用的时候,指向确实指到了obj对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。由于调用 foo() 时 this 被绑定到 obj ,所以 this.a 和 obj.a 是同样的。code

这里还会出现一个问题,隐式丢失。 这个怎么来理解呢?其实就是说在隐式绑定的函数中,可能会出现绑定对象丢失,而后就会应用默认规则,从而把this绑定到全局对象或者undefined上。这种状况的出现取决因而否使用严格模式。

function foo() {
      console.log( this.a );
  }
  var obj = {
      a: 2,
      foo: foo
  };
  var bar = obj.foo; // 函数别名!
  var a = "Kevin"; // a 是全局对象的属性
  bar(); // "Kevin"

来看这个例子,虽然bar是obj.foo的一个引用,可是实际上引用的倒是foo这个函数自己,因此此时,bar()是一个没有任何修饰的函数调用,天然就应用了默认规则进行绑定。

还有一种状况,十分常见,也会出现隐式丢失,那就是在回调函数中。来看下例子:

function foo() {
      console.log( this.a );
  }
  var obj = {
      a: 2,
      foo: foo
  };
  var a = "Kevin"; // a 是全局对象的属性
  setTimeout( obj.foo, 100 ); // "Kevin"

参数传递其实就是一种隐式赋值,所以咱们传入函数时也会被隐式赋值,因此结果和上一个例子同样。就像咱们看到的那样,回调函数丢失 this 绑定是很是常见的。除此以外,还有一种状况 this 的行为会出乎咱们意料:调用回调函数的函数可能会修改 this 。不管是哪一种状况, this 的改变都是意想不到的,实际上你没法控制回调函数的执行方式,所以就没有办法控制会影响绑定的调用位置。

c.显式绑定

在分析隐式绑定时,咱们必须在一个对象内部包含一个指向函数的属性,并经过这个属性间接引用函数,从而把 this 间接(隐式)绑定到这个对象上。那么若是咱们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么作呢?这个时候,咱们就须要使用call或者apply两个方法来调用了。严格来讲,JavaScript 的宿主环境有时会提供一些很是特殊的函数,它们并无这两个方法。可是这样的函数很是罕见,JavaScript 提供的绝大多数函数以及你本身建立的全部函数均可以使用 call(..) 和 apply(..) 方法。这两个方法是如何工做的呢?它们的第一个参数是一个对象,它们会把这个对象绑定到this ,接着在调用函数时指定这个 this 。由于你能够直接指定 this 的绑定对象,所以咱们称之为显式绑定。

来看下这个例子:

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. 硬绑定
可是显式绑定的一个变种能够解决这个问题。
咱们来看这样一个例子

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

 咱们来看看这个变种究竟是怎样工做的。咱们建立了函数 bar() ,并在它的内部手动调用了 foo.call(obj) ,所以强制把 foo 的 this 绑定到了 obj 。不管以后如何调用函数 bar ,它总会手动在 obj 上调用 foo 。这种绑定是一种显式的强制绑定,所以咱们称之为硬绑定。硬绑定的典型应用场景就是建立一个包裹函数,传入全部的参数并返回接收到的全部值:

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

 另外一种使用方法是建立一个 i 能够重复使用的辅助函数:

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 ,它的用法以下:

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 的上下文并调用原始函数。


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

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(..) 实现了显式绑定,这样你能够少些一些代码。

d.new绑定

这是第四条也是最后一条 this 的绑定规则,在讲解它以前咱们首先须要澄清一个很是常见的关于 JavaScript 中函数和对象的误解。在传统的面向类的语言中,“构造函数”是类中的一些特殊方法,使用 new 初始化类时会调用类中的构造函数。一般的形式是这样的:

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 表达式中的函数调用会自动返回这个新对象。
再来看看下面的例子

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

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

优先级

有种状况咱们须要进行考虑,就是在函数调用过程当中,某个位置若是出现应用了多条绑定规则怎么办?那么要解决这种问题,咱们就须要知道规则的优先级。

毫无疑问,默认规则是四条规则中最低的,因此暂时不作考虑。因此首先,咱们须要比对一下显式绑定与隐式绑定的优先级,看看哪个更高一些。

来看以下的例子:

function foo() {
      console.log( this.a );
  }
  var obj1 = {
      a: 2,
      foo: foo
  };
  var obj2 = {
      a: 3,
      foo: foo
  };
  obj1.foo(); // 2
  obj2.foo(); // 3
  obj1.foo.call( obj2 ); // 3
  obj2.foo.call( obj1 ); // 2

 能够看到,显式绑定优先级更高,也就是说在判断时应当先考虑是否能够应用显式绑定。

接下来,咱们须要比对下隐式和new的优先级,看看谁高谁低。来看以下例子:

function foo(something) {
        this.a = something;
    }
    var obj1 = {
        foo: foo
    };
    var obj2 = {};
    obj1.foo( 2 );
    console.log( obj1.a ); // 2
    obj1.foo.call( obj2, 3 );
    console.log( obj2.a ); // 3
    var bar = new obj1.foo( 4 );
    console.log( obj1.a ); // 2
    console.log( bar.a ); // 4

能够看到 new 绑定比隐式绑定优先级高。可是 new 绑定和显式绑定谁的优先级更高呢?

但这里咱们须要注意一个问题:new 和 call / apply 没法一块儿使用,所以没法经过 new foo.call(obj1) 来直接进行测试。可是咱们可使用硬绑定来测试它俩的优先级。

而后回忆下硬绑定,Function.prototype.bind(...)建立了一个新的包装函数,这个函数会忽略当前this的绑定,而且强制把咱们提供的对象绑定到this上。那么由此看来,new绑定的优先级彷佛比硬绑定(显式绑定)要底,可是真的是这样的么?

这里有个例子:

function foo(something) {
    this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar(3);
console.log( obj1.a ); // 2
console.log( baz.a ); // 3

 

这个结果彷佛有点出乎意料,bar被硬绑定到了obj1上,可是new bar(3)并非按照以前所想的那样把obj1.a修改成3。偏偏相反的是,new修改了硬绑定(到 obj1 的)而且调用 bar(..) 中的 this 。由于使用了
new 绑定,咱们获得了一个名字为 baz 的新对象,而且 baz.a 的值是 3。

判断this

综上所述,咱们能够根据优先级来判断函数在某个调用位置应用的是哪条规则。也能够按照以下的顺序来进行判断:

1. 函数是否在 new 中调用( new 绑定)?若是是的话 this 绑定的是新建立的对象。
var bar = new foo();


2. 函数是否经过 call 、 apply (显式绑定)或者硬绑定调用?若是是的话, this 绑定的是指定的对象。
var bar = foo.call(obj2);


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


4. 若是都不是的话,使用默认绑定。若是在严格模式下,就绑定到 undefined ,不然绑定到全局对象。
var bar = foo();
就是这样。对于正常的函数调用来讲,理解了这些知识你就能够明白 this 的绑定原理了。

this词法

关于this的指向,以及优先级,咱们上面已经总结了不少,可是这个时候就不得不提ES6了,由于在其中有一个没法使用上述规则的特殊函数类型:箭头函数

箭头函数并非使用 function 关键字定义的,而是使用被称为“胖箭头”的操做符 => 定义的。箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)做用域来决定 this 。
咱们来看看箭头函数的词法做用域:

function foo() {
// 返回一个箭头函数
  return (a) => {
    //this 继承自 foo()
    console.log( this.a );
  };
}
var obj1 = {
  a:2
};
var obj2 = {
  a:3
};
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是 3 !

 

 


foo()内部建立的箭头函数会捕获调用时 foo() 的 this 。因为 foo() 的 this 绑定到 obj1 ,bar (引用箭头函数)的 this 也会绑定到 obj1 ,箭头函数的绑定没法被修改。 ( new 也不行!)

箭头函数最经常使用于回调函数中,例如事件处理器或者定时器:

function foo() {
    setTimeout(() => {
      // 这里的 this 在此法上继承自 foo()
      console.log( this.a );
  },100);
}
var obj = {
    a:2
};
foo.call( obj ); // 2

 

 

箭头函数能够像 bind(..) 同样确保函数的 this 被绑定到指定对象,此外,其重要性还体如今它用更常见的词法做用域取代了传统的 this 机制。实际上,在 ES6 以前咱们就已经在使用一种几乎和箭头函数彻底同样的模式。

function foo() {
  var self = this; // lexical capture of this
  setTimeout( function(){
    console.log( self.a );
  }, 100 );
}
var obj = {
  a: 2
};
foo.call( obj ); // 2

 

 

虽然 self = this 和箭头函数看起来均可以取代 bind(..) ,可是从本质上来讲,它们想替代的是 this 机制。若是你常常编写 this 风格的代码,可是绝大部分时候都会使用 self = this 或者箭头函数来否认 this 机制,那你或许应当:


1. 只使用词法做用域并彻底抛弃错误 this 风格的代码;


2. 彻底采用 this 风格,在必要时使用 bind(..) ,尽可能避免使用 self = this 和箭头函数。


固然,包含这两种代码风格的程序能够正常运行,可是在同一个函数或者同一个程序中混合使用这两种风格一般会使代码更难维护,而且可能也会更难编写。

 

总结

若是要判断一个运行中函数的 this 绑定,就须要找到这个函数的直接调用位置。找到以后就能够顺序应用下面这四条规则来判断 this 的绑定对象。


1. 由 new 调用?绑定到新建立的对象。


2. 由 call 或者 apply (或者 bind )调用?绑定到指定的对象。


3. 由上下文对象调用?绑定到那个上下文对象。


4. 默认:在严格模式下绑定到 undefined ,不然绑定到全局对象。

 

ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法做用域来决定this ,具体来讲,箭头函数会继承外层函数调用的 this 绑定(不管 this 绑定到什么) 。这d其实和 ES6 以前代码中的 self = this 机制同样。

 

ps.其实关于这篇,例如对于this指向的显式绑定与后续的优先级,我都属于能理解是什么意思,可是却无法用我本身的语言去概括与总结...因此那部份内容仍是已整理为主。但愿在后续的工做中可以有更多的几乎去实践博客中所说起的语法糖,同时也但愿这篇博文能给你们带来一点点帮助。若是博文中有错误或者不详之处,请各位批评指正!完结撒花~

相关文章
相关标签/搜索