美文网首页前端开发那些事儿
【前端进阶】深入浅出 JavaScript 中的 this

【前端进阶】深入浅出 JavaScript 中的 this

作者: Gopal | 来源:发表于2020-10-19 22:35 被阅读0次

    笔者最近在看 你不知道的JavaScript上卷,里面关于 this 的讲解个人觉得非常精彩。JavaScript 中的 this 算是一个核心的概念,有一些同学会对其有点模糊和小恐惧,究其原因,现在对 this 讨论的文章很多,让我们觉得 this 无规律可寻,就像一个幽灵一样

    本篇文章,算是一篇读书笔记,当然也加上了很多我的个人理解,我觉得肯定对大家有所帮助

    执行上下文

    在理解 this 之前,我们先来看下什么是执行上下文

    简而言之,执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行

    JavaScript 中有三种执行上下文类型

    • 全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文
    • 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个
    • eval 函数执行上下文 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以在这里我不会讨论它

    这里我们先得出一个结论,非严格模式和严格模式中 this 都是指向顶层对象(浏览器中是window)

    console.log(this === window); // true
    'use strict'
    console.log(this === window); // true
    this.name = 'vnues';
    console.log(this.name); // vnues
    

    后面我们的讨论更多的是针对函数执行上下文

    this 到底是什么?为什么要用 this

    this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调 用时的各种条件

    牢记:this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式

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

    看个实例,理解为什么要用 this,有时候,我们需要实现类似如下的代码:

    function identify(context) {
      return context.name.toUpperCase();
    }
    function speak(context) {
      var greeting = "Hello, I'm " + identify(context);
      console.log(greeting);
    }
    var me = {
      name: "Kyle"
    };
    speak(me); //hello, 我是 KYLE
    

    这段代码的问题,在于需要显示传递上下文对象,如果代码越来越复杂,这种方式会让你的代码看起来很混乱,用 this 则更加的优雅

    var me = {
      name: "Kyle"
    };
    
    function identify() {
      return this.name.toUpperCase();
    }
    function speak() {
      var greeting = "Hello, I'm " + identify.call(this);
      console.log(greeting);
    }
    speak.call(me); // Hello, 我是 KYLE
    

    this 的四种绑定规则

    下面我们来看在函数上下文中的绑定规则,有以下四种

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

    默认绑定

    最常用的函数调用类型:独立函数调用,这个也是优先级最低的一个,此事 this 指向全局对象。注意:如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此 this 会绑定 到 undefined,如下所示

    var a = 2;  //  变量声明到全局对象中
    function foo() {
      console.log(this.a);   // 输出 a
    }
    
    function bar() {
      'use strict';
      console.log(this); // undefined
    }
    foo();
    bar();
    

    隐式绑定

    还可以我们开头说的:this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式

    先来看一个例子:

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

    当调用 obj.foo() 的时候,this 指向 obj 对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因为调 用 foo()this 被绑定到 obj,因此 this.a 和 obj.a 是一样的

    记住:对象属性引用链中只有最顶层或者说最后一层会影响调用位置

      function foo() {
        console.log(this.a);
      }
      var obj2 = {
        a: 42,
        foo: foo
      };
      var obj1 = {
        a: 2,
        obj2: obj2
      };
      var a = 1;
      obj1.obj2.foo(); // 42
      let bar = obj1.obj2.foo;
      bar(); // 1
    

    间接引用

    另一个需要注意的是,你有可能(有意或者无意地)创建一个函数的“间接引用”,在这 种情况下,调用这个函数会应用默认绑定规则

    function foo() {
      console.log(this.a);
    }
    var a = 2;
    var o = { a: 3, foo: foo };
    var p = { a: 4 };
    o.foo(); // 3
    (p.foo = o.foo)(); // 2
    

    另一个需要注意的是,你有可能(有意或者无意地)创建一个函数的“间接引用”,在这 种情况下,调用这个函数会应用默认绑定规则

    赋值表达式 p.foo = o.foo 的返回值是目标函数的引用,因此调用位置是 foo()而不是 p.foo() 或者 o.foo()。根据我们之前说过的,这里会应用默认绑定

    显示绑定

    在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把 this 间接(隐式)绑定到这个对象上。 那么如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么
    做呢?

    Javascript 中提供了 applycallbind 方法可以让我们实现

    不同之处在于,call()apply() 是立即执行函数,并且接受的参数的形式不同:

    • call(this, arg1, arg2, ...)
    • apply(this, [arg1, arg2, ...])

    bind() 则是创建一个新的包装函数,并且返回,而不是立刻执行

    • bind(this, arg1, arg2, ...)

    看如下的例子:

      function foo(b) {
        console.log(this.a + '' + b);
      }
      var obj = {
        a: 2,
        foo: foo
      };
      var a = 1;
      foo('Gopal'); // 1Gopal
      obj.foo('Gopal'); // 2Gopal
      foo.call(obj, 'Gopal'); // 2Gopal
      foo.apply(obj, ['Gopal']); // 2Gopal
      let bar = foo.bind(obj, 'Gopal');
      bar(); // 2Gopal
    

    被忽略的 this

    如果你把 null 或者 undefined 作为 this 的绑定对象传入 callapply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则

    function foo() {
      console.log(this.a);
    }
    var a = 2;
    foo.call(null); // 2
    

    利用这个用法使用 apply(..) 来“展开”一个数组,并当作参数传入一个函数。
    类似地,bind(..) 可以对参数进行柯里化(预先设置一些参数)

      function foo(a, b) {
        console.log("a:" + a + ", b:" + b);
      }
      // 把数组“展开”成参数
      foo.apply(null, [2, 3]); // a:2, b:3
      // 使用 bind(..) 进行柯里化
      var bar = foo.bind(null, 2);
      bar(3); // a:2, b:3
    

    new绑定

    当我们使用构造函数 new 一个实例的时候,这个实例的 this 指向是什么呢?

    我们先来看下使用 new 来调用函数,或者说发生构造函数调用时,会执行什么操作,如下:

    • 创建(或者说构造)一个全新的对象
    • 这个新对象会被执行[[原型]]连接,将对象(实例)的 __proto__ 和构造函数的 prototype 绑定
    • 这个新对象会绑定到函数调用的 this
    • 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象

    原理实现类似如下:

    function create (ctr) {
        // 创建一个空对象
        let obj = new Object()
        // 链接到构造函数的原型对象中
        let Con = [].shift.call(arguments)
        obj.__proto__ = Con.prototype
        // 绑定this
        let result = Con.apply(obj, arguments);
        // 如果返回是一个对象,则直接返回这个对象,否则返回实例
        return typeof result === 'object'? result : obj;
    }
    

    注意:let result = Con.apply(obj, arguments); 实际上就是指的是新对象会绑定到函数调用的 this

      function Foo(a) {
        this.a = a;
      }
      var bar = new Foo(2);
      console.log(bar.a); // 2
    

    特殊情况——箭头函数

    我们之前介绍的四条规则已经可以包含所有正常的函数。但是 ES6 中介绍了一种无法使用 这些规则的特殊函数类型:箭头函数

    箭头函数不使用 this 的四种标准规则,而是根据定义时候的外层(函数或者全局)作用域来决 定 this。也就是说箭头函数不会创建自己的 this,它只会从自己的作用域链的上一层继承 this

    function foo() {
      // 返回一个箭头函数
      // this 继承自 foo()
      return (a) => {
        console.log(this.a);
      }
    };
    
    var obj1 = {
      a: 2
    };
    var obj2 = {
      a: 3
    };
    var bar = foo.call(obj1);
    bar.call(obj2); // 2, 不是 3 !
    

    foo() 内部创建的箭头函数会捕获调用时 foo()this。由于 foo()this 绑定到 obj1bar(引用箭头函数)的 this 也会绑定到 obj1,箭头函数的绑定无法被修改。(new 也不 行!)

    总结——this 优先级

    判断是否为箭头函数,是则按照箭头函数的规则

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

    1. new 调用?绑定到新创建的对象
    2. call 或者 apply(或者 bind)调用?绑定到指定的对象
    3. 由上下文对象调用?绑定到那个上下文对象
    4. 默认:在严格模式下绑定到 undefined,否则绑定到全局对象

    如下图所示:

    参考

    相关文章

      网友评论

        本文标题:【前端进阶】深入浅出 JavaScript 中的 this

        本文链接:https://www.haomeiwen.com/subject/jghepktx.html