JavaScript中this的使用规则

发布于 大漠

在绝大多数情况下,函数的调用方式决定了this的值。this不能在执行期间被赋值,在每次函数被调用时this的值也可能会不同。在函数中this到底取何值,是在函数真正被调用执行的时候确定下来的,函数定义的时候确定不了。

因为 this 的取值是函数执行上下文(context)的一部分,每次调用函数,都会产生一个新的执行上下文环境。当代码中使用了 this,这个 this 的值就直接从执行的上下文中获取了,而不会从作用域链中搜寻。

这篇文章介绍了JavaScript中this的一些规则,这些规则很简单。最重要的规则是,this决定调用一个函数时的回调是什么。下面简单的罗列一些这方面的规则。

有关于this一篇文章非常的不错,详细介绍了JavaScript中的this怎么使用。

全局中的this

在全局运行上下文中(任何函数体外部),this指代全局对象,无论是否在严格模式下:

console.log(this.document === document); // true

// 在浏览器中,全局对象为 window 对象:
console.log(this === window); // true

this.a = 37;
console.log(window.a); // 37

构造函数中的this

如果调用一个函数时使用了new关键词,函数内的this绑定一个新对象,即:它的this与即将被创建的新对象绑定。

function ConstructorExample() {
    console.log(this); // => {}
    this.value = 10;
    console.log(this); // => {value: 10}
}
new ConstructorExample();

**注意:**当构造器返回的默认值是一个this引用的对象时,可以手动设置返回其他的对象,如果返回值不是一个对象,返回this

function C() {
    console.log(this);  // => {}
    this.a = 37;
    console.log(this); // => {a: 37}
}

var o = new C();

console.log(o.a); // => 37

function C2() {
    console.log(this); // => {}
    this.a = 37;
    console.log(this); // => {a: 37}
    return {a: 38};
}

o = new C2();

console.log(o.a); // => 38

在最后的例子中C2,因为在调用构造函数的过程中,手动的设置了返回对象,与this绑定的默认对象被取消(本质上这使得语句this.a = 37成了“僵尸”代码,实际上并不是真正的“僵尸”,这条语句执行了但是对于外部没有任何影响,因此完全可以忽略它)。

关于JavaScript中有关于new更多的介绍,可以阅读下面的文章:

构造函数prototype属性时,this的指向:

function foo() {
    console.log(this); // => {}
    this.x = 10;
}

foo.prototype.getX = function () {
    console.log(this); // => {x: 10}
    console.log(this.x); // => 10
}

var bar = new foo();

bar.getX();

callapply中的this

当一个函数的函数体中使用了this时,通过所有函数都从Function对象的原型中继承的call()apply()方法调用时,它的值可以绑定到一个指定的对象上。

function sum (c, d) {
    console.log(this); // => {a: 1, b: 3}
    return this.a + this.b + c + d;
}

var o = {
    a: 1,
    b: 3
}

sum.call(o, 5, 7); // => 16

sum.apply(o, [10, 20]); // => 34

使用call()apply()函数的时候要注意,如果传递的this值不是一个对象,JavaScript将会尝试使用内部ToObject操作将其转换为对象。因此,如果传递的值是一个原始值,比如7'foo',那么就会使用相关构造函数将它转换为对象,所以原始值7通过new Number(7)被转换为对象,而字符串'foo'使用new String('foo')转化为对象,例如:

function bar() {
    console.log(this);
    console.log(Object.prototype.toString.call(this));
    console.log(this);
}
bar.call(7);
bar.call('foo');
bar.apply(7);
bar.call('foo');

bind中的this

ECMAScript5中引入了Function.prototype.bind。调用f.bind(someObject)会创建一个与f具有相同函数体和作用域的函数,但是在这个新函数中,this将永久地被绑定到了bind的第一个参数,无论这个函数是如何被调用的。

function f(){
    console.log(this);
    return this.a;
}

var g = f.bind({a: 'azerty'});

console.log(g());

var o = {
    a: 37,
    f: f,
    g: g
}

console.log(o.f(), o.g());

对象方法中的this

当以对象里的方法的方式调用函数时,它们的this是调用该函数的对象。下面的例子中,当o.f()被调用时,函数内的this将绑定到o对象。

var o = {
    prop: 37,
    f: function() {
        console.log(this); // => {prop: 37, f: function}
        return this.prop;
    }
}

console.log(o.f()); // => 37

**注意:**在何处或者如何定义调用函数完全不会影响到this的行为。在上一个例子中,我们在定义o的时候为其成员f定义了一个匿名函数。但是,我们也可以首先定义函数然后再将其附属到o.f。这样做this的行为也一致:

var obj = {
    prop: 37
}

function fn(){
    console.log(this); // =>  {prop: 37, f: function}
    return this.prop;
}

obj.f = fn; // => 37

console.log(o.f());

这说明this的值只与函数f作为o的成员被调用有关系

类似的,this的绑定只受最近的成员引用的影响。在下面的这个例子中,我们把一个方法g当作对象o.b的函数调用。在这次执行期间,函数中的this将指向o.b。事实上,这与对象本身的成员没有多大关系,最靠近的引用是最重要的。

var o = {
    prop: 37
}
function independent() {
    console.log(this); // => {prop: 42, g: function}
    return this.prop;
}
o.b {
    g: independent,
    prop: 42
}
console.log(o.b.g()); // =>42

相同的概念在定义在原型链中的方法也是一致的。如果该方法存在于一个对象的原型链上,那么this指向的是调用这个方法的对象,表现得好像是这个方法就存在于这个对象上一样。

var obj = {
    fn: function() {
        console.log(this); // => {a: 1, b: 4}
        return this.a + this.b
    }
}

var prop =  Object.create(obj);

prop.a = 1;
prop.b = 4;

console.log(prop.fn()); // => 5

在这个例子中,对象prop没有属于它自己的fn属性,它的fn属性继承自它的原型。但是这对于最终在obj中找到fn属性的查找过程来说没有关系;查找过程首先从prop.fn的引用开始,所以函数中的this指向prop。也就是说,因为fn是作为prop的方法调用的,所以它的this指各了prop。这是JavaScript的原型继承中的一个有趣的特性。

再次,相同的概念也适用于函数作为一个getter或者一个setter调用。作为gettersetter函数都会绑定this到从设置属性或得到属性的那个对象。

function modules() {
    console.log(this); // =>  {re: 1, im: -1}
    return Math.sqrt(this.re * this.re + this.im * this.im)
}

var obj = {
    re: 1,
    im: -1,
    get phase() {
        console.log(this); // =>  {re: 1, im: -1}
        return Math.atan2(this.im, this.re);
    }
}

Object.defineProperty(obj, 'modules', {
    get: modules, 
    enumerable: true,
    configurable: true
})

console.log(obj.phase, obj.modules); // => -0.7853981633974483 1.4142135623730951

直接调用

因为下面的代码不是在严格模式下执行,并且this的值不会在函数执行时被设置,此时的this的值会默认设置为全局对象。

function f1(){
    console.log(this);
    return this;
}
f1(); // => Window {stop: ƒ, open: ƒ, alert: ƒ, ...}

然而,在严格模式下,this将保持他进入执行环境的值,所以下面的this将会默认为undefined:

function f2(){
    'use strict'; // 这里是严格模式
    console.log(this); // => undefined
    return this;
}
f2(); // => undefined

在严格模式下,如果this未被执行的上下文环境定义,那么它将会默认为undefined

**注意:**这个规则和对象方法中的this有点类似,不同之处在于。函数声明为一个方法而不是自动成为全局对象window。因此这是一个隐式方法调用。当我们调用fn(),它被解释为window.fn(),也就是window

function fn() {
    console.log(this); //  {stop: f, open: f, alert: f, confirm: f, prompt: f...}
    return this;
}
fn();
console.log(fn === window.fn); // => true

DOM事件处理函数中的this

在一个HTML DOM事件处理程序里,this始终指向这个处理程序所绑定的HTML DOM节点:

function Listener(){   
    document.getElementById('foo').addEventListener('click', this.handleClick);     //这里的 this 指向 Listener 这个对象。不是强调的是这里的 this
}
Listener.prototype.handleClick = function (event) {
    console.log(this);    // => <div id="foo"></div>
}
var listener = new Listener();
document.getElementById('foo').click();

这个很好理解,就相当于是给函数传参数,使handleClick运行时上下文改变了,相当于下面这样的代码:

var obj = {
    x: 10,
    fn: function() {
        console.log(this);     // => Window
        console.log(this.x);   // => undefined
    }
};
function foo(fn) {
    fn();
} 
foo(obj.fn);

你也可以通过bind切换上下文:

function  Listener(){
    document.getElementById('foo').addEventListener('click',this.handleClick.bind(this));      
}
Listener.prototype.handleClick = function (event) {
    console.log(this);    //Listener {}
}
var listener = new Listener();
document.getElementById('foo').click();

**注意:**当函数被用作事件处理函数时,虽然它的this指向触发事件的元素,但一些浏览器在使用addEventListener的函数动态添加监听函数时不遵守这个约定。

当代码被内联处理函数调用时,它的this指向监听器所在的DOM元素:

<button onclick="alert(this.tagName.toLowerCase());">Show this</button>

上面的alert()会显示button。注意只有外层代码中的this是这样的:

<button onclick="alert((function(){return this})());">Show inner this</button>

在这种情况下,没有设置内部函数的this,所以它指向globalwindow对象,即非严格模式下调用的函数未设置this时指向的默认对象。

箭头函数中的this

前面的其实都可以说:this指向调用该方法的对象。当使用箭头函数的时候,情况就有所不同了:箭头函数内部的this是词法作用域,由下下文确定。

var obj = {
    x: 10,
    foo: function() {
        var fn = () => {
            return () => {
                return () => {
                    console.log(this);      // => {x: 10, foo: function}
                    console.log(this.x);    // => 10
                }
            }
        }
        fn()()();
    }
}
obj.foo();

现在,箭头函数完全修复了 this 的指向,this 总是指向词法作用域,也就是外层调用者 obj

如果使用箭头函数,以前的这种 hack 写法:

var self = this;

就不再需要了。

var obj = {
    x: 10,
    foo: function() {
        var fn = () => {
            return () => {
                return () => {
                    console.log(this);    // => {x: 10, foo: function}
                    console.log(this.x);  // => 10
                }
            }
        }
        fn.bind({x: 14})()()();
        fn.call({x: 14})()();
    }
}
obj.foo();

由于 this 在箭头函数中已经按照词法作用域绑定了,所以,用 call()或者 apply()调用箭头函数时,无法对 this 进行绑定,即传入的第一个参数被忽略。

总结

这篇文章罗列了JavaScript中this在各种场景下使用的规则。上面展示的都是单一的场景,但有的时候会有多个场景的出现。对于这方面自己还不能理解。理解的同学可以指点指点。虽然看了书,自己也亲自撸码了,总感觉JavaScript中的this是那么的神秘。自己依旧是云里来雾里去。

扩展阅读

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《图解CSS3:核心技术与案例实战》。

如需转载,烦请注明出处:https://www.fedev.cn/javascript/the-simple-rules-to-this-in-javascript.htmlAir Jordan XIII Low