Daibor Cyber Space

学习笔记、一些思考和记录

this —— JavaScript 里磨人的小妖精

Posted at — Feb 6, 2018

前言

在很多面向对象的语言当中,都出现了 this 这个关键词,它在JS中的表现与其他语言略有区别[1],也是很多人入门前端的一块绊脚石,弄懂了 this 也就在很大程度上弄懂了JS面向对象编程的内涵(如果此前没有接触过其它面向对象语言的话)。

在读这篇总结之前,建议确认了解了以下概念:

  1. JS中的严格模式和非严格模式

  2. ES6语法中的箭头函数

  3. 执行上下文(作用域)

  4. 原型和构造函数

  5. 简单的DOM操作

那就先从最简单的开始吧!

全局上下文

无论是否为严格模式,全局上下文中的 this 都代指全局对象。 在浏览器环境内就是 window 对象;在 Node 中就是 global 对象。

console.log(this === window);//true
var a = 'test';
console.log(window.a);//'test'
console.log(this.a);//'test'

函数上下文

简单来说就是一句话: this 的值取决于函数被调用的方式,或者说 this 指的是调用函数的那个对象

简单调用

在非严格模式下, this 的值不是通过调用设置的,所以默认指向全局对象。

function f1(){
	return this;
}
f1() === window;//true

在严格模式下, this 将保持它进入执行上下文时的值,如果未在执行的上下文中定义,将会默认为 undefined ,如下代码所示。

"use strict";
function f2(){
	return this;
}
f2() === undefined;//true

作为对象的方法

作为对象方法调用函数时,它们的 this 是调用该函数的对象。

var o = {
	a: 11,
	f: function(){
		return this.a;
	}
}
o.f();//11

这和定义函数的位置无关!!! 这和定义函数的位置无关!!! 这和定义函数的位置无关!!! 比如下面这段代码,在全局上下文定义一个函数,并把它赋值到对象内,同样可以实现跟上面相同的结果:

var a = 13;
function f1(){
	return this.a;
}
var o = {
	a: 11,
}
o.f = f1;//不带括号的函数名表示函数体本身,带括号则表示函数运行后返回的结果
o.f();//11
f1();//13
window.f1();//13

看看最后一行,是不是就能明白为什么说 this 指向的是调用函数的对象了?其实在简单调用那一节中也可以用这种方法去理解记忆,在全局上下文中声明的变量是全局对象的属性,声明的函数其实就是全局对象的方法,也就是 window.func()

那对象内部对象的方法中 this 又指向什么呢?当然是距离它最近的对象啦,毕竟调用这个函数的就是最近的对象。

o.b = {
	g: f1,
	a: 14
}
o.b.g();//14

同样的规则,也适用在定义在对象原型链上的方法,如果一个方法存在于一个对象的原型链上,那么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

对于 getter/setter 中调用的函数,用作 getter/setter 的函数都会把this绑定到设置或获取属性的对象。

作为构造函数调用

当一个函数用作构造函数时,他的this被绑定到正在构造的新对象上。

function C(){
  this.a = 37;
}

var o = new C();
console.log(o.a); // logs 37


function C2(){
  this.a = 37;
  return {a:38};
}

o = new C2();
console.log(o.a); // logs 38

箭头函数

与封闭词法上下文的this保持一致,在全局代码中被设置为全局对象。

var globalObject = this;
var foo = (() => this);
console.log(foo() === globalObject); // true

// 作为对象的一个方法调用,与上文所提的不同
var obj = {foo: foo};
console.log(obj.foo() === globalObject); // true

// 尝试使用call来设定this
console.log(foo.call(obj) === globalObject); // true

// 尝试使用bind来设定this
foo = foo.bind(obj);
console.log(foo() === globalObject); // true

即使将this传递call、bind、apply,也将被忽略,但调用添加参数仍然就可以,第一个参数应设置为null。 也就是说,无论如何,箭头函数的this被设置为它被创建时的上下文,这同样适用于在其他函数内创建的箭头函数,它们的this被设置为封闭词法上下文:

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

// 作为obj对象的一个方法来调用bar,把它的this绑定到obj。
// 将返回的函数的引用赋值给fn。
var fn = obj.bar();

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

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

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

作为DOM事件处理函数

这时的this指向触发事件的元素。

// 被调用时,将关联的元素变成蓝色
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);
}

作为内联事件处理函数

this指向监听器所在DOM元素(只有外层代码),如果内部是一个函数的话,它将指向window/global(非严格模式下的默认指向)

<button onclick="alert(this.tagName.toLowerCase())">显示button</button>
<button onclick="alert(function () {this.tagName.toLowerCase()})">显示button</button>

相关内容

在JS中,定义时上下文、运行时上下文和上下文是可以改变的三个概念,组成了this的复杂使用。其实这三者简单来说,就是正常你想运行的函数也好,方法也好后面加上一个.bind(),然后这个原本正常的函数和方法就好像被放到了一个特定的上下文(作用域)中,比如说你.方法名.call(window)之后,就好像把这个方法/函数放到了最外层中执行。

apply/call方法

首先来说,他们的功能没有任何区别,只在使用时,apply要接收一个参数数组,而call要把参数逐个列出来。

function cat () {
}

cat.prototype = {
	food: 'fish',
	say: function () {
		alert('I love' + this.food)
	}
}

var blackCat = new cat(),whiteDog;
blackCat.say();
blcakCat.say.call(whiteDog)

用的比较多的,通过document.getElementsByTagName选择的dom 节点是一种类似array的array。它不能应用Array下的push,pop等方法。我们可以通过:

var domNodes =  Array.prototype.slice.call(document.getElementsByTagName("*"));

这样domNodes就可以应用Array下的所有方法了。 ——来源

那如何利用这两个方法polyfill一个bind方法呢?

Function.prototype.bind = Function.prototype.bind || function(context){
	var self = this;
    return function(){
      return self.apply(context, arguments);
    };
}

使用apply将一个argument数组打印出来:

function log(){
      var args = Array.prototype.slice.call(arguments);
      args.unshift('(app)');//改造数组,段首前加一个字符串

      console.log.apply(console, args);
};

bind方法

bind()方法会创建一个新函数,称为绑定函数,调用绑定函数时,绑定函数会以创建它时传入bind()方法的第一个参数作为this,传入bind()方法的第二个以及以后的参数加上绑定函数运行时本身的参数,按照顺序作为原函数的参数调用原函数。

More

用大量实例代码讲解apply、call和bind

comments powered by Disqus