this
参考《你不知道的JavaScript上卷》中的this部分
参考文章 关于JavaScript中this的软绑定
1. JavaScript中的this
1.1 关于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
console.log(foo.count); //0
console.log(count); //NaN
执行foo.count=0
时,的确向函数对象foo添加了一个属性count,但由于foo函数内部代码中的this不是指向函数自身的,所以this.count
不等于foo.count
,虽然函数名相同,根对象并不相同。而函数内部的this.count
会创建一个全局变量,且值为NaN。
- this指向函数的作用域
function foo(){
var a=2;
this.bar();
}
function bar(){
console.log(this.a);
}
foo(); //undefined
bar()无法访问foo()作用域里的变量a,使用this不可能在词法作用域中查到什么。
总结:
1.this既不指向函数自身也不指向函数的词法作用域;
2.this是在函数被调用时发生的绑定,它的绑定和函数声明的位置没有关系,它指向什么完全取决于函数在哪里被调用。
1.2 调用位置
调用位置就是函数在代码中被调用的位置,而不是声明的位置。最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。调用位置就是当前正在执行的函数的前一个调用中。
function first(){
//当前调用栈是:first
//当前调用位置是全局作用域
second();
}
function second(){
//当前调用栈是:first -> second
//当前调用位置是first
}
first();
1.3 绑定规则
- 默认绑定
独立函数调用。(可把这条规则看作是无法应用其他规则时的默认规则)
在foo()函数内部中,如果使用严格模式,则默认绑定不能绑定到全局对象,而绑定到undefined。
在严格模式下调用foo()则不影响绑定。
//在foo()函数内部中,如果使用严格模式
function foo(){
"use strict";
console.log(this.a);
}
var a=2;
foo(); //TypeError: Cannot read property 'a' of undefined
//在严格模式下调用foo()
function foo(){
console.log(this.a);
}
var a=2;
(function(){
"use strict";
foo(); //2
})(); //将脚本文件包裹在一个自执行的匿名函数中,这样相当于所有语句都在一个严格模式的函数中执行
- 隐式绑定
当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。(在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接(隐式)绑定到这个对象中)
对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。
function foo(){
console.log(this.a); //因为调用foo()时this被绑定到obj,因此this.a和obj.a是一样的。
}
var obj = {
a:2,
foo:foo
};
obj.foo(); //2
function foo(){
console.log(this.a);
}
var obj2 = {
a:42,
foo:foo
};
var obj1 = {
a:2,
obj2:obj2
};
obj1.obj2.foo(); //42
- 一个常见的this绑定问题是隐式绑定的函数会丢失绑定对象,从而应用默认绑定。(即隐式丢失)
//bar是obj.foo的一个引用,但是它引用的只是foo函数本身,即只是把foo函数内容赋值给bar。
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo:foo
};
var bar=obj.foo;
var a="global";
bar(); //global
//回调函数
//参数传递其实就是一种隐式赋值,即fn = obj.foo,同理只是把foo函数内容赋值给fn
function foo(){
console.log(this.a);
}
function doFoo(fn){
fn();
}
var obj = {
a:2,
foo:foo
};
var a="global";
doFoo(obj.foo); //global
//把函数传入语言内置的函数,结果一样
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo:foo
};
var a="global";
setTimeout(obj.foo, 100); //global
- 显式绑定
使用call()和apply()方法:第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this。
function foo(){
console.log(this.a);
}
var obj = {
a:2
};
foo.call(obj); //2
//通过foo.call(),可以在调用foo时强制把它的this绑定到obj上。
- 通过硬绑定(显式绑定的一个变种)解决丢失绑定问题
硬绑定的bar不可能再修改它的this
function foo(){
console.log(this.a);
}
var obj = {
a:2
};
var bar=function(){
foo.call(obj); //强制把foo的this绑定到了obj
}
bar(); //2
setTimeout(bar, 100); //2
bar.call(window); //2,没办法修改this
硬绑定的主要使用方法是:
1.创建一个包裹函数,负责接收参数并返回值;
2.创建一个可以重复使用的辅助函数
由于硬绑定是一种常用的模式,所以ES5提供了内置的方法bind(),会把指定的参数设置为this的上下文并调用函数使用指定的this。
- new绑定
使用new来调用函数,会自动执行下面的操作:
1.创建一个全新的对象。
2.这个新对象会被执行[[Prototype]]连接。
3.这个新对象会绑定到函数调用的this。
4.如果函数没有返回其他对象,则自动返回这个新对象。
function foo(num){
this.a=num;
}
var bar=new foo(2); //这里会构造一个新对象并把它绑定到foo()调用中的this上
console.log(bar.a); //2
如果函数里面返回一个对象,则this会绑定到返回的这个对象中。若返回一个非对象类型数据,则不会出现这样的情况,this仍会绑定到新对象。
function foo(num){
this.a=num;
return {a:200};
}
var bar=new foo(2);
console.log(bar.a); //200
- this绑定的四条规则的优先级
1.new绑定:this绑定的是新创建的对象(var bar = new foo()
)
2.显式绑定:this绑定的是指定的对象(var bar = foo.call(obj2)
)
3.隐式绑定:this绑定的是那个上下文对象(var bar = obj1.foo()
)
4.默认绑定:严格模式下绑定到undefined,否则绑定到全局对象
在new中使用硬绑定,主要目的是预先设置函数的一些函数,这样在使用new进行初始化时就可以只传入其余的参数。
function foo(p1,p2,p3){
this.val=p1+p2+p3;
}
var bar=foo.bind(null, "p1"); //使用null是因为无需关心硬绑定的this是什么,因为使用new时this会被修改
var baz=new bar("p2","p3"); //通过new把this绑定到baz上
console.log(baz.val); //"p1p2p3"
1.4 绑定例外
- 把null和undefined作为this的绑定对象传入call、apply、bind,这些值在调用时会被忽略,实际应用的是默认绑定规则
常用的做法是使用apply()来展开一个数组并当做参数传入一个函数、bind()对参数进行柯里化(预先设置一些参数)。
这些方法都需要传入一个参数作为this的绑定对象,如果函数不关心this的话,可以传入null作为一个占位值。
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
但总是使用null来忽略this绑定可能会产生一些副作用,因此更安全的做法是传入一个特殊的对象,比如创建一个DMZ对象(即一个空的非委托的对象)。
创建一个空对象最简单的方法是Object.create(null)
。
Object.create(null)
和{}
很像,但是并不会创建Object.prototype
这个委托,因此比{}
更空。
- 间接引用,会应用默认绑定规则
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
从1.3的隐式丢失问题中可以了解到,赋值表达式p.foo=o.foo
的返回值是目标函数的引用,因此调用位置是foo()
而不是p.foo()
或者o.foo()
,因此会应用默认绑定。
- 软绑定(给默认绑定指定一个全局对象和undefined以外的值,就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改this的能力)
下面的代码,首先检查调用时的this,如果this绑定到全局对象或者undefined,那就把指定的默认对象obj绑定到this,否则不会修改this。此外这段代码还支持可选的柯里化。
if(!Function.prototype.softBind){
Function.prototype.softBind = function(obj){
var fn=this;
var args=[].slice.call(arguments, 1);
var bound=function(){
return fn.apply((!this||this===(window||global))?obj:this, args.concat.apply(args,arguments));
};
bound.prototype=Object.create(fn.prototype);
return bound;
};
}
function foo(){
console.log("name: "+this.name);
}
var obj={name: "obj"};
var obj2={name: "obj2"};
var 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
- 首先先看
[].slice.call(arguments, 1)
这个写法。因为arguments是一个对象,不是真正的数组对象,只是与数组类似,所以不具有slice()方法。所以这条语句的过程就是先将传入进来的第一个参数转为数组,再调用slice()方法(即通过call显式绑定来实现arguments变相有slice()方法)。 - 再看
(! this || this === (window || global)) ? obj : this
,这条语句的作用是判断函数的调用位置,即它的this指向,将this绑定到现在正在指向的函数(即隐式绑定或显式绑定)。
对于fooOBJ();
,调用时此处的this绑定到全局对象,则将this绑定到obj中,所以输出的结果是name: obj
。
对于obj2.foo();
,调用时此处的this绑定到obj2中,所以this不变,输出name: obj2
,此处相当于隐式绑定。
同理,对于fooOBJ.call(obj3);
,调用时此处的this绑定到obj3,相当于显式绑定。
对于setTimeout(obj2.foo, 10);
,由1.3的隐式丢失问题中可以了解到回调函数相当于一个隐式的传参,如果没有软绑定的话,这里将会应用默认绑定将this绑定到全局环境上,但有软绑定,所以这里this还是指向obj。 - 可以看到,软绑定版本的foo()可以手动将this绑定到obj2或者obj3上,但如果应用默认绑定,则会将this绑定到obj。
- 此代码还支持柯里化。apply()传入的第二个参数
args.concat.apply(args,arguments)
就是运行 foo 所需要的参数,由上面的args(外部参数)和内部的arguments(内部参数)连接成。下面代码就是实现柯里化的例子。
function add(a,b){
return this.num+a+b;
}
var obj={num: 1};
var func=add.softBind(obj, 2);
func(3); //6
2. ES6中的this
ES6的箭头函数无法使用之前所说的四条规则,它是根据外层(函数或者全局)作用域来决定this。具体来说,箭头函数会继承外层函数调用的this绑定。
function foo(){
return (a)=>{
console.log(this.a);
};
}
var obj1={a:1};
var obj2={a:2};
var bar=foo.call(obj1);
bar.call(obj2); //1
foo()内部创建的箭头函数会捕获调用时foo()的this。由于foo()的this绑定到obj1,bar(引用箭头函数)的this也会绑定到obj1,且箭头函数的绑定无法被修改,new也不行,所以bar.call(obj2);
语句并没有改变bar的this绑定。
箭头函数最常用于回调函数,例如事件处理器或者定时器,下面是不使用箭头函数和使用箭头函数的结果。
var a=200;
function foo(){
setTimeout(function display(){
console.log(this.a); //回调函数丢失this绑定
}, 100);
}
var obj={a:2};
foo.call(obj); //200
var a=200;
function foo(){
setTimeout(()=>{
console.log(this.a); //this在词法上继承自foo(),foo()的this指向obj。
}, 100);
}
var obj={a:2};
foo.call(obj); //2