this是JavaScript中最复杂的机制之一,被自动定义在所有函数的作用域中。“When a function of an object was called , the object will be passed to the execution context as 'this' value.”当一个函数被调用时,拥有这个函数的对象会作为this传入,所以this可以是全局对象,当前对象乃至任何对象。
例如:
function f(){
var name = "Funny";
console.log(this.name); //undefined
console.log(this); //window
}
f(); //f();与window.f();效果相同
调用函数f,因为函数f是最外层的函数,所以它拥有全局作用域,换句话说,它是全局对象(window)的一个方法(全局变量是全局对象的属性或方法)。console.log(this.name)
,程序执行到这里时,会对全局作用域进行RHS查询名为name的变量,由于不存在,所以控制台输出‘undefined’,console.log(this)
,输出全局对象window。
为什么要用this这个关键字?倘若我们不用this:
var me = {
name: "Kyle"
};
var you = {
name: "Reader"
};
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,I'm KYLE
如果不用this来传递上下文对象,而采用显式传递上下文对象,这会让代码变得越来越混乱,尤其当使用模式越来越复杂的时候(上面的例子代码特别简单)。函数自动引用合适的上下文对象十分重要:
var you = {
name: "Reader";
};
var me = {
name: "Kyle"
};
function identify(){
return this.name.toUpperCase();
}
function speak(){
var greeting = "Hello,I'm " + identify.call(this);
console.log(greeting);
}
identify.call(you); //READER
speak.call(me); //Hello,I'm KYLE
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
按道理,count是计算函数foo调用的次数,foo.count = 0
的确为函数对象foo添加一个名为count的属性,但是foo函数内部的this.count中的this并非指向foo函数对象,而是指向window(全局对象)。在非严格模式下,this.count++;
,会对count进行LHS查询,然而并没有在全局作用域中找到,所以就创建一个名为count的全局变量(严格模式下,程序会抛出引用异常);随着函数foo的调用,全局对象的属性count就递增,而函数对象的属性count则不变化,当然,console.log(count);
会输出4。
如果要从函数对象内部引用它本身,那么只使用this是不够的。我们可以强制this指向foo函数本身:
function foo(num){
console.log("foo: " + num);
this.count++;
}
foo.count = 0;
var i;
for(i = 0; i < 10; i++){
if(i > 5){
foo.call(foo,i);
}
}
console.log(foo.count); //4
大多数情况下this并非指向函数的作用域
this在任何情况下都不会指向函数的词法作用域,作用域和对象类似,可见的标识符都是它的属性,但是作用域无法通过JS代码访问,它存在于JS引擎内部。下面是个经典的错误例子:
function foo(){
var a = 2;
this.bar();
}
function bar(){
console.log(this.a);
}
foo(); //ReferenceError: a is not defined
这段代码首先通过this.bar()来引用函数bar(意外的成功了),通常省略前面的this,此外,开发者还试图用this联通foo()和bar()的词法作用域,使得bar()可以访问foo()作用域中的变量a,当然这是不可能的。至于为什么抛出这样一个异常,这里就不再赘述了。
this到底是一样什么样的机制???
this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。(动态作用域的定义瞬间浮现在脑海里)。当一个函数被调用时,会创建一个活动记录(有时也叫执行上下文)。这个纪录会包含函数在哪里被调用(调用栈)、函数调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。
调用位置
要理解this的绑定过程,首先要理解调用位置。
下面的例子中的注释就分析了调用栈,显然易懂,不再赘述。
function baz(){
//当前调用栈:baz
//当前调用位置:全局作用域
console.log("baz");
bar();// <-- bar的调用位置
}
function bar(){
//当前调用栈:baz->bar
//当前调用位置:baz
console.log("bar");
foo(); <--foo的调用位置
}
function foo(){
//当前调用栈:baz->bar->foo
//当前调用位置:bar
console.log("foo");
}
baz(); <-- baz的调用位置
绑定规则
首先找到调用位置,然后判断需要按照哪条绑定规则进行应用,如果多条规则都适用,则按优先级。
默认绑定
最常见的函数调用类型:独立函数调用。
function foo(){
console.log(this.a);
}
var a = 0;
foo(); //0
函数foo调用时应用了默认绑定,this指向了全局对象window。foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能应用默认绑定。严格模式下,则不能将全局对象用于默认绑定,因此this会被绑定到undefined。
隐式绑定
调用位置是否有上下文对象?是否被某个对象拥有或者包含? function foo(){ console.log(this.a); } var obj = { a: 2, foo: foo }; obj.foo(); //2 无论直接在obj中定义还是先定义在添加为引用属性,函数foo严格上来说都不属于这个对象,但是在函数被调用的时候,可以认为obj拥有或者包含这个函数,调用位置会使用obj上下文来引用函数。当函数引用有上下文对象时,隐式绑定会把函数调用中的this绑定到这个上下文对象。所以调用foo()时,this被绑定到obj,因而这里的this.a和obj.a就是一样的。对象属性引用链只有上一层在调用位置起作用。
被隐式绑定的函数会丢失绑定对象,然后它应用默认绑定(非严格模式下)。
function foo(){
console.log(this.a);
}
function doFoo(fn){
//fn其实引用的是foo
fn();
}
var obj = {
a: 2,
foo: foo
};
var a = "oops,global";
doFoo(obj.foo); //“oops,global"
参数传递是一种隐式赋值,所以这种会造成隐式丢失(显式赋值也会造成隐式丢失)。
显式绑定
使用函数的call(...)和apply(...)方法。它们的第一个参数是对象,显然是为this准备的,在调用函数时,将它绑定到this。
function foo(){
console.log(this.a);
}
var obj = {
a: 2
};
foo.call(obj); //2
显式绑定一样无法解决之前的丢失绑定问题,但显式绑定的变种可以解决这个问题。
硬绑定
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
首先我们创建了一个函数bar(),并在它的内部调用了foo.call(obj),所以我们强制把foo的this绑定到了obj。之后无论怎么调用函数bar,它都会再一次手动地在obj上调用foo。硬绑定是一种非常常用的内置方法。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);
var b = bar(3); //2 3;
console.(b); //5
new绑定
使用new来调用函数,会自动执行如下操作:
1、构造一个新对象
2、这个新对象会被执行[[Protorype]]连接
3、这个新对象会被绑定到函数调用的this
4、如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
function foo(a){
this.a = a;
}
var bar = new foo(2);
console.log(bar.a); //2
使用new来调用函数foo,我们会构造一个新对象并把它绑定到foo调用中的this上。
优先级
正常情况:new绑定>显式绑定>隐式绑定>默认绑定
特殊情况:当把null或者undefined作为绑定对象传入call、apply、bind,这些操作会被忽略,最后应用默认绑定。创建一个函数的“间接引用”,即前面提的绑定丢失,最终也是应用默认绑定。
软绑定
给默认绑定指定一个全局对象和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;
};
}
首先检查调用时的this,如果this绑定到全局对象或undefined,那就把指定的默认对象obj绑定到this,否则不会修改this。
ES6中的新玩法
箭头函数不是用function关键字定义的,而是用操作赋 => 定义的,这是ES6中新增的语法糖之一。箭头函数不适用于this的绑定规则,而是根据外层作用域来决定this,无论最外层绑定到了什么,它都会继承下来。
首先箭头函数的词法作用域:
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
foo()内部的箭头函数会捕获调用foo()时的this,这里是obj1,所以bar的this绑定到了obj1,箭头函数的绑定无法修改。
箭头函数常用于回调函数中,例如事件处理器或定时器:
function foo(){
setTimeout( () => { //这里的this在此法上继承自foo()
console.log(this.a);
},100);
}
var obj = {
a:2
};
foo.call(obj); //2