全面理解 JavaScript 中的 this

10年服务1亿Tian开发工程师

很多人当谈到 JavaScript 中的 this 的时候会感到头疼,因为在 JavaScript 中,this 是动态绑定,或称为运行期绑定的,这就导致 JavaScript 中的 this 关键字有能力具备多重含义,带来灵活性的同时,也为初学者带来不少困惑。

上下文 vs 作用域

每个函数调用都有与之相关的作用域和上下文。首先需要澄清的问题是上下文和作用域是不同的概念。很多人经常将这两个术语混淆。

作用域(scope) 是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。而上下文(context)是用来指定代码某些特定部分中 this 的值。

从根本上说,作用域是基于函数(function-based)的,而上下文是基于对象(object-based)的。换句话说,作用域是和每次函数调用时变量的访问有关,并且每次调用都是独立的。上下文总是被调用函数中关键字 this 的值,是调用当前可执行代码的对象的引用。说的通俗一点就是:this 取值,是在函数真正被调用执行的时候确定的,而不是在函数定义的时候确定的。

全局上下文

无论是否在严格模式下,在全局执行上下文中(在任何函数体外部)this 都指向全局对象。当然具体的全局对象和宿主环境有关。

在浏览器中, window 对象同时也是全局对象:

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

NodeJS 中,则是 global 对象:

console.log(this); // global

函数上下文

由于其运行期绑定的特性,JavaScript 中的 this 含义要丰富得多,它可以是全局对象、当前对象或者任意对象,这完全取决于函数的调用方式。JavaScript 中函数的调用有以下几种方式:作为函数调用,作为对象方法调用,作为构造函数调用,和使用 applycall 调用。下面我们将按照调用方式的不同,分别讨论 this 的含义。

作为函数直接调用

作为函数直接调用时,要注意 2 种情况:

非严格模式

在非严格模式下执行函数调用,此时 this 默认指向全局对象。

function f1(){
  return this;
}
//在浏览器中:
f1() === window;   //在浏览器中,全局对象是window

//在Node中:
f1() === global;

严格模式 ‘use strict’;

在严格模式下,this 将保持他进入执行上下文时的值,所以下面的 this 并不会指向全局对象,而是默认为 undefined 。

'use strict'; // 这里是严格模式
function test() {
  return this;
};

test() === undefined; // true

作为对象的方法调用

在 JavaScript 中,函数也是对象,因此函数可以作为一个对象的属性,此时该函数被称为该对象的方法,在使用这种调用方式时,内部的 this 指向该对象。

var Obj = {
  prop: 37,
  getProp: function() {
    return this.prop;
  }
};

console.log(Obj.getProp()); // 37

上面的例子中,当 Obj.getProp() 被调用时,方法内的 this 将指向 Obj 对象。值得注意的是,这种行为根本不受函数定义方式或定义位置的影响。在前面的例子中,我们在定义对象 Obj 的同时,将成员 getProp 定义了一个匿名函数。但是,我们也可以首先定义函数,然后再将其附加到 Obj.getProp 。所以,下面的代码和上面的例子是等价的:

var Obj = {
  prop: 37
};

function independent() {
  return this.prop;
}

Obj.getProp = independent;

console.log(Obj.getProp()); // logs 37

JavaScript 非常灵活,现在我们把对象的方法赋值给一个变量,然后直接调用这个函数变量又会发生什么呢?

var Obj = {
  prop: 37,
  getProp: function() {
    return this.prop;
  }
};

var test = Obj.getProp
console.log(test()); // undefined

可以看到,这时候 this 指向全局对象,这个例子 test 只是引用了 Obj.getProp 函数,也就是说这个函数并不作为 Obj 对象的方法调用,所以,它是被当作一个普通函数来直接调用。因此,this 指向全局对象。

一些坑

我们来看看下面这个例子:

var prop = 0;
var Obj = {
  prop: 37,
  getProp: function() {
    setTimeout(function() {
        console.log(this.prop) // 结果是 0 ,不是37!
    },1000)
  }
};

Obj.getProp();

正如你所见, setTimeout 中的 this 向了全局对象,这里不是把它当作函数的方法使用吗?这一点经常让很多初学者疑惑;这种问题是很多异步回调函数中也会普遍会碰到,通常有个土办法解决这个问题,比如,我们可以利用 闭包 的特性来处理:

var Obj = {
  prop: 37,
  getProp: function() {
    var self = this; 
    setTimeout(function() {
        console.log(self.prop) // 37
    },1000)
  }
};

Obj.getProp();

其实,setTimeoutsetInterval 都只是在全局上下文中执行一个函数而已,即使是在严格模式下:

'use strict';

function foo() {
  console.log(this); // Window
}

setTimeout(foo, 1);

记住 setTimeoutsetInterval 都只是在全局上下文中执行一个函数而已,因此 this 指向全局对象。 除非你实用箭头函数,Function.prototype.bind 方法等办法修复。至于解决方案会在后续的文章中继续讨论。

作为构造函数调用

JavaScript 支持面向对象式本赛季,与主流的面向对象式本赛季语言不同,JavaScript 并没有类(class)的概念,而是使用基于原型(prototype)的继承方式。作为又一项约定通用的准则,构造函数以大写字母开头,提醒调用者使用正确的方式调用。

当一个函数用作构造函数时(使用 new 关键字),它的 this 被绑定到正在构造的新对象,也就是我们常说的实例化出来的对象。

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

var p = new Person('');
console.log(p.name); // ""

几个陷阱

如果构造函数具有返回对象的 return 语句,则该返回对象将是 new 表达式的结果。

function Person(name) {
  this.name = name;
  return { title : "Tian开发" };
}

var p = new Person('');
console.log(p.name); // undefined
console.log(p.title); // "Tian开发"

相应的,JavaScript 中的构造函数也很特殊,如果不使用 new 调用,则和普通函数一样, this 仍然执行全局:

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

var p = Person('');

箭头函数中的 this

在箭头函数中,this 与 封闭的词法上下文的 this 保持一致,也就是说由上下文确定。

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

obj.foo 是一个匿名函数,无论如何, 这个函数中的 this 指向它被创建时的上下文(在上面的例子中,就是 obj 对象)。这同样适用于在其他函数中创建的箭头函数:这些箭头函数的this 被设置为外层执行上下文。

// 创建一个含有bar方法的obj对象,bar返回一个函数,这个函数返回它自己的this,
// 这个返回的函数是以箭头函数创建的,所以它的this被永久绑定到了它外层函数的this。
// bar的值可以在调用中设置,它反过来又设置返回函数的值。
var obj = {
    bar: function() {
        var x = (() => this);
        return x;
    }
};

// 作为obj对象的一个方法来调用bar,把它的this绑定到obj。
// x所指向的匿名函数赋值给fn。
var fn = obj.bar();

// 直接调用fn而不设置this,通常(即不使用箭头函数的情况)默认为全局对象,若在严格模式则为undefined
console.log(fn() === obj); // true

// 但是注意,如果你只是引用obj的方法,而没有调用它(this是在函数调用过程中设置的)
var fn2 = obj.bar;
// 那么调用箭头函数后,this指向window,因为它从 bar 继承了this。
console.log(fn2()() == window); // true

在上面的例子中,一个赋值给了 obj.bar 的函数(称为匿名函数 A),返回了另一个箭头函数(称为匿名函数 B)。因此,函数B的this被永久设置为 obj.bar(函数A)被调用时的 this 。当返回的函数(函数B)被调用时,它this始终是最初设置的。在上面的代码示例中,函数B的 this 被设置为函数A的 this ,即 obj,所以它仍然设置为 obj,即使以通常将 this 设置为 undefined 或全局对象(或者如前面示例中全局执行上下文中的任何其他方法)进行调用。

填坑

我们回到上面 setTimeout 的坑:

var prop = 0;
var Obj = {
  prop: 37,
  getProp: function() {
    setTimeout(function() {
        console.log(this.prop) // 结果是 0 ,不是37!
    },1000)
  }
};

Obj.getProp();

通常情况我,我们在这里期望输出的结果是 37 ,用箭头函数解决这个问题相当简单:

var Obj = {
  prop: 37,
  getProp: function() {
    setTimeout(() => {
        console.log(this.prop) // 37
    },1000)
  }
};

Obj.getProp();

原型链中的 this

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

var o = {
  f : function(){ 
    return this.a + this.b; 
  }
};
var p = Object.create(o);
p.a = 1;
p.b = 4;

console.log(p.f()); // 5

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

你也会看到下面这种形式的老代码,道理是一样的:

function Person(name) {
  this.name = name;
}
Person.prototype = {
  getName:function () {
    return this.name
  }
};
var p = new Person('');
console.log(p.getName()); // ""

getter 与 setter 中的 this

再次,相同的概念也适用时的函数作为一个 getter 或者 一个 setter 调用。用作 gettersetter 的函数都会把 this 绑定到正在设置或获取属性的对象。

function sum() {
  return this.a + this.b + this.c;
}

var o = {
  a: 1,
  b: 2,
  c: 3,
  get average() {
    return (this.a + this.b + this.c) / 3;
  }
};

Object.defineProperty(o, 'sum', {
    get: sum, enumerable: true, configurable: true});

console.log(o.average, o.sum); // logs 2, 6

作为一个DOM事件处理函数

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

// 被调用时,将关联的元素变成蓝色
function bluify(e){
  console.log(this === e.currentTarget); // 总是 true

  // 当 currentTarget 和 target 是同一个对象是为 true
  console.log(this === e.target);        
  this.style.backgroundColor = '#A5D9F3';
}

// 获取文档中的所有元素的列表
var elements = document.getElementsByTagName('*');

// 将bluify作为元素的点击监听函数,当元素被点击时,就会变成蓝色
for(var i=0 ; i < elements.length; i++){
  elements[i].addEventListener('click', bluify, false);
}

作为一个内联事件处理函数

当代码被内联on-event 处理函数调用时,它的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,所以它指向 global/window 对象(即非严格模式下调用的函数未设置 this 时指向的默认对象)。

使用 apply 或 call 调用

JavaScript 中函数也是对象,对象则有方法,applycall 就是函数对象的方法。这两个方法异常强大,他们允许切换函数执行的上下文环境(context),即 this 绑定的对象。很多 JavaScript 中的技巧以及类库都用到了该方法。让我们看一个具体的例子:

function Point(x, y){ 
   this.x = x; 
   this.y = y; 
   this.moveTo = function(x, y){ 
       this.x = x; 
       this.y = y; 
   } 
} 
 
var p1 = new Point(0, 0); 
p1.moveTo(1, 1); 
console.log(p1.x,p1.y); //1 1

var p2 = {x: 0, y: 0}; 
p1.moveTo.apply(p2, [10, 10]);
console.log(p2.x,p2.y); //10 10

在上面的例子中,我们使用构造函数生成了一个对象 p1,该对象同时具有 moveTo 方法;使用对象字面量创建了另一个对象 p2,我们看到使用 apply 可以将 p1 的方法 apply 到 p2 上,这时候 this 也被绑定到对象 p2 上。另一个方法 call 也具备同样功能,不同的是最后的参数不是作为一个数组统一传入,而是分开传入的:

function Point(x, y){ 
   this.x = x; 
   this.y = y; 
   this.moveTo = function(x, y){ 
       this.x = x; 
       this.y = y; 
   } 
} 
 
var p1 = new Point(0, 0); 
p1.moveTo(1, 1); 
console.log(p1.x,p1.y); //1 1

var p2 = {x: 0, y: 0}; 
p1.moveTo.call(p2, 10, 10); // 只是参数不同
console.log(p2.x,p2.y); //10 10

.bind() 方法

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

function f(){
  return this.a;
}

//this被固定到了传入的对象上
var g = f.bind({a:"azerty"});
console.log(g()); // azerty

var h = g.bind({a:'yoo'}); //bind只生效一次!
console.log(h()); // azerty

var o = {a:37, f:f, g:g, h:h};
console.log(o.f(), o.g(), o.h()); // 37, azerty, azerty

填坑

上面我们已经讲了使用箭头函数填 setTimeout 的坑,这次我们使用 bind 方法来试试:

var prop = 0;
var Obj = {
  prop: 37,
  getProp: function() {
    setTimeout(function() {
        console.log(this.prop) // 37
    }.bind(Obj),1000)
  }
};

Obj.getProp();

同样可以填坑,但是看上去没有使用箭头函数来的优雅。

小结

本文介绍了 JavaScript 中的 this 关键字在各种情况下的含义,虽然这只是 JavaScript 中一个很小的概念,但借此我们可以深入了解 JavaScript 中函数的执行环境,而这是理解闭包等其他概念的基础。掌握了这些概念,才能充分发挥 JavaScript 的特点,才会发现 JavaScript 语言特性的强大。

参考阅读:

赞(0) 打赏
未经允许不得转载:WEBTian开发 » 全面理解 JavaScript 中的 this

评论 1

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
  1. #-49

    很是全面很是长

    1年前 (2017-12-08)回复

Tian开发相关广告投放 更专业 更精准

联系我们

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏