【你不知道的JavaScript】(四)this的全面解析

一、关于this 1. 为什么使用this

  • this 提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将 API 设计 得更加简洁并且易于复用。
  • this 使函数可以自动引用合适的上下文对象。
2. 对this的误解
  • 指向自身
  • 指向函数的作用域
3. this是什么?
  • this 既不指向函数自身也不指向函数的词法作用域;
  • this的指向,是在函数被调用的时候确定的,也就是执行上下文被创建时确定的;
  • this 的指向和函数声明的位置没有任何关系,只取决于函数的调用位置(也就是函数的调用方法);
  • 在函数执行过程中,this一旦被确定,就不可更改了。
var a = 10; var obj = { a: 20 }function fn () { this = obj; // 这句话试图修改this,运行后会报错 console.log(this.a); }fn();

二、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 的调用位置

(二)绑定规则
  1. 默认绑定(独立函数调用——无法应用其他规则时的默认规则),this指向全局对象window
function foo() { console.log( this.a ); // this指向全局对象 } var a = 2; foo(); // 2

对于默认绑定来说,决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。
如果函数体处于严格模式,this会被绑定到undefined,否则this会被绑定到全局对象。
function foo() { console.log( this.a ); } var a = 2; (function(){ "use strict"; foo(); // 2,严格模式下与 foo() 的调用位置无关 })();

  1. 隐式绑定
在一个函数上下文中,this由调用者提供,由调用函数的方式来决定。如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; obj.foo(); // 2 // 调用位置会使用 obj 上下文来引用函数, // 因此你可以说函数被调用时 obj 对象“拥有”或者“包含”它 // 所以此时的 this 指向调用 foo 函数的 obj 对象

对象属性引用链中只有最顶层或者说最后一层会影响调用位置,也就是说this指向最终调用函数的对象。举例来说:
function foo() { console.log( this.a ); }var obj2 = { a: 42, foo: foo }; var obj1 = { a: 2, obj2: obj2 }; obj1.obj2.foo(); // 42,此时的 this 指向 obj2 对象

隐式丢失
function foo() { console.log( this.a ); }var obj = { a: 2, foo: foo }; var bar = obj.foo; // 函数别名!函数的引用而不是函数的调用!!! var a = "oops, global"; // a 是全局对象的属性 bar(); // "oops, global"// 虽然 bar 是 obj.foo 的一个引用,但是实际上,它引用的是foo 函数本身, // 因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定

  1. 显式绑定(call(..)apply(..) 方法)
call()apply()方法,它们的第一个参数是一个对象,它们会把这个对象绑定到this,接着在调用函数时指定这个this
function foo() { console.log( this.a ); } var obj = { a:2 }; foo.call( obj ); // 2 // 在调用 foo 时强制把它的 this 绑定到 obj 上

  1. new绑定
JavaScript 中,构造函数只是一些使用new操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被new操作符调用的普通函数而已。
使用new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:
  1. 创建(或者说构造)一个全新的对象;
  2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
  3. 执行构造函数中的代码(为这个新对象添加属性、方法等);
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function foo(a) { this.a = a; } var bar = new foo(2); console.log( bar.a ); // 2 // 使用new 来调用foo(..)时,我们会构造一个新对象并把它绑定到foo(..)调用中的this上

(三)判断 this this的判断可以按照下面的优先级顺序来判断函数在某个调用位置应用的是哪条规则:
  1. 函数是否在new 中调用(new 绑定)?
    如果是的话,this 绑定的是新创建的对象。
var bar = new foo();

  1. 函数是否通过callapply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。
var bar = foo.call(obj2);

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

  1. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。
var bar = foo();

(四)绑定例外
  1. 被忽略的this
null 或者undefined作为this的绑定对象传入callapply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
function foo() { console.log( this.a ); } var a = 2; foo.call( null ); // 2

  1. 间接引用
间接引用最容易在赋值时发生;间接引用时,调用这个函数会应用默认绑定规则。
function foo() { console.log( this.a ); } var a = 2; var o = { a: 3, foo: foo }; var p = { a: 4 }; o.foo(); // 3 (p.foo = o.foo)(); // 2 // 赋值表达式 p.foo=o.foo 的返回值是目标函数的引用,也就是 foo 函数的引用 // 因此调用位置是 foo() 而不是 p.foo() 或者 o.foo()

(五)this词法 箭头函数并不是使用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, // this一旦被确定,就不可更改,所以箭头函数的绑定无法被修改。(new 也不行!)

【你不知道的JavaScript】(四)this的全面解析
文章图片
this的指向 小结
如果要判断一个运行中函数的this绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断 this 的绑定对象。
  1. new 调用?绑定到新创建的对象。
  2. call 或者 apply(或者 bind)调用?绑定到指定的对象。
  3. 由上下文对象调用?绑定到那个上下文对象。
  4. 默认:在严格模式下绑定到 undefined,否则绑定到全局对象。
【【你不知道的JavaScript】(四)this的全面解析】ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定 this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这其实和 ES6 之前代码中的 self = this 机制一样。

    推荐阅读