函数内 this 既不指向函数自身,也不指向函数的 词法作用域。
this 实际上在函数被调用时发生绑定,this 指向什么 ** 完全 ** 取决于函数在哪里,怎样被调用。
this 的默认绑定
缺省的规则。
最常用的函数调用类型:** 独立函数调用 **。
有一个细节:
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined
只要函数的定义是严格模式,那么默认调用时,this 会绑定到全局对象上。
function foo() {
console.log( this.a );
}
var a = 2;
(function(){
"use strict";
foo(); // 2
})();
上面的函数定义没有在严格模式,但是执行时是在严格模式,结果 this 依旧可以绑定到全局对象上。
this 的隐式绑定
函数的调用位置是否有上下文对象,是否被某个对象包含。this 由此来自动隐式地绑定。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
虽然函数 foo 不属于对象 obj ,但是 foo 作为属性添加到了 obj 中,但是函数被调用时,对象 obj 就 “拥有了” 函数 foo。
当函数引用有上下文 ** 对象 ** 时,** 隐式绑定 ** 规则会把函数调用中的 this
绑定到这个上下文对象。因为调用 foo() 时 this 被绑定到 obj,因此 this.a 和 obj.a 是一样的。
对象属性引用链中,只有最近的一层在调用中发生了隐式绑定。
再看下面代码:
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
delete obj2.a;
obj1.obj2.foo(); // undefined
obj2.__proto__ = obj1;
obj1.obj2.foo(); // 2
代码说明三点:
- 首先在最接近函数调用的对象中,obj2 中寻找属性
- 没找到,则报
undefined
- 若存在继承关系,则会向上寻找
隐式绑定的丢失 this
被隐式绑定的函数有时会丢失绑定对象,然后就会引用默认绑定,继而将 this 绑定到全局或 undefined。
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; var bar = obj.foo; // 函数别名! var a = "oops, global"; // a 是全局对象的属性 bar(); // "oops, global"
虽然 bar 是 obj.foo 的一个引用,但是实际上,它引用的是 *foo 函数本身 *,因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。相当于直接调用了 foo()
更常见的隐式绑定丢失发生在参数传递时:
function foo() { console.log( this.a ); } function doFoo(fn) { // fn 其实引用的是 foo fn(); // <-- 调用位置! } var obj = { a: 2222, foo: foo }; var a = "全聚德"; // a 是全局对象的属性 doFoo( obj.foo ); // "全聚德"
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一
个例子一样。这种情况放在内置函数里也一样。内置函数不会帮你把函数绑定。JavaScript 环境中内置的 setTimeout() 函数实现和下面的伪代码类似:function setTimeout(fn,delay) { // 等待 delay 毫秒 fn(); // <-- 调用位置! }
调用回调函数的函数可能会修改 this。在一些流行的 JavaScript 库中事件处理器常会把回调函数的 this 强制绑定到触发事件的 DOM 元素上。这在一些情况下可能很有用,但是有时它可能会让你感到非常郁闷
this 的显式绑定
隐式绑定一般是在对象内部包含指向函数的属性,使用时通过属性间接引用函数,从而隐式绑定 this 到对象上。
显式绑定就是强制把 this 扔给某个函数。
使用 call
和 apply
函数 。
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
foo.call( obj ); // 2
通过 foo.call(..)
,我们可以在调用 foo 时强制把它的 this 绑定到 obj 上。
call 原始值,会自动 ** 装箱 **。原始值会被转换成它的对象形式(也就是 new String(..)、new Boolean(..) 或者 new Number(..))。
如图
从 this 的绑定结果来说,call 和 apply 是一样的。区别是传递的参数
绑定丢失
显式绑定没有直接解决丢失绑定问题,因为在函数调用时,作为参数传递的 this 可能没法拿到。而且 call 和 apply 是直接执行函数,在作为参数传递的场景中不能使用。
怎么解决?
硬绑定
就是在外面套一个包裹函数。
思考下面的代码:
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
// 套一个函数
var bar = function() {
foo.call( obj );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// 硬绑定的 bar 不可能再修改它的 this
bar.call( window ); // 2
函数 bar(),并在它的内部手动调用了 foo.call(obj)
,因此强制把 foo 的 this 绑定到了 obj。无论之后如何调用函数 bar,它总会手动在 obj 上调用 foo。这种绑定是一种显式的强制绑定,因此我们称之为 ** 硬绑定 **。
常见用法:
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
// 简单的辅助绑定函数,返回一个可执行函数
function bind(fn, obj) {
return function() {
// 这里的 arguments 是内层这个匿名返回函数的 arguments
return fn.apply( obj, arguments );
};
}
var obj = {
a:2
};
var bar = bind( foo, obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5
由于硬绑定是一种非常常用的模式,所以 ES5 提供了内置的方法 Function.prototype.bind
,它的用法就不说了🙄
API 调用时的上下文
第三方库的许多函数,以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一
个可选的参数。
例如,Array.prototype.forEach
arr.forEach(callback(currentValue [, index [, array]])[, thisArg])
callback 函数后的 thisArg 就是用来指定 callback 函数执行时的 this 值。
let s = "淦";
function fn(e) {
console.log(e + this)
}
[1,2,3].forEach(fn, s);
// 输出:
1 淦
2 淦
3 淦
这里不要用箭头函数,因为箭头函数的 this 在定义时已经固定为了外层的 this(这里固定为了全局对象)。
new 绑定 this
在 Java 中,构造函数是一个类的特殊方法,使用 new 初始化时会调用类的构造函数。
在 JavaScript 中,new 的使用方法看起来和 Java 一样。但是,其机制与 Java 这种面向类的语言完全不同。
JavaScript 中,构造函数只是使用 new
操作符时被调用的函数。他们并不会属于某个类,更不会实例化一个类。因为 new 后面只是一个普通的函数。
使用 new 调用函数,会 ** 自动 ** 执行下面的操作:
- 构造一个全新对象
- 新对象会被连接到一个
[[Prototype]]
- 新对象绑定到函数调用的 this
- 如果函数没有其他返回对象,那么 new 表达式中的函数会自动返回这个新对象
箭头函数
ES6 引入的箭头函数并不是使用 function 关键字定义的,而是使用被称为 “胖箭头” 的操作符 => 定
义的。
箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this。
function foo() {
// 返回一个箭头函数
return (a) => {
//this 继承自 foo()
console.log( this.a );
};
}
var obj1 = {
a:2
};
var obj2 = {
a:3
}
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是 3 !
foo 函数内部的箭头函数会捕获执行 foo()
的 this,foo.call( obj1 );
执行完后,箭头函数的 this 就指向 obj1,并且永远不变了。就算是 new 也不行。
实际上,在 ES6 之前我们就已经在使用一种几乎和箭头函数完全一样的模式。
function foo() {
let self = this;
// let that = this;
setTimeout( function(){
console.log( self.a );
}, 100 );
}
var obj = {
a: 2
};
foo.call( obj ); // 2
虽然 self = this
和箭头函数看起来都可以取代 bind(..)
,但是从本质上来说,它们想替代的是 this 机制。
如果你经常编写 this 风格的代码,但是绝大部分时候都会使用 self = this
或者箭头函数来否定 this 机制,那你或许应当:
- 只使用词法作用域并完全抛弃错误 this 风格的代码;
- 完全采用 this 风格,在必要时使用 bind(..),尽量避免使用 self = this 和箭头函数。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论。