【进阶第 3 期】this 的全面解析

184 阅读7分钟

前言

尽管很多开发者每天都在使用 JavaScript,却不知道这背后发生了什么,该篇文章是本系列前端进阶之路文章的第三篇,旨在深入探讨 JavaScript 及浏览器工作原理,将前端相关如网络、页面渲染、浏览器安全、javascript执行机制等知识点串联起来,帮助更多开发者找到自己定位(查漏补缺),达到提升自己技能同时,游刃有余解决工作中难题(这些难题往往就是你对某些概念理解不够深刻)。

首先,我们为什么要搞懂、吃透this?

  • this使用频率很高,如果我们不懂this,那么在看别人的代码或者是源码的时候,就会很吃力。
  • 工作中,滥用this,却没明白this指向的是什么,而导致出现问题,但是自己却不知道哪里出问题了
  • 合理的使用this,可以让我们写出简洁且复用性高的代码。
  • 面试的高频问题,回答不好,抱歉,出门右拐,不送。

不管出于什么目的,我们都需要把this这个知识点整的明明白白的,Just do it。

【一】this是什么?

首先要知道this在javascript中解决了什么问题。

this提供了一种更优雅的方法来隐式'传递'一个对象的引用,因此可以我们可以将API设计得更加简洁并且易于复用。

实际工作中,往往你会走进的执行上下文 与 this 完全等价的误区, 所以先从执行上下文的视角来理解this最合适不过了。本系列前端进阶之路文章的第一篇调用栈与执行上下文已经详细介绍了执行上下文。

当一段代码被执行时,JavaScript 引擎先会对其进行编译,并创建执行上下文(包含变量环境、词法环境、外部环境(outer)和this),也就是说this跟执行上下文是绑定关系,每个执行上下文都对应着一个this. 而执行上下文分为:全局执行上下文函数执行上下文 eval 执行上下文(本文忽略)。我们就从这个角度来逐步分析

全局执行上下文中的 this

在控制台是输入console.log(this),可以得出结论,全局执行上下文中的 this 指向 window 对象,上文分析作用域链的底端也是window对象

函数执行上下文中的this

// case 1
function foo(){
	console.log(this)
}
foo() // 输出 window对象

// case 2
var bar = {
	name:"this is bar",
    showThis(){
    	console.log(this)
    }
}
bar.showThis() // 输出bar 对象

// case 3
var bar = {
	name:"this is bar",
    showThis(){
    	console.log(this)
    }
}
var foo = bar.showThis
foo() // 输出window对象

通过以上三个case,可以得出以下猜想:

  • 在全局环境中调用一个函数,函数内部的 this 指向的是全局变量 window。
  • 通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身

【二】this绑定规则

2.1、默认绑定

规则:在非严格模式下,默认绑定的this指向全局对象,严格模式下this指向undefined

// case 1 默认绑定
function foo() {
  console.log(this.bar); // this指向全局对象
}
var bar = 2;
foo(); // 2

function foo2() {
  "use strict"; // 严格模式this绑定到undefined
  console.log(this.bar); 
}
foo2(); // TypeError:a undefined

2.2、隐式绑定

规则:函数在调用位置,是否有上下文对象,如果有,那么this就会隐式绑定到这个对象上。

// case 2 隐式绑定
function sayHi(){
    console.log('Hello,', this.name);
}
var person = {
    name: 'foo',
    sayHi: sayHi
}
var name = 'bar';
person.sayHi();

这里要注意的是隐式绑定有一个大陷阱,绑定很容易丢失。

function sayHi(){
    console.log('Hello,', this.name);
}
var person = {
    name: 'foo',
    sayHi: sayHi
}
var name = 'bar';
var Hi = person.sayHi;
Hi();

// 结果:Hello, bar

2.3、显式绑定

规则:我们可以通过apply、call、bind将函数中的this绑定到指定对象上。

// case 3 显式绑定
function sayHi(){
    console.log('Hello,', this.name);
}
var person = {
    name: 'foo',
    sayHi: sayHi
}
var name = 'bar';
var Hi = person.sayHi;
Hi.call(person); // Hi.apply(person)

2.4、new绑定

规则:使用构造调用的时候,this会自动绑定在new期间创建的对象上。

先了解下new 构造函数,new的时候会做哪些事情:

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

2.5、特别地:箭头函数

  • 箭头函数的this指向不会使用上述的四条规则:
function foo() {
  return () => {
    console.log(this.a);
  };
}
let obj1 = {
  a: 1
};
let obj2 = {
  a: 2
};
let bar = foo.call(obj1); // foo this指向obj1
bar.call(obj2); // 输出2 这里执行箭头函数 并试图绑定this指向到obj2
// 輸出 1  

结论

  • 默认绑定:在非严格模式下,默认绑定的this指向全局对象,严格模式下this指向undefined
  • 隐式绑定:函数在调用位置,是否有上下文对象,如果有,那么this就会隐式绑定到这个对象上
  • 显式绑定:通过bind、call、apply可以显示的改变this的值,但值得注意的是箭头函数的this始终为最初设置的值,无法通过该方式改变
  • new 调用构造函数,this将被绑定到正在构造的新对象
  • 箭头函数不会运用上述规则

** 优先级 ** new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

具体这个优先级如何得出,你可以写个demo验证一下,这里暂不展开讨论

【三】this 取值的影响因子

上面我们了解this的绑定规则,那影响this取值的这些因子同时存在时,是绑定规则的优先级如何

3.1 严格模式 vs 非严格模式

针对普通函数,隐式赋值的this绑定默认的全局对象window或者undefiend,取决于是否严格模式

3.2 全局环境 vs 函数内

  • 无论是否在严格模式下,在全局执行环境中(在任何函数体外部)this 都指向全局对象。
  • 在函数内部,this的值取决于函数被调用的方式和是否箭头函数。

3.3 普通函数 vs 箭头函数

ES5 function里面的this谁调用它就指向谁,ES6箭头函数的this是在哪里定义就指向哪里

注意:

  • ES6 中的箭头函数并不会创建其自身的执行上下文,会捕获其所在外部函数的执行上下文的this值,作为自己的this值,无法(通过 bind call apply)改变指向
  • 函数隐式赋值容易导致this丢失(使用回调函数作为引用时特别注意:如setTimeout)
// 隐式赋值
var a = 'a in window'

var obj = {
    a:'a in obj',
    say:function(){
        console.log(this.a)
    }
}

var s = obj.say
s()

【四】this可能取值

4.1 构造函数 :this指向new 出来的对象

function Student(name,age){
    console.log(this) // 指向new 出来的对象
    this.name = name
    this.age = age
}
new student("Tim",18)

4.2 函数作为一个对象的属性:this指向该对象

var name = 'global'
var say = function(){
    console.log(this.name)
}
var obj  = {
    name:"this is obj",
    say:say
}
say()
obj.say()

4.3 call 、apply :this的值就取传入的对象的值

var name = "this is global"
var foo = { name: " this is foo"}
var bar = { name: " this is bar"}
function say(){ console.log(this.name)}

say.call(foo) //  this is foo
say.call(bar) //  this is bar

4.4 全局this===window ,普通函数与它调用上下文有关

console.log(this===window) // true
function say(){console.log(this === window)} // true

结语

本文主要回答了这几个方面的问题:

Q:this 是什么? A:this提供了一种更优雅的方法来隐式'传递'一个对象的引用。

Q:this 绑定规则有哪些? A: 默认绑定 、 隐式绑定 、显式绑定、 new 绑定 ,并指出箭头函数不会应用以上规则

Q:如何判断this的值? A:根据this的绑定的优先级,我可以梳理出一个思路:

  1. 函数是否在new中调用(new绑定)? 如果是的话this绑定的是新创建的对象。 如:var bar = new foo()
  2. 函数是否通过call、apply(显式绑定)、bind或者硬绑定调用?如果是的话,this绑定的是指定的对象。如: var bar = foo.call(obj2)
  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象。 如:var bar = obj1.foo()
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到 全局对象。如: var bar = foo()
  5. 除此之外还要记住当有赋值情况的时候会造成this绑定丢失情况 如:function go(fn){ fn = obj.say() }

思考:如何修复这段代码,达到修改userInfo属性的目的?

let userInfo = { 
	name:"jack.ma", 
	age:13, 
	sex:"male", 
	updateInfo:function(){ 
		//模拟xmlhttprequest请求延时,此处 隐式绑定丢失
        setTimeout(function(){ 
            this.name = "pony.ma" 
            this.age = 39 
            this.sex = "female" 
        },100)
    }
 }
 userInfo.updateInfo()