• javaScript 的 this 究竟是个什么鬼?


    整理自《你不知道的JavaScript(上卷)》

    一、what 和 why

    this 是个什么东西?它是 运行时绑定的 一个记录属性,上下文取决于函数调用时的各种条件。看看书里的具体解释是什么:

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

    为什么要使用 this?为了引用合适的上下文对象。 什么是“合适”而什么又是“上下文对象”我们将在下文中展开探讨。

    二、绑定位置

    既然是为了引用合适的上下文对象,我们得把这个 this 放在合适的地方绑定。接下来介绍四种常见的绑定规则。

    1、默认绑定

    就是最最一般的情况啦,看下面的例子。foo 函数是绑定在全局对象上的(也就是想象最顶层有一个全局对象,foo 是它身上的一个函数挂件),调用的时候 this 就会指向全局对象中的那个 a ,也就是外面的那个 2 。

    在严格模式下会报错 undefined

    function foo(){
      console.log(this.a);
    }
    var a = 2;
    foo(); // 2
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2、隐式绑定

    隐式绑定也是一种非常常见的绑定形式,个人觉得是更通俗意义上的默认绑定方式。牢记一句话:

    this 绑定在运行时最后一个调用它的对象上

    如果隐式绑定丢失了自己的绑定对象,便会执行默认绑定,将函数绑定在 window 全局对象上。

    看下面几个例子。

    function foo(){
      console.log(this.a);
    }
    var obj1 = {
      var a = 42,
    	foo: foo
    }
    var obj2 = {
      var a = 21;
    	obj1: obj1
    }
    obj2.obj1.foo() // 42
    
    ------------------------------------------
    
    var bar = obj1.foo();
    var a = "okok 我最ok"
    bar() // "okok 我最ok"
    
    ------------------------------------------
    
    function doFun(fn){
      fn()
    }
    doFun(obj1.foo) // "okok 我最ok"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    这个例子中第一个输出语句(第12行)最后调用 foo 函数的是 obj1 对象,因此 this 是指向 obj1 中的 a 这个变量的,输出42,没毛病。第二个输出语句(第15行)的函数 bar 是孤零零直接被调用的,那么就会直接绑定到最外面的那个 window 对象,挂在 window 对象上的 a 是啥呢?就是"okok 我最ok"。即使 bar 是由 obj1 赋值而来,那都不用看,不管前面有多少花里胡哨的赋值调用,只需要看最后运行时是谁调用它就行。

    第三个输出语句有点怪异,上面不是说看运行时谁调用它吗?这下是 obj1 调用的,为什么输出的是全局对象上挂载的变量呢?

    参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值

    这句话就是说,可以把 obj1.foo 当作一个整体(也就是函数 foo)赋值给了 doFun,那么在调用 doFun 的时候,实际上还是挂载在最外层的全局对象 window 上了,因此仍然输出了“ok”语句,并没有违反最终运行调用的规则。

    3、显式绑定

    显式绑定的意思是用一些方法在某个对象上强制调用函数。比如使用 call、apply 和 bind 显式绑定 this 的上下文对象。那么接下来就不得不整理一下这三者的区别了。

    callapplybind
    funcA.call(obj, arg1, arg2, …)funcA.apply(obj, [args…])funcA.bind(obj)
    在调用 funcA 函数时,funcA 的this 指向传入的第一个参数,即 obj,后面的参数为调用 funcA 的其他参数【立即执行】和 call 基本一致,只是传参的形式不同,apply 将其他参数合并为数组传入【立即执行】bind 只是作绑定,与前两个一样,funcA 执行的时候会将 obj 作为 this 的指向值,不同点在于它返回的是一个函数而不是执行结果【手动执行】

    可惜,显式绑定仍然无法解决我们之前提出的丢失绑定问题。

    这句话是什么意思呢?首先应该为所谓“丢失绑定”下一个定义,因为前面说了,this 会指向最后一个调用它的对象,那么这个 this 的指向将会是飘忽不定的。比如说在回调函数中,虽然一开始使用上述显式绑定的方法明确了 this 的指向,但在异步操作结束执行回调的时候,这个 this 可就不一定顺着我们的心意了。所以我们需要下面的办法:硬绑定。

    3.1 硬绑定

    搞几个例子看看。

    function foo(something){
      console.log(this.a, something);
      return this.a + something;
    }
    var obj = {
      a: 2
    };
    var bar = function(){
      return foo.apply(obj, arguments);
    };
    var b = bar(3); // 2 3
    console.log(b); // 5
    
    ---------------------------------------------------------------------
    
    function bind(fn, obj){
      return function(){
        return fn.apply(obj, arguments);
      };
    }
    
    var obj = {
      a: 2
    };
    
    var bar = bind(foo, obj);
    var b = bar(3); // 2 3
    console.log(b); // 5
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    上述结果都正确地输出了我们的理想值,那它为什么叫“硬绑定”呢?因为在 bar 函数的内部,foo 的 this 被强制(显式)绑定到了 obj 上,后面的函数不论怎样调用它都能执行我们理想的结果。

    而上面第二个例子中的 bind ,成为了ES5就提供的内置方法 Function.prototype.bind。面试官再让你手写 bind 的时候,就应该信手拈来啦~

    4、new 绑定

    最后一种改变 this 指向的方法了,加油!

    在 js 中,构造函数并不会实例化一个类或者创建一个类,当我们使用 new 操作符的时候,只是调用了一些普通的函数,这个机制与其他语言不一样。即书中所说:

    不存在所谓“构造函数”,只有对函数的“构造调用”。

    当我们使用 new 来调用函数时,会发生以下几件事:

    1. 创建一个全新的对象;
    2. 新对象会被执行 [[ prototype ]] 连接;
    3. 新对象和函数调用的 this 绑定;
    4. 执行构造函数中的代码(如果有);
    5. 如果函数没有其他返回对象则返回这个新创建的对象。

    举个例子:

    function Mother(name){
      this.name = name;
    }
    var son = new Mother("Da");
    
    • 1
    • 2
    • 3
    • 4
    1. 创建一个新对象 son;
    2. 执行 [[ prototype ]] 连接,son.__proto__ = Mother.prototype;
    3. 新对象和函数调用 this 绑定:Mother.call(son, "da");
    4. 执行构造函数 son.name
    5. 返回新对象 son

    三、优先级

    一般而言

    废话不多说:显式绑定 > 隐式绑定 > 默认绑定,如果是 new 绑定,则会创建一个新的对象。

    1. 函数用 new 调用了吗?是——创建新的对象
    2. 函数有用 call、apply 这样的显式绑定吗?是——显式绑定时候的 this
    3. 函数有上下文吗?是——根据上文“隐式绑定”的内容看最后调用的对象即为 this 指向
    4. 啥也没有,挂在 window 上

    特殊情况

    1. 显式绑定会忽略传入为 null 、undefined 的情况,应用默认绑定规则;
    2. 软绑定 —— 请看下面的示例
     if (! Function.prototype.softBind) {
        Function.prototype.softBind = function(obj) {
        var fn = this;
        // 捕获所有 curried 参数
        var curried = [].slice.call (arguments, 1);
        var bound = function() {
        return fn.apply(
           (! this || this === (window || global)) ?
              obj : this,
              curried.concat.apply(curried, arguments)
           );
        };
        bound.prototype = Object.create(fn.prototype);
        return bound;
        };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    可以给默认绑定指定一个全局对象和undefined以外的值,同时保留隐式绑定或者显式绑定修改this的能力

    关键语句是 !this || this === (window || global) ? obj : this,即当发现传入的 this 为空或者是全局对象 window 时,将 this 指向默认的对象,其他时候保留调用时的 this。

    书中还提到了函数柯里化,这块内容可以参考 一些js的奇淫技巧 中关于函数柯里化的部分。其实个人觉得软绑定也属于一种技巧型的操作。


    贴一下语雀原文链接,阅读体验更佳
    欢迎交流!

  • 相关阅读:
    企业c#语言源代码防泄密解决方案
    智能质检火眼金睛,客服问题无所遁形
    【AUTOSAR-Nm】-2.2-通过CAN/Lin...信号报告Nm状态机的跳转
    元宇宙|高阶音频处理能力,让声音「声临其境」
    『LeetCode|每日一题』---->最小路径和
    大模型会毁了初级程序员 —— 对话图灵奖得主 Joseph Sifakis | 新程序员
    C#匿名方法介绍
    C# 高频面试题
    设计模式之享元模式(结构型)
    pat考完了
  • 原文地址:https://blog.csdn.net/qq_43425914/article/details/128147155