你不知道的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 是在函数调用的时候绑定的,它引用的内容完全取决于函数调用的位置。
posted @ 2019-01-25 17:18  隙游尘  阅读(1123)  评论(2编辑  收藏  举报

Hi