JavaScript 中 this 相关知识点详细总结

写在最前:文章转自掘金

thisJavaScript中是非常重要的概念,因为我们用到它的频率非常之高,在享受到它的便利性的同时,与之对应的是它的绑定规则比较难理解。今天我们就来好好总结一下this的相关知识点,这样在使用它的过程中就能更有自信和把握啦!

this是什么

我们先来看一个例子:

    function foo() {
      console.log(this);    //Window
    }
    foo();

我们定义了一个函数foo并执行,在函数体中我们直接打印输出this,发现结果并不是undefined,而是Window对象。这是为什么呢?这就引出了相关的概念:

this是一个很特别的关键字,被自动定义在所有函数的作用域中。

1. this是一个关键字

这句话说出了this的本质其实是一个关键字,只不过有些特别,具体特别在哪里我们下面再介绍。既然this是一个关键词,我们就得注意它应该具备的一个特点了:this无法被重写。我们修改一下刚刚的例子试试:

    function foo() {
      this = "null"; //Uncaught SyntaxError
      console.log(this);
    }
    foo();

可以看到,this的值无法被修改,否则会报错。

2. this被自动定义在所有函数的作用域中

这句话里面包含很多信息点,首先就是自动定义这个说法,指出了this这个关键词在函数作用域中会被自动定义,这也就是为什么我们刚刚在foo函数中可以直接输出this得到Window对象。

其次是所有函数的作用域中,这句话说明了,在所有的的函数作用域中都存在this关键字。但这不是说只有在函数中才有this,假如我们试着在全局直接输出this会是什么结果呢?

console.log(this);  //Window

可以看到,同样输出了Window对象,这说明在全局作用域下同样存在this关键词。

看到这里你可能有点疑问了,为什么举得例子中,this的值都是Window对象,是不是this的值就是等于Window对象呢?我们再看个例子:

    var obj = {
      say: function () {
        console.log(this);
      },
    };
    obj.say();  //{say: ƒ}

我们将例子修改了一下,发现此时输出的this值变成了obj对象,这说明this的值并不是之前猜测的固定等于Window对象,那么this值到底是怎么来的呢?

this的指向值

事实上,我们之前在介绍执行上下文的时候有介绍到,在动态创建执行上下文时,会确认This Binding也就是this的值,它和函数定义的位置无关,而是由函数调用时的绑定规则决定的。

这也就是为什么,在全局的情况下this值也是存在的,正是因为存在全局上下文,而且this的值就存储在这个上下文中。

那么this值具体指向谁,函数调用时的绑定规则是什么,这就是我们接下来要讲的重头戏了。

this的绑定规则

我们刚刚讲了,this的指向值由函数调用时的绑定规则决定,我们现在就来讲讲这些绑定规则分别有哪些。

默认绑定

默认绑定是最基本的绑定规则,它被应用在其他规则均不适用的情况下,因此也是最常见的绑定规则。默认绑定比较典型的一种判断就是:当使用不带任何修饰的函数引用进行调用时,只能使用默认绑定,而不能使用其他绑定规则。
举个例子:

function foo(){
    console.log(this);  //Window
}
foo();

可以看到,这里的foo()就是不带任何修饰的函数调用,foo前面光秃秃的啥也没有。另外你会发现,这里输出的this值为Window对象,那大家就已经知道了这可能就是默认规则下this的指向值,不过不准确,事实上默认规则下this的指向值可以分几种情况:

· 严格模式

在严格模式下,使用默认规则得到的this值会指向undefined,可以看代码:

'use strict'
function foo(){
    console.log(this);  //undefined
}
foo();
· 非严格模式

非严格模式下,如果应用了默认规则,那么this的值会指向Window对象,这也就是为什么我们刚刚举得好几个例子this值都是Window

· 严格模式和非严格模式混用

这种模式比较特殊,如果我们在非严格模式下定义了函数,又在严格模式下调用了函数,最终this的值还是会指向Window对象,看下代码:

    function foo() {
      console.log(this);    //Window
    }
    {
      ("use strict");
      foo();
    }

隐式绑定

讲完了默认的绑定规则,那么肯定要看看一些特殊的绑定规则了。当函数作为某个对象的方法调用时,此时这个对象就是函数的上下文对象,这时候this会指向这个对象,我们来看个例子:

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

可以看到,我们给对象obj定义了一个方法foo,并通过obj.foo()的方式进行调用,此时的obj对象就是函数foo调用时的上下文对象,因此this会指向obj对象,我们把这种情况称为隐式绑定。隐式绑定有个重要的点需要着重说明一下:

隐式丢失

隐式绑定的函数,在有些情况下会丢失绑定的上下文对象,这时候就会应用我们的默认绑定规则,把this指向Window对象(非严格模式)或者undefined(严格模式)。我们先来看一个例子:

    var name = "我是全局的name";
    var obj = {
      name: "我是obj的name",
      foo: function () {
        console.log(this.name); //我是全局的name
      },
    };
    let fn = obj.foo; //这里用变量fn保存foo函数
    fn();

这里我们用一个变量fn保存了obj下的foo函数的引用,此时调用以后输出的是全局对象下定义的name变量值,可见此时的this对象指向了Window
当我们执行let fn = obj.foo时,实际上是将fn指向了函数foo的地址,因此当我们执行fn()的时候实际上就是在执行foo(),上面的代码也就是这样:

    var name = "我是全局的name";
    function foo() {
      console.log(this.name); //我是全局的name
    }
    foo();

这时候实际上就是不带任何修饰的函数调用,因此会应用默认绑定规则。
隐式丢失还有一种情况,就是对象的方法作为参数传递到另外的函数中去,来看个例子:

    var name = "我是全局的name";
    var obj = {
      name: "我是obj的name",
      foo: function () {
        console.log(this.name); //我是全局的name
      },
    };

    function otherFn(fn) {
      fn();
    }
    otherFn(obj.foo);

这个例子和上个例子唯一的不同就是在后面,我们不是通过变量保存foo函数后再调用,而是把foo函数作为参数传递给了另一个函数,然后在另一个函数中调用,这里为什么也发生了隐式丢失呢?
答案是,参数在传递给函数的时候,会发生一个赋值操作,也就是将实参赋值给形参,函数部分的代码可以看成这样:

    function otherFn() {
      var fn = obj.foo; //这里实际上就是将实参赋值给形参
      fn();
    }
    otherFn(obj.foo);

这下我们就发现了,这种情况和上面那种隐式丢失的情况一样,也是不带任何修饰的函数调用,因此也应用了默认规则,发生了隐式丢失。

对象属性引用链

我们刚刚说到,当函数作为对象的方法调用时,会以这个对象作为上下文对象,所以this会指向这个对象,那么当多个对象嵌套在一起后调用方法以后this会指向哪个对象呢?我们来看个例子:

    var obj1 = {
      name: "obj1",
      obj2: {
        name: "obj2",
        obj3: {
          name: "obj3",
          foo: function () {
            console.log(this.name); //obj3
          },
        },
      },
    };
    obj1.obj2.obj3.foo();

是不是看蒙了?不要怕,我们看到结果输出了obj3,事实上当我们通过对象属性调用链来调用方法时,最终起作用的只有函数前面的那个调用对象。比如刚刚的obj1.obj2.obj3.foo(),实际上可以看成obj3.foo()。同理,哪怕是...a.b.c.d.e.foo(),我们只要看最后的e.foo()即可。

显示绑定

我们刚刚介绍了隐式绑定,this的指向值完全由调用对象决定,十分难把控,有没有一个办法,不管我是通过哪个对象调用的,我都可以指定我想要的this指向值呢?有的,在函数的原型上有两个方法call和apply,可以传入指定的对象作为this的指向值,不过两者有一些区别,我们来分别看下用法。

call的用法

call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。
注意:该方法的语法和作用与 apply() 方法类似,只有一个区别,就是 call() 方法接受的是一个参数列表,而 apply() 方法接受的是一个包含多个参数的数组。
我们看看call如何显式的改变this的指向值:

    var obj = {
      name: "obj",
    };
    function foo() {
      console.log(this.name);   //obj
    }
    foo.call(obj);

foo函数调用call方法,并传入obj作为第一个参数,obj会作为foo函数的this指向值,并执行函数。那么call方法是如何实现这样的功能的呢?我们下面再讲,我们先来看下apply

apply的用法

apply的用法和效果和call几乎一样,看例子就可以看出来:

    var obj = {
      name: "obj",
    };
    function foo() {
      console.log(this.name);   //obj
    }
    foo.apply(obj);

从这两个例子来看的话,两者几乎完全相同,但它们还是有区别的,区别就在于它们接收参数的方式不同,我们看个例子:

    var obj = {
      name: "obj",
    };
    function foo(a,b,c) {
      console.log(this.name,a,b,c); //obj 1 2 3
    }
    foo.call(obj,1,2,3);
    foo.apply(obj,[1,2,3]);

可以看到,call函数接收多个参数并传入foo函数中,而apply函数接收的参数放在了一个数组里,然后拆分开来传入了foo函数中,这就是两者的区别。
我们通过调用call或者apply的方式显式的绑定了this的指向值,但是你会发现通过call或者apply的方式会直接调用函数,就没办法把函数连带着this一起传到别的函数中存储或者执行。能不能给函数先绑定好了this值,再套个壳子包装好,这样就可以传来传去的还保留着绑定好的this值了呢?于是我们的bind函数就应运而生了。

bind的用法

bind函数可以传入指定对象作为this的指向对象,除此之外,bind函数还可以接受参数并存储起来,下次调用的时候可以只传入剩余的参数,我们来看个例子:

    //还是刚刚隐式丢失的例子,我们使用bind函数绑定上obj对象看看
    var name = "我是全局的name";
    var obj = {
      name: "我是obj的name",
      foo: function () {
        console.log(this.name);
      },
    };

    function otherFn(fn) {
      //会发现函数被传入进来以后,this依然指向obj而没有发生隐式丢失
      fn(); //我是obj的name
    }
    otherFn(obj.foo.bind(obj)); //我们这里不直接传入,而是把obj.foo包装一下再传入

从这个例子我们可以看到,通过bind绑定了this指向值的函数,即使传入了其他函数中执行也不会丢失this对象。我们再举个例子看看,如何在绑定this的同时存储参数:

    function foo(a, b, c) {
      console.log(a, b, c);
    }

    //第一个参数为null时,非严格模式下会以Window对象作为this指向值,后面会介绍
    //给foo函数套了层壳子,并存储了两个参数1,2
    var bindFoo = foo.bind(null, 1, 2);
    //当调用这个包装函数的时候,传入的参数会连同之前存储的参数一起传给foo函数
    bindFoo(3); //1 2 3

这样我们就实现了预传参数和this值,可以方便传值的函数啦~
还有一种情况,我觉得这种显示绑定的方式太僵硬了,其实我想要一种更灵活的绑定方式,我想预设一个this指向对象,当我不小心应用了默认绑定规则,this指向了Window或者undefined时候把this重新指向我预设的对象,否则的话就指向他本来的this对象,这样可以吗?好家伙,要求还不少,但是我满足你了,那就是我们的softBind啦~

softBind的用法

softBind函数在函数原型上并不存在,是后来创造的,顾名思义就是软绑定,为了实现我们刚刚说的需求而出现的。 既然原型上没有,自然要介绍一下怎么定义实现的啦:

    Function.prototype.softBind = function (obj) {
      //先拿到调用softBind的函数本身
      var fn = this;

      //这里是为了拿到传入的其他参数,并存储起来
      var curried = [].slice.call(arguments, 1); //arguments是类数组所以没有slice方法

      //这里是返回的包装好的函数
      var bound = function () {
        
        //判断this的情况,这里的this是返回的封装函数执行时的this,和调用softBind函数时的this不同
        var that = (!this || this === (window || global)) ? obj : this; //判断this是否空,同时考虑node环境

        //这里的目的是为了把包装时传入的参数,和执行包装函数时传入的参数进行合并,arguments和之前的arguments不同
        var newArguments = [].concat.apply(curried, arguments);

        //这里其实就是调用函数
        return fn.apply(that, newArguments);
      };

      //有一个细节是调整包装好的函数的原型链,使得instanceof能够用于包装好的函数的判断
      bound.prototype = Object.create(fn.prototype);
      return bound;
    };   

看不懂的话慢慢琢磨一下,然后我们来举个例子看看它的用法:

    var name = "我是全局的name";
    var obj1 = {
      name: "我是obj1的name"
    };
    var obj2 = {
      name: "我是obj2的name"
    };

    function foo(){
        console.log(this.name)
    }
    
    //包装一个默认this为obj1的函数
    var fn=foo.softBind(obj);
    
    //当通过obj2调用时,会使用obj2作为this值
    fn.call(obj2);  //我是obj2的name
    
    //当不加修饰符调用时,会应用绑定的this值
    fn();   //我是obj1的name

new绑定

new操作符也可以实现改变this指向,关于new操作符的知识点:

  • 创建一个新对象,将this绑定到新创建的对象
  • 使用传入的参数调用构造函数
  • 将创建的对象的proto_指向构造函数的prototype
  • 如果构造函数没有显式返回一个对象,则返回创建的新对象,否则返回显式返回的对象(即手动返回的对象)
    然后我们来看个例子:
function Foo(name){
    this.name=name;
}

let person=new Foo('xiaowang');
console.log(person);    //xiaowang

可以看到,当函数作为构造函数执行new的过程中,this指向了最终创建的实例person,说明new操作符确实能够改变this的指向。

箭头函数绑定

除了之前介绍的那么多种,还存在着一种ES6中的特殊函数类型:箭头函数。箭头函数中的this比较特殊,它的指向值不是动态决定的,而是由函数定义时作用域中包含的this值确定的,我们举个例子:

    //定义一个箭头函数
    var foo = () => {
      console.log(this.name);
    };
    var name = "我是全局的name";
    var obj1 = {
      name: "我是obj1的name",
    };

    foo.call(obj1); // "我是全局的name"

可以看到,虽然我们调用了call传入了obj1,但最终输出的值还是全局的name,这是因为函数foo定义在全局中,因此this会指向window对象。
不管在什么情况下,箭头函数的this跟外层functionthis一致,外层functionthis指向谁,箭头函数的this就指向谁,如果外层不是function则指向window

绑定规则的优先级

当多个绑定规则同时运用的时候,会使用优先级更高的绑定规则。我们按照由低到高进行排序的话就是:

  1. 默认绑定
    2.隐式绑定
    3.显示绑定
    4.new操作符绑定、箭头函数

接下来我们分别看几个例子来验证它们的优先级:

  • 默认绑定和隐式绑定
var obj = {
      foo: function () {
        console.log(this);  //{foo: ƒ}
      },
    };
    obj.foo();
  • 隐式绑定和显示绑定
    var obj1 = {
      name: "obj1",
      foo: function () {
        console.log(this.name);
      },
    };
    var obj2 = {
      name: "obj2",
    };

    obj1.foo.apply(obj2); //obj2
    obj1.foo.call(obj2); //obj2
    obj1.foo.bind(obj2)(); //obj2
  • 显示绑定和new操作符绑定
    由于callapply只能执行函数,没法和new操作符一起使用,因此我们只对比bind函数和new操作符的优先级。
    var obj = {};
    function foo() {
      this.name = "obj";
    }
    //将foo绑定到obj上
    var fn = foo.bind(obj);

    //执行new操作
    var f1 = new foo();
    console.log(obj.name); //undefined
    console.log(f1.name); //obj

可以看到,foo已经显式绑定obj对象了,最终name值还是赋值到了实例f1上,因此new操作符绑定的优先级是大于显式绑定(bind)的。
你可能会疑惑,bind明明为foo函数套了层壳,按照new操作符的逻辑怎么都不能把里面的this指向改了才对,事实上bind函数内部做了判断,如果和new操作符一起使用的话,要把this让给new操作符的对象,这也坐实了它们之间优先级的关系了。

  • 箭头函数绑定和new操作符绑定
    由于箭头函数是不可构造的,所以无法和new操作符组合,因此我把他们放在了同级。
1607497213.jpg
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352

推荐阅读更多精彩内容