This是什么
this
不是编写时绑定,而是运行时绑定。它依赖于函数调用的上下文条件。this
绑定与函数声明的位置没有任何关系,而与函数被调用的方式紧密相连
当一个函数被调用时,会建立一个称为执行环境的活动记录。这个记录包含函数是从何处(调用栈 —— call-stack)被调用的,函数是 如何 被调用的,被传递了什么参数等信息。这个记录的属性之一,就是在函数执行期间将被使用的 this
引用
This绑定规则
默认绑定
第一种规则源于函数调用的最常见的情况:独立函数调用。可以认为这种 this
规则是在没有其他规则适用时的默认规则
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2 --> 当被调用时,对此方法调用的 this 实施了 默认绑定,所以使 this 指向了全局对象
如果采用 strict mode
那 this
将被设置为 undefined
隐式绑定
也就是确定 调用点是否有一个环境对象(context object)
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo //foo()函数不被 obj 所真正“拥有”或“包含”,只是一个引用属性而已
};
obj.foo(); // 2 调用点使用obj环境来引用函数,所以你可以说obj对象在函数被调用的时间点上“拥有”或“包含”这个 函数引用
当一个方法引用(foo
)存在一个环境对象(obj
)时,隐含绑定 就此发生:这个对象(obj
)应当被用于这个函数 (foo()
)调用的 this
绑定。
隐式丢失
this
绑定最烦人的就是当一个 隐含绑定 丢失了它的绑定,退回到 默认绑定 的情况
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // bar表面上看obj.foo的引用,但实际也只是foo的另一个引用而已
var a = "oops, global"; // `a` 也是一个全局对象的属性
bar(); --> 这个调用点完全符合 默认绑定规则 // "oops, global"
还有一个更微妙的发生方式如下代码:
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"
及时是内建的语言回调函数也是同一个结果
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // `a` 也是一个全局对象的属性
setTimeout( obj.foo, 100 ); // "oops, global" --> obj.foo在内建的回调函数中,
// 也只是一个不同名的引用而已
// 实际函数的调用点在 执行的时候也只是在全局作用下,因此读取到的a属性是 'oops,global'
我们的回调函数丢掉他们的 this
绑定是十分常见的事情。但是 this
使我们吃惊的另一种方式是,接收我们回调的函数故意改变调用的 this
不管哪一种意外改变 this
的方式,你都不能真正地控制你的回调函数引用将如何被执行,所以你(还)没有办法控制调用点给你一个故意的绑定。
明确绑定
绝大多数被提供的函数,当然还有你将创建的所有的函数,都可以访问 call(..)
和 apply(..)
它们接收的第一个参数都是一个用于 this
的对象,之后使用这个指定的 this
来调用函数。因为你已经直接指明你想让 this
是什么,所以我们称这种方式为 明确绑定(explicit binding)。
function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
foo.call( obj ); // 2
强制将 obj做this绑定
如果你传递一个简单基本类型值(string
,boolean
,或 number
类型)作为 this
绑定,那么这个基本类型值会被包装在它的对象类型中(分别是 new String(..)
,new Boolean(..)
,或 new Number(..)
)。这通常称为“封箱(boxing)”。
硬绑定
function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
var bar = function() {
foo.call( obj );// 强制this绑定到obj 并调用foo函数,以后无论怎么调用bar函数,都有此硬绑定存在
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// `bar` 将 `foo` 的 `this` 硬绑定到 `obj`
// 所以它不可以被覆盖
bar.call( window ); // 2
我们创建了一个函数 bar()
,在它的内部手动调用 foo.call(obj)
,由此强制 this
绑定到 obj
并调用 foo
。无论你过后怎样调用函数 bar
,它总是手动使用 obj
调用 foo
。这种绑定即明确又坚定,所以我们称之为 硬绑定(hard binding)
用 硬绑定_ 将一个函数包装起来的最典型的方法,是为所有传入的参数和传出的返回值创建一个通道:
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a: 2
};
var bar = function() {
return foo.apply( obj, arguments ); //bar接收参数,用obj硬绑定this,并且使用foo函数返回值
};
var b = bar( 3 ); // 2 3
console.log( b ); // 5
硬绑定已作为 ES5 的内建工具提供:Function.prototype.bind
...
var bar = foo.bind( obj );//用函数的bind函数替代
...
new
绑定
实际上 JavaScript 的机制和 new
在 JS 中的用法所暗示的面向类的功能 没有任何联系
_
在 JS 中,构造器 仅仅是一个函数,它们偶然地与前置的 new
操作符一起调用。它们不依附于类,它们也不初始化一个类。它们甚至不是一种特殊的函数类型。它们本质上只是一般的函数,在被使用 new
来调用时改变了行为
实际上不存在“构造器函数”这样的东西,而只有函数的构造器调用(就是在前面加new
)。
当在函数前面被加入 new
调用时,也就是构造器调用时,下面这些事情会自动完成:
- 一个全新的对象会凭空创建(就是被构建)
- 这个新构建的对象会被接入原形链(
[[Prototype]]
-linked) - 这个新构建的对象被设置为函数调用的
this
绑定 - 除非函数返回一个它自己的其他 对象,否则这个被
new
调用的函数将 自动 返回这个新构建的对象。
function foo(a) {
this.a = a;
}
var bar = new foo( 2 ); // 新创建了一个bar对象,来作为foo函数的this绑定调用
console.log( bar.a ); // 2
判定this
- 函数是通过
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
如果你传递 null
或 undefined
作为 call
、apply
或 bind
的 this
绑定参数,那么这些值会被忽略掉,取而代之的是 默认绑定 规则将适用于这个调用
function foo() {
console.log( this.a );
}
var a = 2;
foo.call( null ); // 2
更安全的 this
某些“更安全”的做法是:为了 this
而传递一个特殊创建好的对象,这个对象保证不会对你的程序产生副作用。建立一个“DMZ”(非军事区)对象 —— 只不过是一个完全为空,没有委托的对象。
创建 完全为空的对象 的最简单方法就是 Object.create(null)
Object.create(null)
和 {}
很相似,但是没有指向 Object.prototype
的委托,所以它比 {}
“空得更彻底”。
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 我们的 DMZ 空对象
var ø = Object.create( null );
// 将数组散开作为参数
foo.apply( ø, [2, 3] ); // a:2, b:3
// 用 `bind(..)` 进行 currying
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3
Object.create(null)
语义上可能会比 null
更清晰的表达“我想让 this
为空”
词法 this
不适用于这些规则特殊的函数:箭头函数(arrow-function),箭头函数从封闭它的(函数或全局)作用域采用 this
绑定
function foo() {
// 返回一个箭头函数
return (a) => {
// 这里的 `this` 是词法上从 `foo()` 采用的,就是跟谁foo函数的调用绑定this
console.log( this.a );
};
}
var obj1 = {
a: 2
};
var obj2 = {
a: 3
};
var bar = foo.call( obj1 );//foo函数显式绑定obj1作为this,所以箭头函数以这个为准
bar.call( obj2 ); // 2, 不是3!
在 foo()
中创建的箭头函数在词法上捕获 foo()
被调用时的 this
,不管它是什么。因为 foo()
被 this
绑定到 obj1
,bar
(被返回的箭头函数的一个引用)也将会被 this
绑定到 obj1
。一个箭头函数的词法绑定是不能被覆盖的(就连 new
也不行!)
最常见的用法是用于回调,比如事件处理器或计时器:
function foo() {
setTimeout(() => {
// 这里的 `this` 是词法上从 `foo()` 采用
console.log( this.a );
},100);
}
var obj = {
a: 2
};
foo.call( obj ); // 2