this是JavaScript中比较复杂的机制之一,本篇文章希望可以带大家了解this相关的知识。本文内容来自书籍《你不知道的JavaScript(上卷)》,只是自己稍微整理一下。
☕️为什么使用this
问题来了,既然this比较复杂,我们为什么还要使用呢?看一段代码:
function identify() {
return this.name.toUpperCase();
}
function speak() {
var greeting = "Hello, I'm " + identify.call( this );
console.log( greeting );
}
var me = {
name: "Kyle"
};
var you = {
name: "Reader"
};
identify.call( me ); // KYLE
identify.call( you ); // READER
speak.call( me ); // Hello, 我是 KYLE
speak.call( you ); // Hello, 我是 READER
这段代码可以在不同的上下文对象(me和you)复用函数,并且代码中使用了this,如果不使用this代码会是这个样子
function identify(context) {
return context.name.toUpperCase();
}
function speak(context) {
var greeting = "Hello, I'm " + identify( context );
console.log( greeting );
}
identify( you ); // READER
speak( me ); //hello, 我是 KYLE
可以看出来,比起显示地传递上下文对象,使用this这种隐式的传递一个对象的引用,更加方便
⬇️this的误区
关于this,由于它的语义性的问题,会带来很多的误解:
误区一:指向自身
function foo(num) {
console.log( "foo: " + num );
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被调用了多少次?
console.log( foo.count ); // 0
运行后我们发现foo.count仍然是0,说明this并没有指向foo自身。
误区二:指向它的作用域
在某种情况下这个说法是正确的,而在某些情况下这个说法又是错误的,但是要注意!!this 在任何情况下都不指向函数的词法作用域!!
为什么这么说呢?
function bar() {
console.log(1);
}
this.bar(); // 1
在上例中,this指向了全局作用域,但是只是特殊情况,因此会有这个说法是正确的,而在某些情况下这个说法又是错误的结论
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log( this.a );
}
foo();
上文this.a视图引用foo词法作用域定义的变量a,这是永远也不可能实现的
❤️this到底是什么
说了它的使用方式以及误区,那么this到底是什么呢?首先明确一点:this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
调用位置
this是在调用时被绑定的,完全取决于函数的调用位置,因此要搞清楚函数的调用位置,但是某些编程模式会隐藏函数的调用位置,最重要的分析它的调用栈(就是为了达到当前运行位置的所有调用函数)
function baz() {
// 当前调用栈是:baz
// 因此,当前调用位置是全局作用域
bar(); // <-- bar 的调用位置
}
function bar() {
// 当前调用栈是 baz -> bar
// 因此,当前调用位置在 baz 中
console.log( "bar" );
foo(); // <-- foo 的调用位置
}
function foo() {
// 当前调用栈是 baz -> bar -> foo
// 因此,当前调用位置在 bar 中
console.log( "foo" );
}
baz(); // <-- baz 的调用位置
☕️绑定规则
下面介绍this绑定的4种规则,下次看到this出现时,便可以使用这些规则
默认绑定
这是比较常见的函数调用类型:独立函数调用
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2
如何判断应用了默认绑定呢?foo是直接使用不带任何修饰符的函数进行引用调用的
注意:如果使用了严格模式,this会绑定到undefined
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // TypeError: this is undefined
隐式绑定
当函数引用有上下文对象时(严格来说函数被对象“拥有”或者“包含”),隐式绑定规则会把函数调用中的this绑定到这个上下文对象
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
严格来说,foo不属于obj对象,但是落脚点却指向obj对象,因此你可以说函数被调用时 obj 对象“拥 有”或者“包含”它。
隐式丢失
一个最常见的问题就是:隐式绑定会丢失绑定对象,从而执行默认绑定
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其实是一个不带任何修饰的函数调用
另外一种情况就是参数传递。
参数传递其实就是一种隐式赋值,我们在传入函数时也是隐式赋值
function foo() {
console.log( this.a );
}
function doFoo(fn) {
// fn 其实引用的是 foo
fn(); // <-- 调用位置!
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
doFoo( obj.foo ); // "oops, global"
综上所述:有两种情况会导致隐式绑定的绑定丢失。
- 进行引用赋值
var bar = obj.foo; - 进行传递参数
doFoo( obj.foo );
显式绑定
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
foo.call( obj ); // 2
通过foo.call(..)可以在调用时强制把this绑定到obj上,但是这样的方式也无法解决掉丢失绑定问题
var a = 0;
function foo() {
console.log(this.a);
}
var obj1 = {
a:1
};
var obj2 = {
a:2
};
foo.call(obj1);// 1
foo.call(obj2);// 2
我们发现this随着调用一直在改变,即this丢失。
我们可以通过以下方式解决:
硬绑定
创建一个包裹函数,传入所有的参数并返回接收到的所有值
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
API调用的上下文
许多内置函数都提供了一个可选参数,通常被称为上下文context,其作用和bind一样,确保你的回调 函数使用指定的 this。
function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
};
// 调用 foo(..) 时把 this 绑定到 obj
[1, 2, 3].forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome
new绑定
JavaScript中的new机制与面向对象的语言完全不同,实际上,在JavaScript中并不存在所谓的"构造函数",只有对与函数的"构造调用"
使用new来调用函数,或者说发生构造函数调用时的流程:
- 创建(构造一个全新的对象)
- 这个新对象会被执行
[[原型]]连接 - 这个新对象会被绑定到函数调用的
this - 如果函数没有返回其他对象,那么
new表达式中的函数调用会自动返回这个新对象
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
❤️优先级
上面介绍了this的4种绑定规则,那么它们的优先级谁高谁低呢,首先,确认一点的是默认绑定的优先级最低
比较隐式绑定和显示绑定
function foo() {
console.log( this.a );
}
var obj1 = {
a: 2,
foo: foo
}
var obj2 = {
a: 3,
foo: foo
}
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2
可以看出来显示绑定优先级高于隐式绑定
比较new绑定和隐式绑定
function foo(something) {
this.a = something;
}
var obj1 = {
foo: foo
};
var obj2 = {};
obj1.foo( 2 );
console.log( obj1.a ); // 2
obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3
// new 和 隐式绑定同时存在,obj1的a是2,而this指向了bar
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4
可以看出来new绑定高于隐式绑定
比较new绑定和显示绑定
由于new 和 call/apply无法一起使用,我们可以使用硬绑定测试优先级
function foo(something) {
this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar(3);
console.log( obj1.a ); // 2
console.log( baz.a ); // 3
首先bar被强制绑定到obj1上,但是new bar(3)没有预期把obj1.a修改为 3
因此new的优先级大于硬绑定。
但是使用刚开始的裸bind
function foo(something) {
this.a = something;
}
function bind(obj, fn) {
return function() {
fn.apply(obj. arguments);
}
}
var obj1 = {};
var bar = bind( obj1, foo );
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar(3);
console.log( obj1.a ); // 3
console.log( baz.a ); // undefined
会惊奇地发现,new bar(3)把obj1.a修改为 3
因此内置bind的实现是非常复杂的,不在此进行研究,既然这么复杂,为什么还要使用呢?
这种做法称为“部 分应用”,是“柯里化”的一种,它的主要目的是预设函数的一些参数,这样在使用new进行初始化时就可以只传入其余的参数。
function foo(p1,p2) { this.val = p1 + p2;
}
// 之所以使用 null 是因为在本例中我们并不关心硬绑定的 this 是什么
// 反正使用 new 时 this 会被修改
var bar = foo.bind( null, "p1" );
var baz = new bar( "p2" );
baz.val; // p1p2
从上我们可以总结出可以通过以下顺序判断this:
- 函数是否在
new中调用(new绑定)? - 函数是否通过
call、apply(显式绑定)或者硬绑定调用 - 函数是否在某个上下文对象中调用(隐式绑定)
- 如果都不是的话,使用默认绑定
☕️绑定例外
规则总有例外,当你认为应用了其他规则时,有可能只应用了默认规则
被忽略的this
如果我们把null或者undefined作为this的绑定对象传递入call、apply、或者bind,会使用默认绑定规则
function foo() {
console.log( this.a );
}
var a = 2;
foo.call( null ); // 2
那么什么情况下会使用这种方式呢?利用apply展开数组或者bind实现函数柯里化的部分应用
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 把数组“展开”成参数
foo.apply( null, [2, 3] ); // a:2, b:3
// 使用 bind(..) 进行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3
es6中可以使用...来代替``apply(...)```,但是ES6中没有柯里化的相关方法
忽略this会存在一个问题,比如第三方库的函数真的使用了this,我们这种方式把this绑定到了全局作用域,会存在问题,需要使用更安全的this,创建空的非委托的对象Object.create( null )
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 我们的 DMZ 空对象
var ø = Object.create( null );
// 把数组展开成参数
foo.apply( ø, [2, 3] ); // a:2, b:3
// 使用 bind(..) 进行柯里化
var bar = foo.bind( ø, 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
p.foo = o.foo返回的事目标函数的引用,因此调用位置是foo(),而不是p.foo()或者o.foo(),因此还是会调用默认规则
软绑定
硬绑定可以把this强制绑定到指定的对象上,防止函数调用应用默认规则绑定,但是有一个弊端就是无法通过隐式或者显示绑定来修改this
如果可以给默认绑定指定一个全局对象和undefined以外的值,就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改 this 的能力
这种叫做软绑定。
if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this;
console.log('fn', this);
// 捕获所有 curried 参数
var curried = [].slice.call( arguments, 1 );
var bound = function() {
console.log('this', this);
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 <---- 应用了软绑定
☕️this词法
最后介绍es6中的箭头函数,箭头函数不使用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,bar引用箭头函数的this也会绑定到obj1,箭头函数的绑定无法被修改