this全面解析
this绑定的规则
从执行上下文中我们可以知道,函数在运行时会生成函数执行上下文,在这个里面存在着this,而this绑定谁,则由规则去定义。
一共有四条规则,我们按从小到大一一讲解。
默认绑定规则
当其它三条规则不满足时,则使用默认绑定规则,在非严格模式下,this会被指向全局对象,也就是window,但是再严格模式下,会被指向undefined。
function a() {
console.log(this); //window
}
a();
function a() {
"use strict";
console.log(this); //undefined
}
a();
注意这个严格模式声明的位置,它声明在a函数内部,所以在a函数的执行上下文中,this会是undefined,如果你将严格模式定义在全局,那么所有的函数默认绑定就会都是undefined。
"use strict";
function a() {
console.log(this);
}
function b() {
console.log(this);
}
a();
隐式绑定
另一条需要考虑的规则是调用位置是否有上下文对象,简单点来说,它是否有被调用,比如通过某个对象的属性调用函数。
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
但是隐式绑定很容易造成无意间的绑定丢失,也就是说它导致采用默认绑定了。
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"
比如我们可能有时候会拿了对象中某个属性方法单独保存使用,但是这样的话,就会改变它的执行上下文对象,从而导致绑定丢失。
还有就是回调函数,某个对象的属性函数可能会作为回调去使用,这就导致函数被单独执行,也会丢失this绑定。
显式绑定
由于隐式容易丢失this的绑定,所以提供了显式指定this的方法,一共有三个:
- apply
- call
- bind
显式指定后就不会再受隐式绑定的影响。
function foo() {
console.log(this.a);
}
var obj = {
a: 2
};
foo.call(obj); // 2
new绑定
new一个构造函数的时候,他会将函数内的this绑定到新创建的实例对象上,且它是优先级最高的。
需要注意的是new绑定和显式绑定是不能同时存在的,所以不存在冲突情况。
javascript中的new操作符,虽然看起来和其他面向类的语言一样,但是实际在实现上是完全不同的。
在js中的构造函数并不是类,只是一种被new操作符调用的函数而已,所以包括内置的对象函数在内的所有函数,都可以通过new来调用,这是我们常常用开头大写来区分而已,最终只是对于函数的“”构造调用。
是用new操作符会产生以下操作:
- 创建一个全新的对象
- 这个对象原型链接到构造函数的原型
- 这个对象会被函数内的this绑定
- 如果函数没有return出其他对象,这个新对象会作为默认值抛出
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2
绑定规则优先级
首先new绑定和显式绑定不能同时使用,所以这两个属于最高优先,而隐式绑定第二,最后则是默认绑定。
被忽略的this
在使用显式绑定的方法时,js并没有要求一定要传一个有效的对象作为this绑定的对象,而是允许传入null
或者undefined
的。
当使用这两个作为绑定对象是,实际上走的是默认规则。
function foo() {
console.log(this.a);
}
var a = 2;
foo.call(null); // 2
但是使用null
或者undefined
显然会带来一些未知的问题,这种问题甚至难以分析和追踪,为了解决这个问题,在ES5的时候,提供了Object.create
创建对象的方法。
这个方法可以创建原子(MDZ)级的对象(周爱民大佬的解释),也就是无prototype的空对象。
通过绑定一个什么也没有的对象,可以避免this被绑定到全局,从而造成的莫名其妙的问题。
function foo(a, b) {
console.log("a:" + a + ", b:" + b);
}
// 我们的 DMZ 空对象
var a = Object.create(null);
// 把数组展开成参数
foo.apply( a , [2, 3]); // a:2, b:3
// 使用 bind(..) 进行柯里化
var bar = foo.bind( a , 2);
bar(3); // a:2, b:3
间接引用
当函数被赋值时,其实也是有返回值的,返回目标函数的引用,就是被赋值的函数本身。
function foo() {
console.log(this.a);
}
var a = 2;
var o = {
a: 3,
foo: foo
};
var p = {
a: 4
};
o.foo(); // 3
(p.foo = o.foo)(); // 2
此时后三条绑定规则不满足,只能走默认规则。
软绑定
硬绑定这种方式可以把 this 强制绑定到指定的对象(除了使用 new时),防止函数调用应用默认绑定规则。问题在于,硬绑定会大大降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改 this。
如果可以给默认绑定指定一个全局对象和 undefined 以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改 this 的能力。
可以通过一种被称为软绑定的方法来实现我们想要的效果:
if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this;
// 捕获所有 curried 参数
var curried = [].slice.call(arguments, 1);
var bound = function() {
return fn.apply(
(!this || this === (window || global)) ?
obj : this curried.concat.apply(curried, arguments)
);
};
bound.prototype = Object.create(fn.prototype);
return bound;
};
}
function foo() {
console.log("name: " + this.name);
}
var obj = {
name: "obj"
},
obj2 = {
name: "obj2"
},
obj3 = {
name: "obj3"
};
var fooOBJ = foo.softBind(obj);
fooOBJ(); // name: obj
obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看!!!
fooOBJ.call(obj3); // name: obj3 <---- 看!
setTimeout(obj2.foo, 10); // name: obj <---- 应用了软绑定
可以看到,软绑定版本的 foo() 可以手动将 this 绑定到 obj2 或者 obj3 上,但如果应用默认绑定,则会将 this 绑定到 obj。
箭头函数的this
上述的四条规则只能应用于所有正常的函数,ES6新增了一个箭头函数,他的this绑定规则是不一样的。
箭头函数的this是根据外层(函数或者全局)作用域来决定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函数被显式绑定this为obj1时,才会返回箭头函数,此时箭头函数的this从外层获取,所以也是obj1,而且箭头函数无法被显式指定this绑定,哪怕你使用new操作符,虽然它并不会报错
箭头函数可以像 bind(..) 一样确保函数的 this 被绑定到指定对象,此外,其重要性还体现在它用更常见的词法作用域取代了传统的 this 机制。实际上,在 ES6 之前我们就已经在使用一种几乎和箭头函数完全一样的模式。
function foo() {
var self = this; // lexical capture of this
setTimeout(function() {
console.log(self.a);
}, 100);
}
var obj = {
a: 2
};
foo.call(obj); // 2
虽然 self = this 和箭头函数看起来都可以取代 bind(..),但是从本质上来说,它们想替代的是 this 机制。
所以我们可以肯定的说,this的绑定就是在运行时决定的,虽然箭头函数看上去像是书写时决定的,但是它其实获取的是外层的this,外层的this是在运行时在执行上下文环境才有的,所以箭头函数和普通函数他们的this都是运行时决定的。
小结
如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断 this 的绑定对象。
- 由 new 调用?绑定到新创建的对象
- 由 call 或者 apply(或者 bind)调用?绑定到指定的对象。
- 由上下文对象调用?绑定到那个上下文对象。
- 默认:在严格模式下绑定到 undefined,否则绑定到全局对象。
一定要注意,有些调用可能在无意中使用默认绑定规则。如果想“更安全”地忽略 this 绑定,你可以使用一个 DMZ 对象,比如 ? = Object.create(null),以保护全局对象。ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这其实和 ES6 之前代码中的 self = this 机制一样。
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据