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种规则中的哪一种适用。
默认绑定
- 默认绑定常见的情况是独立函数调用,也就是
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
- 严格模式 下,对于 默认绑定 来说全局对象是不合法的,
this
将被设置为undefined
。
看一个例子:
function fn() {
"use strict";
console.log( this.a );
}
var a = 2;
fn(); // TypeError: `this` is `undefined`
- 显示使用
window
调用fn()
则不会报错。
看一个例子:
function fn() {
"use strict";
console.log( this.a );
}
var a = 2;
window.fn(); // 2
- 即便所有的
this
绑定规则都是完全基于调用点的,但如果fn()
的内容没有在 严格模式 下执行,对于默认绑定来说全局对象是唯一合法的。fn()
调用点的 严格模式 状态与此无关。
看一个例子:
function fn() {
console.log( this.a );
}
var a = 2;
(function(){
"use strict";
fn(); // 2
})();
注意: 应尽量避免 严格模式和 非严格模式混淆使用。
隐式绑定
这个规则是: 调用点是否有一个环境对象。当一个方法引用存在一个环境对象时, 这个对象应当被用于这个函数调用的 this
绑定。
- 函数如果被某个对象包含,且作为这个对象的方法调用时,函数里面的
this
就是这个对象。
看一个例子:
function fn() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
}
obj.fn(); // 2
- 只有对象属性引用链的最后一层是影响调用点的。
看一个例子:
function fn() {
console.log(this.a);
}
var obj1 = {
a: 42,
foo: foo
}
var obj2 = {
a: 2,
obj1: obj1
}
obj2.obj1.foo(); // 42
- 当一个隐式绑定丢失了它的绑定,意味着它可能会退回到默认绑定。
看一个例子:
function fn() {
console.log(this.a);
}
var obj = {
a: 2,
fn: fn
};
var bar = obj.fn; // 函数引用!
var a = 'oops, global'; // `a` 也是一个全局对象的属性
bar(); // "oops, global"
- 当传递一个回调函数式,参数传递仅仅是一种隐式的赋值,而且因为是传递一个函数,它是一个隐含的引用赋值,所以最终结果也是指向了全局对象,触发了默认绑定,
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的绑定。
- 通过
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, ['参数一', '参数二'])
。
- 硬绑定
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
调用时,即构造器调用时,会自动执行下面的操作:
- 一个全新的对象会凭空创建(就是被构建)
- 这个新构建的对象会被接入原形链([[Prototype]]-linked)
- 这个新构建的对象被设置为函数调用的 this 绑定
- 除非函数返回一个它自己的其他对象,否则这个被 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;
}
}
四种绑定规则的顺序
- 函数是通过
new
被调用的吗(new
绑定)?如果是,this
就是新构建的对象。
var bar = new foo()
- 函数是通过
call
或apply
被调用(明确绑定),甚至是隐藏在bind
硬绑定 之中吗?如果是,this
就是那个被明确指定的对象。
var bar = foo.call( obj2 )
- 函数是通过环境对象(也称为拥有者或容器对象)被调用的吗(隐含绑定)?如果是,
this
就是那个环境对象。
var bar = obj1.foo()
- 否则,使用默认的
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
指向window
。Node
中,这种情况指向global
,但是在Node cli
下,与浏览器的行为保持一致。严格模式下,this
为undefined
- 在浏览器中
setTimeout
、setInterval
和匿名函数执行时的当前对象是全局对象window
apply
和call
能够强制改变函数执行时的当前对象,让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 原理