maicFir / lessonNote

JS学习笔记
33 stars 11 forks source link

迷失中的this指向,看完这篇就会了 #18

Open maicFir opened 2 years ago

maicFir commented 2 years ago

this是一个比较迷惑的人是东西,尽管你对this有很多的了解,但是面试题里面考察this指向总会让你有种猜谜的感觉,知道一些,但是还是会出错,或许你猜对了,但是又好像解释不太清楚。

嗯,不是你一个人这样,很多人都这样,包括我自己,本质上就是面试埋下的坑,让你跳进去,你想跳过去,那还是不太容易,真正对知识的理解与应用,绝不只是停留在概念与理念,也不是为了完成一道面试题,答不对也没关系,如果面试官给你耐心解释了这道题,那也是一次不错的学习机会。

正文开始...

在阅读本文之前,主要会从以下几点对this的思考

为了了解this,我们先看下this,新建一个index.html1.js

console.log(this, Object.getPrototypeOf(this));

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>this</title>
</head>
<body>
    <div id="app"></div>
    <script src="./1.js"></script>
</body>
</html>

当我们在浏览器打开时,我们会发现this是一个window对象

如果我们在终端直接运行1.js

{} [Object: null prototype] {}

node环境下,全局的this居然是一个{}对象

现在我们在js的最顶部使用use strict采用严格模式。

我们在函数内部写一个this

"use strict"
console.log(this, Object.getPrototypeOf(this));
var publicName = "Maic";
function hello() {
    console.log(this) // undefined
    console.log(this.publicName) // undefined
}
hello();

严格模式下函数内部会是undefined,并且访问publicName会直接报错

为啥use strict严格模式下全局this无法访问

于是查找资料寻得,严格模式主要有以下特征

还有其他的更多的参考js-script

this的指向

在这之前我们很基础的了解到在非严格模式下this指向的是window或者{}对象,在普通函数中this的指向是window全局对象

而你通常会看到this的指向并不都是指向全局对象,而是动态变化的,正因为它会变化,所以令人十分费脑壳

...
function Person() {
    this.age = 10;
    this.name = 'Web技术学苑';
    console.log(this, '111')
}
const person = new Person();
console.log(person, '222'); // Person { age: 10, name: 'Web技术学苑' }

至此你会发现,构造函数内部的this居然就是实例化的那个对象person

我们看下babel对上面一段代码编译成es5的代码

es6代码

var publicName = 'Maic';
const userInfo = {
    publicName: 'Jack',
    getName: () => {
        console.log(this.publicName, '---useInfo')
    }
}
userInfo.getName();

编译后的代码,大概就是下面这样的了

var _this = this;
var publicName = "Maic";
var userInfo = {
  publicName: "Jack",
  getName: function getName() {
    console.log(_this.publicName, "---useInfo");
  }
};
userInfo.getName();

其实箭头函数是非常迷惑人的,而且外面是一个被调用的是一个对象,所以时常会给人一种幻觉,我们常听到一句this指向的是被调用的那个对象,那么这里箭头函数this指向的是window,而const定义的变量会被转换成var

那怎么能让getName指向的是本身自己的useInfo

var publicName = 'Maic';
const userInfo = {
    publicName: 'Jack',
    getName: function(){
        console.log(this.publicName, '---useInfo') // Jack
    }
}
userInfo.getName();

你看当我把箭头函数改成普通函数,这个普通函数内部的this就指向userInfo

this指向被调用的那个对象貌似这句话后又在此时好像又是正确的

我们接下来看下下面一种情况

var publicName = 'Maic';
const userInfo = {
    publicName: 'Jack',
    getName: function(){
        console.log(this.publicName, '---useInfo') // Jack
    }
}
var user = userInfo.getName;
user();

那么此时getName内部的this又是谁呢? 此时你会发现打印的是Maic

此时会发现this指向的是window,也就是说指向的那个被调用者,那被调用者是谁?

上面那段代码同等于下面,你仔细看

var publicName = 'Maic'; // var 定义,实际上等同于window.publicName = publicName
function getName () {
console.log(this.publicName, '---useInfo') // Jack
}
const userInfo = {
    publicName: 'Jack',
    getName
}
// var user = userInfo.getName;
// or 等价于
// window.user = userInfo.getName;
// or 进一步等价
window.user = function getName () {
  console.log(this.publicName, '---useInfo') // Jack
}
// user();
// or 等价于
window.user();

所以你现在是不是很清晰明白this指向的也是被调用的那个对象window

但是有一点必须申明,必须在非严格模式下,此时的this才会指向window

迷失中的this指向

在这之前我们了解到非严格模式下

我们再来看下那些面试题中很迷惑的this

var user = {
    name: 'Maic',
    a: {
        name: 'Tom',
        b: function () {
            console.log(this.name)
        }
    }
}
console.log(user.a.b()) // Tom

没错,你看到的这个打印是Tom,这里直接调用的是b这个方法,被调用的是user.a这个对象,所以在b这个方法内部的this指向了a对象

如果是箭头函数呢

var publicName = "Maic";
...
var user = {
    name: 'Jack',
    a: {
        name: 'Tom',
        b: () => {
            console.log(this.name)
        }
    }
}
console.log(user.a.b()) // Maic

我们会发现通过babel转换后会是这样的

var _this = this;
var user = {
  name: "Jack",
  a: {
    name: "Tom",
    b: function b() {
      console.log(_this.name);
    }
  }
};

所以依然箭头函数内部依然是个全局对象window

我们接下来看一道真实的面试题

var obj = {
    a: 1,
    b: function () {
        console.log(this.a)
    },
    c: () => {
      console.log(this.a)
    }
}
var a = 2;
var objb = obj.b;
var objc = {
  a: 3
}
objc.b = obj.b;
const t = objc.b;
obj.b(); // 1
obj.c(); // 2
objb(); // 2
objc.b(); // 3
obj.b.call(null); // 2
obj.b.call(objc); // 3
t() // 2

我想信绝大大部分第一个obj.b()肯定是可以正确答出来,但是后面的貌似有些迷惑人,时常会让你掉进坑里

我们先看结论打印的依次肯定是

1
2
2
3
2
3
2

obj.b()的调用实际上在之前例子已经有讲,b方法是一个普通方法,内部this指向的就是被调用的obj对象,所以此时内部访问的a属性就是对象obj

var objb = obj.b,当我们看到这样的代码时,其实这段代码可以拆分以下

function b() {
  console.log(this.b)
}
window.objb = b;

本质上就是将对象obj的一个方法b赋值给了window.objb的一个属性

所以objb()的调用也是window.objb()objb方法内部this自然指向的就是window对象,而我们用var a = 2这个默认会绑定在window对象上

obj.c(),因为c是一个箭头函数,所以内部的this就是指向的全局对象

obj.b.call(null)这个null是非常迷惑人,通常来说call不是改变函数内部this的指向吗,但是这里,如果call(null)实际上会默认指向window对象

objc.b()这打印的是3,其实与objb的赋值有异曲同工之笔

...
var objc = {
  a: 3
}
objc.b = obj.b;

本质上就在objc动态的新增了一个属性b,而这个属性b赋值了一个方法,也就是下面这样

objc.b = function() {
  console.log(this.a)
}
objc.b() // 3

如果是const t = objc.b,至此你会发现,当我们执行t()时,此时打印的却是2那是因为const t定义的变量会编译成var从而t变量变成一个全局的window对象下的属性,本质上等价下面

...
// const t = objc.b
var a = 2;
/* 
等价于下面
var t = function() {
  console.log(this.a)
}
*/
// 本质上就是
window.t = function() {
    console.log(this.a)
}

所以你可以把上面那段代码看成下面这样

...
console.log((nobj.a.b).c()); //3
//or 相当于
/*
*
  var n = nobj.a.b;
  n.c()
*/

改变this对象的指向

这个相信很多小伙伴已经耳熟能祥了,call,apply,bind,能手撕call,apply,bind的文章已经不计其数

这里就只讲解如何使用,以及他们在业务中的一些具体使用场景

用一段伪代码举证以下

// index.vue
import configOption from './config'
export default {
  name: 'index',
  computed: {
   optionsBtnGroup() {
     return configOption.call(this)
   }
  },
  methods: {
    handleEdit(id) {
      console.log(id)
    },
    handleDelete(id) {
      console.log(id)
    }
  }
}

对应的template可能就是下面这样几个按钮

<div>
  <a href="javascript:void(0)" v-for="(item, index) in optionsBtnGroup" :key="index" @click="item.handle(item.id)">{{item.text}}</a>
</div>

我们再来看下config.js


export default () => {
  const options = [
    {
      text: '编辑',
      id: 123,
      handle: (id) => {
        this.handleEdit(id)
      }
    },
    {
      text: '删除',
      id: 234,
      handle: (id) => {
        this.handleDelete(id)
      }
    }
  ]
}

正因为在计算属性中用了call所以在config.js中才能访问外部methods的方法,有些人看到这样的代码肯定会说,两个按钮这么搞配置,代码反而多了这么多,还不如模版上放两个按钮完事

是的,确实是,当我们为了使用call而使用反而增加了业务代码的维护成本,正常情况还是建议不要写出上面那段坏代码的味道,我们只要明白在什么时候可以用,什么可以不用就行,不要为了使用而使用,反而本末倒置。

但是有时候如果业务复杂,你想隔离业务的耦合,达到通用,call能帮你减少不少代码量

apply也是可以改变this对象

const userInfo = {
    publicName: 'Jack',
    getName: () => {
        console.log(this.publicName, '---useInfo')
    }
}
function test(...args) {
   console.log(args); // ['hello', 'world']
   console.log(this.publicName);
}

test.apply(userInfo, ['hello', 'world'])

apply会立即执行该函数,如果传入的首个参数是null或者undefined,那么此时内部this指向的是window

另外还有一个方法可以让函数立即执行,也能改变当前函数this指向

...
var publicName = 'Maic';
function test(...args) {
   console.log(args);
   console.log(this.publicName);
}
Reflect.apply(test, {publicName: 'aaa'}, [1,2,3]) // aaa [1,2,3]
Reflect.apply(test, window, ['a', 'b', 'c']) // Maic ['a', 'b', 'c']

这也是可以改变this指向,不过会返回一个新函数,我们常常在react中发现这样用bind显示绑定方案。

我们写个简单的例子,尝试改变页面背景,切换肤色

document.body.addEventListener('click', function () {
    console.log(this) // body
    if (this.style.backgroundColor === 'red') {
        this.style.backgroundColor = 'green'
    } else {
        this.style.backgroundColor = 'red';
    }
})

可以切换背景肤色

以上貌似没有问题,但是你可能会写这样的代码

document.body.addEventListener('click',  () => {
    console.log(this)
    if (this.style.backgroundColor === 'red') {
        this.style.backgroundColor = 'green'
    } else {
        this.style.backgroundColor = 'red';
    }
})

此时内部的this一定指向的window,而且内部访问style报错

于是你会改成这样

const fn = function () {
    if (this.style.backgroundColor === 'red') {
        this.style.backgroundColor = 'green'
    } else {
        this.style.backgroundColor = 'red';
    }
}
document.body.addEventListener('click', fn)

是的,这样是可以的,本质上就是一个fn的形参,内部this指向仍然是document.body

于是为了借助bind,你可以这么做

const body = document.body;
const fn = function () {
    if (this.style.backgroundColor === 'red') {
        this.style.backgroundColor = 'green'
    } else {
        this.style.backgroundColor = 'red';
    }
}.bind(body)
body.addEventListener('click', fn)

这么做也是ok的

不知道你有没有疑问,为什不像下面这么做呢?

const body = document.body;
const fn = function () {
    if (this.style.backgroundColor === 'red') {
        this.style.backgroundColor = 'green'
    } else {
        this.style.backgroundColor = 'red';
    }
}

body.addEventListener('click', fn.bind(this))

如果你仔细看下,其实fn内部this指向是window,所以这是一个常会犯的错误。

还有为啥不是像下面这样

const body = document.body;
const fn = function () {
    if (this.style.backgroundColor === 'red') {
        this.style.backgroundColor = 'green'
    } else {
        this.style.backgroundColor = 'red';
    }
}

body.addEventListener('click', fn.bind(body))

以上功能没有任何问题,但是我们每次点击都会调用bind,从而返回一个新的函数,所以这种方式虽然效果一样,但是性能远不如第一种,为了更好理解,你可以写成下面这样

const body = document.body;
const fn = function () {
    if (this.style.backgroundColor === 'red') {
        this.style.backgroundColor = 'green'
    } else {
        this.style.backgroundColor = 'red';
    }
}
const callback = fn.bind(body)
body.addEventListener('click', callback)

总结