JavaScript中的this

通过谁调用的函数,谁就是this。

JavaScript中的this的值的和其他语言的会更怪(weird)。

由之前从反转链表到执行上下文中我们知道this是执行上下文的一个属性,其值在执行上下文创建阶段(函数被调用时)时确定。

我们知道,执行上下文无非为globalfunctioneval(不讨论)这个三个的属性,因此我们来讨论全局上下文和函数上下文的this。

全局上下文

全局上下文中的this的值是比较容易的,没有那么多条条框框,无论是否处于严格模式,在全局执行环境中(在任何函数体外部)this 都指向全局对象。

console.log(this === window); // => true

let globalVar = "global var";
console.log(this.globalVar); // => "global var"

函数上下文

全局上下文的this是没什么令人费解的,重头戏是函数上下文,因为JS中除了基本类型都是对象类型,this的规则会变得乱起来。

作为普通函数

在非严格模式下, this 的值默认指向全局对象。

function f(){
  return this;
}
//浏览器
window === f(); // => true
//Node.js
globalThis === f(); // => true

处于严格模式this的值为undefined

"use strict";
function f(){
  return this;
}
f() === undefined; // => true

作为对象的方法

当函数作为对象里的方法被调用时,this 被设置为调用该函数的对象。

let obj = {
  prop: 3.14,
  method: function() {
    return this.prop;
  }
}

obj.method(); // => 3.14

即便对象的方法是在全局作用域添加也不会影响这个默认行为:

let obj = {
  PI: 3.1415,
}

function method(){
  return this.PI;
}

//添加方法到obj
obj.method = method;
//不会影响这个行为
obj.method(); // => 3.1415

可以看出调用才是关键。

this的成员绑定使用“就近原则”:

let obj = {
  PI: 3.14,
}
function method(){
  return this.PI;
}
obj.innerObj = {inner: method, PI: 3.1415};
//innerObj是最近的对象引用
obj.innerObj.inner(); // => 3.1415

作为构造函数

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

const O = function(){
  this.PI = 3.14;
}
let PI = 3.1414;
let obj = new O();
obj.PI; // => 3.14

// 当然构造函数也可以返回对象
const O2 = function(){
  this.a = "baz"; //僵尸代码? 但对外界无影响
  return {a: "bar"}
}
let obj2 = new O2();
obj2.a; // => "bar"

作为箭头函数

箭头函数是没有单独的this的。它只会从自己的作用域链的上一层继承this

function Person(){
  this.g = "not arrow";
  setTimeout(() => {
    console.log(this.g);// this的值为继承Person实例的this
  },0);
}
let p = new Person(); // => "not arrow"

处于严格模式时,this的规则将被忽略:

"use strict";
let baz = () => {return this};
baz(); // => 不是undefined 而是window(或globalThis)

再将MDN上的一个例子摘录(抄。。)过来,能够将混乱的this理清一点。

// 创建一个含有bar方法的obj对象,
// bar返回一个函数,
// 这个函数返回this,
// 这个返回的函数是以箭头函数创建的,
// 所以它的this被永久绑定到了它外层函数的this。
// bar的值可以在调用中设置,这反过来又设置了返回函数的值。
var obj = {
  bar: function() {
    var x = (() => this);
    return x;
  }
};

// 作为obj对象的一个方法来调用bar,把它的this绑定到obj。
// 将返回的函数的引用赋值给fn。
var fn = obj.bar();

// 直接调用fn而不设置this,
// 通常(即不使用箭头函数的情况)默认为全局对象
// 若在严格模式则为undefined
console.log(fn() === obj); // true

// 但是注意,如果你只是引用obj的方法,
// 而没有调用它,再次说明了调用才是关键所在!
var fn2 = obj.bar;
// 那么调用箭头函数后,this指向window,因为它从 bar 继承了this。
console.log(fn2()() == window); // true

"手动"更改this

JavaScript中有三个内置函数Function.prototype.call(), Function.prototype.apply()Function.prototype.bind()可以更改函数上下文的this

Function.prototype.call(thisArg, arg1, arg2, ...)

  • thisArg

    • 必选的。在 func 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 nullundefined 时会自动替换为指向全局对象,原始值会被包装(装箱)。

记录一些让我感到weird的例子。。:

  1. 匿名函数调用call()

    let animals = [
      {species: "Cat", name: "Lihua"},
      {species: "Dog", name: "Erha"},
    ]
    
    for(let i = 0; i<animals.length; i++){
        (function(i){
        console.log('#' + i + ' ' + this.species + ': ' + this.name);
      }).call(animals[i], i);
    }
    /*
      #0 Cat: Lihua
      #1 Dog: Erha
    */
    
  2. 不指定第一个参数的值

    非严格模式下,不指定第一个参数的值或者参数为undefinednullthis 的值将会被绑定为全局对象。

    let sData = 'Wisen';
    
    function display() {
      console.log('sData value is %s ', this.sData);
    }
    
    display.call();  // sData value is Wisen
    

    严格模式下,this的值会为undefined

    'use strict';
    
    let sData = 'Wisen';
    
    function display() {
      console.log('sData value is %s ', this.sData);
    }
    
    display.call(); // Cannot read the property of 'sData' of undefined
    

Function.prototype.apply(thisArg, [argsArray])

apply()call()这两个方法比较类似,区别就是call()方法接受的是参数列表,而apply()方法接受的是一个参数数组。因为MDN文档上的例子非常好,可以自行去阅读。

Function.prototype.bind(thisArg[, arg1[, arg2[, ...]]])

与上面两个函数不同的是,bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

简单用法:

this.x = 123; // = window.x 全局变量
let module = {
  x: "local var",
  getX: function(){
    return this.x;
  }
}

module.getX(); // "local var" 作为方法调用

let another = module.getX; // 赋值给全局变量another
another(); // => 123 window.another()..

//使用bind()
another = another.bind(module);
another(); // => "local var"

当返回的绑定函数作为构造函数时,原来提供的 this 就会被忽略,并且返回一个实例(即和普通的构造函数没啥区别),但是提供的参数列表仍然会插入到构造函数调用时的参数列表之前。

function Cat(name, age){
  this.age = age;
  this.name = name;
}
//重写toString()
Cat.prototype.toString = function() {
  return this.name + ',' + this.age;
}
let liHua = new Cat("狸花", 11);
liHua.toString(); // => "狸花,11"

let emptyObj = {};
let nCat = Cat.bind(emptyObj, "三花"); // 绑定一个空对象作为this,并且预设第一个参数,这个参数不会被覆盖!

let sanHua = new nCat(7); // 只传三花的年龄
sanHua.toString(); // => "三花,7" 可见emptyObj被忽略了,this为新实例的上下文

sanHua instanceof Cat; // => true
sanHua instanceof nCat; // => true

还有一点就是绑定函数不能继续向后bind()

function foo(){
  console.log(this.PI);
}

let pi1 = {PI: 3.14};
let pi2 = {PI: 3.1415926};

let boundFun = foo.bind(pi1);
boundFun(); // => 3.14

//链式bind()始终是第一个this
boundFun == foo.bind(pi1).bind(pi2);
boundFun(); // => 3.14

箭头函数和bind, call和apply

如果将this传递给callbind、或者apply来调用箭头函数,它将被忽略。不过你仍然可以为调用添加参数,不过第一个参数(thisArg)应该设置为null

let globalObj = this; //  全局对象
let bar = (() => this);

let obj = {bar: bar};
obj.bar() === globalObj; // => true

// 尝试使用call、bind来设定this
bar.call(obj) === globalObj; // => true
bar.bind(obj) === globalObj; // => true
//...

Reference参考: