• JS中的this(一)


    scope(作用域) 和 context(上下文) 是一个容易迷糊的地方。请参考: Javascript Context和Scope的一些学习总结

    推荐阅读: 深入浅出ES6(1~10)系列

    会用一门语言来写程序,并不代表就能正确地理解和使用该语言。当然, JavaScript 也是如此。尽管JS(JavaScript 的简称)是一门很容易上手的语言, 但里面其实有许多细节, 初学者可能并不了解, 即使是经验丰富的JS高手也偶尔会掉到坑里去。

    很多有经验的程序员对 this 在 JS 内部是如何运作的也是一头雾水。通俗点讲, this 只是一个引用别名(referencing alias) - 这个别名只知道当前指向的那个对象, 而这也是最棘手的地方。

    本文为你理清思路,并介绍 this 关键字的内部运作原理。

    那么, this 是个什么鬼?

    简而言之, this 是一个特殊的标识符关键字 —— 在每个 function 中自动根据作用域(scope) 确定, 指向的是此次调用的 “所有者,owner”。但要完全搞定这个问题, 我们需要回答两个关键问题:

    this 是如何创建的?

    每调用一次 JavaScript 函数时,都会创建一个新的对象, 其中的信息包括: 传入了哪些参数, 函数是如何调用(invoked)的, 函数是在哪里被调用(called)的,等等。该对象中还有一个重要的属性是 this 引用, 函数是哪个对象的方法,this 就会自动绑定到该对象。

    提示: 详情请参考 ECMAScript 语言规范 §10.4.3节 以及其中的相关链接。

    var car = {
      brand: "Nissan",
      getBrand: function(){
        console.log(this.brand);
      }
    };
    
    car.getBrand();
    // output: Nissan
    

    在这个例子中, 使用的是 this.brand, 这是 car 对象的引用。所以此时, this.brand 等价于 car.brand 。

    this 指向的是谁

    在所有 function 中, this 的值都是根据上下文(context, 函数在调用时刻所处的环境)来决定的。this 的作用域(scope) 与函数定义的位置没有关系, 而是取决于函数在哪里被调用( where they are called from ;i.e. the context)。

    每一行JavaScript代码都是在执行上下文(execution context)中运行的。this 指向的对象在每次进入新的执行上下文后是固定的, 直到跳转(shifted)到另一个不同的上下文才发生改变。决定执行上下文(以及 this 的绑定)需要我们去找出调用点(call-site), 调用点即函数在代码中调用的位置。

    让我们看下面的示例:

    var brand = 'Nissan';
    var myCar = {brand: 'Honda'};
    
    var getBrand = function() {
      console.log(this.brand);
    };
    
    myCar.getBrand = getBrand;
    myCar.getBrand();
    // output: Honda
    
    getBrand();
    // output: Nissan
    

    虽然 myCar.getBrand() 和 getBrand() 指向的是同一个函数, 但其中的 this 是不同的,因为它取决于getBrand() 在哪个上下文中调用。

    我们已经知道, function 是哪个对象的方法, 在函数中, this 就绑定到这个对象。

    上面的代码里,第一次函数调用对应的是 myCar 对象, 而第二次对应的是 window 【 此时 getBrand() 等价于 window.getBrand() 】。因此,不同的上下文产生的是不同的结果。

    调用上下文

    下面,让我们看看不同的上下文中this的指向。

    全局作用域(Global Scope)

    所有的JavaScript 运行时都有唯一的全局对象(global object)。在浏览器中,全局对象是window 。而在Node.js里面是 global

    在全局执行上下文中(任何函数之外的代码), this 指向的是全局对象, 不管是否在严格模式下(strict mode)。

    局部作用域(Local Scope)

    在函数中, this 取决于函数是怎么调用的。分三种情况:

    1. 在简单函数调用(Simple Function Call)中使用this

    第一种情形是直接调用一个独立的函数。

    function simpleCall(){
      console.log(this);
    }
    
    simpleCall();
    // output: the Window object
    

    在这种情况下,this值没有被 call 设置。因为代码不是运行在严格模式下, this 又必须是一个对象, 所以他的值默认为全局对象。

    如果是在严格模式(strict mode)下, 进入执行上下文时设置为什么值那就是什么值。如果没有指定, 那么就一直是undefined, 如下所示:

    function simpleCall(){
      "use strict";
      console.log(this);
    }
    
    simpleCall();
    // output: undefined
    

    2. 在对象的方法(Object’s Method)中使用 this

    将函数保存为对象的属性, 这样就转化为一个方法, 可以通过对象调用这个方法。当函数被当成对象的方法来调用时, 里面的 this 值就被设置为调用方法的对象。

    var message = {
      content: "I'm a JavaScript Ninja!",
      showContent: function() {
        console.log(this.content);
      }
    };
    
    message.showContent();   
    // output: I'm a JavaScript Ninja!
    

    showContent() 是 message 对象的一个方法, 所以此时 this.content 等价于 message.content

    3. 在构造函数(Constructor Function)中使用 this

    我们可以通过 new 操作符来调用函数。在这种情况下,这个函数就变成了构造函数(也就是一个对象工厂)。与前面讨论的简单函数调用和方法调用不同, 构造函数调用会传入一个全新的对象作为 this 的值, 并且隐式地返回新构造的这个对象作为结果【简言之, 新构造对象的内存是 new 操作符分配的, 构造函数只是做了一些初始化工作】。

    当一个函数作为构造器使用时(通过 new 关键字), 它的 this 值绑定到新创建的那个对象。如果没使用 new关键字, 那么他就只是一个普通的函数, this 将指向 window 对象。

    function Message(content){
      this.content = content;
      this.showContent = function(){
        console.log(this.content);
      };
    }
    
    var message = new Message("I'm JavaScript Ninja!");
    
    message.showContent();
    // output: I'm JavaScript Ninja!
    

    在上面的示例中, 有一个名为 Message() 的构造函数。通过使用 new 操作符创建了一个全新的对象,名为message。同时还通传给构造函数一个字符串, 作为新对象的content属性。通过最后一行代码中可以看到这个字符串成功地打印出来了, 因为 this 指向的是新创建的对象, 而不是构造函数本身。

    如何正确地使用 this

    在本节中,我们将学习一些决定 this 行为的内部机制。

    在JavaScript中,所有的函数都是对象, 因此函数也可以有自己的方法。所有的函数都有的两个方法, 是 apply()和 call(). 。我们可以通过这两个方法来改变函数的上下文, 在任何时候都有效, 用来显式地设置 this 的值。

    apply() 方法接收两个参数: 第一个是要设置为 this 的那个对象, 第二个参数是可选的,如果要传入参数, 则封装为数组作为 apply() 的第二个参数即可。

    call() 方法 和 apply() 基本上是一样的, 除了后面的参数不是数组, 而是分散开一个一个地附加在后面。

    下面来演练一下:

    function warrior(speed, strength){
      console.log(
        "Warrior: " + this.kind +
        ", weapon: " + this.weapon +
        ", speed: " + speed +
        ", strength: " + strength
      );
    }
    
    var warrior1 = {
      kind: "ninja",
      weapon: "shuriken"
    };
    
    var warrior2 = {
      kind: "samurai",
      weapon: "katana"
    };
    
    warrior.call(warrior1, 9, 5);
    // output: Warrior: ninja, weapon: shuriken, speed: 9, strength: 5
    warrior.apply(warrior2, [6, 10]);
    // output: Warrior: samurai, weapon: katana, speed: 6, strength: 10
    

    在这里,我们有一个工厂函数 warrior(), 通过传入不同的战士对象创建不同类型的warrior(战士)。那么,在工厂函数里面, this 将指向我们通过call() 和/或 apply()传入的对象。

    在第一个函数调用中,我们使用call() 方法来将 this 设置为 warrior1对象, 并传入需要的其他参数, 参数间用逗号分隔。在第二个函数调用中, 其实都差不多, 只是传入的是 warrior2 对象, 并将必要参数封装为一个数组。

    除了call() 和 apply() 以外, ECMAScript 5还增加了 bind() 方法, 在调用一个函数或方法时也可以通过bind 方法来绑定 this 对象。让我们看下面的例子:

    function warrior(kind){
      console.log(
        "Warrior: " + kind +
        ". Favorite weapon: " + this.weapon +
        ". Main mission: " + this.mission
      );
    }
    
    var attributes = {
      weapon: "shuriken",
      mission: "espionage"
    };
    
    var ninja = warrior.bind(attributes, "ninja");
    
    ninja();
    // output: Warrior: ninja. Favorite weapon: shuriken. Main mission: espionage
    

    在这个例子中, bind()方法的使用方式还是类似的, 但 warrior.bind() 创建了一个新的函数(方法体和作用域跟warrior()一样),并没有改动原来的 warrior() 函数。新函数的功能和老的一样, 只是绑定到了attributes对象。

    bind 方法和 call, apply 的区别在于, bind() 之后函数并没有执行, 可以传给其他函数, 在某个适当的时机再调用 】

    总结

    So, 与 this 有关的知识差不多就是这些。作为初学者,掌握这些你就足够来正确地使用它了, 对自己多点信心! 当然, 在使用的过程中也可能有一些问题会困扰你, 那么, 欢迎阅读下一篇文章 掌握JS中的“this” (二)

    推荐阅读

    阮一峰老师的: ECMAScript 6入门

    CNBlog : Javascript Context和Scope的一些学习总结

    infoq: 深入浅出ES6(1~10)系列

    babeljs-ES6: https://babeljs.io/repl/

    掌握JS中的“this” (一)

    掌握JS中的“this” (二)

    相关链接

    原文链接: Revealing the Inner Workings of JavaScript’s “this” Keyword

    原文日期: 2015年05月01日

    翻译日期: 2015年09月17日

    作者: 铁锚 http://blog.csdn.net/renfufei

  • 相关阅读:
    sudo
    Ansible模块
    Ansible自动化运维
    Sersync
    eclipse报错MA
    myeclipse报错MA
    通过StringBuilder的reverse()实现倒序
    位运算实现高效互换
    scanf("%s",s)与gets(s)
    异或运算符实现简单加密
  • 原文地址:https://www.cnblogs.com/shiddong/p/5554249.html
Copyright © 2020-2023  润新知