理解javascript——this


《你不知道的javascript》系列绝对是javascript进阶最好的书籍之一,推荐大家阅读。本文是此书的读书笔记与总结。

使用this的原因

this提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将API设计得更加简洁并且易于复用。

this误区

学习this的第一步是明白this既不指向函数自身也不指向函数的词法作用域

首先我们比较在两段代码:

function foo(num) {
    console.log( "foo: " + num );

    // 记录foo被调用的次数
    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

// foo被调用了多少次?
console.log( foo.count ); // 0 -- WTF?
function foo(num) {
    console.log( "foo: " + num );

    // 记录foo被调用的次数
    // 注意,在当前的调用方式下(参见下方代码),this确实指向foo
    this.count++;
}

foo.count = 0;

var i;

for (i=0; i<10; i++) {
    if (i > 5) {
        // 使用call(..)可以确保this指向函数对象foo本身
        foo.call( foo, i );
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9

// foo被调用了多少次?
console.log( foo.count ); // 4

第一段代码和第二段代码只有一个地方不同,就是在调用foo时使用call保证了this指向函数foo本身,因此也产生了不同的结果,说明this并不是简单的指向函数自身,而是有一套绑定的规则。

this规则

调用位置

在理解this的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置)。通常来说,寻找调用位置就是寻找“函数被调用的位置”,最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。我们关心的调用位置就在当前正在执行的函数的前一个调用中。

function baz() {
    // 当前调用栈是:baz
    // 因此,当前调用位置是全局作用域

    console.log( "baz" );
    bar(); // <-- bar的调用位置
}

function bar() {
    // 当前调用栈是baz -> bar
    // 因此,当前调用位置在baz中

    console.log( "bar" );
    foo(); // <-- foo的调用位置
}

function foo() {
    // 当前调用栈是baz -> bar -> foo
    // 因此,当前调用位置在bar中

    console.log( "foo" );
}

baz(); // <-- baz的调用位置

绑定规则

作用域链查找遵循”就近原则”;this谁调用就指向谁。

找到调用位置,再根据规则判断this的绑定对象

默认绑定

在非严格模式下默认绑定会绑定到全局对象。使用严格模式,this会绑定到undefined。

function test() {
  console.log(this);
};

test();

// Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage, sessionStorage: Storage, webkitStorageInfo: DeprecatedStorageInfo…}

'use strict';
function test() {
  console.log(this);
};

test();

// undefined

隐式绑定

function foo() { 
    console.log( this.a );
}

var obj = { 
    a: 2,
    foo: foo 
};

obj.foo(); // 2

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

  1. 遵循”就近原则”,当出现obj1.obj2.foo()时,只有最后一层会影响调用位置。
  2. 回调函数会丢失this绑定

显式绑定(call,apply)

可以使用函数的call(..)和apply(..)方法
这两个方法是如何工作的呢?它们的第一个参数是一个对象,它们会把这个对象绑定到this,接着在调用函数时指定这个this。因为你可以直接指定this的绑定对象,因此我们称之为显式绑定。

思考下面的代码:

function foo() { 
    console.log( this.a );
}

var obj = { 
    a:2
};

foo.call( obj ); // 2

通过foo.call(..),我们可以在调用foo时强制把它的this绑定到obj上。

如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作this的绑定对象,这个原始值会被转换成它的对象形式(也就是new String(..)、new Boolean(..)或者new Number(..))。这通常被称为“装箱”。

new绑定

在 js 中,为了实现类,我们需要定义一些构造函数,在调用一个构造函数的时候需要加上 new 这个关键字:

function Person(name) {
  this.name = name;
  console.log(this);
}

var p = new Person('kevin');

// Person {name: "kevin"}

我们可以看到当作构造函数调用时,this 指向了这个构造函数调用时候实例化出来的对象,new是最后一种可以影响函数调用时this绑定行为的方法,称之为new绑定。

当然,构造函数其实也是一个函数,如果我们把它当作一个普通函数执行,这个 this 仍然执行全局:

function Person(name) {
  this.name = name;
  console.log(this);
}

var p = Person('kevin');

// Window

优先级

  1. 函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。

var bar = new foo()
2. 函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。

var bar = foo.call(obj2)
3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。

var bar = obj1.foo()
4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。

var bar = foo()
就是这样。对于正常的函数调用来说,理解了这些知识你就可以明白this的绑定原理了。

ES6箭头函数

箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this。

看看箭头函数的词法作用域:

function foo() {
    // 返回一个箭头函数 
    return (a) => {
        //this继承自foo()
        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绑定到obj1,bar(引用箭头函数)的this 也会绑定到obj1,箭头函数的绑定无法被修改。(new也不行!)

箭头函数最常用于回调函数中,例如事件处理器或者定时器:

function foo() { 
    setTimeout(() => {
        // 这里的this在此法上继承自foo()
        console.log( this.a ); 
    },100);
}

var obj = { 
    a:2
};

foo.call( obj ); // 2

箭头函数可以像bind(..)一样确保函数的this被绑定到指定对象,简单来说, 箭头函数中的 this 只和定义它时候的作用域的 this 有关,而与在哪里以及如何调用它无关,同时它的 this 指向是不可改变的。

总结

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

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

ES6中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this,具体来说,箭头函数会继承外层函数调用的this绑定(无论this绑定到什么)。

总之this是javascript中比较复杂的机制,很特别也很方便,需要我们在不断的使用中体会。


文章作者: Nczkevin
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Nczkevin !
评论
  目录