你不知道的this指向

你不知道的this指向


注:网上已经有很多关于this指向的文章,为什么自己还要去写一篇关于this的文章?学习的过程就像是爬山一样,大家都沿着不同的路登山,分享着自己看到的风景拍很多的照片但是这些照片不一定都一样,你不一定能看到别人看到的风景,体会到别人的心情。只有自己去登山,才能看到不一样的风景,自己的体会才更加深刻。

this关键字是JavaScript中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。但是即使是非常有经验的JavaScript开发者也很难说清它到底指向什么。

为什么要使用this

这个问题其实不是很难解释,可以先看看一段代码,比如下面的代码可以想象一样如果没有使用this会出现什么样的问题,并且思考一下如果不使用this应该怎么样去写。

   function sayHi() {
            var name = this.name
            console.log(`Hello World ${name}`)
        }
        var liuli = {
            name: '一行琉璃',
            sayHi: sayHi
        }
        var jianshu = {
            name: '坚书直实',
            sayHi: sayHi
        }
        liuli.sayHi() //Hello World 一行琉璃
        jianshu.sayHi() //Hello World 坚书直实
//如果不使用this,我们就需要显式的给sayHi()传入一个上下文对象
   function sayHi(context) {
            var name = context.name
            console.log(`Hello World ${name}`)
        }
        var liuli = {
            name: '一行琉璃',
            sayHi: sayHi
        }
        var jianshu = {
            name: '坚书直实',
            sayHi: sayHi
        }
        sayHi(liuli) //Hello World 一行琉璃
        sayHi(jianshu) //Hello World 坚书直实

看完上面代码,可以清楚的知道this 提供了一种更优雅的方式来隐式“传递”一个对象引用,并且可以使API设计更加简洁让其能够复用。

随着你的使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用 this 则不会这样。当我们学习完对象和原型时,你就会明白函数可以自动引用合适的上下文对象有多么的重要。

对于this的误解

我们之后会解释 this 到底是如何工作的,但是首先需要消除一些关于 this 的错误认识。 太拘泥于“this”的字面意思就会产生一些误解。有两种常见的对于 this 的解释,第一种是认为this指向自身,第二种是认为this指向它的作用域,但是它们都是错误的。

指向自身

大家都很容易把this理解成指向函数自身,这个角度似乎从英语this的意思来说可以理解。

判断this是不是自身

function fn(){
    console.log(this.name)
}
fn.name = '一行琉璃'
fn()  //undefined 可以看出这里的this并不是
//可以再看一个例子
  function foo(num) {
            console.log("foo: " + num);
            // 记录 foo 被调用的次数
            this.count++;
        }
        foo.count = 0;
        var i;
        for (i = 0; i < 10; i++) {
            if (i > 5) {
                foo(i);
            }
        }
        // foo: 6
        // foo: 7
        // foo: 8
        // foo: 9
        // foo 被调用了多少次?
        console.log(foo.count); // 0 -- WTF?   为什么会出现这种情况呢我们在下面分析

console.log 有 4 条输出,证明 foo() 确实被调用了 4 次,但是foo.count 仍然 是 0。显然从字面意思来理解 this 是错误的。

执行 foo.count = 0 时,的确向函数对象 foo 添加了一个属性 count。但是函数内部代码 this.count 中的 this 并不是指向那个函数对象,所以虽然属性名相同,根对象却并不相 同,困惑随之产生。

或许有疑问,那么我增加的是哪一个count呢?其实这段代码是在无意中创建了一个全局变量count,值为NaN。这里就不做过多解释

这里我们如何去解决这个count问题呢,我们可以在上面代码中创建一个data对象里面有一个属性count值为0,就可以解决但是这个并不利于我们去理解this,只是返回了自己的舒适区——词法作用域。

当然我们也可以改变this的指向来达到目的,这里先不做代码演示。

指向它的作用域

第二种常见的误解是,this 指向函数的作用域。这个问题有点复杂,因为在某种情况下它 是正确的,但是在其他情况下它却是错误的。

判断this是不是指向它的作用域

function foo() {
            var a = 7;
            this.bar();
        }
        function bar() {
            console.log(this.a);
        }
        foo(); //undefined
//显然我们是通过this.bar()无法实现

首先,这段代码试图通过 this.bar() 来引用 bar() 函数。这是绝对不可能成功的,我们之 后会解释原因。调用 bar() 最自然的方法是省略前面的 this,直接使用词法引用标识符。

this到底是什么

排除了一些错误理解之后,我们来看看 this 到底是一种什么样的机制。

之前我们说过 this 在运行时绑定,不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

当一个函数被调用时,会创建一个执行上下文。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this 就是记录的 其中一个属性。

this的全面解析

我们排除了一些对于 this 的错误理解并且明白了每个函数的 this 是在调用 时被绑定的,完全取决于函数的调用位置(也就是函数的调用方法)。

调用位置

下面先看看代码,来尝试着体会调用栈和调用位置

function baz() {
            // 当前调用栈是:baz
            // 因此,当前调用位置是全局作用域
            console.log("baz");
            bar(); // <-- bar 的调用位置
        }
        function bar() {
            // 当前调用栈是 baz => bar
            // 因此,当前调用位置在 baz 中
            console.log("bar");
            foo(); // => foo 的调用位置
        }
        function foo() {
            // 当前调用栈是 baz => bar => foo
            // 因此,当前调用位置在 bar 中
            console.log("foo");
        }
        baz(); // <-- baz 的调用位置

一定要体会如何从调用栈中分析出真正的调用位置,因为决定了this的绑定!

绑定规则
默认绑定

首先最常用的函数调用类型:独立函数调用。这个是无法应用其他规则时的默认规则。

看一下下面的代码可以试着体会

function foo(){
    console.log(this.a)
}
var a=7;
foo();// 7

我们可以清晰的发现,var a=7时声明在全局作用域中的变量就是对全局对象的一个同名属性,本质是一个东西,不是通过复制来进行得到的。

接下来我们可以看到当调用 foo() 时,this.a 被解析成了全局变量 a。为什么?因为在代码中,函数调用时应用了 this 的默认绑定,因此 this 指向全局对象。

但是如果我们在这里使用了严格模式的情况下,全局对象将无法使用默认绑定,this会绑定到undefined。

function foo(){
    'use strict'
    console.log(this.a)
}
var a=7;
foo();//Uncaught TypeError: Cannot read properties of undefined (reading 'a')

总结:我们在非严格模式下,默认绑定能够绑定到全局对象,严格模式下与foo()的调用位置无关;通常来说我们不会在代码中混合去使用到严格模式和非严格模式,但是有时候使用到第三方库的时候,需要注意这一些细节。

隐式绑定

同样先去看一段代码:

function foo() {
            console.log(this.a);
        }
        var obj = {
            a: 2,
            foo: foo
        };
        obj.foo(); // 2

需要注意的是foo函数的声明方式,然后被当成引用属性被添加到obj对象中,

无论是直接在obj定义还是先定义然后再添加引用属性,foo函数严格来说都不是属于obj对象。

但是这里调用位置会根据obj上下文来引用,可以说被调用时obj对象,拥有它

再看一段代码体会:

function foo() {
            console.log(this.a);
        }
        var obj2 = {
            a: 77,
            foo: foo
        };
        var obj1 = {
            a: 7,
            obj2: obj2
        };
        obj1.obj2.foo(); // 77

上面代码中为什么不是输出的a是7呢。原因是因为对象属性引用链中只有最顶层或者最后一层会影响调用位置。

隐式丢失

常见的this绑定问题中就有隐式绑定的函数丢失绑定对象,也就是说会默认绑定,把this绑定到全局对象或者undefined这个取决于是否使用严格模式。

思考一下下面的代码:

function foo() {
            console.log(this.a);
        }
        var obj = {
            a: 2,
            foo: foo
        };
        var bar = obj.foo; // 函数别名
        var a = "global"; //定义一个 a 是全局对象的属性
        bar(); // "global"

实际上这里引用的是foo函数本身,所以使用了默认绑定。

这里的bar其实是引用了obj.foo的地址,这个地址指向的是一个函数,也就是说bar的调用其实符合“独立函数调用”规则。所以它的this不是obj

回调函数中就显得更加的微妙。

function foo() {
            console.log(this.a);
        }
function cc(fn){
            //fn真实其实是引用的foo
            fn();//调用位置
}
        var obj = {
            a: 2,
            foo: foo
        };
        var a = "global"; //定义一个 a 是全局对象的属性
        cc(obj.foo); // "global"

这里主要是参数进行传递的时候进行了隐式赋值, cc(obj.foo),这里把obj.foo赋值给fn就和第一个情况是一样的了。

显示绑定

就像我们刚才看到的那样,在分析隐式绑定时,我们必须在一个对象内部包含一个指向函 数的属性,并通过这个属性间接引用函数,从而把 this 间接(隐式)绑定到这个对象上。

显式绑定的说法是和隐式绑定相对的,指的是通过callapplybind显式地更改this指向。

如何去使用这3种方法,他们的第一个参数是一个对象,会把这个对象绑定到this,接着在调用函数的时候指定这个this。就可以直接指定this的绑定对象,这种绑定被称之为显式绑定。

看下面代码

 function foo() {
            console.log(this.a);
        }
        var obj = {
            a: 7
        };
        foo.call(obj); // 7

通过call方法我们在调用foo的时候把它强制的绑定到了obj上。

apply和call的方式是大同小异的具体的区别体现在其他的参数

注:如果传入了一个原始值(简单数据类型)来当作this绑定对象,会被它转换为对应的对象形式,也就是说new String Boolean Number(装箱)

这三个方法中的bind方法比较特殊,它可以延迟方法的执行,这可以让我们写出更加灵活的代码。它的原理也很容易模拟:

上面两种方法似乎都不太号解决回调中的绑定丢失,下面看这样的方法

先看一段不适用bind方法的代码:

 function foo() {
            console.log(this.a);
        }
        var obj = {
            a: 7
        };
        var bar = function () {
            foo.call(obj);
        };
        bar(); // 7
        setTimeout(bar, 100); // 7
        // 硬绑定的 bar 不可能再修改它的 this
        bar.call(window); // 7

解析一下原因,因为创建了函数bar,在其内部手动调用了call方法,把它强制的this绑定到了obj上,之后无论怎么去调用bar,总是会在obj上调用foo,这属于显式绑定中的强制绑定,我们称之为硬绑定。

硬绑定式非常常用的,所以在ES5的时候提供了内置方法Function.prototype. bind。用法如下:

  function foo(something) {
            console.log(this.a, something);
            return this.a + something;
        }
        var obj = {
            a: 2
        };
        var bar = foo.bind(obj);
        var b = bar(3); // 2 3
        console.log(b); // 5

bind(…) 会返回一个硬编码的新函数,它会把参数设置为 this 的上下文并调用原始函数。

new绑定

Js中new与传统的面向类的语言机制不同,Js中的“构造函数”其实和普通函数没有任何区别。
其实当我们使用new来调用函数的时候,发生了下列事情:

  • 创建一个全新的对象

  • 这个新对象会被执行“原型”链接

  • 这个新对象会被绑定到调用的this

  • 如果函数没有对象类型的返回值,这个对象会被返回

    其中,第三步绑定了this,所以“构造函数”和原型中的this永远指向new出来的实例。

优先级

这里就不去过多的进行对比比较直接看结论把。

以上四条判断规则的权重是递增的。判断的顺序为:

  • 函数是new出来的,this指向实例

  • 函数通过call、apply、bind绑定,this指向绑定的第一个参数

  • 函数在某个上下文对象中调用(隐式绑定),this指向上下文对象

  • 以上都不是,this指向全局对象。

getter与setter中的this

ES6中的gettersetter函数都会把this绑定到设置或获取属性的对象上。

function sum() {
  return this.a + this.b + this.c;
}
var o = {
  a: 1,
  b: 2,
  c: 3,
  get average() {
    return (this.a + this.b + this.c) / 3;
  }
};
Object.defineProperty(o, 'sum', { get: sum, enumerable: true, configurable: true} );
console.log(o.average, o.sum); // logs 2, 6
箭头函数中的this

先看箭头函数和普通函数的重要区别:

1.没有自己的this、super、arguments

2.不能使用new来调用

3.没有原型对象

4.不可以改变this的绑定

5.形参名称不可以重复

箭头函数中没有this绑定,必须通过查找作用域链来决定其值。 如果箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this,否则this的值则被设置为全局对象。 比如:

var name = 'window';
var obj = {
    name: 'cc',
    do: function(){
        // var self = this;
        var arrowDo = () => {
            // console.log(self.name);
            console.log(this.name);
        }
        arrowDo();
    },
    arrowDo2: () => {
        console.log(this.name);
    }
}
obj.do(); // 'cc'
obj.arrowDo2(); // 'window'

总结

如果要判断一个运行中函数的 this 绑定, 就需要找到这个函数的直接调用位置。 找到之后 就可以顺序应用下面这四条规则来判断 this 的绑定对象。

  1. new 调用:绑定到新创建的对象,注意:显示return函数或对象,返回值不是新创建的对象,而是显式返回的函数或对象。
  2. call 或者 apply( 或者 bind) 调用:严格模式下,绑定到指定的第一个参数。非严格模式下,nullundefined,指向全局对象(浏览器中是window),其余值指向被new Object()包装的对象。
  3. 对象上的函数调用:绑定到那个对象。
  4. 普通函数调用: 在严格模式下绑定到 undefined,否则绑定到全局对象。

ES6 中的箭头函数:不会使用上文的四条标准的绑定规则, 而是根据当前的词法作用域来决定this, 具体来说, 箭头函数会继承外层函数,调用的 this 绑定( 无论 this 绑定到什么),没有外层函数,则是绑定到全局对象(浏览器中是window)。 这其实和 ES6 之前代码中的 self = this 机制一样。

喜欢的话可以点个赞大伙!如果上文有错的地方还请各位大佬指出!

参考资料

《你不知道的Javascript》

《MDN-this》

https://juejin.cn/post/6844903746984476686#heading-13

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值