Javascript中的 this

什么是 this? 为什么要用 this?

this不是编写时绑定的,而是运行时绑定。它依赖于函数调用的上下文条件。this绑定与函数声明的位置没有任何关系,而与函数被调用的方式紧密相连。

当一个函数被调用时,会建立一个称为执行环节的活动记录。这个记录包含函数是从何处(调用栈---call-stack)被调用的,函数是如何被调用的,被传递了什么参数等信息。这个记录的属性之一,就是在函数执行期间将被使用的this引用。

this 机制提供了更优雅的方式来隐含地“传递”一个对象引用,导致更加干净的API设计和更容易的复用。

你的使用模式越复杂,你就会越清晰地看到:将执行环境作为一个明确参数传递,通常比传递 this 执行环境要乱.

对this的困惑

想当初,对"this"这个名字用太过于字面的方式考虑而产生了困惑,没有真正的了解this是如何实际工作的。接下来首先要摒弃一些误解。

一:this 指向函数自己

看一个例子:

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

	// 追踪 `fn` 被调用了多少次
	this.count++;
}
fn.count = 0;
for (let i=0; i<5; i++) {
	fn( i );
}
// fn: 0
// fn: 1
// fn: 2
// fn: 3
// fn: 4

console.log(fn.count); // 0

复制代码到控制台打印一下,what??? fn.count为什么是0,console.log( "fn: " + num )明明告诉我们实际调用五次的,初学时的就是这种状态 --- N脸懵逼。这种错误的认为就是我们对于this(在 this.count++ 中)的含义进行了过于字面化的解释。

为什么会出现这种情况??
记住,this的指向与所在方法的调用位置有关,而与方法的声明位置无关。 this.count++这个语句,不小心创建了一个全局变量,此时的this是指向的全局对象。在非严格模式下,全局作用域中函数被独立调用时,它的this默认指向(绑定)window或者global。在严格模式中,它的this为undefined。

这个问题的解决方法有多种,这里给出其中一种-强迫thi指向fn函数对象

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

	// 追踪 `fn` 被调用了多少次
	this.count++;
}
fn.count = 0;
for (let i=0; i<5; i++) {
	fn.call(fn, i );
}
// fn: 0
// fn: 1
// fn: 2
// fn: 3
// fn: 4

console.log(fn.count); // 5

二:this 函数的作用域

this不会以任何方式指向函数的词法作用域。作用域好像是一个将所有可用标识符作为属性的对象,这从内部来说是对的。但是 JavasScript 代码不能访问作用域“对象”。它是 引擎 的内部实现。

看一个例子:

function fn() {
	var a = 2;
	this.bar(); // 这里的this指向全局对象window
}

function bar() {
	console.log( this.a ); // 这里的this指向全局对象window
}

fn(); // undefined

没想到吧,最后输出咋不是2呢?瞎搞了吧哈哈
如果你想用 this 在 fn() 和 bar() 的词法作用域间建立一座桥,使得bar() 可以访问 fn()内部作用域的变量 a。这是不可能的,你不能使用 this 引用在词法作用域中查找东西。如果你想要函数bar访问函数fn里的a 变量,可以用闭包实现:

function fn() {
	var a = 2;
  (function b() {
    console.log( a );
  })()
}

fn(); // 2

调用点

先来了解一下调用点,调用点决定函数执行期间 this 指向哪里。

用代码来展示一下调用栈和调用点:

function baz() {
    // 调用栈是: `baz`
    // 我们的调用点是 global scope(全局作用域)

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

function bar() {
    // 调用栈是: `baz` -> `bar`
    // 我们的调用点位于 `baz`

    console.log( "bar" );
    foo(); // <-- `foo` 的 call-site
}

function foo() {
    // 调用栈是: `baz` -> `bar` -> `foo`
    // 我们的调用点位于 `bar`

    console.log( "foo" );
}

baz(); // <-- `baz` 的调用点

绑定规则

先来了解一下4种绑定规则,用于考察调用点并判定4种规则中的哪一种适用。

默认绑定

  1. 默认绑定常见的情况是独立函数调用,也就是 fn()。这时 this 指向了全局对象 window

看一个例子:

function fn() {
  console.log(this.a);
}
var a = 2;
foo(); // 2

再看例子,给上文误解的例子加深理解:

var count = 0;
function fn(num) {
	console.log( "fn: " + num );

	// 追踪 `fn` 被调用了多少次
	this.count++;
}
fn.count = 0;
for (let i=0; i<5; i++) {
	fn( i );
}
// fn: 0
// fn: 1
// fn: 2
// fn: 3
// fn: 4

console.log(fn.count); // 0
console.log(count); // 5
  1. 严格模式 下,对于 默认绑定 来说全局对象是不合法的,this 将被设置为 undefined

看一个例子:

function fn() {
	"use strict";

	console.log( this.a );
}

var a = 2;

fn(); // TypeError: `this` is `undefined`
  1. 显示使用 window 调用 fn() 则不会报错。

看一个例子:

function fn() {
	"use strict";

	console.log( this.a );
}

var a = 2;

window.fn(); // 2
  1. 即便所有的 this 绑定规则都是完全基于调用点的,但如果 fn() 的内容没有在 严格模式 下执行,对于默认绑定来说全局对象是唯一合法的。fn() 调用点的 严格模式 状态与此无关。

看一个例子:

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

var a = 2;

(function(){
	"use strict";

	fn(); // 2
})();

注意: 应尽量避免 严格模式非严格模式混淆使用。

隐式绑定

这个规则是: 调用点是否有一个环境对象。当一个方法引用存在一个环境对象时, 这个对象应当被用于这个函数调用的 this 绑定。

  1. 函数如果被某个对象包含,且作为这个对象的方法调用时,函数里面的 this 就是这个对象。

看一个例子:

function fn() {
  console.log(this.a);
}
var obj = {
  a: 2,
  foo: foo
}
obj.fn(); // 2
  1. 只有对象属性引用链的最后一层是影响调用点的。

看一个例子:

function fn() {
  console.log(this.a);
}
var obj1 = {
  a: 42,
  foo: foo
}
var obj2 = {
  a: 2,
  obj1: obj1
}
obj2.obj1.foo(); // 42
  1. 当一个隐式绑定丢失了它的绑定,意味着它可能会退回到默认绑定。

看一个例子:

function fn() {
  console.log(this.a);
}
var obj = {
  a: 2,
  fn: fn
};
var bar = obj.fn; // 函数引用!
var a = 'oops, global'; // `a` 也是一个全局对象的属性
bar();  // "oops, global"

  1. 当传递一个回调函数式,参数传递仅仅是一种隐式的赋值,而且因为是传递一个函数,它是一个隐含的引用赋值,所以最终结果也是指向了全局对象,触发了默认绑定,this 绑定到 window中了。

看一个例子:

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

function dofn(fn) {
	// `fn` 只不过 `fn` 的另一个引用

	fn(); // <-- 调用点!
}

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

var a = "oops, global"; // `a` 也是一个全局对象的属性

dofn( obj.fn ); // "oops, global"

或者

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

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

var a = "oops, global"; // `a` 也是一个全局对象的属性

setTimeout( obj.fn, 100 ); // "oops, global"

显式绑定

利用 JavaScript 提供的的 call, apply, bind方法强制一个函数使用某个特定的对象作为this的绑定。

  1. 通过 call或者 apply 强制函数的 this 指向 fn .

看一个例子:

function fn() {
  console.log(this.a);
}
var obj = {
  a: 2
};
fn.call(obj); // 2
fn.apply(obj); // 2

注意: 如果你传递一个简单基本类型值(string,boolean,或 number 类型)作为 this 绑定,那么这个基本类型值会被包装在它的对象类型中(分别是 new String(..),new Boolean(..),或 new Number(..))。这通常称为“封箱(boxing)”。

说明: 就 this 绑定的角度讲,call(..) 和 apply(..) 是完全一样的。它们在处理其他参数上的方式不同,call的参数列表要一个一个列在后面,如fn.call(obj, '参数一', '参数二'), 而apply参数列表使用的是数组,如fn.apply(obj, ['参数一', '参数二'])

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

看一个例子:

function fn(something) {
	console.log( this.a, something );
	return this.a + something;
}

var obj = {
	a: 2
};

var bar = fn.bind( obj );

var b = bar( 3 ); // 2 3
console.log( b ); // 5

注意: 在 ES6 中,bind(..) 生成的硬绑定函数有一个名为 .name 的属性,它源自于原始的 目标函数(target function)。举例来说:bar = fn.bind(..) 应该会有一个 bar.name 属性,它的值为 "bound foo",这个值应当会显示在调用栈轨迹的函数调用名称中。

new 绑定

当函数使用 new 调用时,即构造器调用时,会自动执行下面的操作:

  1. 一个全新的对象会凭空创建(就是被构建)
  2. 这个新构建的对象会被接入原形链([[Prototype]]-linked)
  3. 这个新构建的对象被设置为函数调用的 this 绑定
  4. 除非函数返回一个它自己的其他对象,否则这个被 new 调用的函数将自动返回这个新构建的对象。

看个例子:

function fn(a) {
	this.a = a;
}

var bar = new fn( 2 );
console.log( bar.a ); // 2

new 的创建过程:

var new2 = function(func) {
  var o = Object.create(func.prototype);
  var k = func.call(o);
  if(typeof k === 'object') {
    return k;
  } else {
    return o;
  }
}

四种绑定规则的顺序

  1. 函数是通过 new 被调用的吗(new 绑定)?如果是,this 就是新构建的对象。
var bar = new foo()
  1. 函数是通过 callapply 被调用(明确绑定),甚至是隐藏在 bind 硬绑定 之中吗?如果是,this 就是那个被明确指定的对象。
var bar = foo.call( obj2 )
  1. 函数是通过环境对象(也称为拥有者或容器对象)被调用的吗(隐含绑定)?如果是,this 就是那个环境对象。
var bar = obj1.foo()
  1. 否则,使用默认的 this(默认绑定)。如果在 strict mode 下,就是 undefined,否则是 global 对象。
var bar = foo()

箭头函数里的 this

箭头函数不是通过 function 关键字声明的,而是通过所谓的“大箭头”操作符:=>。
箭头函数是从封闭它的(函数或全局)作用域采用 this 绑定。

看一个例子:

function fn() {
  // 返回一个箭头函数
	return (a) => {
    // 这里的 `this` 是词法上从 `fn()` 采用的
		console.log( this.a );
	};
}

var obj1 = {
	a: 2
};

var obj2 = {
	a: 3
};

var bar = fn.call( obj1 );
bar.call( obj2 ); // 2, 不是3!

最常见的用法是用于回调,比如setTimeout

function fn() {
	setTimeout(() => {
		// 这里的 `this` 是词法上从 `fn()` 采用
		console.log( this.a );
	},100);
}

var obj = {
	a: 2
};

fn.call( obj ); // 2

总结

  • this 总是指向调用它所在方法的对象
  • this 的指向与所在方法的调用位置有关,而与方法的声明位置无关
  • 在浏览器中,调用方法时没有明确对象的, this 指向 windowNode 中,这种情况指向 global,但是在 Node cli 下,与浏览器的行为保持一致。严格模式下,thisundefined
  • 在浏览器中 setTimeoutsetInterval 和匿名函数执行时的当前对象是全局对象 window
  • applycall 能够强制改变函数执行时的当前对象,让 this 指向其他对象
  • es6开始,出现箭头函数(lamda表达式), 是在声明时候绑定this的
  • [绑定特例],自行查看书籍(https://github.com/CuiFi/You-Dont-Know-JS-CN/blob/master/this %26 object prototypes/ch2.md#绑定的特例)

学习链接
你不懂JS: this 是什么?
你不懂JS: this 豁然开朗!
this - JavaScript | MDN 官方中文版
JavaScript 的 this 原理

原文地址:https://www.cnblogs.com/arissy/p/10173002.html