【JS 基础】JS 中的 this

104 阅读5分钟

this 的原理

  • this 指的是什么?
  • 为什么会有 this

上面的问题相信大家也有疑惑的时候,如果感兴趣可以移步阮一峰老师的 JavaScript 的 this 原理,讲的非常详细。

简单总结一下就是:在内存中函数是一个单独的值,所以它可以在不同的环境(context)执行,而 JavaScript 允许在函数体内部,引用当前环境的其他变量。所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。

JS 中的 this

如何正确判断 this?箭头函数的 this 是什么?

1. foo()及obj.foo()的调用方式

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

const obj = {
  a: 2,
  foo: foo
}
obj.foo()

const c = new foo()
  • 对于直接调用 foo 来说,不管 foo 函数被放在了什么地方,this 一定是 window
  • 对于 obj.foo() 来说,我们只需要记住,谁调用了函数,谁就是 this,所以在这个场景下 foo 函数中的 this 就是 obj 对象
  • 对于 new 的方式来说,this 被永远绑定在了 c 上面,不会被任何方式改变 this

2. 箭头函数中的 this

function a() {
  return () => {
    return () => {
      console.log(this)
    }
  }
}
console.log(a()()())

首先箭头函数其实是没有 this 的,箭头函数中的 this 只取决包裹箭头函数的第一个普通函数的 this。在这个例子中,因为包裹箭头函数的第一个普通函数是 a,所以此时的 thiswindow。另外对箭头函数使用 bind 这类函数是无效的。

箭头函数的 this 一旦被绑定,就不会再被任何方式所改变

3. callaplly

最后种情况也就是 bindcallapply 这些改变上下文的 API 了,对于这些函数来说,this 取决于第一个参数,如果第一个参数为空,那么就是 window

下面的代码不在严格模式下,且 this 的值不是由该调用设置的,所以 this 的值默认指向全局对象 window

function f1(){
  return this;
}
//在浏览器中:
f1() === window;   //在浏览器中,全局对象是window

//在Node中:
f1() === global;

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

function f2(){
  "use strict"; // 这里是严格模式
  return this;
}

f2() === undefined; // true

如果要想把 this 的值从一个环境传到另一个,就要用 call 或者apply 方法。

// 将一个对象作为call和apply的第一个参数,this会被绑定到这个对象。
var obj = {a: 'Custom'};

// 这个属性是在global对象定义的。
var a = 'Global';

function whatsThis(arg) {
  return this.a;  // this的值取决于函数的调用方式
}

whatsThis();          // 'Global'
whatsThis.call(obj);  // 'Custom'
whatsThis.apply(obj); // 'Custom'

当一个函数在其主体中使用 this 关键字时,可以通过使用函数继承自Function.prototypecallapply 方法将 this 值绑定到调用中的特定对象。

function add(c, d) {
  return this.a + this.b + c + d;
}

var o = {a: 1, b: 3};

// 第一个参数是作为 this 使用的对象
// 后续参数作为参数传递给函数调用
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16

// 第一个参数也是作为 this 使用的对象
// 第二个参数是一个数组,数组里的元素用作函数调用中的参数
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34

使用 callapply 函数的时候要注意,如果传递给 this 的值不是一个对象,JavaScript 会尝试使用内部 ToObject 操作将其转换为对象。因此,如果传递的值是一个原始值比如 7 'foo',那么就会使用相关构造函数将它转换为对象,所以原始值 7 会被转换为对象,像 new Number(7) 这样,而字符串 'foo' 转化成 new String('foo') 这样,例如:

function bar() {
  console.log(Object.prototype.toString.call(this));
}

//原始值 7 被隐式转换为对象
bar.call(7); // [object Number]

4. bind 方法

ECMAScript 5 引入了 Function.prototype.bind。调用f.bind(someObject)会创建一个与f具有相同函数体和作用域的函数,但是在这个新函数中,this将永久地被绑定到了bind的第一个参数,无论这个函数是如何被调用的。

function f(){
  return this.a;
}

var g = f.bind({a:"azerty"});
console.log(g()); // azerty

var h = g.bind({a:'yoo'}); // bind只生效一次!
console.log(h()); // azerty

var o = {a:37, f:f, g:g, h:h};
console.log(o.f(), o.g(), o.h()); // 37, azerty, azerty

如果对一个函数进行多次 bind,那么上下文会是什么呢?

let a = {}
let fn = function () { console.log(this) }
fn.bind().bind(a)() // => ?

等同于:
// fn.bind().bind(a) 等于
let fn2 = function fn1() {
  return function() {
    return fn.apply()
  }.apply(a)
}
fn2()

可以从上述代码中发现,不管我们给函数 bind 几次,fn 中的 this 永远由第一次 bind 决定,所以结果永远是 window

let a = { name: 'yck' }
function foo() {
  console.log(this.name)
}
foo.bind(a)() // => 'yck'

5. 作为构造函数

当一个函数用作构造函数时(使用new关键字),它的this被绑定到正在构造的新对象。

 // 构造函数这样工作:
 function MyConstructor(){
     // 函数实体写在这里
     // 根据需要在this上创建属性,然后赋值给它们,比如:
     this.fum = "nom";
     // ......
 
     // 如果函数具有返回对象的return语句,
     // 则该对象将是 new 表达式的结果。 
     // 否则,表达式的结果是当前绑定到 this 的对象。
     //(即通常看到的常见情况)。
   }
 
 function C(){
  this.a = 37;
}

var o = new C();
console.log(o.a); // 37


function C2(){
  this.a = 37;
  return {a:38};
}

o = new C2();
console.log(o.a); // 38

this 的优先级

  1. new 的方式优先级最高
  2. bindcallapply
  3. obj.foo()
  4. foo