何为Call-Site
表示的是:函数在哪里被调用的。
一切都是规则
四条军规。
军规一:默认绑定
其他规则不匹配时就用这个规则啦。
第一件值得注意的事情是:全局环境下声明的变量,就是全局对象的属性了。
function foo() {
console.log( this.a );
}
var a = 2; // 自动变成全局对象的属性
foo(); // 2
这里的this
绑定就指向了全局对象,并拿到了全局对象的属性a
.
但是,如果开启了strict
模式,则全局变量就不适用于默认绑定了,this
会变成未定义。
总结:总体来说,this
绑定规则完全是根据Call Site的,全局对象只在非严格模式下才能用于默认绑定。严格模式下,会判定foo
函数的调用为无关。
另外注意一点是,你的程序要么是严格模式,要么是非严格模式,不可以一边严格一边不严格。有时候引入的第三方库和你的代码里的模式会有不同。
隐式绑定
上下文对象。
拥有或者包含其他对象的对象。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
这里的obj
就是上下文对象。
foo
函数用作obj
对象的reference property
。
这里的this
就指向obj
对象。
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
this
认的是最近的这个。
隐式丢失
这是很常见的一类错误。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = bar.foo; // 函数别名,函数的reference
var a = "oops, global";
bar(); // "oops, global"
这里如果是我自己想的话,会觉得输出为2.
之所以又跑到全局的原因是:bar
refer到的是foo
本身,不带有obj
这个上下文对象。
还有更隐蔽更让人摸不到头脑的呢!
function foo() {
console.log( this.a );
}
function doFoo(fn) {
fn(); // fn只会被认为是另一个foo函数的reference
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global";
doFoo(obj.foo); // "oops, global"
这个是真的难以察觉的,因为会认为fn()
是Call Site,但实际上fn
是函数foo
的别名。
即使是内建函数调用也是一样的效果:
function foo() {
console.log( this.a );
}
function doFoo(fn) {
fn(); // fn只会被认为是另一个foo函数的reference
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global";
setTimeout(obj.foo, 100); // "oops, global"
结果是一样的。
如何避免这些问题呢?
显式绑定
在JS中的所有函数都有一些特性可用,通过原型链。
而且,函数有call()
、apply()
方法可用。
看代码就明白了:
function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
foo.call( obj ); // 2
通过foo.call(obj)
可以强行把this
绑定到obj
上。
来来来,我喊到的谁谁就是当前的
this
啦!
硬绑定
functionn foo() {
console.log( this.a );
}
var obj = {
a: 2
};
var bar = function() {
foo.call( obj ); // 强制性把`this`绑定给obj
};
bar(); //2
setTimeout( bar, 100 ); //2
bar.call(window); // 2
在bar
函数内部强制绑定。
最典型的硬绑定的方式是下面这种,可以传递任意参数的:
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a: 2
};
var bar = function() {
return foo.apply( obj, arguments );
}
var b = bar(3); // 2 3
console.log( b ); // 5
这种写法不是特别能理解,bar
函数明明不接受参数,只是在return
返回的是foo.apply( obj, arguments)
。但是调用bar
函数时传递了参数!?
为了表示这种模式,可以用下面的更加可以复用的写法:
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
// 将函数fn和obj绑定在一起
function bind(fn, obj) {
return function() {
return fn.apply( obj, arguments );
};
}
var obj = {
a: 2
};
var bar = bind( foo, obj);
var b = bar( 3 ); // 2 3
console.log( b ); // 5
整体输出效果和上面的案例是一样的。只不过是用bind
这种写法会更加可复用。
既然硬绑定这么普遍,所以这在ES5中是默认提供的了:
Function.prototype.bind
。
具体使用方式如下:
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a: 2
};
var bar = foo.bind( obj ); // 直接就bind了foo函数内的this给obj了
var b = bar( 3 ); //
语言层面实现好,真的大大减轻工作量的!一行就硬绑定,那么我也理解为什么会有函数绑定的用法了~一切都源于`this`这个任性的关键词啊
API内叫作contexts
很多JS内置函数和主机环境提供一个可选关键词,称作context
,设计用来避免使用bind
却能保证回调函数正确使用了this
关键词。
下面的例子一下没看明白,看了两次才看懂:
function foo(el) {
console.log( el, this.id);
}
var obj = {
id: "awesome"
};
// obj就是这样的可选参数,用于接收foo内的this哦~
[1,2,3].forEach( foo, obj );// 1 awesome 2 awesome 3 awesome
[1,2,3].forEach( foo ); // 1 undefined 2 undefined 3 undefined
如果在全局加一个:var id = "global"
,再调用[1,2,3].forEach( foo );
结果就是:
1 "global" 2 "global" 3 "global"
通过new
绑定
最后一条军规了~
仍然是从我们可能犯错的地方出发思考。
重新定义js中的构造器
function foo(a) {
this.a = a;
}
var bar = new foo( 2 );
console.log( bar.a ); // 2
这么干净的就宣布了this
是foo
的了~
所以来认真思考下为什么~~
JS中,用
new
并不表示实例化了一个类,只不过恰巧和有类的语言同名了而已,没有其他特殊因素。
JS中的函数,包含内建函数,能够通过在函数名前加new
,这会使得这类函数调用有个新的名字:constructor call
. 大概翻译为构造器调用,而并非构造器。
这个区别很细微但很重要:实际没有一个构造器函数,只是一种构造器调用,具体构造啥呢?
当函数前面有new
时,下面几条会自动执行:
- 一个全新的对象被建立
- 新建的对象是原型连接的
- 新建的对象被设定为绑定了
this
,意味着在该函数内使用this
就是表示自身 - Unless the function returns its own alternate object, the new- invoked function call will automatically return the newly con‐structed object 这个还不知道如何理解呢
万物皆有序
现在四条军规已经揭晓,我们所需要做的就是找到调用点,并确定使用了哪个规则。
但是如果多条规则同时满足呢?这时候就需要个顺序了。
默认绑定顺序是最靠后的,这个毋庸置疑。
先看显式绑定和隐式绑定的顺序。
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); // 为this.a赋值2,但是你猜this现在指向谁呢?
console.log(obj1.a); // 2,为obj1添加了一个新属性
obj1.foo.call(obj2, 3); // 显式调用,并传递数据
console.log(obj2.a); // 3,this指向obj2
var bar = new obj1.foo(4); // new, this指向函数自身
console.log(obj1.a); // 2
console.log(bar.a); // 4
new
绑定顺序在隐式绑定之前。
但是new
和call/apply
不能一起用。
如何测出new
绑定和显式绑定的顺序呢?
function foo(something) {
this.a = something;
}
var obj1 = {};
var bar = foo.bind(obj1); // 强行赋予this给obj1
bar(2); //
console.log(obj1.a); // 2
var baz = new bar(3);
console.log(obj1.a); // 2, new修改了this的指向
console.log(baz.a); // 3
new
绑定的顺序在显式绑定之前。
再看一个:
function foo(p1,p2) {
this.val = p1 + p2;
}
var bar = foo.bind(null, "p1");
var baz = new bar("p2");
baz.val; // "p1p2"
通过var bar = foo.bind(null, "p1")
将this
绑定到了空对象上,但是通过new
覆盖了this
的绑定,结果是"p1p2",那么,这个p1是如何保留的呢?
关于这部分内容,仍然有待进一步思考理解才能完善。
目前只是一个大概的认知。
还有个softBind
,暂时不深究。
箭头函数的this
规则
在ES6中,箭头函数用的不是上面的四个规则。
function foo() {
return (a) => {
console.log( this.a );
}
}
var obj1 = {
a: 2
};
var obj2 = {
a: 3
};
var bar = foo.call(obj1);
bar.call(obj2);// 2
bar
是foo.call(obj1)
的返回结果,返回的是箭头函数。
在foo
函数内创建的箭头函数,会在调用时捕获foo
中的this
。
The lexical binding of an arrow-function cannot be overridden (even with new!).
function foo() {
setTimeout(() => {
console.log( this.a );
});
}
var obj = {
a: 2
};
foo.call( obj ); // 2
要注意箭头函数和前面四条规则的不同,程序中可以混用,但是在单个函数内可不要两种都出现。
ES6提出的箭头函数是在词法作用域进行的this
绑定。就是说,箭头函数会从闭包函数继承this
绑定。