• 你不知道的JS之 this 和对象原型(一)this 是什么


     原文:你不知道的js系列

    JavaScript 的 this 机制并没有那么复杂

    为什么会有 this?

    在如何使用 this 之前,我们要搞清楚一个问题,为什么要使用 this。

    下面的代码尝试去说明 this 的使用动机:

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

    这段代码使得函数 identify() 和 speak() 可以在多个上下文(me 和 you)对象中重用,不用给每个对象分别创建函数。

    如果不用 this,你也可以将上下文对象直接传入函数:

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

    然而 this 机制可以隐式地传递一个对象引用,使得 API 设计得更简洁和更容易复用。

    你的使用模式越复杂,你就能更加明白,显式传递一个参数经常比传递 this 上下文还混乱。

    困惑

    在解释 this 如何工作之前,必须要先摒弃错误的概念。开发者们总是太过依赖 this 的字面意思。

    引用自身 Itself

    一种普遍的错误是认为 this 指代这个函数自身。

    为什么你会想从一个函数内部引用它自己呢,通常的原因是递归,或者事件回调函数在被调用之后解除绑定。

    JS 新手会认为将函数作为对象引用可以在函数调用期间存储状态(属性的值)。这确实是可以的但是用处有限,后面会介绍其它模式,除了函数对象本身还有更好的存储状态的地方。

    下面的代码会说明,this并不会像我们以为的那样让函数得到对自身的引用:

    function foo(num) {
        console.log( "foo: " + num );
    
        // keep track of how many times `foo` is called
        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
    
    // how many times was `foo` called?
    console.log( foo.count ); // 0 -- WTF?

    foo.count 还是 0 ,循环确实执行了 4 次,console.log 也确实被调用了 4 次。

    foo.count = 0 执行之后,实际上给函数对象 foo 添加了一个属性 count。

    但是在函数内部的 this.count 中,this 实际上并不指向这个函数对象,即使这个属性名字是一样的,但属性所在的对象是不同的。

    如果 foo 的属性 count 的值没有改变,那么我们改变的究竟是什么。实际上,如果你再深究一下,就会发现,这段代码意外地创建了一个全局变量 count,而且当时会有一个值 NaN(具体看这个系列的第二节)。

    很多开发者就会通过别的方式避免这个问题,比如创建另外一个对象储存这个属性 count:

    function foo(num) {
        console.log( "foo: " + num );
    
        // keep track of how many times `foo` is called
        data.count++;
    }
    
    var data = {
        count: 0
    };
    
    var i;
    
    for (i=0; i<10; i++) {
        if (i > 5) {
            foo( i );
        }
    }
    // foo: 6
    // foo: 7
    // foo: 8
    // foo: 9
    
    // how many times was `foo` called?
    console.log( data.count ); // 4

    这确实解决了问题,但是很遗憾这忽略了真正的问题——不理解 this 的含义和用法,只是回到熟悉的词法作用域机制。

    如果想在一个函数对象内部引用自身,this 是不够的,你需要一个标识符:

    function foo() {
        foo.count = 4; // `foo` refers to itself
    }
    
    setTimeout( function(){
        // anonymous function (no name), cannot
        // refer to itself
    }, 10 );

    在第一个函数中,函数被命名为 foo,这个标识符 foo 就可以用来指代这个函数对象自身。

    但在第二段中,回调函数没有名字,所以没办法引用自己。

    注:老派的已经被废弃的 arguments.callee 在函数中可以用来指代正在执行的函数对象。这是在匿名函数内部访问函数对象的唯一方式。

    当然最好的方式还是避免匿名函数的使用。

    另外一种解决办法就是使用 foo 标识符,不使用 this:

    function foo(num) {
        console.log( "foo: " + num );
    
        // keep track of how many times `foo` is called
        foo.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
    
    // how many times was `foo` called?
    console.log( foo.count ); // 4

    然而这种方法同样回避了对 this 的理解。

    另外一种解决这个问题的方式是,将 this 强制绑定到 foo 这个函数对象上:

    function foo(num) {
        console.log( "foo: " + num );
    
        // keep track of how many times `foo` is called
        // Note: `this` IS actually `foo` now, based on
        // how `foo` is called (see below)
        this.count++;
    }
    
    foo.count = 0;
    
    var i;
    
    for (i=0; i<10; i++) {
        if (i > 5) {
            // using `call(..)`, we ensure the `this`
            // points at the function object (`foo`) itself
            foo.call( foo, i );
        }
    }
    // foo: 6
    // foo: 7
    // foo: 8
    // foo: 9
    
    // how many times was `foo` called?
    console.log( foo.count ); // 4

    作用域的引用 Its Scope

    第二个常见的关于 this 的错误理解是,this 指向这个函数的作用域。这是一个有点狡猾的问题,因为在某种意义上这种说法是有些正确的,但在另一种意义上,这又是被误导的。

    首先,this 并没有指向函数的词法作用域。作用域确实就像是一个包含所有标识符属性的对象,但是这个作用域 “对象” 是无法被代码直接访问的,这是引擎内部实现的。

    所以下面的代码是错误的:

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

    你可能觉得这段代码很做作,但这是摘自一些帮助论坛里的真实代码。

    首先,这段代码试图通过 this.bar() 引用函数 bar(),能运行起来也是巧合。调用 bar() 最自然的方式就是直接使用标识符引用,去掉前面的 this。

    然而,写这段代码的开发者其实是想让 bar() 访问 foo() 内部的变量 a,但 this 不能被用来查询词法作用域的。

    this 到底是什么

    前面讲到过,this 是在运行时绑定的,它的上下文环境取决于函数调用的条件。this 的绑定和函数声明的位置没有关系,和函数调用的位置有关。

    当一个函数被调用时,一个执行上下文被创建。这个上下文记录包含函数调用的位置,函数调用的方式以及传入的参数这些信息。this 的引用就是在这个时候决定的。

    在下一节中,会介绍根据一个函数的调用位置确定它执行过程中将如何绑定this。

    小结:

    • this 既不指代函数本身,也不指代函数的词法作用域。
    • this 是在函数调用的时候绑定的,它引用的内容完全取决于函数调用的位置。
  • 相关阅读:
    oc kvc的模式:匹配搜索模式(模式匹配)、装包解包
    编程语言的多态与动态
    swift的@objc总结
    swift函数的调用约定
    PHP实现微信退款的分析与源码实现
    【MySQL】20个经典面试题,全部答对月薪10k+
    redis面试题总结
    【Redis缓存机制】1.Redis介绍和使用场景
    php四种文件加载语句
    做网站-mysql表字段设计
  • 原文地址:https://www.cnblogs.com/xiyouchen/p/10320667.html
Copyright © 2020-2023  润新知