专栏名称: 蚂蚁金服ProtoTeam
数据前端团队
目录
相关文章推荐
宝山消防支队  ·  以案为例 |《油锅起火怎么办?》 ·  昨天  
IT服务圈儿  ·  45K*16薪,进字节了! ·  昨天  
前端早读课  ·  【第3459期】两款 AI 编程助手 ... ·  昨天  
前端大全  ·  解锁 Vue Hooks:让 Vue 开发效率起飞 ·  2 天前  
前端早读课  ·  【第3458期】React ... ·  2 天前  
51好读  ›  专栏  ›  蚂蚁金服ProtoTeam

全面理解 JavaScript 中的 this

蚂蚁金服ProtoTeam  · 掘金  · 前端  · 2017-12-08 00:40

正文

很多人当谈到 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 中函数的调用有以下几种方式:作为函数调用,作为对象方法调用,作为构造函数调用,和使用 apply call 调用。下面我们将按照调用方式的不同,分别讨论 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();

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

'use strict';
 
function foo() {
  console.log(this); // Window
}
 
setTimeout(foo, 1);

记住 setTimeout setInterval 都只是在全局上下文中执行一个函数而已,因此 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 : "前端开发" };
}
 
var p = new Person('愚人码头');
console.log(p.name); // undefined
console.log(p.title); // "前端开发"

相应的,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 调用。用作 getter setter 的函数都会把 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元素:







请到「今天看啥」查看全文