阅读 124
JS中this指向问题

JS中this指向问题

1. 前言

this也算是面试必问的问题之一了。 但很多人对this理解得模模糊糊,工作时得打印出来看看才敢往下写,面试时只能回答个大概,细节经不起推敲。 今天我们就针对这个痛点,解决掉它!

现在我们来看道题:

var number = 5;
var obj = {
    number: 3,
    fn: (function () {
        var number;
        this.number *= 2;
        number = number * 2;
        number = 3;
        return function () {
            var num = this.number;
            this.number *= 2;
            console.log(num);
            number *= 3;
            console.log(number);
        }
    })()
}
var myFun = obj.fn;
myFun.call(null);
obj.fn();
console.log(window.number);
复制代码

答案依次是:10 9 3 27 20
惊不惊喜,意不意外?如果你比较容易就答对了,那说明你对this的理解还是非常到位的。不然,就跟我一块来看看这篇文章呗~ 这道题的解析在思考1哦~

2. this是啥?

在讨论this的时候,一般都会说“指向xxx”。this就是一个指针,在了解具体指向之前,我们先引入几个名词:

  • 默认绑定
  • 隐式绑定
  • 显示绑定
  • new绑定

而且它们的优先级new绑定 > 显示绑定 > 隐式绑定 > 默认绑定

2.1. 默认绑定

  • 在不能应用其它绑定规则时使用,通常是独立函数调用
  • 独立函数:是指在全局上下文中的函数,它的this指向如下:
    • 非严格且处于Node环境:globalThis
    • 非严格且处于Windows环境:window
    • 严格模式下: undefined

没有特殊声明的话,本文都是浏览器环境执行的结果。

function getName(){
  console.log('Name: ', this.name);
}
var name = 'laohuang';
getName();
复制代码

解析:调用getName()时,它处于全局上下文,应用了默认绑定,this指向全局对象window,所以控制台会打印:Name: laohuang

2.2. 隐式绑定

  • 函数的调用是通过某个对象调的,或者说调用位置上存在对象,也即obj.fn()
  • this指向对象属性链中最后一层。比如obj1.obj2.obj3.fn(), this指向obj3
  • 隐式绑定存在绑定丢失的情况。请记住:obj.fn()是隐式绑定,但如果fn()前啥都没有,属于默认绑定。

2.2.1. 典型的obj.fn

function getName(){
  console.log('Name: ', this.name);
}
var laohuang = {
  name: 'laohuang',
  getName: getName
}
var name = 'feifei';
laohuang.getName();
复制代码

解析getName函数在laohuang外部声明。laohuang内部的getName相当于做了一次赋值操作。在调用laohuang.getName()时,调用位置是laohuang,隐式绑定会把getName里的this绑定到laohuang上,所以控制台会打印:Name: laohuang

2.2.2. this指向对象属性链中最后一层

function getName(){
  console.log('Name: ', this.name);
}
var feifei = {
  name: 'feifei',
  getName: getName
}
var laohuang = {
  name: 'laohuang',
  friend: feifei
}
var name = 'FEHuang';
laohuang.friend.getName();
复制代码

解析this指向对象属性链中最后一层,所以隐式绑定会把this绑定到laohuang.frendfeifei上,所以控制台会打印:Name: feifei

2.2.3. 隐式绑定的大陷阱 - 绑定丢失

1. 绑定丢失 - 将函数的引用给另一变量时:

function getName(){
  console.log('Name: ', this.name);
}
var laohuang = {
  name: 'laohuang',
  getName: getName
}
var name = 'FEHuang';
var getNameCopy = laohuang.getName;
getNameCopy();
复制代码

解析var getNameCopy = laohuang.getNamegetName的引用赋值给了getNameCopygetNameCopy直接指向getName方法getNameCopy()前啥都没有,所以它是默认绑定this指向全局上下文。所以控制台会打印:Name: FEHuang

2. 绑定丢失 - 回调函数中:

function getName(){
  console.log('Name: ', this.name);
}
var feifei = {
  name: 'feifei',
  getName: getName
}
var laohuang = {
  name: 'laohuang',
  getName: function() {
    setTimeout(function() {
      console.log('Name: ', this.name)
    })
  }
}
var name = 'FEHuang';
laohuang.getName(); // Name: FEHuang
setTimeout(feifei.getName, 1000); // Name: FEHuang
setTimeout(function() {
  feifei.getName(); // Name: feifei
}, 1000)
复制代码

解析

  • laohuang.getName(): 这里很好理解,setTimeout的回调函数中,this使用的是默认绑定,此时又是非严格模式,因此打印Name: FEHuang
  • setTimeout(feifei.getName, 1000): 这里相当于将feifei.getName的引用直接给了setTimeout第一个变量,最后执行了这个变量。这里绑定丢失,使用了默认绑定,因此指向全局上下文,打印:Name: FEHuang
  • setTimeout(function() { feifei.getName(); }, 1000): 虽然也是在setTimeout的回调中,但这里是直接执行了feifei.getName(),使用隐式绑定,this指向feifei。所以打印:Name: feifei

2.3. 显示绑定

  • 显示绑定就是通过call,apply,bind的方式,显式的指定this所指向的对象。
  • call,applybind的第一个参数,就是对应函数的this所指向的对象。callapply的作用一样,只是传参方式不同。callapply都会执行对应的函数,而bind方法不会。
  • 注意call,apply的特殊传参会被转换:传null/undefined --> 全局上下文;原始值 --> 对象(非严格模式)/原始值(严格模式)

2.3.1. 典型的显示绑定

function getName(){
  console.log('Name: ', this.name);
}
var laohuang = {
  name: 'laohuang',
  getName: getName
}
var name = 'FEHuang';
var getNameCopy = laohuang.getName;
getNameCopy.call(laohuang);
复制代码

解析:显示绑定直接将this绑定到了laohuang,所以控制台会打印:Name: laohuang

2.3.2. 特殊情况 - 使用call, apply, bind时也可能会遇到绑定丢失

那么,使用了显示绑定,是不是意味着不会出现隐式绑定所遇到的绑定丢失呢?显然不是这样的,不信,继续往下看。

function getName(){
  console.log('Name: ', this.name);
}
var laohuang = {
  name: 'laohuang',
  getName: getName
}
var name = 'FEHuang';
var getNameCopy = function(fn) {
  fn();
};
getNameCopy.call(laohuang, laohuang.getName);
复制代码

解析getNameCopy.call(laohuang, laohuang.getName)的确将this绑定到laohuangthis了。但call的第二个参数传的getName的引用,所以在执行fn()的时候,相当于直接调用了getName()方法。所以控制台会打印:Name: FEHuang思考: 如果希望绑定不会丢失,要怎么做?(答案在最后的思考2)

2.3.3. 特殊情况 - call, apply, bind的特殊传参

在非严格模式下使用callapply时,如果用作this的值不是对象,则会被尝试转换为对象。nullundefined被转换为全局对象原始值如 7 或 'foo' 会使用相应构造函数转换为对象

1. 传null/undefined: 会将其转换成全局对象,实际使用默认绑定。

var laohuang = {
  name: 'laohuang'
}
var name = 'FEHuang';
function getName() {
  console.log(this.name);
}
getName.call(null); //FeHuang 
复制代码

解析:实际应用默认绑定,所以控制台会打印:FEHuang

2. 传原始值: 会将其转换成对应的对象

var doSth = function(name){
  console.log(this);
  console.log(name);
}
doSth.call(2, 'laohuang'); // Number{2}, 'laohuang'
var doSth2 = function(name){
  'use strict';
  console.log(this);
  console.log(name);
}
doSth2.call(2, 'laohuang'); // 2, 'laohuang'
复制代码

2.4. new绑定

2.4.1. new干了什么?

MDN上的介绍时这样的:

  1. 创建一个空的简单JavaScript对象(即{});
  2. 链接该对象(设置该对象的constructor)到另一个对象;
  3. 将步骤1新创建的对象作为this的上下文;
  4. 如果该函数没有返回对象,则返回this

2.4.2. 举例

function getName(name) {
    this.name = name
}

var laohuang = new getName('laohuang');
console.log('Name: ', laohuang.name);
复制代码

解析:在var laohuang = new getName('laohuang')这步,会将getName中的this绑定到对象laohuang上。所以控制台会打印:Name: laohuang

2.5. 箭头函数

先来看看箭头函数的特点:

  • 函数体内的this对象,继承的是外层代码块的this注意:箭头函数内的this不是定义时所在的对象,而是外层代码块的this。
  • 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
  • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用rest参数代替。
  • 不可以使用yield命令,因此箭头函数不能用作Generator函数。
  • 箭头函数没有自己的this,所以不能用call()、apply()、bind()这些方法去改变this的指向.
var names = {
    getName0: function() {
        console.log(this);
        return () => {
            console.log(this);
        }
    },
    getName1: function() {
        return function() {
            console.log(this);
            return () => {
                console.log(this);
            }
        }
    },
    getName2: () => {
        console.log(this);
    }
}

// 第一段
var name0 = names.getName0(); // names对象
name0(); // names对象

// 第二段
var name1 = names.getName1();
var _name1 = name1(); // window对象
_name1(); // window对象

// 第三段
names.getName2(); // window对象
复制代码

解析

第一段:

  • names.getName0()对应隐式绑定,this绑定在了names上,所以控制台会打印:names对象;
  • name0()执行的就是箭头函数。箭头函数的this继承上一个代码段的this(即getName0()运行时的this,即names)。所以控制台也会打印:names对象;

第二段:

  • name1names.getName1()运行后返回的一个全新的函数,对应了上边说到的隐式绑定丢失的情况。此时应用的是默认绑定,this指向了全局对象window。所以name1()打印的是:window对象;
  • _name1()执行的是箭头函数。
    • 如果箭头函数的this继承自定义时的对象,那_name1()应该打印names对象才对,但这里打印的是window对象,显然这种理解是错误的。
    • 按照箭头函数的this是继承于外层代码块的this就很好理解了。外层代码块我们刚刚分析了,this指向的是window,因此这里控制台打印:window对象。

第三段:

  • names.getName2()执行的是箭头函数。由于当前的代码块names中不存在this,只能往上层查找。所以这里控制台打印:window对象。

2.5.1. 请牢记:箭头函数中的this继承于外层代码库块的this

因为箭头函数里的this也有可能是动态的哟~ 不信看下面的代码:

var names = {
    getName1: function() {
        return function() {
            console.log(this);
            return () => {
                console.log(this);
            }
        }
    },
}

var name0 = names.getName1();

var n1 = name0(); // window
n1(); // window

var n2 = name0.call(names); // names
n2(); // names  
复制代码

3. 总结

3.1. 如何准确判断this的指向

再来复习一下,绑定的优先级是:new绑定 > 显示绑定 > 隐式绑定 > 默认绑定。
然后我们就可以按以下步骤来判断了:

  • 函数是否在new中调用(new绑定),如果是,那么this绑定的是新创建的对象;
  • 函数是否通过call,apply调用,或者使用了bind(显示绑定),如果是,那么this绑定的就是指定的对象;
  • 函数是否在某个上下文对象中调用(隐式绑定),如果是的话,this绑定的是那个上下文对象。一般是obj.fun();
  • 如果以上都不是,那么使用默认绑定。如果在严格模式下,则绑定到undefined,否则绑定到全局对象(node环境的全局对象是globalThis,浏览器环境就是window);
  • 如果把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则;
  • 如果是箭头函数,箭头函数的this继承的是外层代码块的this。

最后,this指向还需要多加练习,本文只是列举了个大概,只有不断练习才能熟练掌握哦~
有错误,欢迎指正哦~

4. 思考

4.1. 思考1

var number = 5;
var obj = {
    number: 3,
    fn: (function () {
        var number;
        this.number *= 2;
        number = number * 2;
        number = 3;
        return function () {
            var num = this.number;
            this.number *= 2;
            console.log(num);
            number *= 3;
            console.log(number);
        }
    })()
}
var myFun = obj.fn;
myFun.call(null);
obj.fn();
console.log(window.number);
复制代码

解析

  • 定义阶段:在定义obj的时候,fn对应的闭包就执行了。执行闭包中的代码、返回函数时,显然这里不是new绑定 (没有new关键字),不是显示绑定 (没有call, aply, bind),不是隐式绑定(没有obj.fun()),所以是默认绑定,this指向的是全局变量window。数据变化过程如下:
  var number; //新声明的number。因为是闭包,这个number不会被销毁。
  this.number *= 2; // this指向window,所以window.number:5 * 2 --> 10.
  number = number * 2; // 没有通过this调用,这里number是闭包中新声明的那个。闭包的number:undefined --> NAN.
  number = 3; // 闭包的number: NAM --> 3.
复制代码
  • var myFun = obj.fn:将obj.fn的引用赋值给myFun。这里很明显的隐式绑定丢失,所以应该是默认绑定,myFun的this会指向全局变量window。只是赋值操作,没有方法调用,所以数据不变。
  • myFun.call(null):用了call,但第一个参数是null,所以还是应用默认绑定。数据变化如下:
// myFunc
function () {
    var num = this.number; // this指向window。所以num === window.number,值为10.
    this.number *= 2; // this指向window。所以window.number:10 * 2 --> 20.
    console.log(num); // num的值为10,打印10.
    number *= 3; // 没有通过this调用,这里number是闭包中的那个。闭包的number:3 * 3 --> 9.
    console.log(number); // 打印闭包的number,打印9.
}
复制代码
  • obj.fn():典型的默认绑定,这里this指向obj。因此数据变化如下:
// obj.fn
function () {
    var num = this.number; // this指向obj。所以num === obj.number,值为3.
    this.number *= 2; // this指向obj。所以obj.number: 3 * 2 --> 6.
    console.log(num); // num为3,打印3
    number *= 3; //  没有通过this调用,这里number是闭包中的那个。闭包的number:9 * 3 --> 27.
    console.log(number); // 打印闭包的number,打印27
}
复制代码
  • console.log(window.number):此时,window.number值为20。
  • 所以,最后打印的结果依次是:10, 9, 3, 27, 20。

4.2. 思考2

问题:显示绑定 - 如果希望绑定不会丢失,要怎么做?
解答:在调用fn的时候,也给它做个显示绑定。

function getName(){
    console.log('Name: ', this.name);
}
var laohuang = {
    name: 'laohuang',
    getName: getName
}
var name = 'FEHuang';
var getNameCopy = function(fn) {
  fn().bind(this);
};
getNameCopy.call(laohuang, laohuang.getName);
复制代码

5. 参考

文章分类
前端
文章标签