理解 this

222 阅读6分钟

任何足够先进的技术都和魔法无异! ——Arthur C. Clarke

初步理解 this

this 是在函数被调用时发生的绑定,它指向什么完全取决于函数的调用位置(而不是声明位置)

  • this 是在运行时进行绑定的,并不是在编写时绑定
  • 随着你的使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用 this 则不会这样

看几个例子初步理解一下 this:

// 1. 为什么要使用 this
let me = {
  name: "Kyle",
};

// 如果不使用 this,需要显式传入一个上下文对象
function identify(obj) {
  return obj.name.toUpperCase();
}

identify(me); // KYLE

// 改进!
function identify() {
  return this.name.toUpperCase();
}

// call 方法使 identify 中的 this 指向 me,相当于 me.name
identify.call(me); // KYLE

// 2. this 并不像我们所想的那样指向函数本身
function foo() {
  this.count++; // this => window,自动创建了全局变量count
  console.log(this.count); // NaN
}
foo.count = 2; // 此 count 非 this.count -- WTF?
foo();

// 3. 具名函数,可以通过函数名从函数内部引用自身
function foo() {
  foo.count++;
  console.log(foo.count); //3
}
foo.count = 2;
foo();

// 4. 可以强制 this 指向函数自身
function foo() {
  this.count++;
  console.log(this.count); //3
}
foo.count = 2;
foo.call(foo);

那么到底 this 指向的规律是什么呢~ ↓

this 全面解析

this 的指向大致可以分为以下 4 种(以及四种 this 的绑定规则)

  • 作为普通函数调用 (默认绑定,指向全局对象)
  • 作为对象的方法调用 (隐式绑定,指向对象本身)
  • 使用 call 方法 或 apply 方法调用(显示绑定,直接指定 this 的绑定对象)
  • 使用构造器调用(new 绑定,实例绑定到构造函数的 this)

1. 默认绑定

  • 作为普通函数调用时,应用 this 的默认绑定,指向全局对象 window (浏览器环境下)
  • 但要注意全局对象全局变量的区别!ES6 中规则是不同的。(参见扩展)
// var 声明的全局变量,等于全局对象的属性(ES5)即 window.a = 1 。this 又指向全局对象 window。
var a = 1;
function foo() {
  console.log(this.a); // 1
}
foo();

// 注意,而 ES6 中 let 声明的全局变量,不再属于全局对象的属性。this 还是指向全局对象,会返回的 undefined。
let a = 1;
function foo() {
  console.log(this.a); // undefined
}
foo();

2. 隐式绑定

  • 当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。
  • 作为对象的方法调用时,方法中 this 指向对象本身
let obj = {
  key: "Jack",
  setKey: function (key) {
    this.key = key;
  },
  getKey: function () {
    console.log(this); // obj
    return this.key; // 相当于 obj.name
  },
};

obj.setKey("Tom");
obj.getKey(); // Tom
  • 隐式绑定丢失 this
let obj = {
  key: "Jack",
  setKey: function (key) {
    this.key = key;
  },
  getKey: function () {
    console.log(this); // window
    return this.key;
  },
};

let f = obj.getKey; // f 是对 obj.getKey 的引用,但调用位置在全局
f(); // undefined

// 假如全局定义了该属性的话
var key = "122";
let f = obj.getKey;
f(); // 122

// 同上 let 定义了也不行,因为不是定义在 window 上的。
let key = "122";
let f = obj.getKey;
f(); // undefined
  • 同理,回调函数会丢失 this
var obj = {
  a: 2,
  foo: function () {
    console.log(this.a); 
  },
};
function doFoo(fn) {
  fn();
}

var a = "global";
doFoo(obj.foo); // global
  • setTimeout 会导致 this 丢失
var obj = {
  a: 2,
  foo: function () {
    console.log(this.a); // global
  },
};
var a = "global";
setTimeout(obj.foo, 100); // 100ms后函数执行,this指向window

// setTimeout内部实现伪代码
function setTimeout(fn, delay) {
  // 等待 delay 毫秒
  fn(); // 调用位置
}

// 再看一个例子
var obj = {
  a: 2,
  foo: function () {
    setTimeout(function () {
      console.log(this.a);
    }, 100);
  },
};
var a = "global";
obj.foo(); // global
obj.foo.call({ a: "global" }); // global(因为是foo函数进行的call绑定,而不是settimeout的匿名回调函数)

// 可以改成这样!
var obj = {
  a: 2,
  foo: function () {
    let _this = this;
    setTimeout(function () {
      console.log(_this.a);
    }, 100);
  },
};
var a = "global";
obj.foo(); // 2

// 也可以将setTimeout的回调函数改为使用箭头函数形式
function foo() {
  setTimeout(() => {
    console.log("id:", this.id);
  }, 100);
}

var id = 21;
foo.call({ id: 42 }); // 42

3. 显示绑定

可参考文章 《call、apply 和 bind 的用法与区别》

1. call()、apply()、bind() 方法

可以直接指定 this 的绑定对象,称为显式绑定。function.call(thisArg, arg1, arg2, ...) 第一个参数是一个对象,调用该函数将 this 绑定到该对象上。

function foo() {
  console.log(this.a);
}
var obj = { a: 2 };
foo.call(obj); // 把 foo 的 this 绑定到 obj 上,并将参数传入foo

显式绑定仍然无法解决我们之前提出的丢失绑定问题。但是可以采用下面的方式 ↓↓↓

2. 硬绑定方案
function foo() {
  console.log(this.a);
}
var obj = { a: 2 };
var bar = function () {
  foo.call(obj);
};
bar(); // 2
// 之后如何调用函数 bar,它总会手动在 obj 上调用 foo
setTimeout(bar, 100); // 2

4. new 绑定

基本概念

  • 构造函数/构造器:使用 new 操作符时,被调用的普通函数
  • 包括内置对象函数在内的所有函数都可以用 new 来调用,这种函数调用被称为构造函数调用。也叫构造器调用
  • 实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”

构造函数调用时,会自动执行下面的操作:

  • 创建一个全新的对象
  • 新对象会被执行[[原型]]连接
  • 新对象会绑定到函数调用的 this
  • 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象
function foo(a) {
  this.a = a;
}
var bar = new foo(2); // bar 会绑定到 foo 的 this 上
console.log(bar.a); // 2

扩展:ES6 关于顶层对象

顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。但 ES6 做出了改变。

顶层对象

  • 顶层对象/全局对象,在浏览器环境指的是 window 对象,在 Node 指的是 global 对象。ES5 之中,顶层对象的属性全局变量是等价的。
  • ES6 为了保持兼容性,var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性
  • 但 let 命令、const 命令、class 命令声明的全局变量,不属于顶层对象的属性
// 设置了顶层对象的属性,则a就成为了全局变量。很容易创建错误的全局变量。
window.a = 1;
a; // 1

// 全局变量b 由var声明,所以它也在全局对象上。
var b = 1;
window.b; // 1

// 但ES6做出了改变!它不再是顶层对象的属性。
let c = 1;
window.c; // undefined

关于全局的 this 指向

同一段代码为了能够在各种环境,都能取到顶层对象,可以使用 this 变量

  • 全局环境下,this 返回全局对象。Node.js 模块中 this 返回的是当前模块,ES6 模块中 this 返回的是 undefined !(比如用 let 定义的变量并不挂在全局对象上,this 取值默认为 undefined)
  • 函数里面的 this,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this 会指向顶层对象。但是,严格模式下,这时 this 会返回 undefined。
let j = 1;
this.j; // undefined

var k = 1;
this.k; // 1

参考

《你不知道的 JavaScript》(上卷)

es6.ruanyifeng.com/#docs/let#g…